A data structure is a way of arranging data in memory so that the things you need to do with it are cheap. The same thousand names can sit in a row, in a chain, in buckets, or in a tree, and each arrangement makes some questions instant and others slow.
This chapter builds every structure you will meet in real code, from the array up to tries and graphs. Each one runs its main operation in 3D as you scroll, with the cost on show, and each one is a few lines of JavaScript you can run, step through and break.
Every code block runs in your browser. Edit it, break it, press Run.
01 · DATA HAS A SHAPE
Data has a shape
Every program keeps data somewhere: the songs in a playlist, the friends of a user, the words of a dictionary. Memory itself is just one long row of numbered bytes (the Memory page shows it in hardware). A data structure is the plan you lay on top of that row: what goes where, and what points to what.
The plan matters more than the speed of your computer. Here the same 100,000 numbers are kept two ways, as a list and as a set, and asked the same 2,000 questions:
shapes.js
12345678910111213141516171819
// The same 100,000 numbers, kept two ways
constn=100_000;
constlist=Array.from({length:n},(_,i)=>i*2);
constset=newSet(list);
// Ask both the same 2,000 questions: "is x in there?"
The array checks its items one by one; the set jumps straight to the answer. Try n = 1_000_000: the array gets several times slower, the set barely notices.
How to read the costs
To compare structures, programmers count how the work grows as the data grows, written in Big-O notation (chapter 6 is all about it). You only need four for now:
O(1), constant: the same few steps whether there are ten items or ten million.
O(log n), logarithmic: each step halves what is left. A million items take about 20 steps.
O(n), linear: the work grows in step with the data. Twice the items, twice the time.
O(n log n) and O(n²): what good sorting costs, and what comparing every pair costs.
The 3D stage has a plinth for each structure. Scroll on and it walks you along them; each caption carries the cost that defines it.
02 · ARRAYS
Arrays: one block of memory
An array is the simplest structure and the most important: items of the same size, side by side in one contiguous block. Because every slot is the same size, the computer can work out where item i lives with arithmetic, without looking at any other item:
address of a[i] = start + i × size
That is why reading a[5] is O(1): in the 3D the beam jumps from the address calculation straight to slot 5. In JavaScript, a typed array is a real block of memory like this:
array.js
123456789101112
// A typed array is a real block of memory: 8 slots of 4 bytes
consta=newInt32Array([7,3,9,1,4,8,2,6]);
console.log("bytes per element:",a.BYTES_PER_ELEMENT);
console.log("total bytes:",a.byteLength);
// Pretend the block starts at address 0x1000.
// Finding a[i] is one multiply and one add, whatever i is:
constbase=0x1000;
for(constiof[0,1,5,7]){
constaddr=base+i*a.BYTES_PER_ELEMENT;
console.log("a["+i+"] lives at 0x"+addr.toString(16),"→",a[i]);
}
Each Int32Array slot is exactly 4 bytes, so a[5] is 20 bytes after the start. Step through it and watch addr.
C shows the arithmetic in the language itself: a[i] is defined as *(a + i), "the thing at address a plus i elements".
array.c · C
inta[8]={7,3,9,1,4,8,2,6};/* a[i] is defined as *(a + i): start address plus i elements */intx=a[5];/* the compiler emits: load from (a + 5 * 4) */int*p=&a[0];inty=*(p+5);/* the same 8 */
The catch is the other half of the scene. To insert at the front, every item has to shuffle one slot to the right to make room: 8 moves for 8 items, a million for a million. Inserting or deleting anywhere but the end is O(n).
insert-front.js
123456789101112131415161718
// Inserting at the front means moving everything else along
functioninsertAt(arr,index,value){
letmoves=0;
arr.length++;// make room at the end
for(leti=arr.length-1;i>index;i--){
arr[i]=arr[i-1];// shift one slot right
moves++;
}
arr[index]=value;
returnmoves;
}
consta=[7,3,9,1,4,8,2,6];
console.log("moves:",insertAt(a,0,5));
console.log(a.join(" "));
constbig=Array.from({length:1_000_000},(_,i)=>i);
console.log("moves for a million:",insertAt(big,0,-1));
ARRAY
operation
cost
why
read / write a[i]
O(1)
address arithmetic
find a value
O(n)
check each in turn (O(log n) if sorted: binary search)
insert / delete at end
O(1)
nothing moves
insert / delete elsewhere
O(n)
everything after it shifts
03 · DYNAMIC ARRAYS
Dynamic arrays: growing without paying every time
A fixed block can't grow: the memory right after it may belong to something else. So the arrays you use every day (JavaScript's Array, Python's list, Java's ArrayList, C++'s vector) are dynamic arrays: a fixed block with spare room. When it fills up, they allocate a bigger block, copy everything across, and free the old one.
The trick is to grow by a factor, not by a fixed amount. In the 3D, capacity doubles 1 → 2 → 4 → 8 → 16. Most pushes cost one step; the rare ones that trigger a copy cost more (the pink bars), but each copy buys twice as much room as the last:
// A growable array on top of fixed-size blocks, like a JS array or
// Python list underneath: when it is full, allocate double and copy.
classDynArray{
constructor(){
this.capacity=1;
this.length=0;
this.block=newArray(1);
this.copies=0;
}
push(value){
if(this.length===this.capacity){
constbigger=newArray(this.capacity*2);
for(leti=0;i<this.length;i++){
bigger[i]=this.block[i];// the expensive part
this.copies++;
}
this.block=bigger;
this.capacity*=2;
}
this.block[this.length++]=value;
}
}
constd=newDynArray();
for(leti=1;i<=9;i++){
d.push(i);
console.log("push",i," length",d.length," capacity",d.capacity," copies so far",d.copies);
}
constbig=newDynArray();
for(leti=0;i<1_000_000;i++)big.push(i);
console.log("a million pushes cost",big.copies,"copies:",(big.copies/1e6).toFixed(2),"per push");
A million pushes need about one copy each on average, never more than two. Change * 2 to + 10 (grow by a fixed amount) and run it again: the copies explode, because the total becomes O(n²).
Averaged over many operations, push is O(1) amortised: the expensive moments are paid for by the cheap ones around them. The Java documentation states it as a promise: adding an element "has constant amortized time cost". Real growth factors vary, and all of them are factors:
importsysxs=[]foriinrange(10):xs.append(i)print(len(xs),sys.getsizeof(xs))# bytes jump only when it grows
04 · LINKED LISTS
Linked lists: a chain of references
A linked list gives up the contiguous block. Each item lives in its own small node, wherever the memory allocator found room, and holds a reference (a pointer) to the next node. The list itself is just a reference to the first node, the head; the last node points to null.
The 3D shows both sides of that bargain. To reach index 3 there is no arithmetic to do: you must hop from node to node, so get(i) is O(n), and each hop may land on a different part of memory (the lit squares on the back wall), which the cache hates. But once you hold a node, splicing a new one in after it changes two references and moves nothing: O(1), however long the list.
linked-list.js
12345678910111213141516171819202122232425262728
// Each node holds a value and a reference to the next node
Press Step through and watch cur walk the chain inside get.
Linked lists shine when you insert and remove in the middle of a sequence you are already walking, or need stable references to items that never move: the free lists inside memory allocators, the run queues of operating-system schedulers, the order list inside an LRU cache. A doubly linked list adds a prev reference to each node, so you can also walk backwards and unlink a node in O(1) given only the node itself.
05 · STACKS & QUEUES
Stacks and queues: rules about order
Stacks and queues aren't new memory layouts. They are rules about which item comes out next, and that restriction is the whole point: code that uses them can't get the order wrong.
A stack is last in, first out (LIFO), like a pile of plates: push on top, pop from the top. A JavaScript array with push and pop is already one. Every function call you make lands on a stack: the call stack from chapter 4.
A queue is first in, first out (FIFO), like a line at a till: enqueue at the back, dequeue from the front. Printers, web servers and message brokers all keep queues of work.
Stacks are perfect for anything nested. Checking that brackets match is the textbook example: push every opener, and every closer must match the opener on top.
brackets.js
12345678910111213141516
// A stack in JavaScript is just an array: push and pop at the end
for(consttof["(a[1] + b) * {c}","([)]","((x)","f(g(h()))"]){
console.log(balanced(t)?"ok ":"bad ",t);
}
Step through "([)]" to see the exact moment it fails.
A queue on a plain array is a trap: shift() takes from the front, so in principle every other item moves along one slot, making each dequeue O(n). The 3D shows the fix that real queues use: a ring buffer, a fixed array where the head and the tail chase each other round and wrap past the end. Nothing ever moves; both operations are O(1).
ring-buffer.js
1234567891011121314151617181920212223242526272829
// A queue as a ring buffer: a fixed array, a head and a tail that wrap
After the wrap, slots holds e, f, c, d: the queue starts at head = 2 and runs round the corner.
A deque (double-ended queue) allows both ends; Python's collections.deque is the one to reach for there.
06 · HASH MAPS
Hash maps: from a key straight to a bucket
Arrays find things by position. Most of the time you want to find things by name: the price of "apple", the user with this email. A hash map (also called a hash table or dictionary) does this in O(1) on average, and it is the structure you will use most.
The idea: keep an array of buckets, and use a hash function to turn any key into a number. Take the number modulo the bucket count, and that is the bucket. To look the key up later, hash it again and go straight there. In the 3D, six keys go through FNV-1a, a real hash function, and land in eight buckets:
Change a single letter of a key and its hash changes completely. A good hash function spreads similar keys far apart.
A hash function must be deterministic (the same key always gives the same number), fast, and spread keys evenly. It doesn't need to be secret: the cryptographic hashes on the Encryption and password pages add a further promise, that you can't work backwards from the output, which hash tables don't need.
Look at cat and bee: different keys, same bucket. That is a collision, and with more keys than buckets it is certain. The simplest fix, chaining, keeps a small list in each bucket and checks the keys in it one by one. Here is a whole hash map in twenty lines, and JavaScript's own:
sublinearis what the JavaScript standard demands: Maps "must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements". Sets carry the same rule. ECMAScript spec, Map objects
dict.py · Python
ages={"ada":36,"alan":41}ages["grace"]=85# set: O(1) on averageprint(ages.get("alan"))# get: O(1) on averageprint("ada"inages)# membership: O(1) on averageseen=set()# a set is a dict with keys onlyseen.add("owl")
07 · COLLISIONS & LOAD
Collisions and the load factor
A hash map is only fast while its chains stay short. The measure is the load factor, α = n / m: items per bucket. In the 3D the table starts with four buckets; hen and yak collide in bucket 0 and the gauge climbs to 0.75. When the next insert would push it over, the table grows: it allocates twice the buckets and rehashes every key into its new home, because hash % 8 differs from hash % 4. Afterwards hen and yak are apart, and no chain is longer than one.
Try a lower threshold, like 0.5: more memory, fewer collisions. A higher one, like 3: less memory, longer chains.
A resize costs O(n), but like the dynamic array it doubles each time, so it is O(1) amortised. Java's HashMap resizes at a load factor of 0.75, which its documentation calls "a good tradeoff between time and space costs". CPython's dict uses open addressing instead of chains (a colliding key tries other slots in the same array) and resizes when two-thirds full.
HASH MAP
operation
average
worst case
get / set / delete
O(1)
O(n), all keys in one bucket
resize
O(1) amortised
O(n) for that one insert
iterate in key order
not supported
sort the keys first: O(n log n)
08 · SETS
Sets: keys without values
A set holds each value at most once and answers one question fast: "is x in here?" Underneath, it is usually a hash map that keeps only the keys, so add, has and delete are O(1) on average. Adding a value that is already there changes nothing: in the 3D, the second dog dissolves into the first.
Sets also do the operations from maths: union (in either), intersection (in both) and difference (in one but not the other). Each costs time proportional to the sets involved, because every element is checked with one O(1) has.
Newer engines also have these built in: a.union(b), a.intersection(b), a.difference(b), added in ECMAScript 2025. Try them here; an older browser will say they are not a function.
Reach for a set whenever you catch yourself writing array.includes(x) inside a loop: that is O(n) per check, and the first example in this chapter showed what it costs. Removing duplicates, tracking "already visited" in a search, and comparing two lists of IDs are all set jobs.
09 · SEARCH TREES
Binary search trees: order and speed
Hash maps find a key fast but know nothing about order: they can't give you "every price between 10 and 20" or "the next name after Smith". A binary search tree (BST) can. Each node has up to two children, with one rule: everything in the left subtree is smaller, everything in the right is bigger.
To find a key, start at the root and compare: smaller, go left; bigger, go right. Each comparison throws away a whole subtree. In the 3D, the search for 65 takes 4 steps among 9 keys. Walking the tree in order (left subtree, node, right subtree) visits the keys sorted, for free.
Step through search and watch node fall down the tree. Then insert 45 and work out, before you run it, where it lands.
Search, insert and delete all cost O(h), where h is the height of the tree. Trees are everywhere in computing: the file system on your disk, the parse tree from chapter 1, the HTML of this page (the DOM), and the objects inside git, where every commit points to a tree of folders and files.
10 · BALANCED TREES
Balanced trees: keeping the height down
O(h) is only good news if h is small, and a plain BST makes no promise. Insert keys that are already sorted and each one goes to the right of the last: the 3D shows 1 to 7 forming a chain of height 7, which is just a linked list with extra steps. Finding 7 takes 7 comparisons.
The same 1,000 keys: height 1,000 inserted in order, height 10 built middle-first. Sorted input is common in real life (timestamps, IDs), so this isn't a rare accident.
Self-balancing trees fix this by restructuring as they go. After each insert or delete they check the shape and, where one side has grown too tall, perform rotations: local rearrangements that lift a child above its parent while keeping the left-smaller, right-bigger order. The height stays near log₂ n, so every operation is O(log n) guaranteed. In the 3D, the chain rearranges into a tree of height 3 and finding 7 takes 3 steps.
AVL trees (Adelson-Velsky and Landis, 1962), the first: the two subtrees of any node differ in height by at most one.
Red-black trees (Guibas and Sedgewick, 1978): a looser rule that needs fewer rotations. Java's TreeMap is one.
B-trees (Bayer and McCreight, 1972): nodes with hundreds of keys each, so the tree is only three or four levels deep even for huge tables. They suit disks, where reading one big block costs about the same as reading one byte. Almost every database index is a B-tree, chapter 11's subject.
index.sql · SQL
-- A database index is a balanced tree (a B-tree) over a columnCREATEINDEXusers_by_emailONusers(email);-- Now this is a walk down a few tree levels, not a scan of every rowSELECT*FROMusersWHEREemail='ada@example.com';
11 · HEAPS
Heaps: always know the smallest
Sometimes you don't need everything in order, only the most urgent item: the next timer to fire, the cheapest route so far, the task with the highest priority. A heap keeps exactly that at hand. A min-heap is a tree with a weaker rule than a BST: every parent is smaller than or equal to its children. So the smallest item is always at the root: peek is O(1).
The clever part is where it lives. A heap is a complete tree (every level full except the last, which fills from the left), so it fits in a plain array with no pointers at all: the children of index i are at 2i + 1 and 2i + 2. The 3D shows the tree and its array together. push(2) adds 2 at the end and swaps it up past bigger parents; pop() takes the root, moves the last item to the top and swaps it down past smaller children. Both are O(log n): at most one swap per level.
// Node i has children 2i+1 and 2i+2, and parent floor((i-1)/2).
classMinHeap{
constructor(items=[]){this.a=items;}
push(x){
consta=this.a;
a.push(x);
leti=a.length-1;
while(i>0){
constp=Math.floor((i-1)/2);
if(a[p]<=a[i])break;
[a[p],a[i]]=[a[i],a[p]];// bubble up
i=p;
}
}
pop(){
consta=this.a;
consttop=a[0];
constlast=a.pop();
if(a.length>0){
a[0]=last;
leti=0;
while(true){
constl=2*i+1,r=l+1;
letm=i;
if(l<a.length&&a[l]<a[m])m=l;
if(r<a.length&&a[r]<a[m])m=r;
if(m===i)break;
[a[m],a[i]]=[a[i],a[m]];// sift down
i=m;
}
}
returntop;
}
}
consth=newMinHeap([3,8,5,12,10,9,7]);
h.push(2);
console.log("after push(2):",h.a.join(" "));
console.log("pop →",h.pop());
console.log("after pop: ",h.a.join(" "));
constsorted=[];
while(h.a.length)sorted.push(h.pop());
console.log("popping them all:",sorted.join(" "));
Popping everything returns the items sorted. That is heapsort (J. W. J. Williams, 1964): O(n log n), in place.
A heap is the usual way to build a priority queue. Python ships one in heapq; route-finding algorithms like Dijkstra's use one to always expand the closest place next.
priority.py · Python
importheapqtasks=[(3,"email"),(1,"fix the outage"),(2,"review")]heapq.heapify(tasks)# O(n)heapq.heappush(tasks,(0,"fire alarm"))print(heapq.heappop(tasks))# (0, 'fire alarm')print(heapq.heappop(tasks))# (1, 'fix the outage')
12 · GRAPHS
Graphs: anything connected to anything
Lists, trees and heaps all have a fixed shape. A graph has none: it is a set of nodes (vertices) joined by edges, and any node may connect to any other. Road maps, social networks, the links between web pages, the routers of the Internet, the dependencies between packages, and git's history of commits are all graphs. Edges can have a direction (a follows b) and a weight (a road's length).
There are two standard ways to store one, and the 3D shows both beside the same graph:
An adjacency matrix: an n × n grid with a 1 where an edge exists. Checking one edge is O(1), but it always takes n² cells: a million users would need a trillion.
An adjacency list: each node keeps a list of its neighbours. Space is proportional to n + e, and walking a node's neighbours costs only its degree. Most real graphs are sparse (few edges per node), so this is the usual choice.
The most useful thing to do with a graph is to search it. Breadth-first search (BFS) explores in rings, all nodes 1 hop away, then 2, then 3, using a queue, and so finds the fewest hops to everything. In the 3D, each node lights its row of the matrix and list as BFS reads its neighbours. Swap the queue for a stack and you get depth-first search, which dives down one path before backing up.
BFS touches every node and every edge once: O(V + E). Add an edge ["A", "F"] and watch the distances change.
13 · TRIES
Tries: trees of letters
A trie (from retrieval; Edward Fredkin named it in 1960) stores strings by sharing their beginnings. Each edge is one letter, each path from the root spells a prefix, and a mark on a node says "a word ends here". In the 3D, car, cart and cat share c and a.
Looking up a word costs O(length of the word), however many words are stored. More usefully, every word starting with a prefix sits in one subtree: walk down to do and everything below is do, dog, dot. That is autocomplete, spell checkers, and the longest-prefix match routers use to pick where to send a packet.
has("ca") is false: the path exists, but no word ends there. Add "card" and complete "car".
The price is memory: a node per letter, each with its own table of children. Compressed variants (radix trees, which merge chains of single children into one edge) cut that down.
14 · CHOOSING ONE
Choosing the right structure
Every structure is a trade: it makes some operations cheap by making others expensive or by spending memory. So choose by asking what your program does most, then pick the structure that makes that cheap:
CHEAT SHEET (AVERAGE COSTS)
structure
find
insert
delete
best at
array
O(n), O(1) by index
O(n), O(1) at end
O(n)
lists by position, tight loops
linked list
O(n)
O(1) at a node
O(1) at a node
splicing while you walk
stack / queue
top or front only
O(1)
O(1)
undo, parsing, work lines
hash map / set
O(1)
O(1)
O(1)
lookup by key, membership
balanced tree
O(log n)
O(log n)
O(log n)
sorted data, ranges, next/previous
heap
min in O(1)
O(log n)
min in O(log n)
priority, top-k, scheduling
graph (adj. list)
neighbours O(deg)
O(1)
O(deg)
networks, routes, dependencies
trie
O(length)
O(length)
O(length)
prefixes, autocomplete
Look things up by a key or ID? Hash map. Only need yes/no? Set.
Need them in order, or ranges like "between 10 and 20"? A balanced tree, or a sorted array if the data rarely changes (binary search is O(log n)).
Always want the smallest, largest or most urgent? Heap.
Process in arrival order, or the most recent first? Queue, or stack.
Things connected to things? Graph, as an adjacency list unless it is small and dense.
Searching by prefix? Trie.
Everything else? Start with an array. It is the most cache-friendly, the simplest, and fast enough far more often than its Big-O suggests. Measure before you get clever.
Real programs combine them. A word counter uses a map for the counts and an array to rank them; an LRU cache is a hash map plus a doubly linked list; a database is B-trees over pages of arrays.
word-count.js
12345678
// Word count: a Map does the counting, an array does the ranking
consttext="the cat sat on the mat and the dog sat on the cat";
The sort is O(k log k) for k distinct words. For the top 3 out of millions, a heap of size 3 would do it in O(k).
PRACTICE
Now you do it
Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.
1
Reverse a linked list
Turn 1 → 2 → 3 → 4 into 4 → 3 → 2 → 1 by turning the arrows round. No arrays: only change next references. Return the new head. It is the classic linked-list interview question for a reason: it makes you hold three references at once.
Return the indexes of the two numbers in nums that add up to target. The obvious way checks every pair: O(n²). Use a Map from number to index to do it in one pass, O(n).
exercise-2.js
1234567
functiontwoSum(nums,target){
constseen=newMap();// number → its index
// your code here
}
console.log(twoSum([2,7,11,15],9));// [0, 1]
console.log(twoSum([3,2,4],6));// [1, 2]
3
A queue made of two stacks
Build a first-in, first-out queue using only two arrays and only push and pop (no shift). dequeue on an empty queue returns undefined.
Write isMinHeap(a): true when every element of the array is greater than or equal to its parent, using the heap layout (the parent of index i is Math.floor((i - 1) / 2)).
Write hops(adj, from, to): the fewest edges between two nodes of an undirected graph given as an adjacency list, or -1 if there is no path. Use a queue.
exercise-5.js
12345678910
constadj={
A:["B","C"],B:["A","D"],C:["A","D","E"],
D:["B","C","E"],E:["C","D","F"],F:["E"],
};
functionhops(adj,from,to){
// your code here
}
console.log(hops(adj,"A","F"));// 3
6
How many words start with…?
Add a count(prefix) method to this trie that returns how many stored words start with prefix. count("") is every word.
Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.
Why is reading a[i] O(1) in an array but O(n) in a linked list?
A dynamic array copies everything when it grows. Why is push still O(1)?
What is a hash collision, and what does the load factor measure?
Insert 1, 2, 3, …, 1000 into a plain binary search tree. What goes wrong, and what fixes it?
In a min-heap stored in an array, where are the smallest item and the children of index i?
Adjacency matrix or adjacency list: when would you choose each?
GO FURTHER
Resources
TOOL · FREEVisuAlgo ↗Steven Halim, National University of SingaporeAnimated linked lists, hash tables, heaps, BSTs, AVL trees and graphs. Type your own operations and watch each step.
TOOL · FREEData Structure Visualizations ↗David Galles, University of San FranciscoA classic: every structure in this chapter (and red-black trees, B-trees, tries) as an interactive animation.
BOOK · FREEOpen Data Structures ↗Pat MorinA free, open textbook that builds each structure in code and proves its costs. Editions in Java, C++ and pseudocode.
COURSE · FREEMIT 6.006 Introduction to Algorithms ↗MIT OpenCourseWareLectures 2–8 cover arrays, linked lists, hashing, binary trees, AVL trees and heaps, with problem sets.
BOOK · FREEAlgorithms, 4th edition (booksite) ↗Robert Sedgewick and Kevin WayneThe free companion site to the textbook: stacks, queues, symbol tables, balanced search trees, hash tables and graphs, with code.
DOCS · FREEMDN: Keyed collections ↗MDN Web DocsMap, Set, WeakMap and WeakSet in JavaScript, and when to use a Map instead of a plain object.
TOOL · FREEPython Tutor ↗Philip GuoStep through your own linked list or tree and see every object and arrow drawn as you go.
PRACTICENeetCode roadmap ↗Practice problems grouped by structure: arrays and hashing, stacks, linked lists, trees, tries, heaps, graphs. Core list free.
ECMAScript® 2026 Language Specification: Map objects "Maps must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements." The same rule applies to Set.
MDN: Map Any value can be a key; iteration follows insertion order.