Skip to content

Advanced Data Structures

Stacks and queues model LIFO/FIFO workflows, linked lists avoid costly shifts, trees represent hierarchies, graphs model networks, and tries power fast prefix lookups. You rarely need these for every app, but knowing them helps you reach for the right structure when performance or modeling starts to matter.

While Arrays and Objects cover most day-to-day needs, understanding advanced structures unlocks the ability to solve more complex problems.

While not native classes in JS (usually implemented with Arrays), these patterns are extremely useful.

  • Stack (LIFO): Last In, First Out. Think “Undo” functionality.
  • Queue (FIFO): First In, First Out. Think background job processing.
// Stack: Undo history
const history: string[] = [];
history.push('Type A');
history.push('Type B');
const lastAction = history.pop(); // 'Type B'
// Queue: Task processing
const taskQueue: string[] = [];
taskQueue.push('Email User');
taskQueue.push('Generate Report');
const nextTask = taskQueue.shift(); // 'Email User'
class Stack<T> {
private items: T[] = [];
/** Add item on top of the stack */
push(item: T): void {
this.items.push(item);
}
/** Remove and return the top item from the stack */
pop(): T | undefined {
if (this.items.length === 0) return;
return this.items.pop();
}
/** View the top item without removing it */
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
class Queue<T> {
private items: T[] = [];
/** Add item to the end of the queue */
enqueue(item: T): void {
this.items.push(item);
}
/**
* Remove and return the item at the front of the queue
* Note: shift() is O(n). For true O(1) operations, use a Linked List.
*/
dequeue(): T | undefined {
if (this.items.length === 0) return;
return this.items.shift();
}
/** View the front item without removing it */
peek(): T | undefined {
return this.items[0];
}
}

A linear collection of data elements where each element points to the next.

Each node contains data and a reference (pointer) to the next node.

Use case: An implementation of a Stack or Queue where you need constant-time O(1) insertion and deletion at the beginning, without shifting all elements like an Array would.

class ListNode<T> {
value: T;
next: ListNode<T> | undefined = undefined;
constructor(value: T) {
this.value = value;
}
}
class LinkedList<T> {
head: ListNode<T> | undefined = undefined;
/** O(1) insertion at the start */
prepend(value: T): void {
const newNode = new ListNode(value);
newNode.next = this.head;
this.head = newNode;
}
}
const list = new LinkedList<string>();
list.prepend('Third');
list.prepend('Second');
list.prepend('First');
// Structure: First -> Second -> Third -> undefined

Each node has pointers to both the next and previous nodes.

Use case: Browser history (Back/Forward buttons), or a music playlist.

class DoublyNode<T> {
value: T;
next: DoublyNode<T> | undefined = undefined;
prev: DoublyNode<T> | undefined = undefined;
constructor(value: T) {
this.value = value;
}
}
class DoublyLinkedList<T> {
head: DoublyNode<T> | undefined = undefined;
tail: DoublyNode<T> | undefined = undefined;
/** O(1) insertion at the end */
append(value: T): void {
const newNode = new DoublyNode(value);
if (!this.tail) {
this.head = this.tail = newNode;
} else {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
}
/** O(1) insertion at the start */
prepend(value: T): void {
const newNode = new DoublyNode(value);
if (!this.head) {
this.head = this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
}
/** O(1) deletion from the end */
removeLast(): T | undefined {
if (!this.tail) return undefined;
const value = this.tail.value;
if (this.head === this.tail) {
this.head = this.tail = undefined;
} else {
this.tail = this.tail.prev;
this.tail!.next = undefined;
}
return value;
}
/** O(1) deletion from the start */
removeFirst(): T | undefined {
if (!this.head) return undefined;
const value = this.head.value;
if (this.head === this.tail) {
this.head = this.tail = undefined;
} else {
this.head = this.head.next;
this.head!.prev = undefined;
}
return value;
}
}
// Usage example: Music player playlist
const playlist = new DoublyLinkedList<string>();
playlist.append('Bohemian Rhapsody');
playlist.append('Stairway to Heaven');
playlist.append('Hotel California');
// Navigate through the playlist
let currentTrack = playlist.head;
console.log(currentTrack?.value); // 'Bohemian Rhapsody'
currentTrack = currentTrack?.next; // Move forward
console.log(currentTrack?.value); // 'Stairway to Heaven'
currentTrack = currentTrack?.prev; // Move backward
console.log(currentTrack?.value); // 'Bohemian Rhapsody'

A hierarchical structure consisting of nodes, with a single root node and children nodes.

Use case: The DOM (Document Object Model), file systems, and client-side route trees are all examples of tree structures.

class TreeNode<T> {
value: T;
children: TreeNode<T>[] = [];
constructor(value: T) {
this.value = value;
}
addChild(node: TreeNode<T>): void {
this.children.push(node);
}
}
// Modeling a simple HTML structure
const html = new TreeNode('html');
const body = new TreeNode('body');
const div = new TreeNode('div');
const p = new TreeNode('p');
html.addChild(body);
body.addChild(div);
div.addChild(p);

A specific type of tree where each node has at most two children (left and right). For any node, the left child is smaller and the right child is larger (or equal, depending on how duplicates are handled).

Use case: Efficient searching and sorting — O(log n) for balanced trees. Note: An unbalanced BST (e.g., inserting sorted values) degrades to O(n).

class BinaryNode<T extends number | string> {
value: T;
left: BinaryNode<T> | undefined = undefined;
right: BinaryNode<T> | undefined = undefined;
constructor(value: T) {
this.value = value;
}
}
class BinarySearchTree<T extends number | string> {
root: BinaryNode<T> | undefined = undefined;
insert(value: T): void {
const newNode = new BinaryNode(value);
if (!this.root) {
this.root = newNode;
return;
}
this.insertNode(this.root, newNode);
}
private insertNode(node: BinaryNode<T>, newNode: BinaryNode<T>): void {
if (newNode.value < node.value) {
// Go Left
if (!node.left) node.left = newNode;
else this.insertNode(node.left, newNode);
} else {
// Go Right
if (!node.right) node.right = newNode;
else this.insertNode(node.right, newNode);
}
}
search(value: T): boolean {
return this.searchNode(this.root, value);
}
private searchNode(node: BinaryNode<T> | undefined, value: T): boolean {
if (!node) return false;
if (value === node.value) return true;
return value < node.value
? this.searchNode(node.left, value)
: this.searchNode(node.right, value);
}
}
const bst = new BinarySearchTree<number>();
bst.insert(10);
bst.insert(5);
bst.insert(15);
// 10
// / \
// 5 15
bst.search(5); // true
bst.search(20); // false

A collection of nodes (vertices) and edges that connect pairs of nodes. Graphs can be directed (Twitter followers) or undirected (Facebook friends).

Use case: Social networks, recommendation engines (“People who bought X also bought Y”), and routing algorithms (Google Maps).

class Graph<T> {
// Adjacency List: Map<Node, Array<Neighbors>>
adjacencyList: Map<T, T[]> = new Map();
addVertex(vertex: T): void {
if (this.adjacencyList.has(vertex)) return;
this.adjacencyList.set(vertex, []);
}
addEdge(vertex1: T, vertex2: T): void {
// Ensure vertices exist before adding edge
this.addVertex(vertex1);
this.addVertex(vertex2);
this.adjacencyList.get(vertex1)!.push(vertex2);
this.adjacencyList.get(vertex2)!.push(vertex1); // Undirected
}
}
const socialNetwork = new Graph<string>();
socialNetwork.addVertex('Alice');
socialNetwork.addVertex('Bob');
socialNetwork.addVertex('Charlie');
socialNetwork.addEdge('Alice', 'Bob');
socialNetwork.addEdge('Alice', 'Charlie');
// Alice is friends with Bob and Charlie

A specialized tree used to store associative data structures. A node’s position in the tree defines the key with which it is associated.

Use case: Autocomplete / Typeahead systems. If you type “Re”, a Trie can efficiently find “React”, “Redux”, “Remix” without scanning a whole database.

class TrieNode {
children: Map<string, TrieNode> = new Map();
isEndOfWord: boolean = false;
}
class Trie {
root: TrieNode = new TrieNode();
insert(word: string): void {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new TrieNode());
}
current = current.children.get(char)!;
}
current.isEndOfWord = true;
}
search(word: string): boolean {
let current = this.root;
for (const char of word) {
const child = current.children.get(char);
if (!child) return false;
current = child;
}
return current.isEndOfWord;
}
}
const autocomplete = new Trie();
autocomplete.insert('react');
autocomplete.insert('redux');
// Efficiently stores 'r', 'e', then branches for 'a' and 'd'

Advanced data structures help you model problems more naturally and push performance when arrays and objects start to strain. You do not need them for every project, but keeping them in your toolbox makes the next hard problem feel a lot smaller.

Previous: Revisit Data Structures for the JS basics.