Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/data-structures/deque/Deque.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
75 changes: 75 additions & 0 deletions src/data-structures/deque/README.md
Original file line number Diff line number Diff line change
@@ -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)
190 changes: 190 additions & 0 deletions src/data-structures/deque/__test__/Deque.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});