01/14Data has a shape

LEARN TO PROGRAM · CHAPTER 05 · INTERMEDIATE

Data structures

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

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

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
int a[8] = {7, 3, 9, 1, 4, 8, 2, 6};
 
/* a[i] is defined as *(a + i): start address plus i elements */
int x = a[5];           /* the compiler emits: load from (a + 5 * 4) */
int *p = &a[0];
int y = *(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
ARRAY
operationcostwhy
read / write a[i]O(1)address arithmetic
find a valueO(n)check each in turn (O(log n) if sorted: binary search)
insert / delete at endO(1)nothing moves
insert / delete elsewhereO(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:

dynamic-array.js

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:

HOW REAL DYNAMIC ARRAYS GROW
implementationnew capacity
V8 (Chrome, Node.js)old + old/2 + 16
CPython listabout 1/8 extra: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76…
Go slices2× below 256 elements, easing towards 1.25×
list_growth.py · Python
import sys
 
xs = []
for i in range(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

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

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

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:

hash.js

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:

hash-map.js
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 average
print(ages.get("alan"))      # get: O(1) on average
print("ada" in ages)         # membership: O(1) on average
 
seen = set()                 # a set is a dict with keys only
seen.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.

resize.js

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
operationaverageworst case
get / set / deleteO(1)O(n), all keys in one bucket
resizeO(1) amortisedO(n) for that one insert
iterate in key ordernot supportedsort 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.

sets.js

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.

bst.js

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.

balanced.js

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 column
CREATE INDEX users_by_email ON users (email);
 
-- Now this is a walk down a few tree levels, not a scan of every row
SELECT * FROM users WHERE email = '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.

min-heap.js

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
import heapq
 
tasks = [(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.

graph.js

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.

trie.js

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)
structurefindinsertdeletebest at
arrayO(n), O(1) by indexO(n), O(1) at endO(n)lists by position, tight loops
linked listO(n)O(1) at a nodeO(1) at a nodesplicing while you walk
stack / queuetop or front onlyO(1)O(1)undo, parsing, work lines
hash map / setO(1)O(1)O(1)lookup by key, membership
balanced treeO(log n)O(log n)O(log n)sorted data, ranges, next/previous
heapmin 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
trieO(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

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.

exercise-1.js
2

Two sum, with a Map

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
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.

exercise-3.js
4

Is it a min-heap?

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)).

exercise-4.js
5

Fewest hops (breadth-first search)

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
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.

exercise-6.js

RECALL

Check yourself, from memory

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 · FREEVisuAlgoSteven 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 VisualizationsDavid 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 StructuresPat 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 AlgorithmsMIT 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 collectionsMDN Web DocsMap, Set, WeakMap and WeakSet in JavaScript, and when to use a Map instead of a plain object.
  • TOOL · FREEPython TutorPhilip GuoStep through your own linked list or tree and see every object and arrow drawn as you go.
  • PRACTICENeetCode roadmapPractice problems grouped by structure: arrays and hashing, stacks, linked lists, trees, tries, heaps, graphs. Core list free.

ON THIS SITE

See it running

SOURCES

Where the facts come from

  1. 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.
  2. MDN: Map Any value can be a key; iteration follows insertion order.
  3. MDN: Set
  4. MDN: Int32Array 4 bytes per element, in one contiguous ArrayBuffer.
  5. CPython source: list_resize (Objects/listobject.c) Over-allocates about 1/8 extra: capacities 0, 4, 8, 16, 24, 32, 40, 52, 64, 76…
  6. CPython source: dict USABLE_FRACTION (Objects/dictobject.c) A dict's table is resized when it is two-thirds full.
  7. V8 source: JSObject::NewElementsCapacity (src/objects/js-objects.h) New capacity = old + old/2 + 16.
  8. Go source: nextslicecap (src/runtime/slice.go) Doubles below 256 elements, then moves smoothly towards 1.25×.
  9. Java SE 21: HashMap "The default load factor (.75) offers a good tradeoff between time and space costs."
  10. Java SE 21: ArrayList "Adding an element has constant amortized time cost."
  11. Java SE 21: TreeMap A red-black tree with guaranteed log(n) time for get, put and remove.
  12. PEP 456: Secure and interchangeable hash algorithm Python 3.4 switched string hashing to SipHash to resist hash-flooding attacks.
  13. Crosby & Wallach (2003), Denial of Service via Algorithmic Complexity Attacks, USENIX Security Chosen keys that all collide turn a hash table's O(1) into O(n).
  14. FNV hash (Fowler, Noll, Vo) The offset basis 0x811c9dc5 and prime 0x01000193 used in this chapter's hash.
  15. Adelson-Velsky & Landis (1962), via Wikipedia: AVL tree The first self-balancing binary search tree.
  16. Guibas & Sedgewick (1978), A dichromatic framework for balanced trees Red-black trees.
  17. Bayer & McCreight (1972), Organization and maintenance of large ordered indexes, Acta Informatica B-trees, the index structure inside most databases.
  18. Williams (1964), Algorithm 232: Heapsort, Communications of the ACM The binary heap and heapsort.
  19. Fredkin (1960), Trie memory, Communications of the ACM Named the trie, from "retrieval".
  20. Drepper (2007), What Every Programmer Should Know About Memory Cache lines (64 bytes on x86), and why sequential access through an array is so much faster than chasing pointers.
  21. Python docs: heapq
  22. PostgreSQL docs: Index types B-tree is the default index type.
  23. Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4th ed. (MIT Press, 2022) Chapters 10–13 (elementary structures, hash tables, binary search trees, red-black trees), 6 (heaps) and 20 (graphs).