From 1c6aa863ebb832d8fbcea13d562ac006ef3851df Mon Sep 17 00:00:00 2001 From: Nocturn9x Date: Tue, 15 Mar 2022 16:34:50 +0100 Subject: [PATCH] Refactored test and benchmark, extended README, added clear and clearPop methods, added equality operator, reversedPairs iterator and extend procedures --- README.md | 35 ++++- src/nimdeque.nim | 6 +- src/private/queues/linked.nim | 60 +++++++-- tests/linked.nim | 235 ++++++++++++++++++++++++++-------- 4 files changed, 274 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index e96fda6..31b8f9a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ beginning is an O(n) operation). ### LinkedDeque -A `LinkedDeque` is a deque based on a doubly linked list +A `LinkedDeque` is a deque based on a doubly linked list. ```nim import nimdeque @@ -30,10 +30,15 @@ queue.addLeft(-1) queue.addLeft(-2) # Pops the first element in O(1) time -queue.pop(0) +queue.pop() # Pops the last element in O(1) time queue.pop(queue.high()) +# This can also be written as +queue.pop(^1) + +# Pops element at position n +queue.pop(n) # Supports iteration for i, e in queue: @@ -54,8 +59,32 @@ echo 0 in queue # true assert queue[0] == -1 assert queue[^1] == queue[queue.high()] +# It's possible to extend a deque with other deques or with seqs +# of compatible type +var other = newLinkedDeque[int]() +other.add(9) +other.add(10) +queue.extend(@[5, 6, 7, 8]) +queue.extend(other) ``` +--------------------- +## Notes -__Note__: This is mostly a toy, there are no performance guarantees nor particular optimizations other than very obvious ones. With +- All queue constructors take an optional `maxSize` argument which limits the size of the queue. The + default value is 0 (no size limit). When `maxSize > 0`, the queue will discard elements from the head when + items are added at the end and conversely pop items at the end when one is added at the head. Calling `insert` + on a full queue will raise an `IndexDefect` +- Two deques compare equal if they have the same elements inside them, in the same order. The value of `maxSize` is + disregarded in comparisons +- Calls to `extend()` **do not** raise any errors when the queue is full. They're merely an abstraction over a for + loop calling `self.add()` with every item from the other iterable +- Deques in this module do not support slicing. Use the built-in `seq` type if you need fast random accessing and/or slicing + capabilities +- The objects in this module are **all** tracked references! (Unlike the `std/deques` module which implements them as value + types and gives `var` variants of each procedure) + +## Disclaimer + +This is mostly a toy, there are no performance guarantees nor particular optimizations other than very obvious ones. With that said, the collections _do_ work and are tested somewhat thoroughly (please report any bugs!) diff --git a/src/nimdeque.nim b/src/nimdeque.nim index 0a930fe..05f0b0d 100644 --- a/src/nimdeque.nim +++ b/src/nimdeque.nim @@ -29,4 +29,8 @@ export `[]` export `[]=` export pairs export linked.`$` -export insert \ No newline at end of file +export insert +export extend +export reversedPairs +export clear +export clearPop \ No newline at end of file diff --git a/src/private/queues/linked.nim b/src/private/queues/linked.nim index aadb217..62b5651 100644 --- a/src/private/queues/linked.nim +++ b/src/private/queues/linked.nim @@ -59,8 +59,6 @@ proc newLinkedDeque*[T](maxSize: int = 0): LinkedDeque[T] = if maxSize < 0: raise newException(ValueError, "maxSize cannot be less than zero") result.maxSize = maxSize - if result.maxSize == 0: - result.maxSize = int.high() proc len*[T](self: LinkedDeque[T]): int = @@ -162,6 +160,21 @@ proc pop*[T](self: LinkedDeque[T], pos: BackwardsIndex): T = result = self.pop(self.size - int(pos)) +proc clear*[T](self: LinkedDeque[T]) = + ## Clears the deque in constant time + ## (relies on the GC to clean up!) + self.head = nil + self.tail = nil + self.size = 0 + + +proc clearPop*[T](self: LinkedDeque[T]) = + ## Clears the deque by repeatedly + ## calling self.pop() in O(n) time + while self.len() > 0: + discard self.pop() + + proc add*[T](self: LinkedDeque[T], val: T) = ## Appends an element at the end ## of the queue @@ -172,7 +185,7 @@ proc add*[T](self: LinkedDeque[T], val: T) = self.tail = newNode if self.head == nil: self.head = newNode - elif self.size == self.maxSize: + elif self.maxSize > 0 and self.size == self.maxSize: discard self.pop() inc(self.size) @@ -184,11 +197,12 @@ proc addLeft*[T](self: LinkedDeque[T], val: T) = ## takes constant time var node = newDequeNode(val) var head = self.head - if self.size == self.maxSize: + if self.maxSize > 0 and self.size == self.maxSize: discard self.pop(self.high()) self.head = node self.head.next = head - head.prev = node + if head != nil: + head.prev = node inc(self.size) @@ -199,12 +213,12 @@ proc insert*[T](self: LinkedDeque[T], pos: int, val: T) = ## respectively. In all other cases, all items ## are "shifted" by 1 (shifted is in quotes because ## no shifting actually occurs, but the result is - ## the same). The operation takes roughly constant - ## time and the complexity becomes O(n) the closer + ## the same). The operation takes constant time at + ## the ends, but the complexity grows to O(n) the closer ## the index gets to the middle of the deque. This ## proc raises an IndexDefect if the queue's max ## size is reached - if self.size == self.maxSize: + if self.maxSize > 0 and self.size == self.maxSize: raise newException(IndexDefect, &"LinkedDeque has reached its maximum size ({self.maxSize})") if pos == 0: self.addLeft(val) @@ -250,6 +264,24 @@ iterator reversed*[T](self: LinkedDeque[T]): T = node = node.prev +iterator reversedPairs*[T](self: LinkedDeque[T]): auto = + ## Implements pairwise reversed iteration + var i = 0 + for e in self.reversed(): + yield (i, e) + inc(i) + + +proc `==`*[T](self: LinkedDeque[T], other: LinkedDeque[T]): bool = + ## Compares two LinkedDeque objects + if self.high() != other.high(): + return false + for i, item in self: + if item != other[i]: + return false + return true + + proc contains*[T](self: LinkedDeque[T], val: T): bool = ## Returns if the given element is in ## the deque @@ -259,6 +291,18 @@ proc contains*[T](self: LinkedDeque[T], val: T): bool = return false +proc extend*[T](self: LinkedDeque[T], other: LinkedDeque[T]) = + ## Extends self with the items from other + for item in other: + self.add(item) + + +proc extend*[T](self: LinkedDeque[T], other: seq[T]) = + ## Extends self with the items from other + for item in other: + self.add(item) + + proc `$`*[T](self: LinkedDeque[T]): string = ## Returns a string representation ## of the deque diff --git a/tests/linked.nim b/tests/linked.nim index 64a18d4..ccc0d0b 100644 --- a/tests/linked.nim +++ b/tests/linked.nim @@ -11,123 +11,258 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import stats +import times import random import strformat import ../src/nimdeque when isMainModule: - const size = 1000 + const size = 1500 + const benchSize = 150000 + + echo &"Running tests with queue of size {size}" var deque = newLinkedDeque[int]() - echo &"Generating {size} values" + var testStart = cpuTime() + echo &"\t- Checking add()" for i in countup(0, size - 1, 1): deque.add(i) - echo "\t- Checking length" doAssert deque.len() == size - echo "Checking iteration" - echo "\t- Checking indeces" + + echo "\t- Checking iteration" for i in countup(0, size - 1, 1): doAssert deque[i] == i - echo "\t- Checking pairs" for i, e in deque: - assert i == e - echo "\t- Checking reversed iteration" + doAssert i == e var j = 0 for e in deque.reversed(): - assert size - j - 1 == e + doAssert size - j - 1 == e inc(j) - echo "Checking contains" + for i, e in deque.reversedPairs(): + doAssert size - i - 1 == e + + echo "\t- Checking contains()" for i in countup(0, size - 1, 1): doAssert i in deque - echo "Popping off the head" + doAssert 48574857 notin deque + doAssert -0xfffffff notin deque + echo "\t- Checking pop(0)" doAssert deque.pop() == 0 - echo "\t- Checking length" doAssert deque.len() == size - 1 - echo "\t- Checking new head" doAssert deque[0] == 1 - echo "Popping off the tail" + + echo "\t- Checking pop(^1)" doAssert deque.pop(deque.high()) == size - 1 - echo "\t- Checking length" doAssert deque.len() == size - 2 - echo "\t- Checking new tail" doAssert deque[deque.high()] == size - 2 - echo "Re-checking values" + + echo "\t- Re-checking values" for i in countup(0, size - 3, 1): doAssert deque[i] == i + 1 - echo "Checking addLeft" + + echo "\t- Checking addLeft()" deque.addLeft(0) - echo "\t- Checking length" doAssert deque.len() == size - 1 - echo "\t- Re-checking head" doAssert deque[0] == 0 - echo "Re-checking values" + for i in countup(0, size - 2, 1): doAssert deque[i] == i - echo "Checking insert(3)" + + echo "\t- Checking insert(3)" var oldLen = deque.len() deque.insert(3, 69420) - echo "\t- Checking length" doAssert oldLen + 1 == deque.len() - echo "\t- Checking inserted value" doAssert deque.pop(3) == 69420 - echo "\t- Checking length" doAssert deque.len() == oldLen - echo &"Checking insert({size - 2})" + + echo &"\t- Checking insert({size - 2})" oldLen = deque.len() deque.insert(size - 2, 0x42362) - echo "\t- Checking length" doAssert oldLen + 1 == deque.len() - echo "\t- Checking inserted value" doAssert deque.pop(size - 1) == 0x42362 - echo "\t- Checking length" doAssert deque.len() == oldLen - echo &"Checking insert({size div 2})" + + echo &"\t- Checking insert({size div 2})" oldLen = deque.len() deque.insert(size div 2, 0xf7102) - echo "\t- Checking length" doAssert oldLen + 1 == deque.len() - echo "\t- Checking inserted value" doAssert deque.pop(size div 2) == 0xf7102 - echo "\t- Checking length" doAssert deque.len() == oldLen + randomize() let idx = rand(size - 1) - echo &"Checking insert({idx})" + echo &"\t- Checking insert({idx})" oldLen = deque.len() deque.insert(size - 2, idx) - echo "\t- Checking length" doAssert oldLen + 1 == deque.len() - echo "\t- Checking inserted value" doAssert deque.pop(size - 1) == idx - echo "\t- Checking length" doAssert deque.len() == oldLen - echo "Checking backwards indeces" + + echo "\t- Checking backwards indeces" for i in countdown(deque.high(), 1): doAssert deque[^i] == deque[deque.len() - i] deque.add(deque.pop(^1)) doAssert deque[deque.high()] == deque[^1] - echo &"Checking maxSize ({size div 2})" + + echo &"\t- Checking queue with maxSize {size div 2}" var queue = newLinkedDeque[int](size div 2) - echo &"\t- Generating {size div 2} values" for i in countup(0, (size div 2) - 1): queue.add(i) - echo "\t- Checking length" doAssert queue.len() == size div 2 var temp = queue[0] - echo "\t- Testing append at the end" queue.add((size div 2) + 1) - echo "\t- Checking length" doAssert queue.len() == size div 2 - echo "\t- Checking item" doAssert queue[^1] == (size div 2) + 1 - echo "\t- Checking displacement" doAssert queue[0] == temp + 1 - echo "Testing prepend at the beginning" queue.addLeft(0) - echo "\t- Checking length" doAssert queue.len() == size div 2 - echo "\t- Checking item" doAssert queue[0] == 0 - echo "\t- Checking displacement" doAssert queue[^1] == (size div 2) - 1 - echo "All tests passed!" + + echo "\t- Testing extend()" + var old = deque.len() + var s = @[1, 2, 3, 4, 5, 6] + deque.extend(s) + for i in countup(size + 5, deque.high()): + doAssert deque[i] == s[i] + doAssert old + len(s) == deque.len() + + echo "\t- Testing clear()" + deque.clear() + doAssert deque.len() == 0 + doAssertRaises(IndexDefect, echo deque[0]) + echo &"Tests completed in {cpuTime() - testStart} seconds" + + ## End of tests, start of benchmark + echo &"\nRunning benchmarks with queue size of {benchSize} against a seq" + var q = newLinkedDeque[int]() + var q2: seq[int] = @[] + var t: seq[float] = @[] + var tmp: float + var st: RunningStat + var benchStart = cpuTime() + var start = cpuTime() + echo &" Benchmarking LinkedDeque.add()" + for i in countup(0, benchSize - 1): + tmp = cpuTime() + q.add(i) + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + echo &" Benchmarking seq.add()" + for i in countup(0, benchSize - 1): + tmp = cpuTime() + q2.add(i) + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + echo &" Benchmarking LinkedDeque.pop(0)" + for i in countup(0, size * 10 - 1): + tmp = cpuTime() + discard q.pop() + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + echo &" Benchmarking seq.del(0)" + for i in countup(0, size * 10 - 1): + tmp = cpuTime() + q2.del(0) + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + q2 = @[] + echo &" Benchmarking LinkedDeque.addLeft()" + for i in countup(0, benchSize - 1): + tmp = cpuTime() + q.addLeft(i) + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + echo &" Benchmarking seq.insert(0)" + for i in countup(0, benchSize - 1): + tmp = cpuTime() + q2.insert(i, 0) + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + start = cpuTime() + echo " Benchmarking random access for LinkedDeque (10000 times)" + for i in countup(0, 10000): + tmp = cpuTime() + discard q[rand(benchSize - 1)] + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + echo " Benchmarking random access for seq (10000 times)" + for i in countup(0, 10000): + tmp = cpuTime() + discard q2[rand(benchSize - 1)] + t.add(cpuTime() - tmp) + st.push(t) + echo &""" + - Done in {cpuTime() - start} seconds. Results (in seconds): + - min: {st.min} + - max: {st.max} + - avg: {st.mean()} + - stdev: {st.standardDeviation()}""" + st.clear() + t = @[] + q2 = @[] + + echo &"Total benchmark time: {cpuTime() - benchStart}" + echo &"Total execution time: {cpuTime() - testStart}" \ No newline at end of file