diff --git a/src/data-structures/deque/Deque.js b/src/data-structures/deque/Deque.js new file mode 100644 index 0000000000..7487ec6fe0 --- /dev/null +++ b/src/data-structures/deque/Deque.js @@ -0,0 +1,106 @@ +import LinkedList from '../linked-list/LinkedList'; + +export default class Deque { + constructor() { + // We use a doubly linked list internally so that both front and back + // operations run in O(1) time. + this.linkedList = new LinkedList(); + } + + /** + * Check if the deque is empty. + * @return {boolean} + */ + isEmpty() { + return !this.linkedList.head; + } + + /** + * Read the element at the front of the deque without removing it. + * @return {*} + */ + peekFront() { + if (!this.linkedList.head) { + return null; + } + return this.linkedList.head.value; + } + + /** + * Read the element at the back of the deque without removing it. + * @return {*} + */ + peekBack() { + if (!this.linkedList.tail) { + return null; + } + return this.linkedList.tail.value; + } + + /** + * Add a new element to the front (head) of the deque. + * @param {*} value + */ + addFront(value) { + this.linkedList.prepend(value); + } + + /** + * Add a new element to the back (tail) of the deque. + * @param {*} value + */ + addBack(value) { + this.linkedList.append(value); + } + + /** + * Remove the element from the front (head) of the deque. + * @return {*} + */ + removeFront() { + const removedHead = this.linkedList.deleteHead(); + return removedHead ? removedHead.value : null; + } + + /** + * Remove the element from the back (tail) of the deque. + * @return {*} + */ + removeBack() { + const removedTail = this.linkedList.deleteTail(); + return removedTail ? removedTail.value : null; + } + + /** + * Return the number of elements in the deque. + * @return {number} + */ + get size() { + let count = 0; + let currentNode = this.linkedList.head; + while (currentNode) { + count += 1; + currentNode = currentNode.next; + } + return count; + } + + /** + * Convert the deque to an array (front to back). + * @return {*[]} + */ + toArray() { + return this.linkedList + .toArray() + .map((linkedListNode) => linkedListNode.value); + } + + /** + * Return a string representation of the deque. + * @param {function} [callback] + * @return {string} + */ + toString(callback) { + return this.linkedList.toString(callback); + } +} diff --git a/src/data-structures/deque/README.md b/src/data-structures/deque/README.md new file mode 100644 index 0000000000..29d5be51cf --- /dev/null +++ b/src/data-structures/deque/README.md @@ -0,0 +1,75 @@ +# Deque (Double-Ended Queue) + +A **deque** (pronounced "deck", short for **double-ended queue**) is a linear data +structure that generalises both a stack and a queue. Elements can be added or +removed from **either end** — the front (head) or the back (tail) — in **O(1)** time. + +``` +addFront(3) addBack(4) + ↓ ↓ +┌───┬───┬───┬───┐ +│ 3 │ 1 │ 2 │ 4 │ ← internal linked list +└───┴───┴───┴───┘ + ↑ ↑ +removeFront() removeBack() +``` + +## Operations + +| Method | Description | Time | +| ------------- | -------------------------------------------- | ---- | +| `addFront(v)` | Insert element `v` at the front | O(1) | +| `addBack(v)` | Insert element `v` at the back | O(1) | +| `removeFront()` | Remove and return the front element | O(1) | +| `removeBack()` | Remove and return the back element | O(1) | +| `peekFront()` | Return the front element without removing it | O(1) | +| `peekBack()` | Return the back element without removing it | O(1) | +| `isEmpty()` | Return `true` if the deque has no elements | O(1) | +| `size` | Return the number of elements | O(n) | + +> **Note:** `size` is O(n) because the underlying linked list does not cache +> length. If you call `size` frequently, consider maintaining an internal counter. + +## Complexity + +| | | +| --------- | ---- | +| Space | O(n) | +| addFront | O(1) | +| addBack | O(1) | +| removeFront | O(1) | +| removeBack | O(1) | +| peekFront | O(1) | +| peekBack | O(1) | + +## Use Cases + +A deque is the right tool when you need **O(1) access at both ends**: + +- **Sliding window maximum/minimum** — maintain candidates in a monotonic deque + so each element is pushed and popped at most once (overall O(n)). +- **Browser history** — navigate backward (`removeFront`) and forward + (`removeBack`) through pages. +- **Undo / redo stacks** — push actions to the back, undo from the back, + redo from the front. +- **Palindrome checking** — compare characters from both ends simultaneously. +- **Work-stealing schedulers** (e.g. Java's `ForkJoinPool`) — threads push/pop + from their own back, while idle threads steal from another thread's front. + +## Implementation Note + +This implementation is backed by the project's existing `LinkedList` (a doubly +linked list). This gives O(1) `prepend` (for `addFront`) and O(1) `append` / +`deleteTail` (for `addBack` / `removeBack`), with no need to shift array +elements. + +An alternative implementation using a circular buffer (fixed-size array) offers +better cache locality but requires resizing logic. The linked-list approach is +chosen here to stay consistent with the other data structures in this project. + +## References + +- [Deque — Wikipedia](https://en.wikipedia.org/wiki/Double-ended_queue) +- [Deque Data Structure — GeeksForGeeks](https://www.geeksforgeeks.org/deque-set-1-introduction-applications/) +- [▶ Deque in 3 minutes — YouTube](https://www.youtube.com/watch?v=kLBuJ1998Do) +- [Sliding Window Maximum using Deque — YouTube](https://www.youtube.com/watch?v=2SXqBsTR6a8) \ No newline at end of file diff --git a/src/data-structures/deque/__test__/Deque.test.js b/src/data-structures/deque/__test__/Deque.test.js new file mode 100644 index 0000000000..fc191f1c2c --- /dev/null +++ b/src/data-structures/deque/__test__/Deque.test.js @@ -0,0 +1,190 @@ +import Deque from '../Deque'; + +describe('Deque', () => { + it('should create an empty deque', () => { + const deque = new Deque(); + + expect(deque).not.toBeNull(); + expect(deque.isEmpty()).toBe(true); + expect(deque.size).toBe(0); + }); + + it('should peek at the front and back of an empty deque', () => { + const deque = new Deque(); + + expect(deque.peekFront()).toBeNull(); + expect(deque.peekBack()).toBeNull(); + }); + + it('should return null when removing from an empty deque', () => { + const deque = new Deque(); + + expect(deque.removeFront()).toBeNull(); + expect(deque.removeBack()).toBeNull(); + }); + + it('should add elements to the back and remove from the front (queue behaviour)', () => { + const deque = new Deque(); + + deque.addBack(1); + deque.addBack(2); + deque.addBack(3); + + expect(deque.isEmpty()).toBe(false); + expect(deque.size).toBe(3); + expect(deque.peekFront()).toBe(1); + expect(deque.peekBack()).toBe(3); + + expect(deque.removeFront()).toBe(1); + expect(deque.removeFront()).toBe(2); + expect(deque.removeFront()).toBe(3); + expect(deque.removeFront()).toBeNull(); + expect(deque.isEmpty()).toBe(true); + }); + + it('should add elements to the front and remove from the back (reversed queue)', () => { + const deque = new Deque(); + + deque.addFront(1); + deque.addFront(2); + deque.addFront(3); + + expect(deque.size).toBe(3); + expect(deque.peekFront()).toBe(3); + expect(deque.peekBack()).toBe(1); + + expect(deque.removeBack()).toBe(1); + expect(deque.removeBack()).toBe(2); + expect(deque.removeBack()).toBe(3); + expect(deque.removeBack()).toBeNull(); + expect(deque.isEmpty()).toBe(true); + }); + + it('should add elements to the front and remove from the front (stack behaviour)', () => { + const deque = new Deque(); + + deque.addFront('a'); + deque.addFront('b'); + deque.addFront('c'); + + expect(deque.peekFront()).toBe('c'); + expect(deque.removeFront()).toBe('c'); + expect(deque.removeFront()).toBe('b'); + expect(deque.removeFront()).toBe('a'); + expect(deque.isEmpty()).toBe(true); + }); + + it('should support mixed addFront and addBack operations', () => { + const deque = new Deque(); + + // Build: [3, 1, 2, 4] + deque.addBack(1); + deque.addBack(2); + deque.addFront(3); + deque.addBack(4); + + expect(deque.size).toBe(4); + expect(deque.peekFront()).toBe(3); + expect(deque.peekBack()).toBe(4); + expect(deque.toArray()).toEqual([3, 1, 2, 4]); + }); + + it('should support mixed removeFront and removeBack operations', () => { + const deque = new Deque(); + + deque.addBack(1); + deque.addBack(2); + deque.addBack(3); + deque.addBack(4); + + expect(deque.removeFront()).toBe(1); + expect(deque.removeBack()).toBe(4); + expect(deque.removeFront()).toBe(2); + expect(deque.removeBack()).toBe(3); + expect(deque.isEmpty()).toBe(true); + }); + + it('should handle a single element correctly', () => { + const deque = new Deque(); + + deque.addBack(42); + + expect(deque.size).toBe(1); + expect(deque.peekFront()).toBe(42); + expect(deque.peekBack()).toBe(42); + + expect(deque.removeFront()).toBe(42); + expect(deque.isEmpty()).toBe(true); + expect(deque.peekFront()).toBeNull(); + expect(deque.peekBack()).toBeNull(); + }); + + it('should handle object values', () => { + const deque = new Deque(); + + const obj1 = { key: 'value1' }; + const obj2 = { key: 'value2' }; + + deque.addBack(obj1); + deque.addFront(obj2); + + expect(deque.peekFront()).toEqual({ key: 'value2' }); + expect(deque.peekBack()).toEqual({ key: 'value1' }); + expect(deque.removeFront()).toEqual({ key: 'value2' }); + expect(deque.removeFront()).toEqual({ key: 'value1' }); + }); + + it('should convert to array correctly', () => { + const deque = new Deque(); + + expect(deque.toArray()).toEqual([]); + + deque.addBack(1); + deque.addBack(2); + deque.addFront(0); + + expect(deque.toArray()).toEqual([0, 1, 2]); + }); + + it('should convert to string correctly', () => { + const deque = new Deque(); + + deque.addBack(1); + deque.addBack(2); + deque.addBack(3); + + expect(deque.toString()).toBe('1,2,3'); + }); + + it('should convert to string with a custom callback', () => { + const deque = new Deque(); + + deque.addBack({ value: 1, key: 'test1' }); + deque.addBack({ value: 2, key: 'test2' }); + + const toString = (value) => `${value.key}:${value.value}`; + + expect(deque.toString(toString)).toBe('test1:1,test2:2'); + }); + + it('should track size correctly after many operations', () => { + const deque = new Deque(); + + expect(deque.size).toBe(0); + + deque.addBack(1); + expect(deque.size).toBe(1); + + deque.addFront(0); + expect(deque.size).toBe(2); + + deque.removeFront(); + expect(deque.size).toBe(1); + + deque.removeBack(); + expect(deque.size).toBe(0); + + deque.removeBack(); + expect(deque.size).toBe(0); + }); +});