diff --git a/README.md b/README.md index b308fd5..e96fda6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,60 @@ # nimdeque -Various deque implementations in pure nim. See the tests directory for usage examples +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 + +```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 +queue.pop(0) + +# Pops the last element in O(1) time +queue.pop(queue.high()) + +# 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()] + +``` __Note__: This is mostly a toy, there are no performance guarantees nor particular optimizations other than very obvious ones. With diff --git a/src/private/queues/linked.nim b/src/private/queues/linked.nim index 1e4edf5..aadb217 100644 --- a/src/private/queues/linked.nim +++ b/src/private/queues/linked.nim @@ -28,10 +28,15 @@ type ## optimized for ~O(1) ## access at both ends ## based on a doubly - ## linked list + ## linked list. If + ## maxSize is specified + ## and the queue is full, + ## older items are discarded + ## to make room for newer ones head: DequeNode[T] tail: DequeNode[T] size: int + maxSize: int proc newDequeNode[T](val: T): DequeNode[T] = @@ -43,37 +48,19 @@ proc newDequeNode[T](val: T): DequeNode[T] = result.val = val -proc newLinkedDeque*[T]: LinkedDeque[T] = +proc newLinkedDeque*[T](maxSize: int = 0): LinkedDeque[T] = ## Initializes a new, empty - ## LinkedDeque object + ## LinkedDeque object with an + ## optional size limit. A maxSize + ## of 0 indicates no size limit + ## for the queue new(result) result.size = 0 - - -proc add*[T](self: LinkedDeque[T], val: T) = - ## Appends an element at the end - ## of the queue - var newNode = newDequeNode(val) - if self.head == nil: - self.head = newNode - else: - newNode.prev = self.tail - self.tail.next = newNode - self.tail = newNode - inc(self.size) - - -proc addLeft*[T](self: LinkedDeque[T], val: T) = - ## Behaves like add(), but inserts a - ## value at the beginning instead - ## of at the end. This operation - ## takes constant time - var node = newDequeNode(val) - var head = self.head - self.head = node - self.head.next = head - head.prev = node - inc(self.size) + 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 = @@ -84,11 +71,17 @@ proc len*[T](self: LinkedDeque[T]): int = proc high*[T](self: LinkedDeque[T]): int = ## Returns the index of the last - ## element in the deque and -1 if + ## element in the deque or -1 if ## the deque is empty result = self.len() - 1 +proc maxSize*[T](self: LinkedDeque[T]): int = + ## Returns the maximum size of the + ## queue (0 = no limit) + result = self.maxSize + + proc getNode[T](self: LinkedDeque[T], i: int): DequeNode[T] = ## Low level method for indexing and getting ## a node object back @@ -117,31 +110,6 @@ proc getNode[T](self: LinkedDeque[T], i: int): DequeNode[T] = dec(pos) -proc insert*[T](self: LinkedDeque[T], pos: int, val: T) = - ## Inserts the given value at the given - ## position. When pos equals 0 or self.high(), - ## this proc is equivalent to addLeft and add - ## 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 index gets to the middle of the deque - if pos == 0: - self.addLeft(val) - elif pos == self.high(): - self.add(val) - else: - var node = self.getNode(pos) - var prev: DequeNode[T] = node.prev - var newNode = newDequeNode(val) - node.prev = newNode - prev.next = newNode - newNode.next = node - newNode.prev = prev - inc(self.size) - - proc `[]`*[T](self: LinkedDeque[T], i: int): T = ## Implements indexing into the queue result = self.getNode(i).val @@ -189,9 +157,68 @@ proc pop*[T](self: LinkedDeque[T], pos: int = 0): T = dec(self.size) -proc pop*[T](self: LinkedDeque[T], i: BackwardsIndex): T = +proc pop*[T](self: LinkedDeque[T], pos: BackwardsIndex): T = ## Same as self.pop but for backwards indeces - result = self.pop(self.size - int(i) - 1) + result = self.pop(self.size - int(pos)) + + +proc add*[T](self: LinkedDeque[T], val: T) = + ## Appends an element at the end + ## of the queue + var newNode = newDequeNode(val) + newNode.prev = self.tail + if self.tail != nil: + self.tail.next = newNode + self.tail = newNode + if self.head == nil: + self.head = newNode + elif self.size == self.maxSize: + discard self.pop() + inc(self.size) + + +proc addLeft*[T](self: LinkedDeque[T], val: T) = + ## Behaves like add(), but inserts a + ## value at the beginning instead + ## of at the end. This operation + ## takes constant time + var node = newDequeNode(val) + var head = self.head + if self.size == self.maxSize: + discard self.pop(self.high()) + self.head = node + self.head.next = head + head.prev = node + inc(self.size) + + +proc insert*[T](self: LinkedDeque[T], pos: int, val: T) = + ## Inserts the given value at the given + ## position. When pos equals 0 or self.high(), + ## this proc is equivalent to addLeft and add + ## 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 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: + raise newException(IndexDefect, &"LinkedDeque has reached its maximum size ({self.maxSize})") + if pos == 0: + self.addLeft(val) + elif pos == self.high(): + self.add(val) + else: + var node = self.getNode(pos) + var prev: DequeNode[T] = node.prev + var newNode = newDequeNode(val) + node.prev = newNode + prev.next = newNode + newNode.next = node + newNode.prev = prev + inc(self.size) iterator items*[T](self: LinkedDeque[T]): T = @@ -219,7 +246,7 @@ iterator reversed*[T](self: LinkedDeque[T]): T = ## the queue backwards var node = self.tail while node != nil: - yield node + yield node.val node = node.prev @@ -240,4 +267,7 @@ proc `$`*[T](self: LinkedDeque[T]): string = result &= $item if i < self.high(): result &= ", " - result &= "])" \ No newline at end of file + result &= "]" + if self.maxSize > 0: + result &= &", maxSize={self.maxSize}" + result &= ")" \ No newline at end of file diff --git a/tests/linked.nim b/tests/linked.nim index 68678cc..64a18d4 100644 --- a/tests/linked.nim +++ b/tests/linked.nim @@ -25,8 +25,17 @@ when isMainModule: echo "\t- Checking length" doAssert deque.len() == size echo "Checking iteration" + echo "\t- Checking indeces" 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" + var j = 0 + for e in deque.reversed(): + assert size - j - 1 == e + inc(j) echo "Checking contains" for i in countup(0, size - 1, 1): doAssert i in deque @@ -34,13 +43,13 @@ when isMainModule: doAssert deque.pop() == 0 echo "\t- Checking length" doAssert deque.len() == size - 1 - echo "\t- Popping off the tail" + echo "\t- Checking new head" + doAssert deque[0] == 1 + echo "Popping off the tail" doAssert deque.pop(deque.high()) == size - 1 echo "\t- Checking length" doAssert deque.len() == size - 2 - echo "Checking new head" - doAssert deque[0] == 1 - echo "Checking new tail" + echo "\t- Checking new tail" doAssert deque[deque.high()] == size - 2 echo "Re-checking values" for i in countup(0, size - 3, 1): @@ -95,4 +104,30 @@ when isMainModule: echo "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})" + 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!"