d29f29a4b8 | ||
---|---|---|
src | ||
tests | ||
.gitignore | ||
LICENSE | ||
README.md |
README.md
nimdeque
Various deque implementations in pure nim. A deque (short for "double-ended queue") is a data type that is optimized for access towards its ends. A deque's most interesting feauture is the ~O(1) time that it takes to pop/append at either ends (as opposed to regular lists where appending at the beginning is an O(n) operation).
Examples
LinkedDeque
A LinkedDeque
is a deque based on a doubly linked list.
import nimdeque
queue = newLinkedDeque[int]()
# Appends at the end
queue.add(1)
queue.add(2)
queue.add(3)
# Prepends at the beginning
queue.addLeft(0)
queue.addLeft(-1)
queue.addLeft(-2)
# Pops the first element in O(1) time
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:
echo i, " ", e
# Reversed iteration too!
for e in queue.reversed():
echo e
echo queue.len()
echo 5 in queue # false
echo 0 in queue # true
# Item accessing works just like regular sequence types in Nim.
# Note that the further the item is from either end of the
# queue, the higher the time it takes to retrieve it. For
# fast random access, seqs should be used instead
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)
# Clears the queue in O(1) time
queue.clear()
# Clears the queue in O(n) time
queue.clearPop()
Notes
- All queue constructors take an optional
maxSize
argument which limits the size of the queue. The default value is 0 (no size limit). WhenmaxSize > 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. Callinginsert
on a full queue will raise anIndexDefect
- 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 callingself.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 givesvar
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!). The tests directory contains some benchmarks as well as the test suite used to validate the behavior of the queues.
Why? There's std/deques!
- I was bored during my programming class
- That only provides a deque based on
seq
s - The deque in that module is a value type
- I was bored during my programming class
Performance metrics
To outline just how much faster a deque is than a regular dynamic array when doing insertion and popping near the ends, here are some performance metrics I took on my laptop with an 8-core Ryzen 7 4700U.
Both collections have been filled with 500000 elements and the time taken for each operation has been recorded. The average, minimum and maximum time taken are shown here, as well as the standard deviation for each micro-benchmark.
Appending at the end
LinkedDeque
Time taken: 0.325591755
seconds. Results:
- min:
0.0
- max:
0.010444388
- avg:
3.415999800000153e-07
- stdev:
1.643969031599315e-05
seq
Time taken: 0.283606824
seconds. Results:
- min:
2.499999999239222e-07
- max:
0.001853333999999984
- avg:
2.750403260000028e-07
- stdev:
3.929418946682074e-06
Popping the head
LinkedDeque
Time taken: 0.008184640000000076
seconds. Results:
- min:
2.499999999239222e-07
- max:
1.186499999994428e-05
- avg:
2.64712133333553e-07
- stdev:
9.675646231323721e-08
seq
Time taken: 2.3356288
seconds. Results:
- min:
0.0001166700000001519
- max:
0.002396426000000229
- avg:
0.0001549421693333322
- stdev:
8.55174518472766e-05
Adding at the left side
LinkedDeque
Time taken: 0.3028111230000001
seconds. Results:
- min:
2.699999996913505e-07
- max:
0.01986077199999992
- avg:
3.19837340000251e-07
- stdev:
2.808711098829249e-05
seq
Time taken: 23.515489255
seconds. Results:
- min:
2.700000001354397e-07
- max:
0.002371210999999818
- avg:
4.657242209800181e-05
- stdev:
3.14827288610087e-05
Random access (10000 times)
LinkedDeque
Time taken: 8.208724628999995
seconds. Results:
- min:
9.920000003660334e-07
- max:
0.002548661999998814
- avg:
0.0008192961785821358
- stdev:
0.0004795569727666707
seq
Time taken: 0.00333773400000581
seconds. Results:
- min:
1.599999990276046e-07
- max:
9.759999997527302e-06
- avg:
1.632506749111258e-07
- stdev:
9.612951140246536e-08
Popping the tail
LinkedDeque
Time taken: 0.004907793000000993
seconds. Results:
- min:
1.499999982002009e-07
- max: '8.515999994074264e-06`
- avg:
1.609196000041869e-07
- stdev:
9.069790474947947e-08
seq
Time taken: 0.004914629999994702
seconds. Results:
- min:
1.499999982002009e-07
- max:
6.697999999971671e-06
- avg:
1.62365333385613e-07
- stdev:
5.410771724203826e-08
TODOs
There are many possible implementations for double-ended queues: the current one is based on the usual textbook implementation of a doubly linked list, but that isn't the best choice for cache locality and has significant memory overhead for each link in the chain; Other possibilities involve using a list of subarrays to alleviate both of these issues, while some other options make use of ring buffers or specialized dynamic arrays growing from the center that can be used to allow even fast random accessing and can be made really efficient using lazy evaluation. The goal of this module is to implement most (possibly all) of these approaches, because I find them fascinating.