Reading Time 9
Number of Words 2003
Data structures are ways to organize and store data so that operations (like insertion, deletion, search, traversal) can be performed efficiently. In JavaScript, built-in structures like arrays and objects are useful, but understanding custom structures (like linked lists, stacks, queues, trees, graphs, hash tables) lets you solve complex problems with more control.
Below is a structured walkthrough of key data structures, with pros, use cases, and JavaScript-based examples.
Table of Contents
- Why Data Structures Matter
- Built-in Structures (Arrays, Objects, Map, Set)
- Custom Data Structures
- Linked List
- Stack
- Queue
- Tree (Binary Tree, BST)
- Graph
- Hash Table / Hash Map
- When & Why to Use Each
- Example Implementations in JavaScript
- Performance Considerations
- Tips & Best Practices
1. Why Data Structures Matter
Good data structures make your code:
- Faster: operations like lookup, insertion, deletion are optimized
- Cleaner: logic becomes more intuitive
- Scalable: performance holds up with larger datasets
- Maintainable: structure makes code easier to reason about
Without proper structures, you may end up with convoluted or inefficient solutions.
2. Built-in Structures
Arrays
- Ordered list of elements
- Access by index (
arr[0]) - Useful methods:
push,pop,shift,unshift,splice,slice,map,filter,reduce
Example – deduplicating via Set + array:
const arr = [1, 2, 3, 2, 1, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]
Arrays are versatile, but some operations (like inserting or deleting at front) can be costly (O(n)).
Objects, Map, Set
- Object: key-value store (string or symbol keys)
- Map: key-value store with any type keys, predictable iteration order
- Set: collection of unique values
const map = new Map();
map.set('a', 1);
map.set(2, 'two');
console.log(map.get('a')); // 1
console.log(map.get(2)); // "two"
These are great for dictionaries, caches, or sets of unique data.
3. Custom Data Structures
Linked List
A sequence of nodes where each node stores a value and a pointer/reference to the next node.
class ListNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
append(value) {
const newNode = new ListNode(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
}
prepend(value) {
const newNode = new ListNode(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head = newNode;
}
this.length++;
}
insert(index, value) {
if (index <= 0) {
this.prepend(value);
return;
}
if (index >= this.length) {
this.append(value);
return;
}
const newNode = new ListNode(value);
let curr = this.head;
for (let i = 0; i < index - 1; i++) {
curr = curr.next;
}
newNode.next = curr.next;
curr.next = newNode;
this.length++;
}
remove(index) {
if (!this.head) return null;
if (index <= 0) {
const removed = this.head;
this.head = this.head.next;
this.length--;
if (!this.head) this.tail = null;
return removed.value;
}
let curr = this.head;
for (let i = 0; i < index - 1; i++) {
curr = curr.next;
}
const removed = curr.next;
curr.next = removed.next;
if (removed === this.tail) {
this.tail = curr;
}
this.length--;
return removed.value;
}
toArray() {
const arr = [];
let curr = this.head;
while (curr) {
arr.push(curr.value);
curr = curr.next;
}
return arr;
}
}
// Demo:
const list = new LinkedList();
list.append(10);
list.append(20);
list.prepend(5);
list.insert(1, 7);
console.log(list.toArray()); // [5, 7, 10, 20]
list.remove(2);
console.log(list.toArray()); // [5, 7, 20]
Stack & Queue
We already improved on stacks and queues earlier, so see those versions. (Stack = LIFO, Queue = FIFO)
Tree & Binary Search Tree (BST)
A tree is a hierarchical structure. A binary tree is a tree where each node has up to two children. A binary search tree maintains the property: left subtree values < node < right subtree values.
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BST {
constructor() {
this.root = null;
}
insert(value) {
const node = new TreeNode(value);
if (!this.root) {
this.root = node;
return;
}
let curr = this.root;
while (true) {
if (value < curr.value) {
if (!curr.left) {
curr.left = node;
return;
}
curr = curr.left;
} else {
if (!curr.right) {
curr.right = node;
return;
}
curr = curr.right;
}
}
}
contains(value) {
let curr = this.root;
while (curr) {
if (value === curr.value) return true;
curr = value < curr.value ? curr.left : curr.right;
}
return false;
}
// Traversals
inOrder(node = this.root, arr = []) {
if (node) {
this.inOrder(node.left, arr);
arr.push(node.value);
this.inOrder(node.right, arr);
}
return arr;
}
preOrder(node = this.root, arr = []) {
if (node) {
arr.push(node.value);
this.preOrder(node.left, arr);
this.preOrder(node.right, arr);
}
return arr;
}
postOrder(node = this.root, arr = []) {
if (node) {
this.postOrder(node.left, arr);
this.postOrder(node.right, arr);
arr.push(node.value);
}
return arr;
}
}
// Demo:
const bst = new BST();
bst.insert(10);
bst.insert(5);
bst.insert(15);
bst.insert(7);
console.log(bst.contains(7)); // true
console.log(bst.contains(3)); // false
console.log(bst.inOrder()); // [5, 7, 10, 15]
console.log(bst.preOrder()); // [10, 5, 7, 15]
console.log(bst.postOrder()); // [7, 5, 15, 10]
Graph
A graph is a set of nodes (vertices) connected by edges. Edges may be directed or undirected. You can represent graphs by adjacency lists or matrices.
class Graph {
constructor() {
this.adjList = new Map();
}
addVertex(v) {
if (!this.adjList.has(v)) {
this.adjList.set(v, []);
}
}
addEdge(v, w) {
this.addVertex(v);
this.addVertex(w);
this.adjList.get(v).push(w);
this.adjList.get(w).push(v); // undirected
}
// BFS traversal
bfs(start) {
const visited = new Set();
const queue = [start];
const result = [];
visited.add(start);
while (queue.length > 0) {
const v = queue.shift();
result.push(v);
for (const neighbor of this.adjList.get(v)) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
}
// Demo:
const graph = new Graph();
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');
console.log(graph.bfs('A')); // e.g. ["A", "B", "C", "D", "E"]
Hash Table / Hash Map
A hash table is a data structure that maps keys to values using a hash function. In JS, Map is a built-in hashed map. But you can build a simple hash map:
class HashMap {
constructor(size = 42) {
this.buckets = Array(size).fill(null).map(() => []);
}
hash(key) {
let hash = 0;
const str = String(key);
for (let char of str) {
hash = (hash * 31 + char.charCodeAt(0)) % this.buckets.length;
}
return hash;
}
set(key, value) {
const idx = this.hash(key);
const bucket = this.buckets[idx];
for (const pair of bucket) {
if (pair[0] === key) {
pair[1] = value;
return;
}
}
bucket.push([key, value]);
}
get(key) {
const idx = this.hash(key);
const bucket = this.buckets[idx];
for (const pair of bucket) {
if (pair[0] === key) {
return pair[1];
}
}
return undefined;
}
has(key) {
return this.get(key) !== undefined;
}
remove(key) {
const idx = this.hash(key);
const bucket = this.buckets[idx];
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
bucket.splice(i, 1);
return true;
}
}
return false;
}
}
// Demo:
const map2 = new HashMap();
map2.set('name', 'Alice');
map2.set('age', 30);
console.log(map2.get('name')); // "Alice"
console.log(map2.has('age')); // true
map2.remove('age');
console.log(map2.has('age')); // false
4. When & Why to Use Each
| Structure | Strengths / Use Cases | Limitations |
|---|---|---|
| Array | Simple, random access, built-in methods | Insert/delete in middle/front can be slow |
| Linked List | Fast insertion/deletion at ends or middle | No random access; more pointers |
| Stack / Queue | Localized push/pop or FIFO behavior | Limited access to middle elements |
| BST / Tree | Hierarchical data, sorted operations | Can degrade if unbalanced |
| Graph | Modeling relationships (networks, dependencies) | Complexity of algorithms, cycle detection |
| Hash Table | Fast key-based lookup, flexible keys | Collisions handling, hash function quality |
Use the simplest structure that meets your performance and functional needs.
5. Performance Considerations
- Understand time complexities (Big O): for insertion, deletion, lookup, traversal
- Watch for worst-case scenarios (e.g. unbalanced tree, hash collisions)
- Choose proper implementation: e.g. array vs linked list
- Avoid memory leaks by cleaning references
6. Tips & Best Practices
- Always test with edge cases: empty, single element, large data
- Use existing structures (
Map,Set,Array) when possible - Keep your interface simple and consistent
- Encapsulate internal implementation details (don’t expose internals)
- Add utility conversion methods (
toArray,toString) - Add unit tests to ensure correctness