2022-03-14 10:36:41 +01:00
|
|
|
# nimdeque
|
|
|
|
|
2022-03-15 12:26:09 +01:00
|
|
|
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
|
|
|
|
|
2022-03-15 16:34:50 +01:00
|
|
|
A `LinkedDeque` is a deque based on a doubly linked list.
|
2022-03-15 12:26:09 +01:00
|
|
|
|
|
|
|
```nim
|
|
|
|
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
|
2022-03-15 16:34:50 +01:00
|
|
|
queue.pop()
|
2022-03-15 12:26:09 +01:00
|
|
|
|
|
|
|
# Pops the last element in O(1) time
|
|
|
|
queue.pop(queue.high())
|
2022-03-15 16:34:50 +01:00
|
|
|
# This can also be written as
|
|
|
|
queue.pop(^1)
|
|
|
|
|
|
|
|
# Pops element at position n
|
|
|
|
queue.pop(n)
|
2022-03-15 12:26:09 +01:00
|
|
|
|
|
|
|
# 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()]
|
|
|
|
|
2022-03-15 16:34:50 +01:00
|
|
|
# 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)
|
2022-03-15 16:57:23 +01:00
|
|
|
|
|
|
|
# Clears the queue in O(1) time
|
|
|
|
queue.clear()
|
|
|
|
# Clears the queue in O(n) time
|
|
|
|
queue.clearPop()
|
2022-03-15 12:26:09 +01:00
|
|
|
```
|
2022-03-14 18:29:12 +01:00
|
|
|
|
2022-03-15 16:34:50 +01:00
|
|
|
---------------------
|
|
|
|
## Notes
|
|
|
|
|
|
|
|
- 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
|
2022-03-15 17:22:03 +01:00
|
|
|
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!
|
|
|
|
|
|
|
|
1. I was bored during my programming class
|
|
|
|
2. That only provides a deque based on `seq`s
|
|
|
|
3. The deque in that module is a value type
|
|
|
|
4. I was bored during my programming class
|
|
|
|
|
2022-03-16 10:11:38 +01:00
|
|
|
|
|
|
|
## 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.
|