Refactored test and benchmark, extended README, added clear and clearPop methods, added equality operator, reversedPairs iterator and extend procedures

This commit is contained in:
Nocturn9x 2022-03-15 16:34:50 +01:00
parent 35956f1464
commit 1c6aa863eb
4 changed files with 274 additions and 62 deletions

View File

@ -11,7 +11,7 @@ beginning is an O(n) operation).
### LinkedDeque ### LinkedDeque
A `LinkedDeque` is a deque based on a doubly linked list A `LinkedDeque` is a deque based on a doubly linked list.
```nim ```nim
import nimdeque import nimdeque
@ -30,10 +30,15 @@ queue.addLeft(-1)
queue.addLeft(-2) queue.addLeft(-2)
# Pops the first element in O(1) time # Pops the first element in O(1) time
queue.pop(0) queue.pop()
# Pops the last element in O(1) time # Pops the last element in O(1) time
queue.pop(queue.high()) queue.pop(queue.high())
# This can also be written as
queue.pop(^1)
# Pops element at position n
queue.pop(n)
# Supports iteration # Supports iteration
for i, e in queue: for i, e in queue:
@ -54,8 +59,32 @@ echo 0 in queue # true
assert queue[0] == -1 assert queue[0] == -1
assert queue[^1] == queue[queue.high()] 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!) that said, the collections _do_ work and are tested somewhat thoroughly (please report any bugs!)

View File

@ -29,4 +29,8 @@ export `[]`
export `[]=` export `[]=`
export pairs export pairs
export linked.`$` export linked.`$`
export insert export insert
export extend
export reversedPairs
export clear
export clearPop

View File

@ -59,8 +59,6 @@ proc newLinkedDeque*[T](maxSize: int = 0): LinkedDeque[T] =
if maxSize < 0: if maxSize < 0:
raise newException(ValueError, "maxSize cannot be less than zero") raise newException(ValueError, "maxSize cannot be less than zero")
result.maxSize = maxSize result.maxSize = maxSize
if result.maxSize == 0:
result.maxSize = int.high()
proc len*[T](self: LinkedDeque[T]): int = 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)) 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) = proc add*[T](self: LinkedDeque[T], val: T) =
## Appends an element at the end ## Appends an element at the end
## of the queue ## of the queue
@ -172,7 +185,7 @@ proc add*[T](self: LinkedDeque[T], val: T) =
self.tail = newNode self.tail = newNode
if self.head == nil: if self.head == nil:
self.head = newNode self.head = newNode
elif self.size == self.maxSize: elif self.maxSize > 0 and self.size == self.maxSize:
discard self.pop() discard self.pop()
inc(self.size) inc(self.size)
@ -184,11 +197,12 @@ proc addLeft*[T](self: LinkedDeque[T], val: T) =
## takes constant time ## takes constant time
var node = newDequeNode(val) var node = newDequeNode(val)
var head = self.head var head = self.head
if self.size == self.maxSize: if self.maxSize > 0 and self.size == self.maxSize:
discard self.pop(self.high()) discard self.pop(self.high())
self.head = node self.head = node
self.head.next = head self.head.next = head
head.prev = node if head != nil:
head.prev = node
inc(self.size) inc(self.size)
@ -199,12 +213,12 @@ proc insert*[T](self: LinkedDeque[T], pos: int, val: T) =
## respectively. In all other cases, all items ## respectively. In all other cases, all items
## are "shifted" by 1 (shifted is in quotes because ## are "shifted" by 1 (shifted is in quotes because
## no shifting actually occurs, but the result is ## no shifting actually occurs, but the result is
## the same). The operation takes roughly constant ## the same). The operation takes constant time at
## time and the complexity becomes O(n) the closer ## the ends, but the complexity grows to O(n) the closer
## the index gets to the middle of the deque. This ## the index gets to the middle of the deque. This
## proc raises an IndexDefect if the queue's max ## proc raises an IndexDefect if the queue's max
## size is reached ## 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})") raise newException(IndexDefect, &"LinkedDeque has reached its maximum size ({self.maxSize})")
if pos == 0: if pos == 0:
self.addLeft(val) self.addLeft(val)
@ -250,6 +264,24 @@ iterator reversed*[T](self: LinkedDeque[T]): T =
node = node.prev 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 = proc contains*[T](self: LinkedDeque[T], val: T): bool =
## Returns if the given element is in ## Returns if the given element is in
## the deque ## the deque
@ -259,6 +291,18 @@ proc contains*[T](self: LinkedDeque[T], val: T): bool =
return false 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 = proc `$`*[T](self: LinkedDeque[T]): string =
## Returns a string representation ## Returns a string representation
## of the deque ## of the deque

View File

@ -11,123 +11,258 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import stats
import times
import random import random
import strformat import strformat
import ../src/nimdeque import ../src/nimdeque
when isMainModule: when isMainModule:
const size = 1000 const size = 1500
const benchSize = 150000
echo &"Running tests with queue of size {size}"
var deque = newLinkedDeque[int]() var deque = newLinkedDeque[int]()
echo &"Generating {size} values" var testStart = cpuTime()
echo &"\t- Checking add()"
for i in countup(0, size - 1, 1): for i in countup(0, size - 1, 1):
deque.add(i) deque.add(i)
echo "\t- Checking length"
doAssert deque.len() == size doAssert deque.len() == size
echo "Checking iteration"
echo "\t- Checking indeces" echo "\t- Checking iteration"
for i in countup(0, size - 1, 1): for i in countup(0, size - 1, 1):
doAssert deque[i] == i doAssert deque[i] == i
echo "\t- Checking pairs"
for i, e in deque: for i, e in deque:
assert i == e doAssert i == e
echo "\t- Checking reversed iteration"
var j = 0 var j = 0
for e in deque.reversed(): for e in deque.reversed():
assert size - j - 1 == e doAssert size - j - 1 == e
inc(j) 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): for i in countup(0, size - 1, 1):
doAssert i in deque 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 doAssert deque.pop() == 0
echo "\t- Checking length"
doAssert deque.len() == size - 1 doAssert deque.len() == size - 1
echo "\t- Checking new head"
doAssert deque[0] == 1 doAssert deque[0] == 1
echo "Popping off the tail"
echo "\t- Checking pop(^1)"
doAssert deque.pop(deque.high()) == size - 1 doAssert deque.pop(deque.high()) == size - 1
echo "\t- Checking length"
doAssert deque.len() == size - 2 doAssert deque.len() == size - 2
echo "\t- Checking new tail"
doAssert deque[deque.high()] == size - 2 doAssert deque[deque.high()] == size - 2
echo "Re-checking values"
echo "\t- Re-checking values"
for i in countup(0, size - 3, 1): for i in countup(0, size - 3, 1):
doAssert deque[i] == i + 1 doAssert deque[i] == i + 1
echo "Checking addLeft"
echo "\t- Checking addLeft()"
deque.addLeft(0) deque.addLeft(0)
echo "\t- Checking length"
doAssert deque.len() == size - 1 doAssert deque.len() == size - 1
echo "\t- Re-checking head"
doAssert deque[0] == 0 doAssert deque[0] == 0
echo "Re-checking values"
for i in countup(0, size - 2, 1): for i in countup(0, size - 2, 1):
doAssert deque[i] == i doAssert deque[i] == i
echo "Checking insert(3)"
echo "\t- Checking insert(3)"
var oldLen = deque.len() var oldLen = deque.len()
deque.insert(3, 69420) deque.insert(3, 69420)
echo "\t- Checking length"
doAssert oldLen + 1 == deque.len() doAssert oldLen + 1 == deque.len()
echo "\t- Checking inserted value"
doAssert deque.pop(3) == 69420 doAssert deque.pop(3) == 69420
echo "\t- Checking length"
doAssert deque.len() == oldLen doAssert deque.len() == oldLen
echo &"Checking insert({size - 2})"
echo &"\t- Checking insert({size - 2})"
oldLen = deque.len() oldLen = deque.len()
deque.insert(size - 2, 0x42362) deque.insert(size - 2, 0x42362)
echo "\t- Checking length"
doAssert oldLen + 1 == deque.len() doAssert oldLen + 1 == deque.len()
echo "\t- Checking inserted value"
doAssert deque.pop(size - 1) == 0x42362 doAssert deque.pop(size - 1) == 0x42362
echo "\t- Checking length"
doAssert deque.len() == oldLen doAssert deque.len() == oldLen
echo &"Checking insert({size div 2})"
echo &"\t- Checking insert({size div 2})"
oldLen = deque.len() oldLen = deque.len()
deque.insert(size div 2, 0xf7102) deque.insert(size div 2, 0xf7102)
echo "\t- Checking length"
doAssert oldLen + 1 == deque.len() doAssert oldLen + 1 == deque.len()
echo "\t- Checking inserted value"
doAssert deque.pop(size div 2) == 0xf7102 doAssert deque.pop(size div 2) == 0xf7102
echo "\t- Checking length"
doAssert deque.len() == oldLen doAssert deque.len() == oldLen
randomize() randomize()
let idx = rand(size - 1) let idx = rand(size - 1)
echo &"Checking insert({idx})" echo &"\t- Checking insert({idx})"
oldLen = deque.len() oldLen = deque.len()
deque.insert(size - 2, idx) deque.insert(size - 2, idx)
echo "\t- Checking length"
doAssert oldLen + 1 == deque.len() doAssert oldLen + 1 == deque.len()
echo "\t- Checking inserted value"
doAssert deque.pop(size - 1) == idx doAssert deque.pop(size - 1) == idx
echo "\t- Checking length"
doAssert deque.len() == oldLen doAssert deque.len() == oldLen
echo "Checking backwards indeces"
echo "\t- Checking backwards indeces"
for i in countdown(deque.high(), 1): for i in countdown(deque.high(), 1):
doAssert deque[^i] == deque[deque.len() - i] doAssert deque[^i] == deque[deque.len() - i]
deque.add(deque.pop(^1)) deque.add(deque.pop(^1))
doAssert deque[deque.high()] == deque[^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) var queue = newLinkedDeque[int](size div 2)
echo &"\t- Generating {size div 2} values"
for i in countup(0, (size div 2) - 1): for i in countup(0, (size div 2) - 1):
queue.add(i) queue.add(i)
echo "\t- Checking length"
doAssert queue.len() == size div 2 doAssert queue.len() == size div 2
var temp = queue[0] var temp = queue[0]
echo "\t- Testing append at the end"
queue.add((size div 2) + 1) queue.add((size div 2) + 1)
echo "\t- Checking length"
doAssert queue.len() == size div 2 doAssert queue.len() == size div 2
echo "\t- Checking item"
doAssert queue[^1] == (size div 2) + 1 doAssert queue[^1] == (size div 2) + 1
echo "\t- Checking displacement"
doAssert queue[0] == temp + 1 doAssert queue[0] == temp + 1
echo "Testing prepend at the beginning"
queue.addLeft(0) queue.addLeft(0)
echo "\t- Checking length"
doAssert queue.len() == size div 2 doAssert queue.len() == size div 2
echo "\t- Checking item"
doAssert queue[0] == 0 doAssert queue[0] == 0
echo "\t- Checking displacement"
doAssert queue[^1] == (size div 2) - 1 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}"