Ways to arrange data so the work you do with it is fast.
From what an array really is to tries, heaps, balanced trees and persistent structures, in C++: the language where you can see how data sits in memory. Each structure is explained in everyday terms, drawn, built by hand and measured with Big O, so you can choose the right one for a job and explain why. C++ is taught as you go, from your first program, and every module has code you write, compile and run in the page until its tests pass.
What you’ll be able to do
Explain how arrays, linked lists, stacks, queues, hash tables, trees, heaps, tries and graphs store data, and what each operation costs.
Describe the cost of an operation in time and memory with Big O, including amortised cost.
Write C++: types, functions, structs and classes, references, pointers and memory, templates, and the standard library.
Build each structure from scratch in C++, and test it.
Use the structures the standard library gives you (std::vector, std::string, std::unordered_map, std::set, std::priority_queue…) knowing what they do inside.
Choose a structure for a problem by the operations it needs, and combine structures, as an LRU cache does.
Modules
What a data structure is· 12 slides
Arrays· 11 slides
Measuring cost· 10 slides
Strings· 10 slides
Linked lists· 14 slides
Stacks· 10 slides
Queues· 9 slides
Hash tables· 8 slides
Maps and sets· 9 slides
Trees· 10 slides
Binary search trees· 11 slides
Heaps and priority queues· 10 slides
Graphs· 9 slides
Tries: trees of prefixes· 9 slides
Balanced trees· 10 slides
Disjoint sets: union-find· 9 slides
Caches: combining structures· 9 slides
Range queries· 10 slides
Persistent structures· 11 slides
Mastery challenge· 10 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: What a data structure is
Data structures1 / 201
Module 1 · What a data structure is
Data is values kept in memory
Data is any information a program works with: a book’s title, a price, a list of songs. While the program runs, data lives in the computer’s memory. Picture memory as one very long row of numbered boxes. Each box holds a small piece of a value, and its number is its address, so the computer can go straight to box 5 000 without looking at the others.
Memory as a row of numbered boxes. A value takes up one or more of them.
Data
The information a program works with: numbers, text, lists of them.
Memory
Where a running program keeps its data: a long row of numbered boxes.
Byte
One box of memory: 8 bits, enough for a number from 0 to 255.
Address
A box’s number, which lets the computer reach it directly.
QuizInteractive · disabled in the PDF
The computer needs the value in box 7 000. What does it do?
The computer needs the value in box 7 000. What does it do?
Data structures2 / 201
Module 1 · What a data structure is
A data structure is a layout plus the operations it makes cheap
A data structure is a way of arranging data in memory, together with the operations you can do on it: read a value, find one, add one, remove one. The arrangement decides how much work each operation takes. No structure is best at everything; each one makes some operations cheap by making others dearer.
Think of a hundred books. You could keep them on a shelf in a row, in a pile on the floor, or as index cards in a box with one drawer per letter. Same books, three layouts, and very different work depending on what you want to do.
Layout
Add a book
Find a book by title
Take the newest book
Shelf, in arrival order
Put it at the end: quick
Look along the whole shelf
The last one: quick
Pile on the floor
Put it on top: quick
Lift books one by one
The top one: quick
Card box, A to Z
Open its drawer: quick
Open one drawer: quick
No idea which it was
The question to ask. Before choosing a structure, ask: which operations will I do most often? Then pick the layout that makes those cheap. The rest of this course is that question, asked of one structure after another.
Put in orderInteractive · disabled in the PDF
Put the steps of choosing a data structure in order.
1.Look up what each operation costs in each candidate layout
2.Pick the layout that makes the frequent operations cheap
3.Check it with realistic amounts of data
4.Work out which of them happen most often
5.List the operations the job needs
Data structures3 / 201
Module 1 · What a data structure is
Four operations to ask about
Almost everything you do with a collection of data is one of four operations. When you meet a new structure, ask how much work each one takes.
Operation
Means
Everyday example
Read
Get the value at a known place
The third song in a playlist
Search
Find where a value is, or whether it is there
Is Dune on the shelf?
Insert
Add a value
Add a task to a to-do list
Delete
Remove a value
Tick a task off and drop it
QuizInteractive · disabled in the PDF
A browser checks whether a web address is already in your bookmarks. Which operation is that?
A browser checks whether a web address is already in your bookmarks. Which operation is that?
Data structures4 / 201
Module 1 · What a data structure is
Your first C++ program
This course writes its structures in C++, a language that lets you see how data sits in memory. You write C++ as text, a compiler turns it into instructions the machine runs, and then it runs. Here the compiler (Clang) works inside this page: press Run.
#include <iostream> brings in the part of the standard library (the code that comes with C++) that prints.
int main() is where every program starts. The { } braces hold its steps, each ending in ;.
std::cout << x prints x. "\n" starts a new line. std:: says the name comes from the standard library.
return 0; tells whoever started the program that it finished well.
Compiler
A program that turns code you write into instructions the machine runs.
`main`
The function every C++ program starts in.
`std::cout`
The standard output: << sends values to it to be printed.
Standard library
Ready-made code that comes with C++, under the name std.
Data structures5 / 201
Module 1 · What a data structure is
Every value has a type
A variable is a named box for a value. In C++ you say its type when you make it, and the type never changes: it tells the compiler how many bytes the value takes and what you can do with it.
C++
1#include<string>23int pages =412; // a whole number4double price =9.99; // a number with a fraction5bool inStock =true; // true or false6char grade ='A'; // one character, in ' '7std::string title ="Dune"; // text, in " "89pages = pages +1; // now 41310// pages = "many"; // error: a string is not an int
Text after // is a comment: the compiler skips it.
Errors before the program runs. Because every type is known, the compiler stops with an error if you put a string where an int belongs, before anything runs. Reading those errors is part of writing C++: they say the line and what did not fit.
if runs some lines only when a condition is true. for repeats lines: for (int i = 0; i < 3; i++) starts i at 0, runs while i < 3, and adds one (i++) after each turn. A function is named steps you can call: it says the type it returns, then its parameters (the values it is given).
C++
1#include<iostream>23inttwice(int n){ // returns an int, is given an int4return n *2;5}67intmain(){8for(int i =0; i <3; i++){9if(i ==1){ // == compares; = would store10 std::cout <<"one!\n";11}else{12 std::cout <<twice(i)<<"\n";13}14}15}
main may leave out return 0;: it is the one function where that is allowed.
QuizInteractive · disabled in the PDF
What does the program above print?
What does the program above print?
Variable
A named box for a value, with a type that never changes.
Type
What kind of value a variable holds: int, double, bool, std::string…
Function
Named steps, given parameters, that may return a value.
Parameter
A value a function is given when it is called.
Data structures7 / 201
Module 1 · What a data structure is
Searching a shelf means looking at every book
A shelf is a row of values, each at a numbered position from 0. In C++ a row of strings is a std::vector<std::string>; the next module shows what it is inside. To find a title you look at position 0, then 1, then 2, until you find it or run out of shelf. This is linear search: with n books, the worst case is n looks.
WalkthroughInteractive · disabled in the PDF
C++
1#include<string>2#include<vector>34intindexOfTitle(const std::vector<std::string>& shelf,5const std::string& title){6for(int i =0; i < shelf.size(); i++){7if(shelf[i]== title)return i;8}9return-1;10}1112// indexOfTitle({"Emma", "Dune", "Beloved"}, "Dune") is 1
Lines 1-2: Bring in strings and vectors from the standard library.
Lines 4-5: It takes the shelf and the title, and returns a position: an int. const …& means "look at the caller’s value, without copying it or changing it" (the next module explains).
Lines 6: Visit each position in turn, from 0 to the last; shelf.size() is how many books there are.
Lines 7:shelf[i] reads the book at position i. If it is the one, stop and say where it is.
Lines 9: Only after looking at every book do we know it is not there: -1.
Lines 12: Emma is not it, Dune is: two looks, the answer is 1.
Find the bugInteractive · disabled in the PDF
This search says "not found" for books that are on the shelf. Which line is wrong?
C++
Data structures8 / 201
Module 1 · What a data structure is
Try it: count the looks
Measuring work by counting steps is the habit this course is built on. Write a search that also says how many books it looked at. To return two numbers at once, it returns a struct: a type you make yourself that groups named values (its members), here index and looks. Result{2, 3} makes one, and r.index reads a member.
Code challengeInteractive · disabled in the PDF
Finish searchShelf(shelf, title): return a Result whose index is the title’s position (or -1) and whose looks is how many books were compared.
C++
1#include<string>2#include<vector>34structResult{5int index;6int looks;7};89ResultsearchShelf(const std::vector<std::string>& shelf,10const std::string& title){11int looks =0;12 // TODO: look at each book, counting the looks13return Result{-1, looks};14}
Data structures9 / 201
Module 1 · What a data structure is
The same data, arranged for a different question
If you mostly look books up by their ISBN (the number printed on the back), a row is the wrong layout. A map is a card box: it files each value under a key. std::unordered_map<std::string, Book> files books under string keys, and catalogue["222"] jumps to the book without looking at the others. How it manages that is the subject of the hash tables module.
A row: search it
std::vector<Book> books ={{"111","Emma"},{"222","Dune"}};for(constBook& b : books){if(b.isbn =="222") std::cout << b.title;}
for (const Book& b : books) is a range-for: it visits each value of a collection in turn, without counting positions yourself.
Building it costs something. Making the card box means visiting every book once. That is worth it when you look things up many times, and wasted if you look once. Choosing a structure is always this kind of trade.
Data structures10 / 201
Module 1 · What a data structure is
Try it: build the card box
Code challengeInteractive · disabled in the PDF
Finish byIsbn(books): return a map whose keys are the books’ ISBNs and whose values are the books themselves.
C++
1#include<string>2#include<unordered_map>3#include<vector>45structBook{6 std::string isbn;7 std::string title;8};910std::unordered_map<std::string, Book>11byIsbn(const std::vector<Book>& books){12 std::unordered_map<std::string,Book> index;13 // TODO: file each book under its ISBN14return index;15}
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<string>3#include<unordered_map>4#include<vector>56intmain(){7 std::vector<std::string> titles;8for(int i =0; i <10000; i++){9 titles.push_back("Book "+ std::to_string(i));10}1112int looks =0;13for(const std::string& t : titles){14 looks++;15if(t =="Book 9999")break;16}17 std::cout <<"Row: looks to find the last book "<< looks <<"\n";1819 std::unordered_map<std::string,int> box;20for(int i =0; i < titles.size(); i++) box[titles[i]]= i;21 std::cout <<"Box: position of the last book "22<< box["Book 9999"]<<"\n";23}
Make the shelf ten times bigger (and look for Book 99999). The row needs ten times the looks; the box still answers at once.
Data structures11 / 201
Module 1 · What a data structure is
What a C++ value takes up
C++ shows you memory more directly than most languages. Each type has a fixed size in bytes, and a variable is its bytes: there is no hidden wrapper around an int.
Type
Usual size
Holds
char, bool
1 byte
One character, or true/false
int
4 bytes
Whole numbers up to about ±2 billion
double
8 bytes
Numbers with a fraction, about 15 digits
Book (a struct)
Its members, side by side (sometimes with a few unused bytes between)
Whatever its members hold
A std::vector<int> keeps its numbers themselves side by side: 4 bytes each, no gaps.
A std::vector<Book> keeps the books themselves in a row, not links to books stored elsewhere.
Copying a vector copies every value in it. That is why functions take const std::vector<…>&: they look at the caller’s vector instead.
Reading nearby memory is cheaper. The processor keeps a small, very fast copy of recently used memory, the cache, and fetches memory in blocks of about 64 bytes, a cache line. Reading values that sit side by side is much faster than jumping around, because one fetch brings in their neighbours too. This is cache locality, and it is why a plain row of numbers often beats cleverer layouts in practice. C++ makes it easy to keep data side by side, which is a big part of why it is fast.
QuizInteractive · disabled in the PDF
What does a std::vector<Book> of three books hold, in its own memory?
What does a std::vector<Book> of three books hold, in its own memory?
Data structures12 / 201
Module 1 · What a data structure is
Recap
Data lives in memory: numbered boxes, each reachable directly by its address.
A data structure is a layout plus operations; the layout decides what each operation costs.
Ask four questions of any structure: how dear are read, search, insert and delete?
A C++ program starts in main, prints with std::cout, and gives every variable a type.
Linear search looks at every value in the worst case.
Rearranging data (a map instead of a row) can turn a search into a direct look-up, at the cost of building it.
C++ keeps values themselves side by side, and values side by side are faster to read, thanks to the cache.
FlashcardsInteractive · disabled in the PDF
Data structure
A way of laying data out in memory, with the operations it supports.
Address
The number of a box of memory, which lets the computer reach it directly.
Read vs search
Read: you know where the value is. Search: you have to find it.
Linear search
Look at each value in turn until you find it or run out.
Compiler
Turns C++ text into instructions the machine runs, and reports errors first.
int main()
Where every C++ program starts.
Struct
A type you make that groups named members: struct Book { std::string title; };
Copy by default
In C++, assigning or storing a value copies it, unless you use a reference.
Cache locality
Values side by side in memory are read faster, a block at a time.
Module 2: Arrays
Data structures13 / 201
Module 2 · Arrays
An array is a row of slots side by side
An array keeps its values in contiguous memory: one slot after another, with no gaps, like seats in a cinema row. Every slot is the same size, because every value has the same type. The slots are numbered from 0, and a slot’s number is its index.
The lengths of four songs, in seconds, as ints. Each slot is 4 bytes, and the array starts at address 1000.
C++
1int lengths[4]={200,185,240,210}; // a C array2lengths[2]=245; // write slot 23int first = lengths[0]; // read slot 0: 200
The oldest kind of array in C++, from the C language: a type, a name and a size.
Array
Values of one type in a numbered row of same-sized slots.
Contiguous
Side by side in memory, with no gaps.
Index
A slot’s number, counted from 0.
Size
How many values the array holds.
Data structures14 / 201
Module 2 · Arrays
Reading by index is one sum
Because the slots are the same size and side by side, the computer works out where any index lives with one small sum, then jumps there. It never walks along the row. Reading lengths[3] takes the same time as reading lengths[0], however long the array is. We call that constant time.
Text
1address of slot i = start + i × slot size23lengths[0] → 1000 + 0 × 4 = 10004lengths[3] → 1000 + 3 × 4 = 1012
This is also why indexes start at 0: the first slot is 0 slots from the start.
QuizInteractive · disabled in the PDF
An array of doubles starts at address 2000, and each double is 8 bytes. Where is index 5?
An array of doubles starts at address 2000, and each double is 8 bytes. Where is index 5?
Data structures15 / 201
Module 2 · Arrays
std::array is a C array that knows its size
A C array does not remember how long it is, and nothing stops you reading past its end: you get whatever bytes sit there, or a crash. std::array<int, 4> (from <array>) is the same four slots in the same memory, wrapped so it knows its size() and works with the standard library. Its size is fixed when you write the code. sizeof(x) says how many bytes x takes, and &x gives its address.
PlaygroundInteractive · disabled in the PDF
C++
1#include<array>2#include<iostream>34intmain(){5int c[4]={200,185,240,210};6 std::array<int,4> a ={200,185,240,210};78 std::cout <<"sizeof(int) = "<<sizeof(int)<<"\n";9 std::cout <<"sizeof(c) = "<<sizeof(c)<<"\n";10 std::cout <<"sizeof(a) = "<<sizeof(a)<<"\n";11 std::cout <<"a.size() = "<< a.size()<<"\n";1213for(int i =0; i < a.size(); i++){14 std::cout <<"a["<< i <<"] is at "<<&a[i]<<"\n";15}16}
The addresses print in hexadecimal (base 16, digits 0 to f), and each is 4 more than the last. Try std::array<double, 4>.
QuizInteractive · disabled in the PDF
What is sizeof(std::array<double, 10>), if a double is 8 bytes?
What is sizeof(std::array<double, 10>), if a double is 8 bytes?
Data structures16 / 201
Module 2 · Arrays
Passing a vector: a copy, a reference, or a const reference
A std::vector<int> (from <vector>) is an array that can grow; the slides after this show how. Passing one to a function copies it, every value, unless you ask otherwise. A reference, written & after the type, is another name for the caller’s variable: no copy, and changes show in the caller. A const reference, const std::vector<int>&, is a reference you promise not to change through.
C++
1voidaddOne(std::vector<int> v){ v.push_back(1);} // a copy2voidaddTwo(std::vector<int>& v){ v.push_back(2);} // the same one34inttotal(const std::vector<int>& v){ // looks only5int sum =0;6for(int x : v) sum += x;7return sum;8}910std::vector<int> nums ={5};11addOne(nums); // nums is still {5}: the copy got the 112addTwo(nums); // nums is {5, 2}
push_back(x) adds x at the end. for (int x : v) visits each value, as in module 1.
Fill the blanksInteractive · disabled in the PDF
One function reads a playlist, one changes it. Give each the right parameter type, so neither copies it.
C++
1intlongest( lengths){2int best =0;3for(int x : lengths)if(x > best) best = x;4return best;5}67voidaddSong( lengths,int seconds){8 lengths.push_back(seconds);9}
`std::vector<T>`
An array of Ts that can grow at the end.
Reference (`T&`)
Another name for an existing variable: no copy.
`const T&`
A reference you can only read through: the usual way to pass big values.
Pass by value
The function gets its own copy of the argument.
Data structures17 / 201
Module 2 · Arrays
Inserting in the middle shifts everything after it
The slots must stay side by side, so to insert a song at index 1, every song from index 1 onwards moves one slot to the right first, starting from the end so nothing is overwritten. Inserting near the front of a long array means moving almost every value.
The array grows by one slot at the end.
Outro moves from [2] to [3].
Waves moves from [1] to [2].
Slot [1] is free: Rain goes in. Two moves for one insert.
Inserting Rain at index 1.
WalkthroughInteractive · disabled in the PDF
C++
1voidinsertAt(std::vector<std::string>& items,int index,2const std::string& value){3 items.push_back("");4for(int i = items.size()-1; i > index; i--){5 items[i]= items[i -1];6}7 items[index]= value;8}
Lines 1-2: A plain reference to the vector, because it changes the caller’s list.
Lines 3: Make room: one more slot at the end (filled with an empty string for now).
Lines 4: Start at the new last slot and walk left, down to the insert point.
Lines 5: Each slot takes the value from its left neighbour.
Lines 7: Now the slot at index is free to overwrite.
Data structures18 / 201
Module 2 · Arrays
Try it: remove from the middle
Removing is the mirror image: everything after the gap moves one slot to the left, then the vector gets one shorter: items.pop_back() drops the last slot.
Code challengeInteractive · disabled in the PDF
Finish removeAt(items, index) without erase: shift the later values left, drop the last slot with pop_back(), and return the value removed.
C++
1#include<string>2#include<vector>34std::stringremoveAt(std::vector<std::string>& items,int index){5 std::string removed = items[index];6 // TODO: move each later value one slot left,7 // then drop the last slot8return removed;9}
Data structures19 / 201
Module 2 · Arrays
A dynamic array grows by doubling
Memory next to an array is often taken by something else, so an array cannot always grow where it is. A dynamic array, which is what std::vector is, asks for a block of memory from the heap (memory a program asks for while it runs) with more slots than it needs: its capacity. While there is room, push_back just fills the next slot. When it is full, it asks for a new block of twice the capacity, copies every value over, and gives the old block back.
Push number
Capacity before
Copies made
Capacity after
1
0
0
1
2
1 (full)
1
2
3
2 (full)
2
4
4
4
0
4
5
4 (full)
4
8
6 to 8
8
0
8
9
8 (full)
8
16
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<vector>34intmain(){5 std::vector<int> v;6for(int i =1; i <=20; i++){7 v.push_back(i);8 std::cout <<"size "<< v.size()9<<", capacity "<< v.capacity()<<"\n";10}11}
Watch the capacity double. Then put v.reserve(20); before the loop: one block from the start, no copies.
Copying is slow, but it happens rarely, and each time the vector doubles, the next copy is twice as far away. After 9 pushes, 1 + 2 + 4 + 8 = 15 copies were made: fewer than 2 per push. Averaged over many pushes, each push costs a small, fixed amount. That kind of average over a whole run is called amortised cost, and the next module makes it precise.
Put in orderInteractive · disabled in the PDF
Put the steps of a push_back into a full vector in order.
1.Ask the heap for a block with twice the capacity
2.See that size equals capacity
3.Copy every value into the new block
4.Add one to the size, and give the old block back
5.Put the new value in the next free slot
Data structures20 / 201
Module 2 · Arrays
Try it: build a dynamic array
A struct can hold functions too, called member functions: a.push(5) calls push on a, and inside it the names capacity, size and slots mean a’s members. Here a fixed row is pretended with a vector made at its full size, std::vector<int>(n); you only ever write inside it. copies counts the values copied, so the tests can check the doubling.
Code challengeInteractive · disabled in the PDF
Finish push: when size equals capacity, double the capacity, copy the values into a new row (adding to copies), then store the value.
Every std::vector operation’s cost follows from the slots. Working at the end is cheap; working at the front or in the middle moves everything after it. Positions in a vector are given as iterators: v.begin() is the first slot, v.begin() + 2 the third, v.end() just past the last. Big O, in the right column, is the short way to write the cost: O(1) is a fixed amount of work, O(n) work that grows with the size n. The next module explains it properly.
Operation
What it does
Cost
v[i], v[i] = x
Read or write a slot
O(1)
push_back, pop_back
Add or remove at the end
O(1) amortised
insert(v.begin(), x), erase(v.begin())
Add or remove at the front
O(n): moves every value
insert, erase in the middle
Add or remove at a position
O(n): moves the rest
std::find(v.begin(), v.end(), x)
Search
O(n): linear search
auto w = v;
Copy
O(n) for n values copied
reserve(n)
Grow the capacity once, ahead of time
O(n), and saves later copies
Predict the outputInteractive · disabled in the PDF
What does this print, and which lines move the most values?
What does this print, and which lines move the most values?
A slow loop that looks fast. Taking items off the front with while (!q.empty()) q.erase(q.begin()); moves the whole vector on every turn: n turns of up to n moves. A vector has no cheap way to drop its first slot. The queues module shows the dependable fix, and std::deque, which has one.
Data structures22 / 201
Module 2 · Arrays
A grid is an array of arrays
A seating plan, a spreadsheet or a game board is a grid: rows and columns. In C++ a grid is often a vector of rows, each row a vector, so grid[row][col] reads one cell. This is a 2D array (two dimensions). std::vector<char> row(4, '.') makes 4 slots holding ., and seats(3, row) makes 3 copies of that row.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<vector>34intmain(){5int rows =3, cols =4;6 std::vector<std::vector<char>>seats(7 rows, std::vector<char>(cols,'.'));8 seats[1][2]='X'; // row 1, column 2 is booked9for(constauto& row : seats){10for(char seat : row) std::cout << seat <<' ';11 std::cout <<"\n";12}1314 // The same grid in one flat vector: index = row × cols + col15 std::vector<char>flat(rows * cols,'.');16 flat[1* cols +2]='X';17for(char seat : flat) std::cout << seat;18 std::cout <<"\n";19}
Book another seat in both versions, and check they agree.
Find the bugInteractive · disabled in the PDF
On a 3 × 4 grid, booking row 1, column 2 marks the wrong seat. Which line is the cause?
C++
Flat grids are kinder to the cache. In a vector of vectors, each row is its own block on the heap, somewhere else in memory. One flat vector, indexed by row × cols + col, keeps every cell side by side. Visiting cells row by row then reads memory in order, which the cache loves; visiting column by column jumps cols slots each step and is slower on big grids, for the same number of steps.
Data structures23 / 201
Module 2 · Arrays
Recap
An array is same-sized slots side by side, numbered from 0; sizeof shows the bytes.
Reading or writing by index is one sum, start + i × size: constant time.
C arrays and std::array have a fixed size; std::vector can grow.
Pass a vector as const std::vector<T>& to read it, std::vector<T>& to change it; plain std::vector<T> copies it.
Inserting or removing in the middle shifts every later value; at the front, all of them.
A vector keeps spare capacity and doubles it when full, so push_back is cheap on average.
A grid is a vector of rows, or one flat vector indexed by row × cols + col.
FlashcardsInteractive · disabled in the PDF
Contiguous
Side by side in memory, no gaps.
Address of index i
start + i × slot size: one sum, whatever the size.
sizeof(x)
How many bytes x takes in memory.
std::array<T, n> vs std::vector<T>
A fixed number of slots, vs a row that grows on the heap.
const T&
A reference you only read through: no copy, no changes.
Why is inserting at the front slow?
Every value has to move one slot right first.
capacity() vs size()
Capacity: slots reserved. Size: slots in use.
Dynamic array growth
When full, double the capacity and copy everything over.
Amortised cost
The cost per operation averaged over a whole run of them.
Iterator
A position in a container: v.begin() is the first slot, v.end() just past the last.
Module 3: Measuring cost
Data structures24 / 201
Module 3 · Measuring cost
Count steps, not seconds
Timing code with a stopwatch tells you about your computer, today. A faster laptop halves the time; the code is the same. To compare ways of doing something, count the steps instead: the simple operations, such as one comparison or one addition, that the code performs. Then ask how that count changes as the input gets bigger. The input’s size is called n: the number of books, songs or tasks.
C++
1inttotal(const std::vector<int>& prices){2int sum =0; // 1 step3for(int p : prices){4 sum += p; // 1 step, n times5}6return sum; // 1 step7}
About n + 2 steps for n prices. Double the prices, and the steps roughly double.
Predict the outputInteractive · disabled in the PDF
About how many times does the inner line run, for n guests?
C++
1for(const std::string& a : guests){2for(const std::string& b : guests){3greet(a, b);4}5}
About how many times does the inner line run, for n guests?
Data structures25 / 201
Module 3 · Measuring cost
Big O keeps only how the cost grows
Exact counts like 3n + 5 are fiddly, and the details depend on what you call a step. Big O notation keeps only the part that matters when n gets large. Two rules get you there: drop the constants (3n becomes n) and keep only the biggest term (n² + n becomes n²). So 3n + 5 steps is written O(n), said "order n".
Exact count
Big O
Why
7
O(1)
A fixed amount, whatever n is
3n + 5
O(n)
Drop the 3 and the 5
n² + 10n
O(n²)
For big n, n² dwarfs 10n
n / 2
O(n)
Halving is a constant too
n
The size of the input: how many values there are.
Big O
How the work grows with n, ignoring constants and smaller terms.
O(1), constant
The same work whatever n is.
O(n), linear
Work that grows in step with n.
Fill the blanksInteractive · disabled in the PDF
Simplify each count to its Big O.
Text
15n + 100 is O()22n² + 3n + 1 is O()342 is O()
Data structures26 / 201
Module 3 · Measuring cost
The common growth rates
Five shapes cover most code you will meet. log n (the logarithm) is how many times you can halve n before reaching 1: about 10 for a thousand, 20 for a million.
Big O
Name
n = 10
n = 1 000
n = 1 000 000
Example
O(1)
constant
1
1
1
Read a[i]
O(log n)
logarithmic
3
10
20
Binary search
O(n)
linear
10
1 000
1 000 000
Linear search
O(n log n)
n log n
33
10 000
20 000 000
Good sorting
O(n²)
quadratic
100
1 000 000
1 000 000 000 000
Every pair
PlaygroundInteractive · disabled in the PDF
C++
1#include<cmath>2#include<iostream>34intmain(){5for(longlong n : {10,100,1000,10000}){6longlong logN = std::round(std::log2(n));7 std::cout <<"n="<< n <<" log n="<< logN8<<" n log n="<< n * logN9<<" n²="<< n * n <<"\n";10}11}
Add 1000000 to the list. At a billion simple steps a second, how long would n² take? (long long is a whole number of 8 bytes: an int stops at about 2 billion, and a million squared is far more.)
Small n hides everything. At n = 10 every row is tiny, which is why slow code often passes its tests and only hurts once real data arrives. Big O is about the day the shop has a million orders.
Data structures27 / 201
Module 3 · Measuring cost
O(log n): halve the problem each step
Looking a word up in a paper dictionary, you open it in the middle, see whether your word comes before or after, and throw away the wrong half. Each look halves what is left, so even a million words need only about 20 looks. On a sorted array this is binary search.
Lines 2-3:low and high mark the part still worth searching: at first, all of it.
Lines 4: Keep going while that part is not empty.
Lines 5: Look in the middle. Dividing two ints in C++ drops the fraction, so 7 / 2 is 3: always a whole index.
Lines 6: Found it: done.
Lines 7: The middle is too small, so the target is to its right: drop the left half.
Lines 8: Otherwise it is to the left: drop the right half.
Lines 10: The part shrank to nothing: the target is not there.
QuizInteractive · disabled in the PDF
Why does binary search need the array to be sorted?
Why does binary search need the array to be sorted?
Why not `(low + high) / 2`?. An int holds up to about 2 147 million. With a vector of 1.5 billion values, low + high can pass that and overflow, which for an int is undefined behaviour in C++: the language promises nothing about what happens next (often the sum wraps round to a negative number). low + (high - low) / 2 is the same middle and never gets bigger than high. The standard library has binary search ready-made, as std::lower_bound and std::binary_search, which the algorithms course uses.
Data structures28 / 201
Module 3 · Measuring cost
Try it: count the halvings
Code challengeInteractive · disabled in the PDF
Finish binarySearch(sorted, target): return a Result with the target’s index (or -1) and how many middle values were compared, looks.
C++
1#include<vector>23structResult{4int index;5int looks;6};78ResultbinarySearch(const std::vector<int>& sorted,int target){9int low =0;10int high = sorted.size()-1;11int looks =0;12 // TODO: look in the middle, drop the wrong half13return Result{-1, looks};14}
Data structures29 / 201
Module 3 · Measuring cost
Loops inside loops multiply
Checking a guest list for a name written twice, the obvious way compares every guest with every other: O(n²). Sorting first puts equal names next to each other, so one pass comparing neighbours is enough. A good sort costs O(n log n), and the pass O(n), so the whole thing is O(n log n): far less for big lists.
Every pair: O(n²)
for(int i =0; i < a.size(); i++)for(int j = i +1; j < a.size(); j++)if(a[i]== a[j])returntrue;returnfalse;
Sort, then neighbours
std::vector<int> s = a; // a copystd::sort(s.begin(), s.end());for(int i =1; i < s.size(); i++)if(s[i]== s[i -1])returntrue;returnfalse;
Code challengeInteractive · disabled in the PDF
Finish hasDuplicate(nums): return true if any number appears twice. Sort the copy it is given and compare neighbours, so a list of 100 000 numbers is quick.
C++
1#include<algorithm>2#include<vector>34boolhasDuplicate(std::vector<int> nums){5 // TODO: sort nums, then compare each value with the one before6returnfalse;7}
Data structures30 / 201
Module 3 · Measuring cost
Best, worst and average cases
The same code can be fast or slow depending on the input. Linear search finds the first book in one look (the best case), needs every look when the book is last or missing (the worst case), and about n / 2 looks when the book is somewhere at random (the average case). Big O is usually quoted for the worst case, because that is the promise you can rely on.
Linear search, n books
Looks
Big O
Best: the first book
1
O(1)
Average: somewhere at random
about n / 2
O(n)
Worst: last, or missing
n
O(n)
QuizInteractive · disabled in the PDF
What is the worst case for binary search on n sorted values?
What is the worst case for binary search on n sorted values?
Data structures31 / 201
Module 3 · Measuring cost
Space cost: the memory it needs
Big O also measures space: the extra memory an operation needs as n grows, beyond the input itself. Reversing a playlist into a new array needs O(n) extra space. Reversing it in place, by swapping the ends and moving inwards, needs only a couple of variables: O(1) extra space.
Fill the blanksInteractive · disabled in the PDF
Reverse the array in place: swap the ends, then move both inwards.
C++
1voidreverseInPlace(std::vector<int>& a){2int left =0;3int right = a.size()-;4while(left right){5int kept = a[left];6 a[left]= a[right];7 a[right]= kept;8 left;9 right;10}11}
Time and space trade. Many speed-ups spend memory: the card box of module 1 used O(n) extra space to turn O(n) searches into instant look-ups. When someone says an approach is faster, ask what it costs in memory.
Data structures32 / 201
Module 3 · Measuring cost
Amortised cost: the average over a whole run
A vector’s push_back is usually one step, and occasionally a copy of everything. Its worst single push is O(n). But copies only happen when the capacity doubles, so over n pushes the copies add up to 1 + 2 + 4 + … which is less than 2n. Total work for n pushes is under 3n: O(n) for all of them, so O(1) amortised per push. This is the promise the C++ standard makes for std::vector::push_back.
Amortised is not average case. Average case is about likely inputs, and an unlucky input can still be slow. Amortised is a guarantee over any sequence of operations: no run of n pushes, however chosen, costs more than O(n) in total. Individual pushes can still be slow, which matters for things like animation frames.
QuizInteractive · disabled in the PDF
An array grows by 10 slots each time it is full, instead of doubling. What does push cost, amortised?
An array grows by 10 slots each time it is full, instead of doubling. What does push cost, amortised?
Constants come back in real life. Big O drops constants, but a computer does not. An O(n) loop over a flat array of numbers can beat an O(log n) structure for a few hundred values, thanks to the cache. This is why std::sort switches to a simple O(n²) method (insertion sort) for small pieces. Big O tells you what happens as n grows; measuring tells you which wins at your n.
Data structures33 / 201
Module 3 · Measuring cost
Recap
Put in orderInteractive · disabled in the PDF
Put these from the slowest-growing to the fastest-growing.
1.O(1)
2.O(n²)
3.O(log n)
4.O(n log n)
5.O(n)
Measure work in steps as a function of the input size n, not in seconds.
Big O drops constants and smaller terms: 3n + 5 is O(n).
Halving the problem each step gives O(log n), as in binary search.
Quote the worst case unless you say otherwise; space has a Big O too.
Amortised cost averages over any run of operations: doubling makes push_back O(1) amortised.
FlashcardsInteractive · disabled in the PDF
Big O
How work grows with n, ignoring constants and smaller terms.
O(1)
Constant: the same work whatever n is.
O(log n)
Halving each step: about 20 steps for a million.
O(n²)
Every pair: a loop inside a loop over the same data.
Worst case
The most work any input of size n can cause.
In place
Changing the input itself, with O(1) extra space.
Amortised
Total cost of a run of operations, divided by how many there were.
Module 4: Strings
Data structures34 / 201
Module 4 · Strings
A string is a sequence of characters
A string is text: a sequence of characters (letters, digits, spaces, punctuation) in order. A std::string (from <string>) is laid out much like a std::vector<char>: the characters sit side by side, each has an index from 0, size() says how many there are, and reading one by index is instant.
C++
1std::string title ="Dune";23std::cout << title.size(); // 44std::cout << title[0]; // D5std::cout << title.back(); // e: the last character6for(char ch : title){7 std::cout << ch <<' '; // D u n e8}
A char is one character, written in single quotes; a string is written in double quotes.
String
Text: a sequence of characters, written in double quotes.
Character (`char`)
One letter, digit, space or symbol: one byte in C++.
Substring
A piece of a string: une is a substring of Dune.
QuizInteractive · disabled in the PDF
std::string s = "bakery"; What is the last character, and how do you read it?
std::string s = "bakery"; What is the last character, and how do you read it?
Data structures35 / 201
Module 4 · Strings
A std::string can change in place
Unlike in many languages, a C++ string is mutable: you can change its characters where they are. s[0] = 'E' overwrites one byte; s += "!" adds to the end; s.insert and s.erase work like a vector’s. And like every C++ value, assigning a string copies it: two variables never share one string by accident.
C++
1std::string name ="emma";2name[0]='E'; // "Emma": changed in place3name +=" Woodhouse"; // "Emma Woodhouse"45std::string copy = name; // a second string, same text6copy[0]='J';7std::cout << name <<" / "<< copy; // Emma … / Jmma …
Find the bugInteractive · disabled in the PDF
shout should turn every letter into a capital, but returns the text unchanged. Which line is wrong?
C++
Data structures36 / 201
Module 4 · Strings
Building a long string piece by piece
A std::string grows like a vector: it keeps spare capacity and doubles it when full. So s += piece only copies the piece, and is cheap amortised. s = s + piece is different: s + piece builds a whole new string, copying everything so far, before it replaces s. Doing that n times copies 1 + 2 + 3 + … characters: O(n²).
O(n²): a new string every time
std::string receipt;for(const std::string& line : lines){ receipt = receipt + line +"\n";}
O(n): add to the end
std::string receipt;receipt.reserve(lines.size()*40); // a guessfor(const std::string& line : lines){ receipt += line; receipt +='\n';}
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<string>34intmain(){5 std::string s;6int last =-1;7for(int i =0; i <200; i++){8 s +='a';9if(s.capacity()!= last){10 last = s.capacity();11 std::cout <<"size "<< s.size()12<<": capacity "<< last <<"\n";13}14}15}
The capacity roughly doubles. The first capacity is not 1: see the note below. Try s.reserve(200); before the loop.
Short strings live inside the string. A std::string variable is a small struct: on a 64-bit machine, 24 or 32 bytes (depending on the library) holding a pointer to the characters on the heap, the size and the capacity. For short text that is wasteful, so libraries use the small string optimisation (SSO): text that fits (up to 22 characters in libc++ on 64-bit, 15 in GCC’s library, 10 in this page’s 32-bit WebAssembly) is stored in those bytes themselves, and nothing is asked of the heap. Most names and words fit, which makes them very cheap to copy.
Predict the outputInteractive · disabled in the PDF
With n characters in the string (and m in the one you search for), the costs follow from the layout. s.find(t) returns the index where t first appears, or the special value std::string::npos when it is not there. s.substr(i, len) copies len characters from index i.
Operation
What it does
Cost
s.size(), s[i]
Size, one character
O(1)
a == b, a < b
Compare two strings
O(n): character by character
s += t, s.push_back(c)
Add to the end
O(m), amortised
s.substr(i, len)
Copy a substring
O(len)
s.find(t)
Search for a substring
O(n × m) at worst
s.insert(0, t), s.erase(0, 1)
Change the front
O(n): moves the rest
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1std::string csv ="tea,,scones";2int at = csv.find(',');3std::cout << at <<" "<< csv.substr(0, at)4<<" "<< csv.find(',', at +1);
What does this print?
Data structures38 / 201
Module 4 · Strings
std::string_view looks at text without copying it
A std::string_view (from <string_view>) is a window onto characters that live somewhere else: just a pointer to the first one and a length. Making one copies no text, and taking a piece of it, v.substr(i, len), is O(1): it only moves the window. It is the cheapest way to pass text you only read.
C++
1#include<string_view>23intcountSpaces(std::string_view text){ // works for any text4int spaces =0;5for(char c : text)if(c ==' ') spaces++;6return spaces;7}89std::string line ="two words";10countSpaces(line); // no copy11countSpaces("a b c"); // no std::string made at all
A view does not own its text. If the string a view looks at is destroyed or changed, the view points at nothing valid, and reading it is undefined behaviour. So pass std::string_view into functions freely, but think twice before keeping one in a struct or returning one: return a std::string instead.
QuizInteractive · disabled in the PDF
A function takes a 1 MB string and returns its first 10 characters. Which costs the least?
A function takes a 1 MB string and returns its first 10 characters. Which costs the least?
Data structures39 / 201
Module 4 · Strings
Try it: reverse a string
Build the reversed text in a new string, reading the original from its last index down to 0.
Code challengeInteractive · disabled in the PDF
Finish reverse(s) with a loop from the last index down to 0, adding each character to out with +=.
C++
1#include<string>23std::stringreverse(const std::string& s){4 std::string out;5 // TODO: from the last character back to the first6return out;7}
Data structures40 / 201
Module 4 · Strings
Two pointers walk in from both ends
A palindrome reads the same both ways: "level", "Was it a car or a cat I saw". To check one, you do not need to build the reversed string. Keep two indexes, one at each end, compare those characters, and step both inwards. This is the two-pointer technique: O(n) time and O(1) extra space. (Here "pointer" just means an index; C++’s own pointers come in the linked lists module.)
left = 0, right = 3: both are n. Move both in.
left = 1, right = 2: both are o. Move both in.
left = 2, right = 1: the pointers have passed. Every pair matched.
Checking "noon": compare the ends, then move in.
Code challengeInteractive · disabled in the PDF
Finish isPalindrome(s), ignoring case, spaces and punctuation. letters already holds only the lower-case letters and digits; compare from both ends.
C++
1#include<cctype>2#include<string>34boolisPalindrome(const std::string& s){5 std::string letters;6for(unsignedchar c : s){7if(std::isalnum(c)) letters += std::tolower(c);8}9int left =0;10int right = letters.size()-1;11 // TODO: compare and move inwards12returnfalse;13}
Data structures41 / 201
Module 4 · Strings
Counting characters with an array of 256
A char is one byte, so there are only 256 possible values. To count how often each appears, keep an array of 256 counts and use the byte itself as the index: counts[c]++. One pass over the string, each update one sum, so the whole count is O(n), with a fixed 1 KB of memory however long the text.
PlaygroundInteractive · disabled in the PDF
C++
1#include<array>2#include<iostream>3#include<string>45intmain(){6 std::string word ="mississippi";7 std::array<int,256> counts{}; // {} starts them all at 089for(unsignedchar c : word) counts[c]++;1011for(int c =0; c <256; c++){12if(counts[c]>0){13 std::cout <<char(c)<<": "<< counts[c]<<"\n";14}15}16}
Try a sentence. The letters come out in byte order, which for plain English letters is A to Z, then a to z.
Code challengeInteractive · disabled in the PDF
Finish isAnagram(a, b): true when b uses exactly the same letters as a, as many times each. Count a’s letters up, then count b’s down.
C++
1#include<array>2#include<string>34boolisAnagram(const std::string& a,const std::string& b){5if(a.size()!= b.size())returnfalse;6 std::array<int,256> counts{};7 // TODO: add 1 for each of a's letters, take 1 for b's8returntrue;9}
Data structures42 / 201
Module 4 · Strings
Characters, bytes and UTF-8
Computers store characters as numbers. Every character has a number in Unicode, its code point: A is 65, é is 233, 😀 is 128 512. A byte only holds 0 to 255, so a std::string stores text in UTF-8: plain English letters take 1 byte each, é takes 2, most emoji take 4. size() and s[i] count bytes, not characters.
The first number is h, 104. The next two make é, the last four 😀. Try a word in another alphabet.
QuizInteractive · disabled in the PDF
What goes wrong with reverse("ok👍") from the earlier slide?
What goes wrong with reverse("ok👍") from the earlier slide?
Even code points are not "characters". What a person sees as one character, a grapheme, can be several code points: 👍🏽 is a thumb plus a skin tone, and a family emoji is several people joined by invisible joiners. Standard C++ has no tools for graphemes; programs that must cut or count text the way people see it use a library such as ICU. For most work (names, codes, English text) treating bytes as characters is fine, and much faster.
Data structures43 / 201
Module 4 · Strings
Recap
A std::string is characters side by side, like a vector of char: s[i] and size() are O(1).
C++ strings are mutable and copied on assignment; char& in a loop changes them in place.
Build long strings with += (and reserve), never s = s + piece.
std::string_view looks at text without copying, but does not own it.
Two pointers from both ends check a palindrome in O(n) time and O(1) space.
A byte has 256 values, so an array of 256 counts tallies characters in O(n).
Strings hold UTF-8 bytes: size() counts bytes, and é and emoji take several.
FlashcardsInteractive · disabled in the PDF
Mutable
Can be changed in place, as a std::string can.
s += t vs s = s + t
+= adds to the end, cheap amortised; s + t builds a whole new string first.
Small string optimisation
Short text stored inside the string object itself, with no heap.
std::string_view
A pointer and a length: a window onto text owned by someone else.
std::string::npos
What find returns when the text is not there.
Two pointers
Two indexes moving through the data, often from both ends inwards.
Code point
A character’s number in Unicode.
UTF-8
Unicode stored as 1 to 4 bytes per code point; std::string holds these bytes.
Grapheme
What a reader sees as one character, possibly several code points.
Module 5: Linked lists
Data structures44 / 201
Module 5 · Linked lists
A linked list is a chain of nodes
Think of a treasure hunt: each clue tells you where the next one is hidden. You cannot jump to the fifth clue; you follow them from the first. A linked list works the same way. Each value lives in a node, a small struct that also holds a pointer: the address of the next node. The first node is the head; the last one is the tail, and its pointer is nullptr: nothing comes after.
A playlist as a linked list. The nodes can be anywhere in memory; only the pointers order them.
Node
One value plus a pointer to the next node.
Pointer
A variable that holds the address of another value.
Head
The first node: where every walk along the list starts.
Tail
The last node, whose next is nullptr.
Data structures45 / 201
Module 5 · Linked lists
A pointer holds an address
int* p is a pointer to an int: a variable whose value is an address. &x gives the address of x, and *p (dereferencing) means "the value at that address", to read or to change. nullptr is the pointer that points at nothing; dereferencing it is undefined behaviour (on most computers a crash; in this page it may quietly read garbage), so check first.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>23intmain(){4int pages =412;5int* p =&pages; // p holds the address of pages6 std::cout <<"pages is at "<< p <<"\n";7 std::cout <<"*p is "<<*p <<"\n";89*p =500; // change pages through p10 std::cout <<"pages is now "<< pages <<"\n";1112int* nothing =nullptr;13if(nothing ==nullptr) std::cout <<"nothing points nowhere\n";14 std::cout <<"a pointer takes "<<sizeof(p)<<" bytes here\n";15}
A pointer is 4 bytes in this page’s 32-bit WebAssembly, and 8 on most computers. Make a second pointer to pages and change it through that.
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1int a =1;2int* p =&a;3int b =*p;4*p =2;5std::cout << a <<" "<< b;
What does this print?
`T*`
A pointer to a T: holds a T’s address.
`&x`
The address of x.
`*p`
The value p points at (dereferencing).
`nullptr`
A pointer to nothing.
Data structures46 / 201
Module 5 · Linked lists
A node is a small struct
A node is a struct with a value and a next, which points at another node or is nullptr. The struct can mention itself (Node*), because a pointer’s size does not depend on what it points at. With a pointer to a struct, p->value reads a member: it is short for (*p).value.
C++
1structNode{2 std::string value;3Node* next;4};56Node outro{"Outro",nullptr};7Node waves{"Waves",&outro};8Node intro{"Intro",&waves};9Node* head =&intro;
Built from the tail backwards, so each node can point at one that already exists.
QuizInteractive · disabled in the PDF
With the nodes above, what is head->next->next->value?
With the nodes above, what is head->next->next->value?
Data structures47 / 201
Module 5 · Linked lists
Walking the list from the head
Visiting every node in order is called traversal. Keep a pointer to the node you are on, start it at the head, and after each node move it to next. When it becomes nullptr, you have fallen off the end.
A variable made inside a function lives on the stack and vanishes when the function ends. A list must outlive that, so its nodes go on the heap: new Node{…} asks the heap for room and returns a pointer, and the node stays until delete gives the memory back. Forget the delete and the memory is lost while the program runs: a memory leak. Use a pointer after its delete and it is dangling: it points at memory that may now hold something else.
C++
1Node*makeList(){2Node* outro =new Node{"Outro",nullptr};3Node* intro =new Node{"Intro", outro};4return intro; // both nodes outlive the function5}67Node* head =makeList();8// … use the list …9delete head->next; // free Outro10delete head; // free Intro11head =nullptr; // so nobody uses it again
Find the bugInteractive · disabled in the PDF
This frees every node, but reads memory it has already given back. Which line is wrong?
C++
Stack
Where a function’s own variables live; they vanish when it returns.
Heap
Memory a program asks for with new, kept until it is freed.
Memory leak
Heap memory nobody frees, and nobody can reach any more.
Dangling pointer
A pointer to memory that has been freed.
Data structures49 / 201
Module 5 · Linked lists
A class looks after its own parts
A class is a struct whose members are private unless you say public: only its own member functions can touch them, so nobody can break its rules from outside. A constructor (named like the class) runs when one is made; a destructor (~ and the name) runs when it goes away, automatically. Code that frees things in a destructor can never forget to: this is called RAII.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<string>34classPlaylist{5public:6Playlist(const std::string& n):name(n){ // constructor7 std::cout <<"made "<< name <<"\n";8}9~Playlist(){ std::cout <<"gone "<< name <<"\n";}1011voidadd(int seconds){ total += seconds;}12intlength()const{return total;}1314private:15 std::string name;16int total =0;17};1819intmain(){20Playlisttrip("Road trip");21 trip.add(200);22 trip.add(185);23 std::cout << trip.length()<<"\n";24{Playlistquick("Short");} // gone at its }25 std::cout <<"end of main\n";26}
: name(n) sets the member before the constructor’s body runs. Try trip.total = 0; in main: the compiler refuses, because it is private.
Put in orderInteractive · disabled in the PDF
Put what the program above prints in order.
1.made Short
2.gone Road trip
3.end of main
4.385
5.made Road trip
6.gone Short
Class
A struct whose members are private by default.
Constructor
Runs when a value of the class is made, to set it up.
Destructor
~Name(): runs when the value goes away, to clean up.
RAII
Tying a resource (memory, a file) to a value, freed by its destructor.
Data structures50 / 201
Module 5 · Linked lists
std::unique_ptr owns what it points at
std::unique_ptr<Node> (from <memory>) is a pointer with a destructor: when it goes away, it deletes its node. Exactly one unique_ptrowns each node, so it cannot be copied, only moved: std::move(a) hands the node over and leaves a empty. std::make_unique<Node>(…) makes a node on the heap, and .get() gives a plain pointer for looking, without owning.
C++
1#include<memory>23structNode{4 std::string value;5 std::unique_ptr<Node> next; // owns the next node6Node(const std::string& v):value(v){}7};89auto head = std::make_unique<Node>("Intro");10head->next = std::make_unique<Node>("Waves");11Node* second = head->next.get(); // looks, does not own1213std::unique_ptr<Node> other = std::move(head); // head is now empty14// No delete anywhere: when other goes, Intro goes, then Waves.
QuizInteractive · disabled in the PDF
Which of these does not compile?
Which of these does not compile?
Freeing a long chain can overflow the stack. When a node goes, its next goes, which frees itsnext, and so on: one destructor call inside another, as deep as the list is long. In this page, a list of 100 000 such nodes crashes with a stack overflow on its way out. A list class therefore frees its nodes in a loop: while (head) head = std::move(head->next); frees one node per turn, with no nesting.
Data structures51 / 201
Module 5 · Linked lists
Adding at either end is one step, with a tail pointer
To prepend (add at the front), make a node whose next is the old head, and make it the head. Nothing moves, whatever the length: O(1). Compare inserting at the front of a vector, which moves every value.
To append (add at the end), you need the last node. Walking there is O(n), so a list class also keeps a tail pointer, and appending becomes: point the old tail at the new node, then move tail to it. O(1) again. The head owns the first node, each node owns the next, and tail is a plain Node*: it only looks.
Make a node for Coda; its next is empty.
The old tail’s next now owns Coda.
Move the list’s tail to Coda. No walking needed.
Appending Coda with a tail pointer.
The empty list is the edge case. When the list is empty, there is no old tail to point from: the new node becomes the head and the tail. Most linked-list bugs are a missed case where the head or the tail should have changed too.
Data structures52 / 201
Module 5 · Linked lists
Try it: a list class with a tail
Code challengeInteractive · disabled in the PDF
Finish append(value) on LinkedList: add a node at the end in O(1) using tail, handling the empty list, and add 1 to size.
To remove a node you make the node before it skip over it: previous->next = std::move(previous->next->next). The removed node has lost its owner, so its unique_ptr deletes it on the spot. Finding the node is O(n); the unlinking itself is O(1), with no shifting at all.
Lines 1-2: It takes the list’s head by reference, because the head itself may change.
Lines 3: An empty list: nothing to remove.
Lines 4-7: Removing the head: the second node becomes the head. There is no node before it.
Lines 8-9: Otherwise walk with previous, always looking one node ahead.
Lines 10-13: The next node is the one: skip over it (which frees it), and stop.
Lines 16: Not found: nothing changed.
Find the bugInteractive · disabled in the PDF
In a list class with a tail, removing the last node leaves things broken. Which line is missing a step?
C++
Data structures54 / 201
Module 5 · Linked lists
Try it: remove every match
A to-do list stored as nodes: remove every task with a given value. A neat trick avoids the special case for the head: put a sentinel, a dummy node, in front of it, so every real node has a node before it. Here the sentinel is an ordinary local variable, and &sentinel is its address.
Code challengeInteractive · disabled in the PDF
Finish removeAll(head, value): remove every node holding value. Walk from the sentinel with previous, unlinking as you go.
C++
1#include<memory>2#include<string>34structNode{5 std::string value;6 std::unique_ptr<Node> next;7Node(const std::string& v):value(v){}8};910voidremoveAll(std::unique_ptr<Node>& head,11const std::string& value){12Nodesentinel("");13 sentinel.next = std::move(head);14Node* previous =&sentinel;15 // TODO: skip each next node that holds value16 head = std::move(sentinel.next);17}
Data structures55 / 201
Module 5 · Linked lists
A doubly linked list points both ways
In a doubly linked list each node also has a prev pointer to the node before it. You can walk backwards, and, given a node, remove it in O(1) without searching for its neighbour: node->prev->next and node->next->prev are updated to skip it. The price is one more pointer per node, and two to keep right on every change. The prev pointers are plain pointers: if both directions owned, each node would own the other, and neither would ever be freed.
Browser history: Back follows prev, Forward follows next.
QuizInteractive · disabled in the PDF
You hold a pointer to a node in the middle of a long list. Removing it is O(1) in which list?
You hold a pointer to a node in the middle of a long list. Removing it is O(1) in which list?
Data structures56 / 201
Module 5 · Linked lists
Linked lists against arrays
Operation
std::vector
Linked list
Read the i-th value
O(1)
O(n): follow i pointers
Add or remove at the front
O(n): shift
O(1)
Add at the end
O(1) amortised
O(1) with a tail
Insert or remove next to a node you hold
O(n): shift
O(1)
Search for a value
O(n)
O(n)
Extra memory per value
None, or spare capacity
One or two pointers, plus the heap’s own notes
Why vectors usually win anyway. Vector values sit side by side, so one cache line brings in many of them. Linked nodes are scattered over the heap, so every ->next can be a slow trip to main memory: pointer chasing. Each node is also its own trip to the heap’s allocator when it is made and freed. For most lists, a vector is faster even at the operations a linked list wins on paper. Linked lists earn their place when you hold pointers to nodes and splice them in and out, as LRU caches and memory allocators do.
The standard library has both kinds ready-made: std::list<T> is doubly linked and std::forward_list<T> singly linked. Their positions are iterators, as with vectors, and inserting or erasing at an iterator (for std::forward_list, just after one) is O(1).
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<list>3#include<string>45intmain(){6 std::list<std::string> tasks ={"shop","cook","eat"};7 tasks.push_front("plan"); // O(1), nothing moves8 tasks.push_back("wash up");910auto it = tasks.begin();11++it; // now at "shop"12 tasks.insert(it,"list"); // O(1): before "shop"1314int step =0;15for(const std::string& t : tasks){16 std::cout <<++step <<" "<< t <<"\n";17}18}
Try tasks[2]: it does not compile, because a list has no cheap way to reach the third node. std::next(tasks.begin(), 2) walks there instead.
Data structures57 / 201
Module 5 · Linked lists
Recap
A pointer holds an address: &x takes one, *p and p->member follow one, nullptr is none.
A linked list is nodes, each holding a value and a pointer to the next; the last points to nullptr.
Nodes live on the heap. new and delete by hand leak or dangle; std::unique_ptr deletes for you.
A class keeps its members private, sets up in a constructor and cleans up in a destructor (RAII).
Traversal follows next from the head until nullptr: O(n).
Prepending is O(1); appending is O(1) too with a tail pointer. A sentinel avoids the head special case.
Doubly linked nodes also point back, so a node you hold can be removed in O(1).
Vectors index in O(1) and are cache-friendly; linked lists win at splicing nodes you hold.
FlashcardsInteractive · disabled in the PDF
Node
A value plus a pointer to the next node.
p->value
The value member of what p points at: (*p).value.
Stack vs heap
Stack: a function’s own variables, gone when it returns. Heap: kept until freed.
Memory leak
Heap memory nobody frees.
Dangling pointer
A pointer to memory that has already been freed.
std::unique_ptr
The single owner of a heap value; it deletes the value when it goes.
std::move
Hands a value’s contents over, leaving the old one empty.
Destructor
~Name(): runs automatically when a value goes away.
Sentinel node
A dummy node before the head, so every real node has one before it.
Doubly linked list
Nodes point to both next and prev; std::list is one.
Pointer chasing
Following pointers scattered in memory: slow for the cache.
Module 6: Stacks
Data structures58 / 201
Module 6 · Stacks
A stack: last in, first out
Think of a pile of plates. You put a clean plate on top, and you take the top one off. You never pull one from the middle. A stack is a collection with exactly that rule: the last value in is the first value out, LIFO for short. It has three operations, all O(1): push (put on top), pop (take the top off) and peek (look at the top without taking it).
Blue platetop: pushed last, popped first
Green plate
White platebottom: pushed first
A stack of plates, top first. Push adds above the top; pop takes the top.
Stack
A collection where you add and remove only at the top.
LIFO
Last in, first out.
Push
Put a value on top.
Pop
Remove the top value and return it.
Peek
Look at the top value without removing it.
Data structures59 / 201
Module 6 · Stacks
A vector’s end is a ready-made top
The end of a std::vector is a perfect top: push_back and pop_back work there without moving anything, O(1) amortised, and back() peeks. Note that pop_back() returns nothing: read back() first if you want the value. Calling either on an empty vector is a mistake, which this page stops with an error.
C++
1std::vector<std::string> plates;2plates.push_back("white");3plates.push_back("green");4plates.push_back("blue");5std::cout << plates.back(); // blue: a peek6plates.pop_back(); // blue is gone7std::cout << plates.back(); // green
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1std::vector<int> s;2s.push_back(1);3s.push_back(2);4s.pop_back();5s.push_back(3);6s.push_back(4);7s.pop_back();8for(int x : s) std::cout << x <<' ';
What does this print?
Data structures60 / 201
Module 6 · Stacks
A template writes code once, for any type
A stack of plates and a stack of numbers work the same way, and only the type of value differs. A template is code with a type left open: template <typename T> says "T is some type, chosen by whoever uses this". std::vector<int> is a template you have used all along. When you write Stack<int>, the compiler makes a copy of the code with T replaced by int.
C++
1template<typenameT>2Tlargest(const std::vector<T>& values){3T best = values[0];4for(constT& v : values){5if(v > best) best = v;6}7return best;8}910largest(std::vector<int>{3,9,4}); // 9: T is int11largest(std::vector<std::string>{"fig","kiwi"}); // "kiwi"
It works for any T that can be copied and compared with >. Strings compare A to Z.
Code with one or more types left open, filled in where it is used.
`template <typename T>`
Opens a template with a type named T.
Class template
A class written for any T, such as std::vector<T>.
Data structures61 / 201
Module 6 · Stacks
Try it: a stack class
What should pop give when the stack is empty? C++ code often answers with std::optional<T> (from <optional>): a box that holds either a T or nothing, std::nullopt. return value; fills it; opt.has_value() asks whether it is full, and *opt takes the value out.
Code challengeInteractive · disabled in the PDF
Finish the class template Stack<T> on a private vector: push, pop and peek (both give std::nullopt when empty), size() and empty().
The head of a linked list is another good top: pushing is prepending, and popping is taking the head off. Both are O(1) every time, not just amortised, because nothing is ever copied. The cost is a heap node per value. A struct can be declared inside a class, as Node is here: it is then ListStack<T>::Node, private to it.
Fill the blanksInteractive · disabled in the PDF
Push onto, and pop from, the head of a list.
C++
1template<typenameT>2classListStack{3structNode{4T value;5 std::unique_ptr<Node> next;6};7 std::unique_ptr<Node> top;89public:10voidpush(constT& value){11 top = std::make_unique<Node>(Node{value, std::move()});12}1314 std::optional<T>pop(){15if(top ==)return std::nullopt;16T value = top->value;17 top = std::move(top->);18return value;19}20};
Same interface, different inside. Both stacks offer the same push, pop and peek, so code using a stack does not care which one it has. Choosing what a structure does separately from how it is built is called an abstract data type: "stack" is the promise, the vector or list is the implementation.
Data structures63 / 201
Module 6 · Stacks
The standard library’s std::stack
std::stack<T> (from <stack>) is exactly that idea: it wraps another container and lets you use only its top. push(x), top() to peek, pop() to remove (it returns nothing, like pop_back), empty() and size(). Inside it is a std::deque unless you choose, as in std::stack<int, std::vector<int>>. Like the vector, it trusts you not to top() or pop() when empty.
The plates come out in reverse order. Try std::stack<std::string, std::vector<std::string>>.
Data structures64 / 201
Module 6 · Stacks
The call stack keeps track of running functions
When a function calls another, the program must remember where to come back to. It keeps a call stack: each call pushes a frame holding the function’s local variables and where it was called from. This is the stack of module 5, where local variables live: it is a stack in exactly this sense. When a function returns, its frame is popped and the caller carries on. The function running now is always the one on top.
Calls go down the page; returns come back up in reverse order.
Put in orderInteractive · disabled in the PDF
checkout calls total, which calls tax. Put what happens to the call stack in order.
1.Push a frame for checkout
2.Pop checkout’s frame
3.Push a frame for tax
4.Pop total’s frame as it returns
5.Push a frame for total
6.Pop tax’s frame as it returns
Stack overflow. The call stack has a fixed amount of memory. A function that calls itself without ever stopping pushes frames until it runs out, and a C++ program simply crashes: there is no error to catch. Programs usually get 1 to 8 MB of stack (8 MB in this page), so the limit is somewhere from thousands to hundreds of thousands of frames, depending on how big each frame is. That limit is why very deep recursion is often rewritten with a stack of your own, which lives in ordinary memory and can grow far larger.
Data structures65 / 201
Module 6 · Stacks
Undo and redo are two stacks
A text editor pushes the old text onto an undo stack before each change. Undo pops the most recent state back, and pushes the current one onto a redo stack. Redo does the opposite. Any new edit clears the redo stack, because the future it remembered no longer follows from the present.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<string>3#include<vector>45std::string text;6std::vector<std::string> undos, redos;78voidtype(const std::string& more){9 undos.push_back(text);10 redos.clear();11 text += more;12}13voidundo(){14if(undos.empty())return;15 redos.push_back(text);16 text = undos.back();17 undos.pop_back();18}1920intmain(){21type("Buy ");type("flour");undo();type("eggs");22 std::cout << text <<" ("<< undos.size()<<" undos, "23<< redos.size()<<" redos)\n";24}
Write a redo() that mirrors undo(), and try undo, undo, redo.
Find the bugInteractive · disabled in the PDF
After undo then a new edit, redo brings back text that no longer fits. Which line is missing a step?
C++
Data structures66 / 201
Module 6 · Stacks
Matching brackets with a stack
Are the brackets in { a: [1, (2)] } balanced? Each closing bracket must match the most recent opening bracket that is still open: last opened, first closed. That is a stack. Push each opener; at each closer, pop and check they are a pair. At the end, nothing may be left open.
WalkthroughInteractive · disabled in the PDF
Text
1input: ( [ ] { ) }23( push stack: (4[ push stack: ( [5] pop [, ok stack: (6{ push stack: ( {7) pop {, but ) closes ( : not balanced
Lines 3-4: Openers go on the stack.
Lines 5:] pops [: a pair.
Lines 6:{ is now the most recently opened.
Lines 7:) pops {: not its partner, so the answer is no, even though the counts of each kind match.
Code challengeInteractive · disabled in the PDF
Finish isBalanced(text): true when every ), ] and } closes the most recent unclosed opener of its kind, and nothing is left open. Other characters are ignored. partnerOf gives a closer’s opener.
C++
1#include<stack>2#include<string>34charpartnerOf(char closer){5if(closer ==')')return'(';6if(closer ==']')return'[';7return'{';8}910boolisBalanced(const std::string& text){11 std::stack<char> open;12for(char ch : text){13 // TODO: push openers; for a closer, check the top and pop14}15returntrue;16}
Data structures67 / 201
Module 6 · Stacks
Recap
A stack adds and removes only at the top: last in, first out.
push, pop and peek are O(1), on a vector’s end or a linked list’s head; pop returns std::optional so an empty stack is not an error.
A class template (template <typename T>) writes the stack once for every type; std::stack is the library’s.
A stack is an abstract data type: a promise of operations, whatever is inside.
The call stack holds a frame per running function; too many and it overflows.
Undo and redo are two stacks; a new edit clears redo.
Matching brackets is a stack problem: each closer pairs with the latest opener.
FlashcardsInteractive · disabled in the PDF
LIFO
Last in, first out: the rule of a stack.
Push, pop, peek
Add on top; remove the top; look at the top. All O(1).
Abstract data type
What a structure promises to do, apart from how it is built.
Call stack
The program’s stack of frames, one per function call still running.
Frame
A function call’s local variables and where to return to.
Stack overflow
Running out of call-stack space, usually from endless recursion.
Template
Code written once for any type T; the compiler makes a copy per type used.
std::optional<T>
A T, or nothing (std::nullopt): how to say "no value" without an error.
std::stack
The library’s stack: push, top, pop (returns nothing), empty, size.
Module 7: Queues
Data structures68 / 201
Module 7 · Queues
A queue: first in, first out
At a bakery, people join at the back of the queue and are served from the front. Whoever arrived first is served first: FIFO. A queue is a collection with that rule. Its operations are enqueue (join at the back), dequeue (leave from the front, returning the value) and peek (look at who is at the front).
People join on the right and are served on the left.
Queue
A collection where values join at the back and leave from the front.
FIFO
First in, first out.
Enqueue
Add a value at the back.
Dequeue
Remove the value at the front and return it.
QuizInteractive · disabled in the PDF
Enqueue Ada, Bo and Cy; dequeue once; enqueue Di; dequeue once. Who is at the front now?
Enqueue Ada, Bo and Cy; dequeue once; enqueue Di; dequeue once. Who is at the front now?
Data structures69 / 201
Module 7 · Queues
Why erasing a vector’s front makes a slow queue
The obvious queue is a vector: push_back to enqueue, erase(v.begin()) to dequeue. It works, but erasing removes the first slot, and a vector keeps its slots side by side from index 0, so every remaining value moves one slot left. Dequeuing from a queue of n is O(n), and serving everyone is O(n²). (This is why std::vector has no pop_front: it would look cheap and not be.)
People in the queue
Moves for one dequeue
Moves to serve everyone
10
9
45
1 000
999
about 500 000
100 000
99 999
about 5 000 000 000
Fine when small. For a queue that never holds more than a few dozen values, erasing the front is perfectly good, and simple: moving a few dozen values that sit side by side is very fast. The fixes on the next slides matter when queues get long: a job queue on a server, or a search through a big map.
Data structures70 / 201
Module 7 · Queues
Don’t move the people, move the front
Instead of moving everyone up after each customer, the baker can just call the next ticket number. Keep the values where they are and remember a head index: the position of the front. Dequeue reads items[head] and adds one to head. Nothing moves, so it is O(1).
WalkthroughInteractive · disabled in the PDF
C++
1std::vector<std::string> items;2int head =0;34items.push_back("Ada"); // enqueue5items.push_back("Bo");6std::string served = items[head]; // "Ada"7head++; // dequeue: the front moves on8int waiting = items.size()- head; // 1
Lines 1-2: The values, and the index of the front.
Lines 4-5: Enqueue is an ordinary push_back onto the end.
Lines 6-7: Dequeue reads the front and moves head along. Ada’s slot is not reused.
Lines 8: The queue is everything from head to the end.
Dead slots pile up. Slots before head still hold old values, so a long-running queue keeps growing. The fix: once more than half the vector is dead, erase the dead part in one go, items.erase(items.begin(), items.begin() + head), and reset head to 0. That copy is O(n) but rare, so dequeue stays O(1) amortised.
Data structures71 / 201
Module 7 · Queues
Try it: a queue with a head index
Code challengeInteractive · disabled in the PDF
Finish Queue<T>: dequeue() returns the front value (or std::nullopt when empty) by moving head, without erase; size() counts what is left.
C++
1#include<optional>2#include<vector>34template<typenameT>5classQueue{6public:7voidenqueue(constT& value){ items.push_back(value);}89 std::optional<T>dequeue(){10if(items.empty())return std::nullopt;11T value = items.front();12 items.erase(items.begin()); // moves everyone: slow13return value;14}1516intsize()const{return0;}1718private:19 std::vector<T> items;20int head =0;21};
Data structures72 / 201
Module 7 · Queues
A ring buffer reuses its slots
A ring buffer (or circular buffer) has a fixed number of slots, its capacity, and treats them as a circle: after the last slot comes slot 0 again. It keeps the front’s index, head, and how many values it holds, count. The next free slot is (head + count) % capacity, where % is the remainder after dividing two whole numbers: it wraps the index back round to 0.
A was dequeued, so head is 1 and B is at the front.
C and D follow in slots 2 and 3.
E goes in (1 + 3) % 4 = 0: the slot A left free. The ring is full.
A ring of 4 slots after enqueuing A, B, C, dequeuing A and enqueuing D, then E.
Fill the blanksInteractive · disabled in the PDF
Work out where the next value goes in a ring of capacity 5.
C++
1// head = 3, count = 2: the queue is in slots 3 and 42int next =(head + count) capacity; //
Data structures73 / 201
Module 7 · Queues
Try it: a ring buffer
Ring buffers are used where memory is fixed and data keeps flowing: audio, keyboard input, the last 100 log lines. They never touch the heap after they are made, which matters in code that must never pause. When it is full, this one refuses new values.
Code challengeInteractive · disabled in the PDF
Finish enqueue (put the value at (head + count) % capacity; return false if full) and dequeue (take from head, wrap it, or std::nullopt if empty).
A deque (said "deck", short for double-ended queue) lets you add and remove at the front and the back, all in O(1). It is a stack and a queue at once. A ring buffer becomes a deque by also moving head backwards: head = (head - 1 + capacity) % capacity. std::deque<T> (from <deque>) is the library’s: push_front, push_back, pop_front, pop_back, front(), back(), and even d[i] in O(1).
Structure
Add
Remove
Typical use
Stack
top
top
Undo, brackets, the call stack
Queue
back
front
Tasks in arrival order, printing
Deque
either end
either end
Recent history with a limit, sliding windows
QuizInteractive · disabled in the PDF
A music app keeps your last 50 songs: each new song goes at the front, and the oldest falls off when there are more than 50. What fits best?
A music app keeps your last 50 songs: each new song goes at the front, and the oldest falls off when there are more than 50. What fits best?
Inside `std::deque`. A std::deque is not one block of memory. It keeps its values in fixed-size blocks (a few kilobytes each in libc++), plus a small array of pointers to the blocks. Growing at either end adds a block when the end one is full, and never moves the values already there, so pointers to them stay valid. d[i] is two steps: which block, then where in it. That second step, and the blocks being apart, make it a little slower than a vector to walk through.
Data structures75 / 201
Module 7 · Queues
Queues everywhere
Printing: documents print in the order they were sent.
Servers: jobs such as sending emails wait in a queue for a free worker.
Event loops: programs with windows, and browsers, keep a queue of events (clicks, key presses, messages) and handle them one at a time.
Searching: breadth-first search, in the trees and graphs modules, visits places nearest first by queueing what it finds.
std::queue<T> (from <queue>) is the library’s queue, a wrapper like std::stack with a std::deque inside: push at the back, front() to peek, pop() from the front (returning nothing), empty() and size().
Make two people arrive in minute 2. Who is served last?
Put in orderInteractive · disabled in the PDF
In a program with an event loop, a button is clicked while a long function is running. Put what happens in order.
1.The long function keeps running
2.The click waits in the event queue
3.The function returns to the event loop
4.The click handler runs
5.The loop dequeues the click
Data structures76 / 201
Module 7 · Queues
Recap
A queue adds at the back and removes from the front: first in, first out.
push_back plus erase(v.begin()) is a queue, but the erase is O(n); fine for small queues only.
A head index makes dequeue O(1): move the front instead of the values.
A ring buffer reuses a fixed set of slots, wrapping indexes with %.
A deque adds and removes at both ends in O(1): std::deque.
std::queue and std::stack wrap a std::deque and allow only their own ends.
Event queues, job queues and breadth-first search are all queues.
FlashcardsInteractive · disabled in the PDF
FIFO
First in, first out: the rule of a queue.
Enqueue, dequeue
Add at the back; remove from the front.
Why is erasing a vector’s front slow?
Every remaining value moves one slot left: O(n).
Head index
The position of the front, moved on each dequeue instead of moving values.
Ring buffer
Fixed slots used as a circle; the next slot is (head + count) % capacity.
%
The remainder after dividing: 5 % 4 is 1. Wraps indexes round.
Deque
A double-ended queue: add and remove at either end. std::deque.
std::queue
push, front, pop (returns nothing), empty: a queue on a deque.
Module 8: Hash tables
Data structures77 / 201
Module 8 · Hash tables
Jump straight to the right drawer
At a theatre cloakroom you hand in your coat and get ticket 42; later, the attendant walks straight to hook 42. No searching. A hash table does the same for any key, such as a name or an ISBN. A hash function turns the key into a number, that number picks a bucket (a slot in a vector), and the value is stored there. Looking the key up later repeats the sum and goes to the same bucket.
You want the price of Dune.
The hash function does a little arithmetic on the letters.
The result, made smaller than the number of buckets, is 5.
Read bucket 5, where set put Dune earlier. No other bucket is looked at.
The key is turned into an index, and a vector read does the rest.
Key
What you look a value up by: a name, an ISBN.
Hash function
Turns a key into a number, always the same number for the same key.
Bucket
One slot of the table’s vector, holding the entries whose keys land there.
Entry
A key together with its value.
Data structures78 / 201
Module 8 · Hash tables
A simple hash function for strings
A good hash function is deterministic (the same key always gives the same number), quick, and spreads different keys evenly over the buckets. A classic one for strings goes byte by byte: multiply what you have so far by a small prime such as 31, add the byte’s value, and take the remainder by the number of buckets.
WalkthroughInteractive · disabled in the PDF
C++
1inthash(const std::string& key,int size){2unsigned h =0;3for(unsignedchar ch : key){4 h =(h *31+ ch)% size;5}6return h;7}89hash("tea",8) // 0: t gives 4, then e gives 1, then a gives 0
Lines 1: It takes the key and the number of buckets, and returns an index from 0 to size - 1.
Lines 2:unsigned is a whole number that is never negative. Its arithmetic wraps round instead of overflowing, which is what hashing wants.
Lines 3: Every byte takes part, so changing any letter usually changes the bucket.
Lines 4: A byte is already a number (t is 116). Multiplying by 31 mixes in the order of letters.
Lines 4: Taking % size at every step keeps h small, so it stays far from the largest unsigned.
Lines 9: The same key gives the same bucket, every time.
QuizInteractive · disabled in the PDF
Why would std::rand() % size, a random number, be a useless hash function?
Why would std::rand() % size, a random number, be a useless hash function?
Data structures79 / 201
Module 8 · Hash tables
Two keys, one bucket: collisions
There are endless possible keys and only so many buckets, so some keys must share a bucket. That is a collision, and every hash table has to deal with it. There are two main ways.
Chaining: each bucket holds a small list of entries. A look-up hashes to the bucket, then searches that short list for the key.
Open addressing: each bucket holds at most one entry. If a key’s bucket is taken, try the next one along (linear probing), and the next, until you find the key or an empty bucket.
buckets
[0]empty
[1]
tea → 2.50
jam → 3.10
[2]
bun → 1.20
[3]empty
Chaining, with 4 buckets: say tea and jam both hash to bucket 1. It holds both.
In C++ a chained table can be a vector of buckets, each bucket a small vector of entries, and each entry a std::pair<std::string, double> (from <utility>): two values in one, read as .first (the key) and .second (the value).
Deleting with open addressing. With linear probing you cannot simply empty a bucket on delete: a later key that probed past it would become unreachable, because the search stops at the first empty bucket. Deleted buckets get a tombstone marker instead, meaning "keep looking". Open addressing keeps every entry in one flat array, which the cache loves: the fastest C++ hash tables (outside the standard library) use it.
Data structures80 / 201
Module 8 · Hash tables
Try it: a hash map with chaining
Each bucket is a vector of Entry pairs. set replaces the value if the key is already in its bucket, and adds a pair otherwise. To change an entry inside the bucket, loop over it by reference, for (Entry& entry : bucket): a plain Entry entry would change a copy.
Code challengeInteractive · disabled in the PDF
Finish set and get. Both find the key’s bucket with hash(key, buckets.size()) and search only that bucket.
C++
1#include<optional>2#include<string>3#include<utility>4#include<vector>56inthash(const std::string& key,int size){7unsigned h =0;8for(unsignedchar ch : key) h =(h *31+ ch)% size;9return h;10}1112template<typenameV>13classHashMap{14public:15usingEntry= std::pair<std::string,V>;16 std::vector<std::vector<Entry>> buckets =17 std::vector<std::vector<Entry>>(8);1819voidset(const std::string& key,constV& value){20 // TODO: replace the key's value, or add a pair21}2223 std::optional<V>get(const std::string& key)const{24return std::nullopt;25}26};
Data structures81 / 201
Module 8 · Hash tables
Deleting an entry
With chaining, deleting is: find the key’s bucket, find the entry in it, and take it out. The order inside a bucket does not matter, so there is a cheap way to take an entry out of a vector: copy the last entry over it, then pop_back(). Nothing shifts. A table also keeps a size, the number of entries, which set raises only for new keys and erase lowers only when something was really removed.
Code challengeInteractive · disabled in the PDF
Finish erase(key): remove the key’s entry from its bucket (copy the last entry over it, then pop_back()), lower size, and return true; return false if the key was not there.
This table grows when it gets full. Afterwards, get misses keys that are there. Which line is wrong?
C++
Data structures82 / 201
Module 8 · Hash tables
Load factor and resizing
The load factor is entries divided by buckets: how full the table is. With 8 buckets and 100 entries it is 12.5, so every look-up searches a list of about 12. To keep buckets short, a table resizes when the load factor passes a limit (0.75 is common; std::unordered_map uses 1.0): it makes about twice as many buckets and rehashes every entry, because hash(key, 16) differs from hash(key, 8).
PlaygroundInteractive · disabled in the PDF
C++
1#include<algorithm>2#include<iostream>3#include<string>4#include<vector>56inthash(const std::string& key,int size){7unsigned h =0;8for(unsignedchar ch : key) h =(h *31+ ch)% size;9return h;10}1112intmain(){13 std::vector<std::string> keys;14for(int i =0; i <1000; i++){15 keys.push_back("book"+ std::to_string(i));16}1718for(int n : {8,64,512,2048}){19 std::vector<int>lengths(n);20for(const std::string& k : keys) lengths[hash(k, n)]++;21int longest =*std::max_element(lengths.begin(), lengths.end());22 std::cout << n <<" buckets: longest has "<< longest <<"\n";23}24}
Change 31 to 1 in the hash, and run it again. What happens to the longest bucket? (std::max_element gives an iterator to the largest value; * reads it.)
QuizInteractive · disabled in the PDF
Resizing copies every entry: O(n). Why is set still called O(1)?
Resizing copies every entry: O(n). Why is set still called O(1)?
Data structures83 / 201
Module 8 · Hash tables
Average O(1), worst case O(n)
With a hash function that spreads keys well and a load factor kept small, each bucket holds a handful of entries, so get, set and erase are O(1) on average. But if many keys land in one bucket, that bucket is a list to search: O(n) in the worst case.
Predict the outputInteractive · disabled in the PDF
With this hash, where do the keys tea, jam and bun go?
With this hash, where do the keys tea, jam and bun go?
Hash flooding. If an attacker knows your hash function, they can send thousands of keys that all collide, turning every look-up into an O(n) search and slowing a server to a crawl. Some languages defend by mixing a random seed into the hash when the program starts (Python and Rust do), or by turning long buckets into balanced trees (Java’s HashMap). The standard std::hash for strings in the common C++ libraries has no random seed (a string hashes the same way in every run), so a C++ server that takes keys from strangers may want a seeded hash of its own.
Data structures84 / 201
Module 8 · Hash tables
Recap
A hash table turns a key into a bucket index with a hash function, then reads that bucket.
A hash function must be deterministic, and should be quick and spread keys evenly.
Collisions are certain: chaining keeps a list per bucket; open addressing probes for a free bucket.
Load factor is entries per bucket; past a limit, double the buckets and rehash everything.
Get, set and erase are O(1) on average, O(n) in the worst case.
std::unordered_map is a chained hash table: the next module puts it to work.
FlashcardsInteractive · disabled in the PDF
Hash function
Turns a key into a number; the same key always gives the same number.
Bucket
A slot of the table’s vector, holding the entries whose keys land there.
Collision
Two different keys landing in the same bucket.
Chaining
Each bucket keeps a list of entries.
Linear probing
If a bucket is taken, try the next one along.
Load factor
Entries divided by buckets: how full the table is.
Rehash
Recompute every key’s bucket after changing the number of buckets.
Tombstone
A marker for a deleted slot in open addressing: keep probing past it.
std::pair<A, B>
Two values in one: .first and .second.
Swap and pop
Remove from a vector in O(1) by copying the last value over it, when order does not matter.
Module 9: Maps and sets
Data structures85 / 201
Module 9 · Maps and sets
Four containers for looking things up
A dictionary (or map) stores values under keys, like the ISBN card box from module 1. A set stores keys alone, each once. The standard library gives you each in two flavours: one built on a hash table, like the one you built in the last module, and one built on a balanced tree, which keeps its keys sorted (the trees modules show how).
Hash table: fast, no order
Balanced tree: sorted
Key → value
std::unordered_map<K, V>
std::map<K, V>
Keys only
std::unordered_set<K>
std::set<K>
Look up, add, remove
O(1) on average
O(log n), always
Walking through it
In no useful order
Smallest key to largest
Header
<unordered_map>, <unordered_set>
<map>, <set>
QuizInteractive · disabled in the PDF
A leaderboard must list players by name, A to Z, and find any player quickly. Which fits?
A leaderboard must list players by name, A to Z, and find any player quickly. Which fits?
Data structures86 / 201
Module 9 · Maps and sets
std::unordered_map: a ready-made hash table
m[key] = value sets a value. m.contains(key) (C++20) asks whether a key is there, and m.count(key) says how often, which for a map is 0 or 1. m.find(key) gives an iterator to the entry, or m.end() if it is missing; the entry is a pair, so it->first is the key and it->second the value. m.erase(key) removes it.
const auto& [item, count] is a structured binding: it unpacks each pair into two names.
Fill the blanksInteractive · disabled in the PDF
Record a sale: take one scone out of stock, and drop the entry when none are left.
C++
1auto it = stock.("scone");2if(it != stock.()){3 it->second--;4if(it->second ==0) stock.(it);5}
Data structures87 / 201
Module 9 · Maps and sets
m[key] adds the key if it is missing
Reading m[key] for a key that is not there does not fail: it inserts the key with a default value (0 for numbers, an empty string, an empty vector) and gives you that. It is exactly right for counting, counts[word]++, and a surprise everywhere else. To only ask, use contains, count or find, which never change the map. On a const map, [] does not even compile.
Predict the outputInteractive · disabled in the PDF
Walking an unordered_map visits its buckets in turn, so the order depends on the hash function and the number of buckets, and can change completely when the table grows. Never rely on it. A std::map walks its keys from smallest to largest, always. Neither remembers the order you added things: for that, keep a vector of keys alongside.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<map>3#include<string>4#include<unordered_map>56intmain(){7 std::unordered_map<std::string,int> hashed;8 std::map<std::string,int> sorted;9for(std::string name : {"Cy","Ada","Eve","Bo","Di"}){10 hashed[name]= name.size();11 sorted[name]= name.size();12}1314for(constauto&[name, n]: hashed) std::cout << name <<' ';15 std::cout <<"\n";16for(constauto&[name, n]: sorted) std::cout << name <<' ';17 std::cout <<"\n";18}
Add more names, and see the first line shuffle while the second stays A to Z.
What is inside each.std::map and std::set are red-black trees, a kind of balanced binary search tree (the balanced trees module builds the idea). std::unordered_map chains its entries: each entry is its own heap node, linked into its bucket. Both are made of separate nodes, which means one heap allocation per entry and pointer chasing on every look-up. The upside: an entry never moves, so a pointer or reference to it stays good while other keys come and go.
Data structures89 / 201
Module 9 · Maps and sets
Sets: each value once
A set keeps each value once: inserting a value that is already there does nothing. s.insert(x) returns a pair whose .second is true if x was new. s.contains(x) is O(1) on average for std::unordered_set, where std::find on a vector is O(n). That makes it the tool for "have I seen this before?" and for removing duplicates.
C++
1std::unordered_set<std::string> tags ={"tea","cake","tea"};2std::cout << tags.size(); // 2: the second tea is dropped3bool added = tags.insert("cake").second; // false: already there4std::cout << tags.contains("tea"); // 1: true56std::vector<std::string> words ={"b","a","b"};7std::set<std::string>unique(words.begin(),words.end()); // a, b
A set can be made from any range of values: the two iterators mark where it starts and ends.
Code challengeInteractive · disabled in the PDF
Finish inBoth(a, b): the titles on both reading lists, in a’s order, each once. Put b in an unordered_set so each check is O(1), and use another set to skip titles already added.
C++
1#include<string>2#include<unordered_set>3#include<vector>45std::vector<std::string>inBoth(const std::vector<std::string>& a,6const std::vector<std::string>& b){7 std::vector<std::string> result;8 // TODO: a set of b, and a set of what you've already added9return result;10}
Data structures90 / 201
Module 9 · Maps and sets
Grouping and counting with a map
Two everyday jobs fit a map perfectly: counting (words in a review: key is the word, value is a count) and grouping (books by author: key is the author, value is a list of titles). Because m[key] starts a missing key at 0 or at an empty vector, each is one line per item: counts[word]++, or groups[author].push_back(title).
Find the bugInteractive · disabled in the PDF
Every count comes out one too high. Which line is wrong?
C++
Code challengeInteractive · disabled in the PDF
Finish byAuthor(books): a std::map from each author to their titles, in the order the books came.
C++
1#include<map>2#include<string>3#include<vector>45structBook{6 std::string title;7 std::string author;8};910std::map<std::string, std::vector<std::string>>11byAuthor(const std::vector<Book>& books){12 std::map<std::string, std::vector<std::string>> groups;13 // TODO: add each title to its author's list14return groups;15}
Data structures91 / 201
Module 9 · Maps and sets
A sorted map answers "what comes next?"
A hash table can only answer "is this exact key here?". A sorted map can also answer questions about nearby keys, in O(log n): m.lower_bound(k) gives the first entry whose key is k or more, m.upper_bound(k) the first that is more than k, m.begin() the smallest and std::prev(m.end()) the largest. Bookings by time, prices by date, the next free seat: these need order.
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1std::map<int, std::string> bookings ={2{9,"Ada"},{11,"Bo"},{14,"Cy"}3};4auto next = bookings.lower_bound(10); // first booking from 10:005std::cout << next->first <<" "<< next->second;
What does this print?
Data structures92 / 201
Module 9 · Maps and sets
Choosing a container
You need
Use
Why
A record with known fields: a book’s title, author, price
A struct
Fixed members, side by side
Values in order, by position
std::vector
O(1) index, cache-friendly
Look-ups by key, nothing about order
std::unordered_map
O(1) on average
Look-ups by key, plus sorted order or "next key"
std::map
O(log n), sorted, lower_bound
"Have I seen this?", or no duplicates
std::unordered_set, or std::set for order
Unique by design
Your own key types. A std::map or std::set needs to compare keys with <, and an unordered one needs a hash function and ==. Strings and numbers come with both. A struct key needs you to supply them: an operator< for the tree (or auto operator<=>(const Key&) const = default; in C++20, which compares member by member), or a hash for the table.
A sorted vector often beats both. Because their entries are separate heap nodes, std::map and std::unordered_map are slower than their Big O suggests. For data that is built once and then only read, a sorted std::vector searched with std::lower_bound (binary search, from <algorithm>) is often faster than either: O(log n) like the tree, but over memory side by side. Libraries outside the standard also offer "flat" hash tables with open addressing, which are faster than std::unordered_map for the same reason.
Data structures93 / 201
Module 9 · Maps and sets
Recap
std::unordered_map and std::unordered_set are hash tables: O(1) on average, no order.
std::map and std::set are balanced trees: O(log n), always sorted by key.
m[key] inserts a default value when the key is missing: perfect for counting, a trap for asking.
Ask with contains, count or find (compare with end()); they never change the map.
insert(x).second says whether a value was new to a set.
Sorted maps answer "next key" questions with lower_bound and upper_bound.
For read-only data, a sorted vector with std::lower_bound is often fastest.
FlashcardsInteractive · disabled in the PDF
Dictionary
Values stored under keys; also called a map.
std::map vs std::unordered_map
A sorted tree, O(log n), vs a hash table, O(1) on average with no order.
m[key] on a missing key
Inserts the key with a default value (0, empty), then returns it.
m.find(key) == m.end()
The key is not in the map.
Structured binding
auto [k, v] = pair; unpacks a pair (or struct) into names.
std::set
Unique values kept sorted; std::unordered_set keeps them unsorted, faster.
lower_bound(k)
The first entry whose key is not less than k.
Red-black tree
The balanced binary search tree inside std::map and std::set.
Module 10: Trees
Data structures94 / 201
Module 10 · Trees
A tree branches out from one root
Lists put things in a line. Plenty of data branches instead: a book has chapters, chapters have sections, sections have paragraphs. A tree is that shape. It has one root at the top; every other node has exactly one parent above it and any number of children below it. Nodes with no children are leaves. Drawn on paper, computing trees grow downwards, root first.
Cookbookroot
Baking
Sconesleaf
Breadleaf
Soups
Leekleaf
Indexleaf
A cookbook as a tree. "Cookbook" is the root; the recipes are leaves.
Tree
Nodes joined so that each, except the root, has exactly one parent.
Root
The one node with no parent, at the top.
Parent, child
A node directly above another, and the node directly below.
Siblings
Children of the same parent: Baking, Soups and Index.
Leaf
A node with no children.
Edge
The link between a parent and a child.
Data structures95 / 201
Module 10 · Trees
Depth, height and subtrees
A node’s depth is how many edges lie between it and the root: the root has depth 0, its children 1, and so on. A tree’s height is the depth of its deepest leaf, the longest path down. Any node together with everything below it is itself a tree, a subtree: Baking with its two recipes is a subtree of the cookbook.
Cookbookdepth 0
Bakingdepth 1
Sconesdepth 2
Breaddepth 2
Soupsdepth 1
Leekdepth 2
Indexdepth 1
Depths, in grey. The height of this tree is 2.
QuizInteractive · disabled in the PDF
A Leek recipe gets two variations as its children: Leek and potato, Leek and pea. What is the cookbook’s height now?
A Leek recipe gets two variations as its children: Leek and potato, Leek and pea. What is the cookbook’s height now?
Data structures96 / 201
Module 10 · Trees
Trees are everywhere
Folders and files on your computer: a folder’s children are what is inside it.
A web page: the browser holds its HTML as a tree of elements, the DOM.
Menus: a menu holds items, some of which open sub-menus.
Code itself: the C++ compiler reads your program into a tree, a syntax tree, before it makes machine code.
recipes/
baking/
scones.md
bread.md
soups/
leek.md
index.md
The cookbook as folders: the same tree, drawn sideways.
QuizInteractive · disabled in the PDF
Which of these is not a tree?
Which of these is not a tree?
Data structures97 / 201
Module 10 · Trees
A tree node holds a vector of children
A node needs its value and its children, and a vector is the natural home for children: in order, any number, empty for a leaf. A struct may hold a std::vector of its own type, so each node simply contains its children: no pointers, and no delete, because each vector frees its nodes when it goes. Nested braces build the whole tree at once.
When nodes must stay where they are. A vector moves its values when it grows, so a pointer to a child can dangle after its parent gets one more child. When other code keeps pointers to nodes, or a node must know its parent, store children as std::vector<std::unique_ptr<TreeNode>>: each node then lives on its own on the heap and never moves, and the vector only holds the owning pointers.
Data structures98 / 201
Module 10 · Trees
Recursion: solve it for the children, then combine
Every child is the root of a smaller tree, so a question about a tree can often be answered by asking the same question of each child and combining the answers. A function that calls itself like this is recursive. It needs a base case that stops it: here, a leaf has no children, so the loop does not run and nothing deeper is asked.
WalkthroughInteractive · disabled in the PDF
C++
1intcountNodes(constTreeNode& node){2int total =1;3for(constTreeNode& child : node.children){4 total +=countNodes(child);5}6return total;7}89countNodes(cookbook) // 7
Lines 1:const TreeNode&: look at the node where it is, without copying its subtree.
Lines 2: Count this node itself.
Lines 3-4: Ask each child’s subtree how many nodes it has, and add them up. Each call is a new frame on the call stack.
Lines 3: For a leaf, children is empty, the loop never runs, and the answer is 1: the base case.
Lines 6: Return the total to whoever asked: the parent’s call, or you.
Lines 9: Cookbook asks Baking (3), Soups (2) and Index (1): 1 + 3 + 2 + 1 = 7.
Find the bugInteractive · disabled in the PDF
This always returns 1. Which line is wrong?
C++
Data structures99 / 201
Module 10 · Trees
Try it: leaves and height
Code challengeInteractive · disabled in the PDF
Finish countLeaves(node): a leaf counts 1; any other node’s count is the sum of its children’s counts.
Finish height(node): the number of edges on the longest path down to a leaf. A leaf has height 0; any other node is 1 more than its tallest child. std::max(a, b) gives the larger of two.
Visiting every node is a traversal. Depth-first traversal goes as deep as it can down one branch before backing up to the next, which is exactly what recursion does. The only choice is when you handle a node: preorder handles it before its children (good for printing a table of contents), postorder after them (good for anything that needs the children’s answers first, like a folder’s total size).
Move the std::cout line below the loop to get postorder, and see how the outline changes.
Data structures101 / 201
Module 10 · Trees
Breadth-first: level by level, with a queue
Breadth-first traversal visits the root, then all nodes at depth 1, then all at depth 2: level by level, like reading an org chart row by row. Recursion cannot do that, because it dives deep. A queue can: take the next node from the front, handle it, and add its children to the back. Children wait behind every node of the level before them. The queue holds pointers to nodes: queuing the nodes themselves would copy every subtree.
Lines 3-4: A std::queue of pointers, starting with the root’s address: &root.
Lines 5-7: While anyone is waiting, take the node at the front.
Lines 8: Handle it. node->value because node is a pointer.
Lines 9-11: Its children join the back, behind the rest of the current level.
Lines 15: All of depth 1 comes before any of depth 2.
Code challengeInteractive · disabled in the PDF
Finish levels(root): a vector per depth, each holding that level’s values left to right. Work one level at a time: the next level is all the current level’s children.
C++
1#include<string>2#include<vector>34structTreeNode{5 std::string value;6 std::vector<TreeNode> children;7};89std::vector<std::vector<std::string>>levels(constTreeNode& root){10 std::vector<std::vector<std::string>> result;11 std::vector<constTreeNode*> level ={&root};12 // TODO: while the level has nodes, record it, then move down13return result;14}
Data structures102 / 201
Module 10 · Trees
When a tree is too deep for recursion
Each recursive call adds a frame to the call stack, so recursion on a tree uses stack space in proportion to its height. Wide, shallow trees such as menus are no problem. A tree hundreds of thousands of levels deep (a very long chain, or badly built data) can overflow the stack, and in C++ that is a crash. The fix is to keep your own stack of nodes still to visit, in a vector on the heap, and loop.
Depth-first, with no recursion. Swap the stack for a queue and it becomes breadth-first.
QuizInteractive · disabled in the PDF
In the loop above, back() and pop_back() take the most recently added node. Which traversal does that give?
In the loop above, back() and pop_back() take the most recently added node. Which traversal does that give?
Freeing a deep tree recurses too. You may never write a recursive function and still recurse: when a TreeNode goes away, its vector destroys each child, which destroys its children, one destructor inside another, as deep as the tree is tall. It is the linked-list problem from module 5. A tree that can be very deep needs a destructor that takes the children apart with a loop and its own stack, so that no node is freed while it still has children.
Data structures103 / 201
Module 10 · Trees
Recap
A tree has one root; every other node has one parent and any number of children; leaves have none.
Depth counts edges from the root; height is the depth of the deepest leaf.
In C++ a node can be struct TreeNode { T value; std::vector<TreeNode> children; };, or hold unique_ptr children so nodes never move.
Pass nodes as const TreeNode&: a plain TreeNode copies the whole subtree.
Recursion answers a tree question by asking each child’s subtree and combining the answers.
Depth-first: preorder handles a node before its children, postorder after.
Breadth-first goes level by level with a std::queue of node pointers; a stack instead gives depth-first.
FlashcardsInteractive · disabled in the PDF
Root
The one node with no parent.
Leaf
A node with no children.
Depth of a node
Edges between it and the root; the root is at 0.
Height of a tree
The depth of its deepest leaf.
Subtree
A node and everything below it.
Base case
Where recursion stops: here, a leaf.
Preorder vs postorder
Handle a node before its children, or after them.
Breadth-first
Level by level, using a queue of nodes to visit.
Default argument
int depth = 0 in a parameter list: used when the caller leaves it out.
Module 11: Binary search trees
Data structures104 / 201
Module 11 · Binary search trees
Smaller keys go left, larger keys go right
Think of the game "guess my number between 1 and 100". You guess 50; "lower" throws away half the numbers at once. A binary search tree (BST) bakes that game into a structure. Recall from the trees module: a tree is nodes joined parent to child, starting at a root, and in a binary tree each node has at most two children, a left one and a right one.
A BST adds one ordering rule: for every node, every key in its left subtree (the child and everything below it) is smaller than the node’s key, and every key in its right subtree is larger. Here are a bookshop’s prices, in pence, arranged that way.
50
30
20
40
70
60
80
Everything left of 50 is smaller than 50; everything right of it is larger. The same holds at every node.
Binary search tree
A binary tree where left subtrees hold smaller keys and right subtrees larger ones.
Key
The value a node is ordered by: a price, a name, a date.
Subtree
A node together with everything below it.
Leaf
A node with no children.
QuizInteractive · disabled in the PDF
A tree has root 50, whose left child is 30, and 30’s right child is 55. Is it a BST?
A tree has root 50, whose left child is 30, and 30’s right child is 55. Is it a BST?
Data structures105 / 201
Module 11 · Binary search trees
A node owns its two children
In C++ a BST node is a struct with a key and two children. Recall from the linked lists module: a std::unique_ptr<Node> is a pointer that owns what it points at, so when it is destroyed, or given something else to hold, the node it held is deleted for you. An empty unique_ptr (it compares equal to nullptr) means "no child here".
C++
1#include<memory>23structNode{4int key;5 std::unique_ptr<Node> left; // smaller keys6 std::unique_ptr<Node> right; // larger keys7explicitNode(int k):key(k){}8};910std::unique_ptr<Node> root; // empty: no tree yet
The tree is owned from the top: the root owns its children, which own theirs.
Ownership flows down. Destroying root destroys its children, which destroy theirs, so the whole tree is freed with no delete written anywhere. When code only needs to look at a node, it uses a plain const Node* (from .get()): a pointer that reads but does not own.
QuizInteractive · disabled in the PDF
The variable root above goes out of scope while holding a tree of 7 nodes. What happens?
The variable root above goes out of scope while holding a tree of 7 nodes. What happens?
Data structures106 / 201
Module 11 · Binary search trees
Search throws away half the tree at each step
To find a key, start at the root and compare. Equal: found. Smaller: go left. Larger: go right. Fall off the bottom (reach an empty child): the key is not there. Each comparison skips a whole subtree.
60 is larger than 50: the whole left side (30 and below) can be skipped.
Lines 1: Searching only reads, so it takes a plain const Node*: a pointer that looks without owning.
Lines 2: Keep going while there is a node to look at.
Lines 3: The key is right here: done. -> reads a member through a pointer.
Lines 4-5: Otherwise go left for smaller keys, right for larger ones. .get() gives the raw pointer a unique_ptr holds (nullptr if empty).
Lines 7: You fell off the tree: the key is not in it.
Lines 10:root.get() lends the root to the search without handing over ownership.
Data structures107 / 201
Module 11 · Binary search trees
Insert walks down to an empty spot
Inserting is a search that does not find the key. Wherever the search falls off the tree, that empty child is exactly where the new key belongs. The version below takes the child pointer by reference (std::unique_ptr<Node>&), so when it reaches an empty one it can fill that very slot in the parent.
C++
1voidinsert(std::unique_ptr<Node>& node,int key){2if(!node){ // an empty slot: fill it3 node = std::make_unique<Node>(key);4return;5}6if(key < node->key)insert(node->left, key);7elseif(key > node->key)insert(node->right, key);8 // an equal key is already here: nothing to do9}1011// in main():12std::unique_ptr<Node> root;13for(int price : {50,30,70,20,40})insert(root, price);
Recursive: the function calls itself on the left or right child. std::make_unique<Node>(key) makes a new node owned by a unique_ptr.
Fill the blanksInteractive · disabled in the PDF
Complete the insert so smaller keys go left and larger ones go right.
The tree is printed on its side: the root at the left, larger keys above. Change the order of the keys and run it again.
Data structures108 / 201
Module 11 · Binary search trees
An in-order walk visits the keys in sorted order
The trees module showed depth-first traversals. The in-order one visits a node’s left subtree, then the node, then its right subtree. In a BST, everything on the left is smaller and everything on the right is larger, so in-order visits the keys from smallest to largest. A BST is a sorted list you can also insert into quickly.
Code challengeInteractive · disabled in the PDF
Finish collect(node, out): push every key of the tree onto out, smallest first. Visit the left subtree, then the node, then the right subtree. Node and insert are ready for you.
C++
1#include<memory>2#include<vector>34structNode{5int key;6 std::unique_ptr<Node> left, right;7explicitNode(int k):key(k){}8};9voidinsert(std::unique_ptr<Node>& node,int key){10if(!node){ node = std::make_unique<Node>(key);return;}11if(key < node->key)insert(node->left, key);12elseif(key > node->key)insert(node->right, key);13}1415voidcollect(constNode* node, std::vector<int>& out){16 // TODO: left subtree, then this key, then right subtree17}
Data structures109 / 201
Module 11 · Binary search trees
The smallest key is the leftmost, the largest the rightmost
To find the cheapest book, keep going left until there is no left child: nothing can be smaller than that node. The most expensive is at the end of the rightmost path. Both take as many steps as the tree is tall. A related question, "the dearest book I can afford", is the floor of a budget: the largest key that is less than or equal to it.
C++
1intminKey(constNode* node){ // node must not be nullptr2while(node->left) node = node->left.get();3return node->key;4}56intmaxKey(constNode* node){7while(node->right) node = node->right.get();8return node->key;9}
A unique_ptr used as a condition is true when it holds a node.
Code challengeInteractive · disabled in the PDF
Write floorKey(node, budget): the largest key less than or equal to budget, or std::nullopt if every key is larger. Walk down like a search, remembering the best key so far.
Removing a node must keep the ordering rule true. How you do it depends on how many children the node has.
The node has
What to do
Example: remove from the price tree
No children (a leaf)
Just cut it off: its parent’s pointer becomes empty.
Remove 20: 30 loses its left child.
One child
Replace the node with its child: the parent adopts the grandchild.
Remove 30 once 20 is gone: 50’s left becomes 40.
Two children
Copy in its successor (the smallest key in its right subtree), then delete the successor from there.
Remove 50: 60 takes its place, and the old 60 is removed.
Why the successor? It is the next key up in sorted order, so it is larger than everything on the left and smaller than everything else on the right: it can sit where the deleted key was. It is the leftmost node of the right subtree, so it has no left child, and deleting it is one of the easy cases.
Put in orderInteractive · disabled in the PDF
Put the steps for deleting a node with two children in order.
1.Search down the tree to the node with the key
2.Copy the successor’s key into the node being deleted
3.Step once into its right subtree
4.Keep going left to find the smallest key there, the successor
5.Delete the successor from the right subtree (it has at most one child)
Lines 2: The key is not in the tree: nothing changes.
Lines 3-4: Like insert: go down one side, passing that child by reference so it can be changed in place.
Lines 5: No left child: the right child (or nothing, for a leaf) takes this node’s place. std::move hands ownership of the child up to the parent’s slot, and the node that slot held is deleted.
Lines 6: No right child: the left child takes its place.
Lines 8-9: Two children: find the successor, one step right then all the way left.
Lines 10-11: Copy its key here and remove it from the right subtree.
Find the bugInteractive · disabled in the PDF
This version of the two-children case breaks the ordering rule. Which line is wrong?
C++
Why `node = std::move(node->right)` is safe. It looks like the node is deleted while you still read its child. A unique_ptr assignment first takes the child out of node->right (leaving it empty), then deletes the old node, whose right is now empty, so the child survives in its new place.
Delete and recursion depth. Every one of these functions recurses as deep as the tree is tall. In a balanced tree of a million keys that is about 20 calls; in a badly shaped one it can be a million, and the program runs out of stack and crashes. Even destroying a tree recurses: each unique_ptr deletes a node that destroys its children. Production trees use loops, or keep themselves balanced, as the balanced trees module shows.
Data structures112 / 201
Module 11 · Binary search trees
Sorted input makes a tree as slow as a list
Every operation walks one path from the root, so its cost is the tree’s height. For search trees it is handy to count height in nodes: the number on the longest path from root to leaf, which is the most comparisons a search can make. (The trees module counted edges, one fewer; the idea is the same.) Keys inserted in a random order give a bushy tree whose height is a small multiple of log₂ n. Keys inserted already sorted give each node a single right child: a skewed tree, which is a linked list with extra steps.
You insert the titles of a bookshop’s catalogue, already in alphabetical order, into a plain BST. How tall is the tree for 1,000 books?
You insert the titles of a bookshop’s catalogue, already in alphabetical order, into a plain BST. How tall is the tree for 1,000 books?
The standard library already has one.std::set<int> and std::map<Key, Value> are binary search trees that rebalance themselves (red-black trees, in every common standard library), so they stay O(log n) even for sorted input. Write your own to learn how they work; reach for theirs in real code.
Data structures113 / 201
Module 11 · Binary search trees
Check a tree really is a BST
Code that builds trees by hand, or a tree loaded from a file, can break the rule. To check it, pass each subtree the range its keys must fall in. Going left, the current key becomes the upper limit; going right, it becomes the lower limit. The limits are long long (a wider whole number than int), so the starting ones, LLONG_MIN and LLONG_MAX (the smallest and largest long long), lie beyond every possible int key.
Code challengeInteractive · disabled in the PDF
Finish isBst(node, low, high): true when every key is strictly between low and high, and both subtrees are valid with tightened limits. tree and leaf build trees by hand for the tests.
C++
1#include<climits>2#include<memory>3structNode{4int key;5 std::unique_ptr<Node> left, right;6};7usingPtr= std::unique_ptr<Node>;8Ptrtree(Ptr l,int key,Ptr r){9return std::make_unique<Node>(Node{key, std::move(l), std::move(r)});10}11Ptrleaf(int key){returntree(nullptr, key,nullptr);}1213boolisBst(constNode* node,longlong low = LLONG_MIN,14longlong high = LLONG_MAX){15if(!node)returntrue;16 // TODO: check node->key, then both subtrees with tighter limits17returntrue;18}
Data structures114 / 201
Module 11 · Binary search trees
Recap
A BST keeps smaller keys in left subtrees and larger keys in right subtrees, at every node.
In C++ each node owns its children through std::unique_ptr; code that only reads uses a const Node*.
Search, insert and delete walk one path down, so they cost the tree’s height.
An in-order traversal gives the keys in sorted order.
Min is the leftmost node, max the rightmost; floor finds the best key under a limit.
Delete: a leaf is cut off, one child is promoted, two children borrow the successor.
Sorted input makes a skewed tree of height n, and every operation O(n); std::set and std::map balance themselves.
FlashcardsInteractive · disabled in the PDF
The BST ordering rule
Every key in a node’s left subtree is smaller, every key in its right subtree larger.
Why pass std::unique_ptr<Node>& to insert?
The reference is the parent’s own child slot, so filling it links the new node in.
In-order traversal
Left subtree, node, right subtree: in a BST, sorted order.
Successor
The next larger key: one step right, then all the way left.
Height
The number of nodes on the longest root-to-leaf path. Operations cost O(height).
Skewed tree
Every node has one child: height n, as slow as a linked list.
Floor of x
The largest key less than or equal to x.
Why check a range, not just children?
A key must respect every ancestor, not only its parent.
Module 12: Heaps and priority queues
Data structures115 / 201
Module 12 · Heaps and priority queues
A priority queue serves the most urgent first
A queue (from the queues module) serves people in the order they arrived. A hospital’s emergency room does not: the most urgent patient is seen next, whoever came first. A priority queue is that idea as a structure. You push items with a priority, and pop always takes out the one with the best priority (here, the smallest number: priority 1 beats priority 5).
You could keep a plain std::vector. Unsorted, pushing is quick but popping means searching every item for the smallest. Sorted, popping is quick but pushing means shifting items to make room. A heap makes both fast.
Kept as
push
pop the smallest
peek at the smallest
Unsorted vector
O(1)
O(n): scan everything
O(n)
Sorted vector
O(n): shift to make room
O(1): take the end
O(1)
Binary heap
O(log n)
O(log n)
O(1)
Priority queue
A collection where you take out the item with the best priority first.
Heap
A tree where each parent is at least as good as its children: the usual way to build a priority queue.
Min-heap
A heap where "best" means smallest, so the smallest item is at the root.
Max-heap
The same with largest at the root.
Data structures116 / 201
Module 12 · Heaps and priority queues
Each parent is smaller than its children, and that is all
A binary heap is a binary tree with two rules. The heap property: every parent is less than or equal to its children, so the smallest item is always at the root. And the shape rule: the tree is complete, every level full except perhaps the last, which fills from the left. Unlike a binary search tree, left and right children are in no particular order.
1
3
5
4
8
9
A min-heap: each node is smaller than the nodes under it, but 8 sits higher than 4, which is on another branch: there is no order across branches.
QuizInteractive · disabled in the PDF
Which of these is true of the min-heap above?
Which of these is true of the min-heap above?
Data structures117 / 201
Module 12 · Heaps and priority queues
A complete tree fits in a vector with no gaps
Because a complete tree has no holes, you can write it down level by level in a std::vector and never need left and right pointers. The root is at index 0, its children at 1 and 2, theirs at 3 to 6, and so on. A node’s relatives are found with a little arithmetic.
Index i
Value
Parent: ⌊(i − 1) / 2⌋
Children: 2i + 1, 2i + 2
0
1
(none: the root)
1 and 2 (values 3, 8)
1
3
0
3 and 4 (values 5, 4)
2
8
0
5 (value 9); 6 is past the end
3
5
1
past the end: a leaf
4
4
1
a leaf
5
9
2
a leaf
Fill the blanksInteractive · disabled in the PDF
Write the index arithmetic for a heap stored from index 0.
C++
1std::size_tparent(std::size_t i){return(i -)/2;}2std::size_tleft(std::size_t i){return2* i +;}3std::size_tright(std::size_t i){return2* i +;}
Why the flat layout matters. No pointers means less memory per item: a heap of a million ints is 4 MB in one block, where a node-based tree, with two child pointers per item (8 bytes each on a 64-bit computer), would be several times that, scattered around memory. Items side by side are what the processor’s cache loves, the same reason std::vector beats std::list for most work. The shape rule exists to make this layout possible.
Data structures118 / 201
Module 12 · Heaps and priority queues
Push adds at the end and sifts up
To push, put the new item at the end of the vector (push_back): the next free spot in the bottom level, so the shape rule holds. The heap property may now be broken between the item and its parent. Fix it by sifting up: while the item is smaller than its parent, swap the two. At most one swap per level, so O(log n).
WalkthroughInteractive · disabled in the PDF
C++
1#include<utility>2#include<vector>34voidheapPush(std::vector<int>& heap,int value){5 heap.push_back(value);6 std::size_t i = heap.size()-1;7while(i >0){8 std::size_t parent =(i -1)/2;9if(heap[parent]<= heap[i])break;10 std::swap(heap[parent], heap[i]);11 i = parent;12}13}
Lines 4: The heap is passed by reference (&), so the function changes the caller’s vector, not a copy.
Lines 5-6: The new value goes at the end; i is where it is now.
Lines 7-8: While it is not the root, find its parent.
Lines 9: The parent is already smaller or equal: the heap is valid again, stop.
Lines 10-11: Otherwise swap them (std::swap, from <utility>, exchanges two values) and carry on from the parent’s place.
QuizInteractive · disabled in the PDF
The heap is [1, 3, 8, 5]. What is it after heapPush(heap, 2)?
The heap is [1, 3, 8, 5]. What is it after heapPush(heap, 2)?
Code challengeInteractive · disabled in the PDF
Finish heapPush: after adding value at the end, sift it up until its parent is smaller or equal. Use std::swap.
C++
1#include<utility>2#include<vector>34voidheapPush(std::vector<int>& heap,int value){5 heap.push_back(value);6 // TODO: sift the new value up7}
Data structures119 / 201
Module 12 · Heaps and priority queues
Pop takes the root and sifts the last item down
The smallest item is at index 0. To remove it without leaving a hole, move the last item into the root (the vector just gets shorter by one, with pop_back) and sift it down: while it is larger than one of its children, swap it with the smaller child. Choosing the smaller child keeps the heap property between the two siblings.
Put in orderInteractive · disabled in the PDF
Pop from the heap [1, 3, 8, 5, 4]. Put what happens in order.
1.Its children are 3 and 8; 3 is smaller and less than 4, so swap: [3, 4, 8, 5]
2.At index 1, its only child is 5, which is larger: stop
3.Remember the root, 1, to return it
4.Return 1
5.Take the last item, 4, off the end and put it at the root: [4, 3, 8, 5]
Code challengeInteractive · disabled in the PDF
Finish siftDown(heap, i): while the item at i is larger than its smaller child, swap them and continue from the child. heapPop uses it.
C++
1#include<optional>2#include<utility>3#include<vector>45voidsiftDown(std::vector<int>& heap, std::size_t i){6 // TODO: children are at 2 * i + 1 and 2 * i + 27}8std::optional<int>heapPop(std::vector<int>& heap){9if(heap.empty())return std::nullopt;10int top = heap[0];11 heap[0]= heap.back();12 heap.pop_back();13if(!heap.empty())siftDown(heap,0);14return top;15}
Data structures120 / 201
Module 12 · Heaps and priority queues
Building a heap from a list takes O(n)
Given a whole list at once, you could push each item: n pushes at O(log n), O(n log n) in all. Heapify is faster. Treat the vector as a tree as it is, then sift down every node that has children, starting from the last one and working back to the root. Leaves, half of all nodes, need no work at all.
C++
1std::vector<int>heapify(std::vector<int> heap){ // a copy2for(std::size_t i = heap.size()/2; i-->0;)3siftDown(heap, i);4return heap;5}67heapify({9,4,7,1,8,2}); // {1, 4, 2, 9, 8, 7}
The last parent is at size / 2 − 1. i-- > 0 tests, then steps down, so the loop runs from there to 0 and stops without i ever going below zero.
Why heapify is O(n), not O(n log n). A sift-down costs the node’s distance to the bottom, not to the top. Half the nodes are leaves (cost 0), a quarter are one level up (cost 1), an eighth cost 2, and so on. The sum n/4 + 2n/8 + 3n/16 + … stays below n. Push-by-push is slower because sift-up costs the distance to the top, and most nodes are far from the top.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<utility>3#include<vector>45voidsiftDown(std::vector<int>& heap, std::size_t i){6while(true){7 std::size_t l =2* i +1, r = l +1, small = i;8if(l < heap.size()&& heap[l]< heap[small]) small = l;9if(r < heap.size()&& heap[r]< heap[small]) small = r;10if(small == i)return;11 std::swap(heap[i], heap[small]);12 i = small;13}14}1516intmain(){17 std::vector<int> heap{9,4,7,1,8,2};18for(std::size_t i = heap.size()/2; i-->0;){19siftDown(heap, i);20 std::cout <<"after node "<< i <<":";21for(int v : heap) std::cout <<" "<< v;22 std::cout <<"\n";23}24}
Watch each parent sink into place. Try a list sorted largest first, the worst case.
Data structures121 / 201
Module 12 · Heaps and priority queues
Heaps schedule work and find the top k
Real heaps hold structs, not just numbers, and compare them by one field: a calendar app keeps reminders in a min-heap ordered by time, so the next one due is always at the root. Route finders keep the nearest unexplored place in a heap (the Algorithms course builds one). And a heap answers top k questions cheaply: the 10 best-selling books out of a million sales.
Scheduling: timers, job queues, a game’s events, ordered by when or how urgent.
Top k: keep a min-heap of size k. Each new item that beats the root replaces it. The heap always holds the k largest seen so far, in O(n log k).
Merging sorted lists: a heap holds the front of each list, and pops the smallest front next.
Median of a stream: a max-heap for the lower half and a min-heap for the upper half.
QuizInteractive · disabled in the PDF
To keep the 10 largest values of a long stream, which heap do you keep, and of what size?
To keep the 10 largest values of a long stream, which heap do you keep, and of what size?
Sorting is not a heap. Sorting the vector after each push is a tempting shortcut: it is O(n log n) per push instead of O(log n). The standard library already has a real heap, on the next slide.
Data structures122 / 201
Module 12 · Heaps and priority queues
The standard library has a heap: std::priority_queue
std::priority_queue (from <queue>) is a binary heap in a std::vector, exactly as you built it. It has push, pop (which removes the top but returns nothing), top (peek), size and empty. One surprise: by default it is a max-heap, so top() is the largest item. For a min-heap, pass the comparison std::greater<>, which means "larger items sink".
C++
1#include<functional>// std::greater2#include<queue>3#include<vector>45std::priority_queue<int> most; // max-heap: top() is largest6std::priority_queue<int, std::vector<int>, std::greater<>> least;7 // min-heap: top() is smallest89// in a function:10least.push(5); least.push(1); least.push(3);11int next = least.top(); // 112least.pop(); // removes 1; top() is now 3
The three template arguments are: what it holds, the container underneath, and how to compare.
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1std::priority_queue<int> q;2for(int v : {4,9,2,7}) q.push(v);3q.pop();4std::cout << q.top();
What does this print?
Data structures123 / 201
Module 12 · Heaps and priority queues
Top k with the standard heap
Under it are three functions in <algorithm> that you can also use on any vector directly: std::make_heap(v.begin(), v.end()) is heapify, std::push_heap sifts up the item you just push_backed, and std::pop_heap moves the top to the end so you can pop_back it. They make max-heaps too, unless you pass std::greater<>() as a last argument.
Code challengeInteractive · disabled in the PDF
Finish topK(values, k): the k largest values, largest first. Keep a min-heap best of at most k items: push each value, and pop whenever it holds more than k.
C++
1#include<algorithm>2#include<functional>3#include<queue>4#include<vector>56std::vector<int>topK(const std::vector<int>& values,int k){7 std::priority_queue<int, std::vector<int>, std::greater<>> best;8 // TODO: keep only the k largest in best9 std::vector<int> out;10while(!best.empty()){ out.push_back(best.top()); best.pop();}11 std::reverse(out.begin(), out.end());12return out;13}
Data structures124 / 201
Module 12 · Heaps and priority queues
Recap
A priority queue always gives back the best item next; a binary heap is the usual way to build one.
Heap property: each parent is at most its children. Shape: a complete tree.
Stored in a vector: children of i at 2i + 1 and 2i + 2, parent at ⌊(i − 1) / 2⌋.
Push: add at the end, sift up. Pop: move the last item to the root, sift down with the smaller child. Both O(log n).
Peek is O(1); heapify builds a heap from a list in O(n).
Uses: scheduling, top k with a size-k min-heap, merging sorted lists, heap sort.
std::priority_queue is a max-heap by default; std::greater<> makes it a min-heap. std::make_heap, push_heap and pop_heap work on a plain vector.
FlashcardsInteractive · disabled in the PDF
Priority queue
Push items with priorities; pop always returns the best one.
Heap property (min-heap)
Every parent is less than or equal to its children.
Complete tree
Every level full except the last, which fills from the left: no gaps in the vector.
Children and parent of index i
2i + 1, 2i + 2; parent ⌊(i − 1) / 2⌋.
Sift up
Swap with the parent while smaller: used by push.
Sift down
Swap with the smaller child while larger: used by pop and heapify.
Heapify
Sift down every parent from the last to the root: O(n).
Top k largest
A min-heap of size k; its root is the one to beat.
std::priority_queue<int> top()
The largest item: it is a max-heap unless you pass std::greater<>.
Module 13: Graphs
Data structures125 / 201
Module 13 · Graphs
A graph is things and the links between them
A city map is places joined by roads. A friends network is people joined by friendships. The web is pages joined by links. All three are graphs: a set of vertices (the things, also called nodes) and a set of edges (the connections, each joining two vertices). A tree, from the trees module, is a special graph: connected, with no loops and one root. A general graph has no root, and can loop back on itself.
Four towns and the roads between them, with distances in kilometres.
Graph
A set of vertices and a set of edges between them.
Vertex
One thing in a graph: a town, a person, a page. Plural vertices.
Edge
A connection between two vertices: a road, a friendship, a link.
Neighbours
The vertices joined to a vertex by an edge. Also adjacent vertices.
Degree
How many edges touch a vertex: how many neighbours it has.
Path
A route along edges from one vertex to another.
Data structures126 / 201
Module 13 · Graphs
Edges can have a direction and a weight
In an undirected graph an edge works both ways: if Ada is friends with Ben, Ben is friends with Ada. In a directed graph each edge has a direction, drawn as an arrow: you can follow someone who does not follow you back, and a one-way street goes one way. A weighted graph gives each edge a number: a distance, a price, a travel time.
Example
Directed?
Weighted?
Friendships on a social network
No: friendship is mutual
No
Who follows whom
Yes
No
Roads with distances
No (two-way roads)
Yes
Flights with prices
Yes: there and back can differ
Yes
Courses and their prerequisites
Yes: "take A before B"
No
QuizInteractive · disabled in the PDF
Web pages link to other pages. What kind of graph is that?
Web pages link to other pages. What kind of graph is that?
In and out. In a directed graph a vertex has an out-degree (arrows leaving it: how many people you follow) and an in-degree (arrows arriving: your followers). An undirected edge can be stored as two directed edges, one each way, which is exactly how the code on the next slides does it.
Data structures127 / 201
Module 13 · Graphs
An adjacency list stores each vertex’s neighbours
The most common way to store a graph is an adjacency list: for each vertex, the list of its neighbours. In C++ the simplest form numbers the vertices 0 to n − 1 and keeps a std::vector<std::vector<int>>, a vector of vectors: adj[v] is the vector of v’s neighbours. Names, if you need them, live in a separate vector at the same numbers. Every undirected edge appears twice, once in each of its ends’ lists.
Four vertices, four edges, eight entries: each road is listed from both ends.
QuizInteractive · disabled in the PDF
In an undirected graph, the edges are Ada–Ben, Ada–Cleo, Ben–Cleo and Cleo–Dev. What is the degree of Cleo, and how many entries are in all the adjacency lists together?
In an undirected graph, the edges are Ada–Ben, Ada–Cleo, Ben–Cleo and Cleo–Dev. What is the degree of Cleo, and how many entries are in all the adjacency lists together?
Data structures128 / 201
Module 13 · Graphs
An adjacency matrix is a grid of yes and no
An adjacency matrix numbers the vertices 0 to n − 1 and keeps an n × n grid: matrix[i][j] is 1 when there is an edge from i to j, 0 when not (or the weight, for a weighted graph). Checking one edge is instant, but the grid holds a cell for every possible pair, edges or not.
The same type as the list, used differently: every row has n entries. Undirected graphs give a symmetric matrix: matrix[i][j] equals matrix[j][i].
V vertices, E edges
Adjacency list
Adjacency matrix
Memory
O(V + E)
O(V²)
Is there an edge u–v?
O(degree of u)
O(1)
List u’s neighbours
O(degree of u)
O(V): scan the row
Add an edge
O(1) (amortised push_back)
O(1)
Add a vertex
O(1): one more empty vector
O(V²): rebuild the grid
QuizInteractive · disabled in the PDF
A social network has 10 million people, each with about 200 friends. Roughly how many cells would a matrix need, compared with entries in adjacency lists?
A social network has 10 million people, each with about 200 friends. Roughly how many cells would a matrix need, compared with entries in adjacency lists?
Data structures129 / 201
Module 13 · Graphs
A graph struct with neighbours and degree
Wrapping the vector of vectors in a struct with member functions keeps the two halves of every undirected edge in step: nobody can add one half and forget the other. The constructor makes n empty neighbour lists at once.
Lines 7:adj(n) makes a vector of n empty vectors: n vertices, no edges yet.
Lines 9-12: An edge is recorded from both sides.
Lines 13-15: Neighbours are returned by const&: the caller reads the vector in place, nothing is copied. The const after the brackets promises the function changes nothing.
Lines 16-19:std::find walks a range and returns where it found the value, or end() if it did not: O(degree).
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<string>3#include<vector>45std::vector<std::string> names{"Ada","Ben","Cleo","Dev"};6std::vector<std::vector<int>>adj(names.size());78voidaddEdge(int a,int b){9 adj[a].push_back(b);10 adj[b].push_back(a);11}1213intmain(){14addEdge(0,1); // Ada–Ben15addEdge(0,2); // Ada–Cleo16addEdge(1,2); // Ben–Cleo17addEdge(2,3); // Cleo–Dev18for(std::size_t p =0; p < adj.size(); p++){19 std::cout << names[p]<<" has "<< adj[p].size()<<":";20for(int f : adj[p]) std::cout <<" "<< names[f];21 std::cout <<"\n";22}23}
Make the graph directed by deleting line 10, and see who loses friends. Then add Ada–Ben a second time: a plain vector lists Ben twice.
Data structures130 / 201
Module 13 · Graphs
Try it: a friends network
When vertices are names and the same edge may be added twice, use the hash tables from earlier modules: an std::unordered_map from each person to an std::unordered_set of friends. A set ignores a friend added again, and checking an edge is O(1) on average.
Code challengeInteractive · disabled in the PDF
Finish Friends: addFriendship(a, b) joins two people both ways (adding them if new), and degree(p) is how many friends p has (0 for someone unknown, without adding them).
For a directed graph, store each edge only at its start. For weights, store each neighbour together with its weight: a small struct Edge, and one vector of edges per vertex. Below, flights between cities with their prices; a return flight is a separate edge and may cost more.
Fill the blanksInteractive · disabled in the PDF
Store a one-way flight with its price, and read a price back.
This friends graph says Ben has no friends after befriend(0, 1) (Ada, Ben). Which line is the bug?
C++
Data structures132 / 201
Module 13 · Graphs
Who can I reach? A first graph search
A common question: starting from one vertex, which vertices can you reach by following edges? It is the trees module’s breadth-first search (BFS) with one addition. A graph can loop back, so you keep a visited mark per vertex and never queue a vertex twice; without it, two friends would send the search back and forth forever. With vertices numbered, the marks are just a std::vector<bool>. The Algorithms course goes much further with graph search.
Code challengeInteractive · disabled in the PDF
Finish reachable(graph, start): mark in seen every vertex reachable from start, including start. Use the std::queue (from the queues module): take a vertex from the front, and push each unseen neighbour, marking it as you push it.
C++
1#include<queue>2#include<vector>3usingGraph= std::vector<std::vector<int>>;45std::vector<int>reachable(constGraph& graph,int start){6 std::vector<bool>seen(graph.size(),false);7 std::queue<int> queue;8 seen[start]=true;9 queue.push(start);10 // TODO: take from the queue, push unseen neighbours11 std::vector<int> out;12for(std::size_t v =0; v < graph.size(); v++)13if(seen[v]) out.push_back(v);14return out;15}
Choosing a representation is choosing costs. A std::vector<std::vector<int>> is one small heap allocation per vertex. Graph libraries often flatten it into one vector of all neighbours plus a vector of offsets, where vertex v’s neighbours are edges[offset[v]] up to edges[offset[v + 1]] (a "compressed sparse row" layout): two allocations in total, everything side by side in memory. It is fast to search but slow to change, so it suits graphs that are built once and read many times, like a road network.
Data structures133 / 201
Module 13 · Graphs
Recap
A graph is vertices and edges; a tree is a graph with no loops and one root.
Numbered vertices make a graph a std::vector<std::vector<int>>: adj[v] is v’s neighbours.
Edges can be directed (one way) and weighted (a number on each).
Adjacency list: each vertex’s neighbours. O(V + E) memory; the usual choice for sparse graphs.
Adjacency matrix: a V × V grid. O(1) edge checks, O(V²) memory; for small or dense graphs.
Undirected edges are stored from both ends; degrees add up to twice the edges.
BFS with a visited set finds everything reachable in O(V + E).
FlashcardsInteractive · disabled in the PDF
Vertex and edge
A thing in a graph, and a connection between two things.
Directed graph
Edges have a direction: following is not mutual.
Weighted graph
Each edge carries a number: a distance, a price, a time.
Degree
The number of edges at a vertex. In and out degree in a directed graph.
Adjacency list
Each vertex’s neighbours, adj[v]: O(V + E) memory.
Adjacency matrix
A V × V grid of 0/1 or weights: O(1) edge check, O(V²) memory.
Sparse graph
Few edges compared with all possible pairs: most real graphs.
Why a visited set in graph search?
Graphs can loop; without it the search revisits vertices forever.
Why find, not [], to ask an unordered_map?
operator[] inserts a missing key; find only looks.
Module 14: Tries: trees of prefixes
Data structures134 / 201
Module 14 · Tries: trees of prefixes
Autocomplete asks "which words start with this?"
You type "ca" in a search box and it offers "car", "cart", "cat". With the words in a std::vector<std::string> or a std::unordered_set, the only way to answer is to check every word with starts_with (a C++20 member of std::string): with a 100,000-word dictionary, 100,000 checks per keystroke. Yet the words share beginnings. A trie (from retrieval, usually said "try") stores each shared beginning once.
Each node is one letter, and a word is a path down from the root. Words that start the same share the same first nodes. A mark on a node says "a word ends here", because "car" ends in the middle of "cart".
(root)
c
a
r ★car
t ★cart
t ★cat
d
o
g ★dog
The words car, cart, cat and dog. Starred nodes end a word.
Trie
A tree where each edge is a character and each path from the root spells a prefix.
Prefix
The start of a word: "ca" is a prefix of "cart".
End mark
A flag on a node saying a whole word ends there.
Data structures135 / 201
Module 14 · Tries: trees of prefixes
A node is 26 child slots and an end flag
A trie node needs two things: its children, found by character, and whether a word ends here. For lowercase words the simplest layout is a fixed std::array of 26 slots, one per letter: slot 0 for a, slot 25 for z. Each slot is a std::unique_ptr<Node> that owns the child, or is empty. The root stands for the empty prefix and holds no letter of its own.
C++
1#include<array>2#include<memory>34structNode{5 std::array<std::unique_ptr<Node>,26> children; // a to z6bool end =false;7};89Node root; // the empty prefix1011// the slot for a letter: 'c' - 'a' is 212// root.children['c' - 'a']
A char is a small number, and the letters a to z are consecutive, so ch - 'a' turns a letter into its slot.
QuizInteractive · disabled in the PDF
Starting from an empty trie, you insert "car", "cart" and "cat". How many nodes are there, not counting the root?
Starting from an empty trie, you insert "car", "cart" and "cat". How many nodes are there, not counting the root?
Lines 4: Start at the root, the empty prefix. &root is its address; node is a plain pointer that walks the trie without owning anything.
Lines 5: Take the word one character at a time: a range-for over a string gives its chars.
Lines 6: A reference to this letter’s slot, so the next line can fill the slot itself, not a copy.
Lines 7: No child for this letter yet: make one. Shared prefixes skip this.
Lines 8: Step down to the child for this letter.
Lines 10: The last node reached ends the word: mark it.
The cost depends on the word, not the dictionary. Inserting a word of length L takes L steps, whether the trie holds ten words or ten million. That is what makes tries good for prefixes: the work follows what you typed.
Only a to z. For 'A' or 'é', ch - 'a' is not between 0 and 25. Here the hardened standard library stops the program ("out-of-bounds access"); an unchecked build would write over memory it does not own. Check or convert letters (std::tolower) before indexing. A std::string holds bytes, and a letter like é is two bytes in UTF-8, so a trie over bytes needs 256 slots or a map.
Data structures137 / 201
Module 14 · Tries: trees of prefixes
has and startsWith differ only at the last step
Both walk down letter by letter and fail as soon as a letter has no child. Reaching the end of the input, startsWith is satisfied: some word goes through here. has also needs the end flag, or "ca" would count as a word.
Code challengeInteractive · disabled in the PDF
Finish find(root, text) (the node at the end of text, or nullptr), then use it in has (a whole word) and startsWith (any word begins with it). The tests build a trie with the insert from the last slide.
For suggestions, walk to the prefix’s node, then visit everything below it with a depth-first traversal (the trees module), carrying the letters so far. Every node with the end flag is a word to suggest.
Put in orderInteractive · disabled in the PDF
Put the steps of wordsWithPrefix(root, "ca") in order.
1.If a letter was missing, return an empty list
2.Walk from the root down "c" then "a"
3.At each node, if its end flag is set, add its text to the results
4.Start a depth-first visit at the "a" node, with the text "ca"
5.For each child, visit it with the text plus the child’s letter
Code challengeInteractive · disabled in the PDF
Finish collect(node, text, out): push text onto out if the node ends a word, then collect from each child that exists, with its letter (char('a' + i)) added to text. The tests’ wordsWithPrefix walks to the prefix and calls it.
Add words that share less ("zebra", "quiz") and watch the saving shrink.
Data structures140 / 201
Module 14 · Tries: trees of prefixes
The price is memory
A trie saves letters, but each node costs far more than one character. sizeof(Node) is 26 pointers plus a flag: 108 bytes in this browser’s 32-bit WebAssembly, 216 bytes on a 64-bit computer, nearly all of it empty slots. Each node is also its own heap allocation. For a typical dictionary a plain trie can use many times the memory of the words themselves. That is fine for a few thousand words in a search box, and a reason to think twice for millions.
An array of 26 slots per node (what you built): fast and simple, but 26 slots even when one is used.
A map per node, std::unordered_map<char, std::unique_ptr<Node>> or std::map: only the children that exist, any character, but slower to step through.
A compressed trie (radix tree): a chain of single-child nodes becomes one node holding a string, such as "kshop". Far fewer nodes.
A sorted vector of words with std::lower_bound: find the first word ≥ the prefix, then read forward while words still start with it. Little memory, no updates in the middle.
QuizInteractive · disabled in the PDF
Which job does a trie do clearly better than a std::unordered_set of words?
Which job does a trie do clearly better than a std::unordered_set of words?
Tries in the wild. Routers look up IP addresses in tries over bits (longest-prefix matching). Web frameworks match URL paths with radix trees, one path segment per edge. Spell checkers and phone keyboards use tries, often compressed into a DAWG (a trie whose identical endings are shared too, like "-ing").
Data structures141 / 201
Module 14 · Tries: trees of prefixes
Counting through a trie
A trie can answer "how many words start with this?" in O(P) if each node also keeps an int count: how many inserted words pass through it. Insert adds one to the count of every node on the word’s path.
Find the bugInteractive · disabled in the PDF
After inserting car, cart and cat, countPrefix(root, "car") should be 2, but it says 1. Which line is the bug?
C++
Data structures142 / 201
Module 14 · Tries: trees of prefixes
Recap
A trie stores words as paths of characters from a root; shared prefixes are stored once.
Each node: 26 owning child slots (std::array<std::unique_ptr<Node>, 26>), and an end flag.
Insert, has and startsWith cost the length of the word, not the size of the dictionary.
has needs the end flag; startsWith only needs the path to exist.
Suggestions: walk to the prefix, then depth-first through everything below it; the slots give alphabetical order.
The price is memory: maps per node, radix trees or sorted vectors are the trade-offs.
FlashcardsInteractive · disabled in the PDF
Trie
A prefix tree: each edge a character, each path a prefix.
Why the end flag?
A word can end at a node that also continues: "car" inside "cart".
Slot for a lowercase letter ch
ch - 'a': 0 for a, 25 for z.
Cost of startsWith in a trie
O(P), the length of the prefix.
Cost of startsWith over a hash set
O(n × P): every word must be checked.
Radix tree
A compressed trie: single-child chains merged into one node.
A trie’s main cost
Memory: one node per distinct prefix, mostly empty slots, each its own allocation.
Module 15: Balanced trees
Data structures143 / 201
Module 15 · Balanced trees
A search tree is only as fast as it is short
The binary search trees module ended with a problem: search, insert and delete cost the tree’s height, and keys arriving in sorted order make a tree as tall as it is big. A balanced tree is a search tree that fixes its own shape as it changes, so its height stays close to log₂ n however the keys arrive.
Keys
Height of a perfectly balanced tree
Height of a skewed tree
1,000
10
1,000
1,000,000
20
1,000,000
1,000,000,000
30
1,000,000,000
PlaygroundInteractive · disabled in the PDF
C++
1#include<algorithm>2#include<iostream>3#include<memory>4#include<random>5#include<vector>6structNode{7int key;8 std::unique_ptr<Node> left, right;9explicitNode(int k):key(k){}10};11voidinsert(std::unique_ptr<Node>& n,int key){12if(!n){ n = std::make_unique<Node>(key);return;}13insert(key < n->key ? n->left : n->right, key);14}15intheight(constNode* n){16return n ?1+ std::max(height(n->left.get()),17height(n->right.get())):0;18}19intmain(){20 std::vector<int>keys(1000);21for(int i =0; i <1000; i++) keys[i]= i;22 std::unique_ptr<Node> a, b;23for(int k : keys)insert(a, k);24 std::shuffle(keys.begin(), keys.end(), std::mt19937(42));25for(int k : keys)insert(b, k);26 std::cout <<"sorted input, height "<<height(a.get())<<"\n"27<<"shuffled input, height "<<height(b.get())<<"\n";28}
The same 1,000 keys in two orders. std::shuffle mixes them with a random number generator; change its seed (42) and run it again: the shuffled height changes, the sorted one never does.
Data structures144 / 201
Module 15 · Balanced trees
Height and the balance factor
Here, the height of an empty tree is 0 and a single node has height 1; any other node is 1 more than its taller child. A node’s balance factor is the height of its left subtree minus the height of its right one. An AVL tree (after its inventors, Adelson-Velsky and Landis) keeps every balance factor at −1, 0 or 1: at each node, the two sides differ in height by at most one.
50h 4, bf 2
30h 3, bf 2
20h 2, bf 1
10h 1, bf 0
70h 1, bf 0
Each node shows its height h and balance factor bf. Both 50 and 30 are out of balance (bf 2); 30 is the lowest, and fixing it fixes 50 too.
Code challengeInteractive · disabled in the PDF
Write height(node) (0 for nullptr, otherwise 1 plus the taller child’s height) and isBalanced(node): true when every node’s subtrees differ in height by at most 1. std::max and std::abs help.
A rotation rearranges three links so a child becomes its parent’s parent, and the order of keys stays exactly the same. A right rotation at 50 lifts its left child 30: 50 moves down to the right, and 30’s right subtree (40, which is between 30 and 50) moves across to become 50’s left.
Before: 50 at the top
50 / \ 30 60 / \ 20 40
After rotating right at 50
30 / \ 20 50 / \ 40 60
C++
1// top is the parent's slot that owns this subtree2voidrotateRight(std::unique_ptr<Node>& top){3 std::unique_ptr<Node> lifted = std::move(top->left); // 304 top->left = std::move(lifted->right); // 40 moves across5 lifted->right = std::move(top); // 50 goes down right6 top = std::move(lifted); // 30 is the new top7}
O(1): four hand-overs of ownership, however big the subtrees are. Each std::move passes a node to a new owner and leaves the old pointer empty, so no node is ever owned twice or by nobody.
QuizInteractive · disabled in the PDF
An in-order traversal of the tree before the rotation gives 20, 30, 40, 50, 60. What does it give after?
An in-order traversal of the tree before the rotation gives 20, 30, 40, 50, 60. What does it give after?
Data structures146 / 201
Module 15 · Balanced trees
Try it: rotate left
A left rotation is the mirror image: the right child comes up, the old top goes down to the left, and the lifted node’s left subtree moves across to become the old top’s right.
Code challengeInteractive · disabled in the PDF
Write rotateLeft(top): lift top->right, move its left subtree across to top->right, put the old top under it on the left, and make top own the lifted node.
An AVL tree inserts like a plain BST, then walks back up the path, updating each node’s height (each node stores it, in an int height member). The first node found with a balance factor of 2 or −2 is fixed with one or two rotations, depending on which grandchild the new key went into. The rotations update the stored heights of the two nodes they move.
Lines 1-3: Called on each node on the way back up from an insert. Each node stores its height, so this is O(1). *n is the node itself, passed by reference.
Lines 4: The left side is two taller.
Lines 5: Left-right case: the left child leans right, so first straighten it with a left rotation.
Lines 6: Now it is a left-left case: one right rotation fixes it. n is the parent’s slot, so the parent now owns the new top.
Lines 7-10: The mirror image for a right side that is too tall.
Lines 11: Balanced enough: nothing to do.
QuizInteractive · disabled in the PDF
You insert 10, 20 and 30, in that order, into an empty AVL tree. What happens?
You insert 10, 20 and 30, in that order, into an empty AVL tree. What happens?
How tall can an AVL tree get?. The thinnest possible AVL tree of height h has a number of nodes that grows like the Fibonacci numbers, which bounds the height at about 1.44 log₂ n. A million keys: at most about 28 levels, against 20 for a perfect tree.
Data structures148 / 201
Module 15 · Balanced trees
Red-black trees balance more loosely
A red-black tree colours each node red or black and keeps four rules: the root is black; a red node never has a red child; every path from a node down to an empty spot passes the same number of black nodes; and empty spots count as black. Together they mean the longest path is at most twice the shortest, so the height stays below 2 log₂(n + 1).
AVL tree
Red-black tree
Balance rule
Sides differ by at most 1
No path more than twice another
Height for a million keys
At most about 28
At most about 40
Lookups
Slightly faster: shorter
Slightly slower
Inserts and deletes
At most 2 rotations per insert, but a delete may rotate at every level
At most 2 rotations per insert, 3 per delete, plus recolouring
Found in
Lookup-heavy indexes
std::map and std::set in every major C++ standard library, Java’s TreeMap, the Linux kernel
QuizInteractive · disabled in the PDF
A map is changed constantly and read about as often. Why do standard libraries often pick a red-black tree over an AVL tree for it?
A map is changed constantly and read about as often. Why do standard libraries often pick a red-black tree over an AVL tree for it?
Data structures149 / 201
Module 15 · Balanced trees
std::set and std::map are balanced trees you can use today
Because they are search trees, std::set and std::map can answer questions a hash table cannot: they iterate in sorted order, and lower_bound(x) finds the first key ≥ x in O(log n), upper_bound(x) the first key > x. Both return an iterator, a position in the container that * reads, ++ and -- (or std::prev) move, and end() means "past the last".
C++
1std::set<int> prices{20,30,40,50,60,70,80}; // <set>23auto it = prices.lower_bound(45); // *it is 504auto up = prices.upper_bound(50); // *up is 605prices.lower_bound(99)== prices.end(); // true: none67for(int p : prices) std::cout << p <<" "; // 20 30 ... 80
Sorted input does not hurt them: they rebalance with red-black rotations as you insert.
Code challengeInteractive · disabled in the PDF
Write floorKey(prices, budget) with the set’s own search: the largest price ≤ budget, or std::nullopt. Find the first price above the budget with upper_bound; the floor, if any, is just before it.
Reading from a disk is thousands of times slower than reading memory, and a disk is read in pages of a few kilobytes at a time. A B-tree makes each node one page, holding many sorted keys (often hundreds) with a child on either side of each key. The tree is so wide that it is very shallow, and all leaves sit at the same depth. Nodes split in two when full and merge when too empty, which keeps it balanced.
30 | 60
10 | 20
40 | 45 | 50
70 | 80 | 90
A small B-tree. To find 45: in the root, 45 is between 30 and 60, so take the middle child.
With 1,000 keys per node, three levels hold a billion keys, and the top levels stay cached in memory: finding any row takes one or two disk reads. That is why the indexes of SQLite, PostgreSQL and MySQL are B-trees (mostly the B+ tree variant, which keeps all values in the leaves and links the leaves for fast range scans), and why file systems such as Btrfs and APFS are built on them.
QuizInteractive · disabled in the PDF
Why does a database not use a binary tree for its index on disk?
Why does a database not use a binary tree for its index on disk?
Data structures151 / 201
Module 15 · Balanced trees
Skip lists: balance by coin flips
A skip list gets the same O(log n) without any tree. It is a sorted linked list (the linked lists module) with express lanes above it. Each node is in the bottom lane; it also joins the lane above with a coin flip, and the one above that with another, and so on. A search runs along the top lane until the next node would overshoot, drops down a lane, and repeats.
Finding 40: lane 2 to 30, drop to lane 1 (50 overshoots), drop to lane 0, then one step to 40.
About half the nodes reach lane 1, a quarter lane 2: about log₂ n lanes, each crossed in a few steps.
The cost is expected O(log n): it depends on luck, not on the order the keys arrive, and bad luck is very unlikely.
No rotations: inserting only links one node into a few lists, which is simpler and easier to make safe for many threads at once.
Redis keeps its sorted sets in skip lists.
Randomness instead of rules. A plain BST is fast on random input and slow on sorted input. A skip list (or a treap, a BST with random priorities) moves the randomness inside the structure, so no input order is bad. It is the same idea as shuffling before inserting, without needing all the keys up front.
Data structures152 / 201
Module 15 · Balanced trees
Recap
Search trees cost their height; balanced trees keep it near log₂ n whatever the input.
Height: 0 for empty, 1 more than the taller child. Balance factor: left height minus right height.
Rotations lift a child above its parent in O(1) and keep the keys’ order; with unique_ptr they are four moves of ownership.
AVL trees keep every balance factor in −1 to 1, fixing inserts with one or two rotations.
Red-black trees balance more loosely, with fewer rotations: std::map and std::set.
lower_bound and upper_bound find the first key ≥ or > a value in O(log n).
B-trees are wide and shallow, one node per disk page: database indexes and file systems.
Skip lists reach O(log n) expected with randomly built express lanes.
FlashcardsInteractive · disabled in the PDF
Balance factor
Height of the left subtree minus height of the right.
AVL rule
Every node’s balance factor is −1, 0 or 1.
Right rotation
The left child rises; its right subtree moves across under the old top.
Left-right case
Rotate the left child left, then the node right.
Red-black tree
Coloured nodes; no path more than twice another; few rotations per change. Inside std::map.
lower_bound(x) / upper_bound(x)
The first key ≥ x / the first key > x.
Iterator
A position in a container: *it reads it, ++it moves on, end() is past the last.
B-tree
Many keys per node, all leaves at one depth: few disk reads per lookup.
B+ tree
A B-tree with values only in linked leaves: good for range scans.
Skip list
A sorted linked list with random express lanes: expected O(log n).
Module 16: Disjoint sets: union-find
Data structures153 / 201
Module 16 · Disjoint sets: union-find
Which group is this in, and are these two together?
A friends network keeps growing: Ada meets Ben, Ben meets Cleo, Dev meets Eve. A friend circle is everyone you can reach through friendships. Two questions come up again and again: are Ada and Cleo in the same circle? And, when Cleo meets Dev, merge their two circles into one. A graph search (the graphs module) answers the first, but it walks the whole circle every time.
A disjoint-set structure, also called union-find, does both in almost constant time. "Disjoint" means no item is in two sets at once. It has exactly two operations: find (which group is this item in?) and union (merge the groups of these two items).
Disjoint sets
Groups that never share an item: every item is in exactly one.
Find
Give the group an item is in, as one member that stands for the whole group.
Union
Merge the groups of two items into one.
Representative
The one member a group is known by: two items are together exactly when they have the same one.
QuizInteractive · disabled in the PDF
Which of these can a plain union-find do?
Which of these can a plain union-find do?
Data structures154 / 201
Module 16 · Disjoint sets: union-find
Each group is a tree, and its root names it
Number the items 0 to n − 1 and give each a parent. Following parents always leads to a root, an item that is its own parent, and that root is the group’s representative. So each group is a tree with links pointing up, and all the groups together are a forest. The whole structure is one std::vector<int>: parent[i].
Two circles. Arrows point to each item’s parent; Ada and Dev are roots.
C++
1// Ada Ben Cleo Dev Eve2std::vector<int> parent{0,0,1,3,3};3// find(2): 2 → 1 → 0, and parent[0] is 0: the root is Ada4// find(4): 4 → 3, and parent[3] is 3: the root is Dev
Data structures155 / 201
Module 16 · Disjoint sets: union-find
Find climbs to the root; union links two roots
One C++ detail first: union is a keyword (it names a rarely used kind of struct), so the merge function is called unite. The rest is a struct with one vector and two member functions.
With the struct above and 4 items, what are parent[3] and find(3) after these calls?
Data structures156 / 201
Module 16 · Disjoint sets: union-find
Try it: count friend circles
Every union that joins two different groups leaves one group fewer. Start with n groups and count down.
Code challengeInteractive · disabled in the PDF
Finish countCircles(n, friendships): for each pair, find both roots; if they differ, link one under the other and count one group fewer. Return how many groups are left.
Unions can build long chains, and then every find climbs the whole chain. Path compression fixes this as a side effect of finding: once find knows the root, it points every item it passed straight at it. The next find from any of them takes one step.
Before find(4)
0└─ 1 └─ 2 └─ 3 └─ 4
After find(4)
0├─ 1├─ 2├─ 3└─ 4
Fill the blanksInteractive · disabled in the PDF
Make find compress the path: each item on the way gets the root as its parent.
Union by size: hang the small tree under the big one
Which root goes under which matters. Keep a size for every root, in a second std::vector<int> that starts at all 1s. Hanging the smaller tree under the larger one makes only the smaller group’s items one step further from the root. An item only moves down when its group at least doubles, which can happen at most log₂ n times, so no path is ever longer than log₂ n, even without compression. Union by rank is the same idea using an upper bound on height instead of size.
Find the bugInteractive · disabled in the PDF
This union by size builds tall trees. Which line is wrong?
C++
Put in orderInteractive · disabled in the PDF
Put the steps of a union by size with path compression in order.
1.Add the smaller group’s size to the larger root’s size
2.If the roots are the same, stop: already together
3.Set the smaller root’s parent to the larger root
4.Find the root of b, compressing its path
5.Make the root of the larger group the one that stays on top
6.Find the root of a, compressing its path
Data structures159 / 201
Module 16 · Disjoint sets: union-find
Together: nearly constant time
Version
Cost per operation
Neither trick
O(n) in the worst case: one long chain
Union by size only
O(log n)
Path compression only
O(log n) amortised
Both
O(α(n)) amortised: at most 4 for any n you will ever meet
Amortised (from the arrays module) means averaged over a long run of operations: one find may be slow, but it flattens the path so later ones are fast. α(n) is the inverse Ackermann function, which grows so slowly that it is below 5 for any n smaller than a number with far more digits than there are atoms in the universe. In practice, union-find is constant time.
Proven optimal. Robert Tarjan proved the α(n) bound in 1975, and later results showed no structure of this kind can do better. It is a rare case of a simple twenty-line struct whose analysis is famously hard. Path halving (each item on the way points to its grandparent) gets the same bound with one pass and no recursion.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<numeric>3#include<vector>45std::vector<int>parent(2000);6longlong steps =0;78intfind(int x){9while(parent[x]!= x){10 steps++;11 x = parent[x];12}13return x;14}1516intmain(){17 std::iota(parent.begin(), parent.end(),0);18 // a worst case: each union hangs the big chain under a new root19for(int i =1; i <2000; i++) parent[find(i -1)]= i;20 steps =0;21for(int i =0; i <2000; i++)find(i);22 std::cout <<"steps for 2,000 finds without the tricks: "23<< steps <<"\n";24}
Add path compression to find (point everything at the root) and run it again.
Data structures160 / 201
Module 16 · Disjoint sets: union-find
Union-find for pixels, networks and cycles
Connected pixels: a paint program’s "fill" or counting islands on a map. Each cell is an item; union each cell with matching neighbours, and each root is one region.
Networks: are these two computers connected yet, as cables are added one by one?
Cycles: adding an edge between two items already in the same group closes a loop. unite returning false says exactly that.
Cheapest network: Kruskal’s algorithm (in the Algorithms course) adds the cheapest edges first, skipping any that would close a cycle.
Code challengeInteractive · disabled in the PDF
Write firstCycle(n, cables): cables join computers one at a time. Return the index of the first cable that joins two computers already connected, or -1 if none does. UnionFind is ready, with both tricks.
Groups that share no items: each item is in exactly one.
Find
Follow parents to the root, the group’s representative.
Union
Find both roots; if different, hang one under the other.
Path compression
During find, point every item passed at the root.
Union by size
Hang the smaller group’s root under the larger’s.
α(n)
The inverse Ackermann function: below 5 for any practical n.
Cycle with union-find
An edge whose ends already share a root closes a cycle.
What union-find cannot do
Split a group, or tell you the path between two items.
Module 17: Caches: combining structures
Data structures162 / 201
Module 17 · Caches: combining structures
A cache keeps answers you may need again
A librarian keeps the books people asked for this week on a trolley by the desk, instead of walking to the stacks each time. The trolley is a cache: a small, fast store of things that are slow to get, kept in case they are asked for again. A request the trolley can answer is a hit; one that means a walk to the stacks is a miss. The trolley has room for only so many books, so when it is full, one has to go back: an eviction.
Look in the cache first. A hit answers straight away.
A miss fetches the answer from the slow source.
The answer is stored for next time, evicting an old entry if the cache is full.
Every cache, from a processor’s to a website’s, works this way.
Cache
A small fast store of answers that are slow to get.
Hit / miss
The answer was in the cache / it had to be fetched.
Hit rate
The share of requests that are hits.
Capacity
How many entries the cache may hold.
Eviction
Removing an entry to make room for a new one.
QuizInteractive · disabled in the PDF
A cache answers 900 of 1,000 requests itself. A hit takes 1 ms and a miss 100 ms. What is the average?
A cache answers 900 of 1,000 requests itself. A hit takes 1 ms and a miss 100 ms. What is the average?
Data structures163 / 201
Module 17 · Caches: combining structures
Which entry goes when the cache is full?
Policy
Evicts
Good when
Weak when
LRU, least recently used
The entry unused for longest
Recent requests predict the next ones (most real traffic)
One big scan pushes out everything useful
LFU, least frequently used
The entry used fewest times
Popularity is stable: the bestsellers stay
Old favourites hang on after they stop being asked for
FIFO, first in, first out
The oldest entry
Simplicity matters more than hit rate
It evicts popular entries just for being old
Random
Any entry
You need something tiny and fast
Unlucky evictions
TTL, time to live
Entries older than a set time
Answers go stale: prices, news
Used alone, it does not bound memory
QuizInteractive · disabled in the PDF
A music app caches songs on the phone. People replay the songs they have been listening to lately. Which policy fits best?
A music app caches songs on the phone. People replay the songs they have been listening to lately. Which policy fits best?
Data structures164 / 201
Module 17 · Caches: combining structures
The classic LRU: a hash map plus a doubly linked list
An LRU cache needs two things at once: find any entry by its key quickly, and keep the entries in order of use. No single structure does both, so the classic design combines two, each doing what it is best at. A doubly linked list (the linked lists module) keeps entries in order of use, most recent at the front; with links both ways, any entry can be unlinked in O(1). A hash map from key to list node finds any entry in O(1), without walking the list.
The map finds a node in one step; the list says how recently each was used.
Put in orderInteractive · disabled in the PDF
Put the steps of get(key) in the map-plus-list LRU in order.
1.Look the key up in the map
2.If it is missing, return nothing (std::nullopt)
3.Link the node in at the front of the list
4.Unlink its node from where it is in the list
5.Return the node’s value
Sentinels. A hand-written version keeps two dummy nodes, head and tail, that are always there. Every real node then has a node on each side, so linking and unlinking never need an if for "the first one" or "the last one". std::list does the same inside: its end() is a sentinel node that the list joins into a ring.
Data structures165 / 201
Module 17 · Caches: combining structures
std::list moves a node in O(1) with splice
C++ hands you both halves ready-made: std::list is a doubly linked list and std::unordered_map a hash map. What joins them is the iterator. An iterator into a std::list points at one node, and it stays valid while other nodes are added, removed or moved around; only erasing its own node breaks it. So the map can store, for each key, an iterator to that key’s node.
list.splice(where, list, it) unlinks the node at it and links it back in before where. No node is made, copied or freed: only a few pointers change. Splicing to begin() is "mark as most recently used".
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<iterator>3#include<list>4#include<string>56intmain(){7 std::list<std::string> order{"c","b","a"}; // most recent first8auto it = std::next(order.begin(),2); // the node "a"9 order.splice(order.begin(), order, it); // "a" to the front10for(constauto& key : order) std::cout << key <<" ";11 std::cout <<"\nit still points at "<<*it <<"\n";12 std::cout <<"least recent: "<< order.back()<<"\n";13}
std::next(it, 2) steps an iterator forward twice. Splice "b" to the front instead, and see it keep pointing at "a".
QuizInteractive · disabled in the PDF
Why does the classic C++ LRU keep its entries in a std::list and store list iterators in the map, rather than keeping entries in a std::vector and storing indexes?
Why does the classic C++ LRU keep its entries in a std::list and store list iterators in the map, rather than keeping entries in a std::vector and storing indexes?
Lines 1-4: Each list node holds its key as well as its value: when the last node is evicted, you need its key to erase it from the map too.
Lines 8: The list keeps the order of use.
Lines 9-10: The map goes from a key to an iterator: the position of that key’s node in the list.
Lines 13-14:find looks without changing anything. A miss returns std::nullopt.
Lines 15-17: A hit: found->second is the list iterator. Splice its node to the front, then read the value through it.
Asking with `[]` changes the map.index[key] on a missing key does not say "not found": it inserts the key with an empty value and returns that. In a cache this quietly creates broken entries that point nowhere. Use find (or contains, from C++20) to ask, and [] only to store.
struct or class. Here every member is public for brevity. A real cache would be a class with order and index under private:, so no outside code could change one without the other.
Data structures167 / 201
Module 17 · Caches: combining structures
Try it: the map-plus-list LRU
Code challengeInteractive · disabled in the PDF
Finish get and put. get splices a hit to the front. put updates and splices an existing key; otherwise it adds the entry at the front, stores its iterator in index, and, over capacity, evicts order.back() from both structures.
A pure function always gives the same result for the same arguments and changes nothing else. Its results can be cached: memoising a function means each argument is computed once and remembered, here in a std::unordered_map from argument to result. Unlike an LRU, a plain memo cache never evicts, so it suits a small set of possible arguments.
PlaygroundInteractive · disabled in the PDF
C++
1#include<iostream>2#include<unordered_map>34longlong calls =0;5std::unordered_map<int,longlong> memo;67longlongways(int steps){ // climb 1 or 2 stairs at a time8 calls++;9if(steps <=1)return1;10auto hit = memo.find(steps);11if(hit != memo.end())return hit->second;12longlong result =ways(steps -1)+ways(steps -2);13 memo[steps]= result;14return result;15}1617intmain(){18 std::cout <<"ways to climb 30 stairs: "<<ways(30)<<"\n";19 std::cout <<"calls: "<< calls <<"\n";20}
Delete the three memo lines (10, 11 and 13) and run it: the same answer, with over a million calls instead of 59.
Find the bugInteractive · disabled in the PDF
This memoised cost keeps recomputing for some arguments, slowly. Which line is the bug?
C++
A memoiser for any function. Writing one memoise that wraps any function takes a template plus a lambda (a function written inline, which the Algorithms course uses a lot). The map then lives inside the lambda, so each memoised function gets its own. The key must be hashable: memoising on a struct needs a hash for it, or a std::map with <.
Data structures169 / 201
Module 17 · Caches: combining structures
Expiry: entries that go stale
Some answers are only good for a while: today’s exchange rate, a weather forecast. A TTL (time to live) stores each entry with the time it expires. The simplest version expires lazily: nothing runs in the background, and get checks the time, erasing an entry it finds out of date. Passing the current time in as an argument, now, makes it testable: a test can move time forward without waiting.
Code challengeInteractive · disabled in the PDF
Finish TtlCache: set stores the value with expires = now + ttl; get returns it only while now is before expires, and erases it once it has expired.
The stampede. When a popular entry expires, hundreds of requests can miss at the same moment and all hit the slow source together. Fixes: let the first miss fetch while the others wait for its answer, refresh entries a little before they expire, or add a random jitter to each TTL.
Data structures170 / 201
Module 17 · Caches: combining structures
Recap
A cache keeps answers that are slow to get; hits are cheap, misses dominate the cost.
Eviction policies: LRU (the usual default), LFU, FIFO, random; TTL for answers that go stale.
The classic LRU combines a hash map (find in O(1)) with a doubly linked list (reorder in O(1)).
In C++: std::list of entries, std::unordered_map from key to list iterator, and splice to move a node to the front.
Keep combined structures in step: every entry in one is in the other.
Memoising caches a pure function’s results; ask with find or contains, never by a special value or with [].
TTL stores an expiry time; pass the time in so tests control it.
FlashcardsInteractive · disabled in the PDF
Hit rate
The share of requests a cache answers itself.
LRU
Least recently used: evict the entry unused for longest.
LFU
Least frequently used: evict the entry used fewest times.
list.splice(list.begin(), list, it)
Moves the node at it to the front in O(1); it stays valid.
Why store list iterators in the map?
They stay valid while other nodes move, so the map always finds its node.
Why a doubly linked list in an LRU?
To unlink any entry in O(1), given only the entry.
Sentinel node
A dummy node at an end of a list, so links never need special cases.
Memoise
Wrap a pure function to remember its result for each argument.
Lazy expiry
Check an entry’s expiry when it is read, instead of in the background.
Module 18: Range queries
Data structures171 / 201
Module 18 · Range queries
Totals over a range, asked again and again
A bookshop records its sales for each day of the year. The manager keeps asking questions like "how many books did we sell from day 40 to day 75?" A range query asks for a total (or a minimum, a maximum…) over a run of neighbouring positions, from to to. Adding up the run each time takes one step per day in it: O(n) per question.
C++
1longlongsalesBetween(const std::vector<int>& sales,int from,2int to){3longlong total =0;4for(int day = from; day <= to; day++) total += sales[day];5return total;6}
Correct, and fine once. Not fine a million times.
QuizInteractive · disabled in the PDF
A dashboard shows 10,000 range totals over 1,000,000 data points, each range about half the data. About how many additions does the loop above do?
A dashboard shows 10,000 range totals over 1,000,000 data points, each range about half the data. About how many additions does the loop above do?
Range query
A question about a run of positions: the sum from from to to.
Inclusive range
Both ends count: days 2 to 4 means days 2, 3 and 4.
Point update
Changing the value at one position.
Data structures172 / 201
Module 18 · Range queries
Prefix sums: subtract two running totals
Keep a running total as you go: prefix[i] is the sum of the first i values, so prefix[0] is 0. The total from from to to is everything up to to, minus everything before from: prefix[to + 1] - prefix[from]. One pass to build, then any range in O(1).
Index
0
1
2
3
4
5
6
sales
3
1
4
1
5
9
prefix
0
3
4
8
9
14
23
WalkthroughInteractive · disabled in the PDF
C++
1std::vector<int> sales{3,1,4,1,5,9};23std::vector<longlong>prefix(sales.size()+1,0);4for(std::size_t i =0; i < sales.size(); i++)5 prefix[i +1]= prefix[i]+ sales[i];6// prefix is {0, 3, 4, 8, 9, 14, 23}78longlongsum(int from,int to){9return prefix[to +1]- prefix[from];10}1112sum(2,4); // 14 - 4 = 10, that is 4 + 1 + 5
Lines 3: One more slot than there are values, all 0. Slot 0 stays 0: the sum of no values. This one extra slot removes every special case.
Lines 4-5: Each entry is the previous total plus the next value: one pass, O(n).
Lines 8-10: Everything up to and including to, minus everything before from.
Lines 12: Days 2 to 4: prefix[5] is 3 + 1 + 4 + 1 + 5 = 14; take away prefix[2], 3 + 1 = 4.
QuizInteractive · disabled in the PDF
With the same prefix, what is sum(0, 5), and what is sum(3, 3)?
With the same prefix, what is sum(0, 5), and what is sum(3, 3)?
Why `long long`. An int holds up to about 2.1 billion. A year of sales, or a million values of a few thousand each, can add up to more, and in C++ a signed int that overflows is undefined behaviour: the program may print nonsense, or the compiler may assume it never happens and remove your checks. Running totals go in long long (up to about 9.2 × 10¹⁸) even when the values themselves are int.
Data structures173 / 201
Module 18 · Range queries
Try it: a range-sum struct
Find the bugInteractive · disabled in the PDF
sum(1, 3) should be 1 + 4 + 1 = 6 but gives 5. Which line is the bug?
C++
Code challengeInteractive · disabled in the PDF
Finish RangeSum: fill prefix in the constructor, then make sum(from, to) return the total of values[from] through values[to] in O(1).
A park is mapped as a grid, each cell holding how many trees grow there: a std::vector<std::vector<int>>, grid[row][col]. "How many trees in this rectangle?" works the same way. P[r][c] is the total of the rectangle from the top-left corner up to (but not including) row r and column c. Each entry is built from its neighbours: add the total above and the total to the left, and subtract the corner that both counted.
Fill the blanksInteractive · disabled in the PDF
Complete the build, and the query for rows r1–r2 and columns c1–c2, both inclusive.
C++
1// P has one more row and column than grid, all starting at 02P[r +1][c +1]= grid[r][c]+ P[r][c +1]+ P[r +1][c]3 P[r][c];45longlong total = P[r2 +1][c2 +1]6 P[r1][c2 +1]7 P[r2 +1][c1]8 P[r1][c1];
The query: the whole block, minus the strips above and to the left, plus the corner removed twice.
Data structures175 / 201
Module 18 · Range queries
Try it: trees in a rectangle
Code challengeInteractive · disabled in the PDF
The constructor builds P. Write count(r1, c1, r2, c2): the total of the rectangle from row r1, column c1 to row r2, column c2, both corners included.
C++
1#include<vector>2usingGrid= std::vector<std::vector<int>>;34structGridSum{5 std::vector<std::vector<longlong>> P;6explicitGridSum(constGrid& grid)7:P(grid.size()+1,8 std::vector<longlong>(grid[0].size()+1,0)){9for(std::size_t r =0; r < grid.size(); r++)10for(std::size_t c =0; c < grid[0].size(); c++)11 P[r +1][c +1]= grid[r][c]+ P[r][c +1]+ P[r +1][c]12- P[r][c];13}14longlongcount(int r1,int c1,int r2,int c2)const{15return0;16}17};
A vector of vectors is many blocks. Each inner std::vector is its own allocation, so rows are not side by side in memory. For big grids, store all cells in one std::vector<long long> of rows × cols and read cell (r, c) at r * cols + c: one block, friendlier to the cache, and one allocation instead of one per row.
Data structures176 / 201
Module 18 · Range queries
When values change, prefix sums fall behind
A late return changes Tuesday’s sales. Every running total from Tuesday on is now wrong, and fixing them is O(n). If changes are rare, rebuild. If changes and questions are mixed, thousands of each, you need a structure that is fast at both.
Structure
Range sum
Change one value
Memory
Plain vector
O(n)
O(1)
n
Prefix sums
O(1)
O(n)
n + 1
Fenwick tree
O(log n)
O(log n)
n + 1
Segment tree
O(log n)
O(log n)
about 2n to 4n
QuizInteractive · disabled in the PDF
A live scoreboard changes a score 5,000 times a second and is asked for range totals 5,000 times a second, over 100,000 players. Which fits?
A live scoreboard changes a score 5,000 times a second and is asked for range totals 5,000 times a second, over 100,000 players. Which fits?
Data structures177 / 201
Module 18 · Range queries
A Fenwick tree stores sums of power-of-two blocks
A Fenwick tree (or binary indexed tree) is one vector, numbered from 1, where cell i holds the sum of a block of values ending at i. The block’s length is the value of the lowest 1 bit in i’s binary form, written i & -i in code (& keeps the bits two numbers share; -i flips i’s bits and adds one, which leaves exactly the lowest 1 in common).
i
Binary
i & -i
Cell i holds the sum of
1
0001
1
value 1
2
0010
2
values 1–2
3
0011
1
value 3
4
0100
4
values 1–4
5
0101
1
value 5
6
0110
2
values 5–6
7
0111
1
value 7
8
1000
8
values 1–8
The sum of values 1 to 7 is then three cells, 7 + 6 + 4 (blocks 7, 5–6 and 1–4): from i, subtract i & -i to jump to the next block down. Changing value 5 touches the cells whose blocks contain it, 5, 6 and 8: from i, addi & -i to jump up. Either way, about log₂ n cells.
QuizInteractive · disabled in the PDF
Which cells does the sum of values 1 to 6 read?
Which cells does the sum of values 1 to 6 read?
PlaygroundInteractive · disabled in the PDF
C++
1#include<bitset>2#include<iostream>34intmain(){5for(int i =1; i <=12; i++){6int low = i &-i;7 std::cout << i <<" "<< std::bitset<4>(i)<<" covers "8<< i - low +1<<" to "<< i <<"\n";9}10}
std::bitset<4>(i) prints the lowest 4 bits of i. Try 16 or 24 (with std::bitset<5>), and see how far back each block reaches.
Data structures178 / 201
Module 18 · Range queries
A Fenwick tree in code
WalkthroughInteractive · disabled in the PDF
C++
1structFenwick{2 std::vector<longlong> tree;34explicitFenwick(int n):tree(n +1,0){}56voidadd(int i,longlong delta){7for(; i <static_cast<int>(tree.size()); i += i &-i)8 tree[i]+= delta;9}10longlongprefix(int i)const{11longlong sum =0;12for(; i >0; i -= i &-i) sum += tree[i];13return sum;14}15};
Lines 4: One cell per value, plus an unused cell 0 so indexes start at 1.
Lines 6-9:add: a value changed by delta. Climb to every block that contains it, adding i & -i each time. static_cast<int> turns the unsigned size into an int to compare with i.
Lines 10-14:prefix(i): the sum of values 1 to i. Collect blocks going down, removing the lowest bit each time.
Code challengeInteractive · disabled in the PDF
add is written. Finish prefix(i) (the sum of values 1 to i), then range(from, to): the sum of values from to to, both included.
C++
1#include<vector>23structFenwick{4 std::vector<longlong> tree;5explicitFenwick(int n):tree(n +1,0){}67voidadd(int i,longlong delta){8for(; i <static_cast<int>(tree.size()); i += i &-i)9 tree[i]+= delta;10}11longlongprefix(int i)const{12return0;13}14longlongrange(int from,int to)const{15return0;16}17};
Data structures179 / 201
Module 18 · Range queries
Segment trees answer more kinds of question
A Fenwick tree needs an answer you can subtract, so it does sums but not minimums. A segment tree is a binary tree where each node covers a range and stores its answer: the root covers everything, its children each half, down to single values at the leaves. A query combines the few nodes that exactly cover the range; an update fixes one leaf and the nodes above it. Both are O(log n), for any way of combining that does not care how you group it (sum, min, max, greatest common divisor). Like a heap, it is usually stored in a vector, with the children of node i at 2i and 2i + 1.
90–3
40–1
30
11
52–3
42
13
A segment tree of sums over {3, 1, 4, 1}. A query for positions 1–2 combines the two leaves 1 and 4.
Choosing between them. Use prefix sums when the data does not change. Use a Fenwick tree for sums with updates: a dozen lines, small and fast. Use a segment tree for min, max or anything you cannot subtract, and for range updates ("add 5 to days 10 to 20"), which it handles in O(log n) with lazy propagation: a node remembers an update meant for its whole range and passes it down only when a query needs it.
Data structures180 / 201
Module 18 · Range queries
Recap
A range query asks for a total (or min, max…) over positions from to to.
Prefix sums: prefix[i] is the sum of the first i values; a range is prefix[to + 1] - prefix[from], O(1).
Keep totals in long long: signed int overflow is undefined behaviour.
2D prefix sums answer rectangle totals with four reads, by inclusion-exclusion.
Prefix sums cost O(n) per change; mixed changes and questions need something else.
A Fenwick tree stores power-of-two blocks; i & -i jumps between them. Both operations O(log n).
Segment trees handle min, max and range updates, at more memory and code.
FlashcardsInteractive · disabled in the PDF
Prefix sum
A running total: prefix[i] is the sum of the first i values.
Range sum from prefix sums
prefix[to + 1] - prefix[from].
Why std::vector<long long> for running totals?
Totals outgrow int (about 2.1 billion), and signed overflow is undefined behaviour.
Inclusion-exclusion (2D)
Whole block − strip above − strip left + the corner removed twice.
i & -i
The value of i’s lowest 1 bit: the length of Fenwick cell i’s block.
Fenwick tree
One vector of block sums: point updates and prefix sums in O(log n).
Why not prefix sums for minimums?
You cannot subtract minimums the way you subtract sums.
Segment tree
A tree of ranges, each storing its answer: O(log n) queries and updates for any associative operation.
Lazy propagation
A segment tree node holds an update for its range and passes it down only when needed.
Module 19: Persistent structures
Data structures181 / 201
Module 19 · Persistent structures
Changing data in place forgets the past
Every structure so far changed in place: push_back alters the vector, insert rewires the tree. The old version is gone. Yet often you want it back: an editor’s undo, a spreadsheet’s version history, a game you can rewind.
An immutable value never changes after it is made (in C++, a const one). To "change" it, you make a new value. A persistent structure is an immutable one built so that each update is cheap: the new version reuses most of the old one, and every old version stays usable.
Mutable
Can be changed in place after it is made.
Immutable
Never changes after it is made; updates make new values.
Version
The state of a structure after some update. Old versions stay valid.
Persistent structure
An immutable structure whose updates make new versions cheaply, keeping the old ones.
Structural sharing
A new version pointing at the unchanged parts of the old one instead of copying them.
Predict the outputInteractive · disabled in the PDF
auto v2 = v1; // copies every itemv2.push_back("Dune");// O(n) time and memory// for every single change
Structural sharing
The new version is a few new pieces plus pointers to the old, unchanged pieces. Since nothing is ever changed in place, sharing is safe.
Two versions of a list. Adding 4 at the front made one new cell; 3, 2 and 1 are shared.
Why sharing needs immutability. If version 2 could change the cell holding 3, version 1 would see the change too. Sharing is only safe because no one is allowed to change a cell after it is made. That rule is what makes old versions trustworthy.
Data structures183 / 201
Module 19 · Persistent structures
Shared cells need shared owners: std::shared_ptr
A shared cell has several owners: every version that points at it. A std::unique_ptr allows only one, so persistent structures use std::shared_ptr (also from <memory>). It keeps a reference count, the number of shared_ptrs pointing at the object, next to it. Copying a shared_ptr adds one; destroying one takes one away; when the count reaches 0, the object is deleted. use_count() reads the count.
C++
1auto a = std::make_shared<constint>(42); // count 12{3 std::shared_ptr<constint> b = a; // count 2: two owners4} // b gone: count 15a =nullptr; // count 0: deleted
const int in the angle brackets: nobody can change the value through these pointers, which is what makes sharing it safe.
`std::shared_ptr<T>`
A pointer that shares ownership: the object lives until the last shared_ptr to it is gone.
Reference count
How many shared_ptrs point at an object right now.
`std::make_shared<T>(…)`
Makes an object and its count in one allocation, and returns a shared_ptr to it.
QuizInteractive · disabled in the PDF
Version 1 is a list of three cells. Versions 2 and 3 each put one new cell in front of version 1. What is the reference count of version 1’s first cell?
Version 1 is a list of three cells. Versions 2 and 3 each put one new cell in front of version 1. What is the reference count of version 1’s first cell?
Data structures184 / 201
Module 19 · Persistent structures
A persistent linked list
The linked list from the linked lists module becomes persistent when each cell is reached through a std::shared_ptr<const Cell>: read-only, and shared. Adding at the front makes one new cell pointing at the old list: O(1), and the old list is untouched. This is the oldest persistent structure, used by Lisp since the 1950s.
Lines 1-6: A list is a pointer to a cell (a head value and the rest of the list), or nullptr for empty. struct Cell; declares the name first, so List can mention it before the struct is written out.
Lines 8-10:prepend makes one new cell in front of an existing list: O(1). Moving tail in saves one count increment and decrement.
Lines 12: The list 3, 2, 1.
Lines 13-14: Two new versions, 4, 3, 2, 1 and 9, 3, 2, 1, from the same old one.
Lines 16-17:== on shared_ptrs compares addresses: the same cell, not an equal one. Three versions, five cells in all.
Every past version is still there, for free. Try doc->head = "x"; and read the compiler’s error: the cells are const.
Data structures185 / 201
Module 19 · Persistent structures
Try it: change one item, share the rest
Changing the item at position i cannot touch the old cells, so the cells before it are copied (each new one points at the next new one), and everything after it is shared. That is O(i), and the old list still reads as before.
Code challengeInteractive · disabled in the PDF
Write update(list, index, value): a new list with value at index, sharing every cell after it with the old list. If the list is too short, return the very same list. Recurse: at index 0 make one cell in front of the old tail; otherwise update the tail, and copy this cell only if the tail came back different.
C++
1#include<memory>2#include<string>3structCell;4usingList= std::shared_ptr<constCell>;5structCell{6 std::string head;7List tail;8};9Listcell(std::string head,List tail){10return std::make_shared<Cell>(Cell{std::move(head), std::move(tail)});11}1213Listupdate(constList& list,int index,const std::string& value){14 // TODO: copy up to index, share after it15return list;16}
Data structures186 / 201
Module 19 · Persistent structures
Path copying: a new tree in O(log n)
Trees share even better. To insert into an immutable binary search tree, copy only the nodes on the path from the root down to where the new key goes. Each copied node points at its one new child and shares its other, untouched subtree. In a balanced tree the path is about log₂ n nodes, so a million-key tree makes about 20 new nodes per insert.
The old tree: 50, with 30 (and 20 below it) on the left and 70 on the right.
40 belongs right of 30. Make it as a new node.
Copy 30, with 20 shared on the left and the new 40 on the right.
Copy 50, with the new 30′ on the left and the old 70 side shared. The new root is the new version; the old root still is the old one.
Inserting 40. Only 50 and 30 are copied (marked ′); 20 and the whole 70 side are shared.
Put in orderInteractive · disabled in the PDF
In what order are the new nodes finished when a recursive persistent insert adds 40?
1.The new leaf 40 is made
2.The new root is returned as the new version
3.A copy of 30 is made, with right child 40
4.A copy of 50 is made, with left child the new 30
Data structures187 / 201
Module 19 · Persistent structures
Try it: a persistent insert
Code challengeInteractive · disabled in the PDF
Write insert(tree, key) for an immutable BST: return a new tree with key added, copying only the nodes on the path (node(left, key, right) makes one). If the key is already there, return the very same tree, not a copy.
Same pointer, same contents: change detection for free
Programs that redraw a screen, save to disk or sync over a network want to know what changed since last time. Comparing two big structures item by item is slow. With persistent data, an unchanged part is the same object, so comparing two shared_ptrs with == answers "did anything in here change?" instantly. Copying a whole state is cheap too: it is a few pointers.
C++
1structCart{ std::vector<std::string> items;};2structUser{ std::string name;};3structState{4 std::shared_ptr<constCart> cart;5 std::shared_ptr<constUser> user;6};78State next = state; // copies two pointers9auto cart = std::make_shared<Cart>(*state.cart); // copy the cart10cart->items.push_back("Dune");11next.cart = cart;1213next.cart == state.cart // false: on the path to the change14next.user == state.user // true: shared, nothing to redraw
*state.cart is the cart itself; make_shared<Cart>(*state.cart) copies it into a new, private one that is safe to change before anyone else sees it.
Find the bugInteractive · disabled in the PDF
Here the cart is held by std::shared_ptr<Cart>, not const. After addToCart, the old state has the new book too. Which line is the bug?
C++
Libraries that do it for you. Persistent vectors and maps in Clojure, and C++ libraries such as immer, store data as wide trees with 32 children per node, so an update copies about log₃₂ n nodes: 4 for a million items. Lookups stay close to a plain array’s speed because each node is a small contiguous block.
Data structures189 / 201
Module 19 · Persistent structures
What persistence costs
Change one item in n
Time
Extra memory
Old version kept?
Mutable vector
O(1)
None
No
Copy the whole vector
O(n)
O(n) per version
Yes
Persistent list, at position i
O(i)
O(i) per version
Yes
Persistent balanced tree
O(log n)
O(log n) per version
Yes
Gains: free undo and history, safe sharing between parts of a program (and between threads, since nothing changes), and change detection with ==.
Costs: many small allocations, pointer-chasing instead of tight vectors, and a reference count updated on every shared_ptr copy. The count is changed with atomic (thread-safe) instructions, so it is slower than a plain ++; pass shared_ptrs by const& when you do not keep them.
A common middle path: mutate freely inside a function on data nobody else can see, and only hand out values that will not change.
Reference counts cannot free cycles. If A holds a shared_ptr to B and B holds one to A, both counts stay above 0 forever: a leak. Persistent structures are safe, since every new cell points only at older ones, but a graph of shared_ptrs is not. And as with unique_ptr, freeing a very long list frees cell after cell recursively, which can overflow the stack.
QuizInteractive · disabled in the PDF
Which job gains most from a persistent structure?
Which job gains most from a persistent structure?
Data structures190 / 201
Module 19 · Persistent structures
Bonus: a Bloom filter trades certainty for space
One last trade-off, of a different kind. A Bloom filter answers "have I seen this?" in a few bytes per item, by allowing one kind of mistake. It is an array of m bits, all 0, and k different hash functions (the hash tables module). To add an item, hash it k ways and set those k bits to 1. To ask about an item, check its k bits: if any is 0, it was definitely never added; if all are 1, it probably was, since other items may have set those bits.
PlaygroundInteractive · disabled in the PDF
C++
1#include<bitset>2#include<cstdint>3#include<iostream>4#include<string>56constexprint m =64;7std::bitset<m> bits;89intspot(const std::string& text, std::uint32_t seed){10 std::uint32_t h =2166136261u^ seed; // FNV-1a hashing11for(unsignedchar ch : text) h =(h ^ ch)*16777619u;12return h % m;13}14voidadd(const std::string& t){15for(std::uint32_t s =1; s <=3; s++) bits.set(spot(t, s));16}17boolmightHave(const std::string& t){18for(std::uint32_t s =1; s <=3; s++)19if(!bits.test(spot(t, s)))returnfalse;20returntrue;21}22intmain(){23for(auto name : {"ada","ben","cleo","dev"})add(name);24 std::cout << std::boolalpha <<"ada? "<<mightHave("ada")25<<", zoe? "<<mightHave("zoe")<<"\n"26<< bits.count()<<" of "<< m <<" bits set\n";27}
std::bitset<64> is 64 bits packed into 8 bytes. Unsigned numbers wrap around on overflow (unlike signed ones), which is what a hash wants. Add 30 more names and ask about ones you never added: false "maybe"s start to appear.
QuizInteractive · disabled in the PDF
A sign-up form checks a Bloom filter of taken usernames before asking the database. What can the filter’s answer tell you for sure?
A sign-up form checks a Bloom filter of taken usernames before asking the database. What can the filter’s answer tell you for sure?
Sizing one. With about 10 bits per item and 7 hash functions, the false positive rate is about 1%, however long the items are: storing a million long URLs takes about 1.2 MB. Each halving of the false positive rate costs roughly 1.44 more bits per item.
Data structures191 / 201
Module 19 · Persistent structures
Recap
An immutable value never changes; a persistent structure makes cheap new versions and keeps the old ones.
Structural sharing reuses unchanged parts, which is only safe because nothing changes in place.
std::shared_ptr<const T> gives shared, read-only ownership; a reference count frees each part when no version needs it.
A persistent linked list adds at the front in O(1); changing position i copies i cells.
Path copying: an insert into an immutable tree copies the root-to-leaf path, O(log n) when balanced.
An unchanged part is the same pointer, so == detects change instantly. Copying a shared_ptr shares, it does not copy.
Persistence buys history and safe sharing, at the price of allocations, counts and a slower constant.
A Bloom filter says "definitely not" or "probably": a few bits per item, no false negatives.
FlashcardsInteractive · disabled in the PDF
Immutable
Never changed after it is made.
Persistent structure
Every update gives a new version; old versions stay usable.
Structural sharing
New versions point at the unchanged parts of old ones.
std::shared_ptr
Shared ownership: a reference count, and the object is deleted when it reaches 0.
Path copying
Copy only the nodes from the root to the change; share every other subtree.
Testing for sharing
== on two shared_ptrs: the very same object, not an equal one.
Copying a shared_ptr
Copies the pointer and adds an owner; the object itself is not copied.
What shared_ptr cannot free
Cycles: objects that own each other.
Bloom filter
k hashed bits per item: "definitely not" or "probably yes".
False positive / negative
Saying yes wrongly / saying no wrongly. Bloom filters only make the first.
Module 20: Mastery challenge
Data structures192 / 201
Module 20 · Mastery challenge
How this challenge works
Each slide is one problem, the kind you meet when you build, debug or review real code. Most need two structures working together. Work each out on paper first: draw the structure, step through a small example, then write the code. If one stumps you, the explanation names the module it comes from; go back, then return.
Predict the outputInteractive · disabled in the PDF
What does this print?
C++
1std::unordered_map<std::string,int> stock{{"emma",2}};2if(stock["dune"]==0) std::cout <<"out of dune, ";3std::cout << stock.size();
What does this print?
Data structures193 / 201
Module 20 · Mastery challenge
Choose a structure by the operations you need
You need to…
Reach for
Because
Keep items in order, index them, add at the end
std::vector
O(1) index and amortised push_back; contiguous
Look things up by a key
A hash table (std::unordered_map, std::unordered_set)
O(1) on average
Keep keys sorted, find the next larger, scan a range
A balanced search tree (std::map, std::set); a B-tree on disk
O(log n), in order
Always take the smallest or largest next
A heap (std::priority_queue)
O(log n) push and pop, O(1) peek
Serve in arrival order
A queue (std::queue, a ring buffer)
O(1) at both ends
Undo the latest thing, match brackets
A stack (a vector’s back)
O(1) at the top
Find everything starting with a prefix
A trie
O(prefix length) to get there
Merge groups and ask "together?"
Union-find
Nearly O(1) amortised
Sum ranges while values change
A Fenwick or segment tree
O(log n) both ways
Keep the n most recently used, evicting the least recently used
An LRU: std::list plus std::unordered_map
O(1) per use
Keep every past version
A persistent structure (std::shared_ptr<const …>)
Shares what did not change
QuizInteractive · disabled in the PDF
A calendar app receives reminders in any order, each with a time. It must always fire the earliest pending reminder next, and new reminders keep arriving. Which structure?
A calendar app receives reminders in any order, each with a time. It must always fire the earliest pending reminder next, and new reminders keep arriving. Which structure?
Put in orderInteractive · disabled in the PDF
Put the steps of choosing a structure for a new feature in order.
1.Pick the simplest one that is fast enough
2.List the operations the feature needs
3.Compare what each candidate structure costs for those operations
4.Measure with realistic data, and change only if it is too slow
5.Estimate how often each runs, and how big the data gets
Data structures194 / 201
Module 20 · Mastery challenge
Build it: a stack that knows its minimum
A stack (the stacks module) with one more operation: min(), the smallest value currently on it, in O(1). Scanning the stack is O(n). The trick is a second stack that stores, for each level, the minimum so far, so popping restores the previous minimum automatically.
Code challengeInteractive · disabled in the PDF
Finish MinStack: keep mins in step with items, so min() returns the smallest value on the stack (or std::nullopt when empty) in O(1), including after pops.
A bookshop’s search box wants the k most searched words. Two structures: a std::unordered_map counts each word (hash tables), and a min-heap of size k keeps the best k counts seen so far (heaps). Each entry goes in as a std::pair of count and word, so pairs compare by count first; when the heap grows past k, popping removes the weakest.
Code challengeInteractive · disabled in the PDF
Write topWords(words, k): count each word, keep heap (a min-heap of {count, word} pairs) at most k entries big, and return the words most frequent first.
A photo app caches thumbnails, which come in different sizes, so the limit is total bytes, not a number of entries. Putting in one large image may evict several small ones, and an image larger than the whole budget is not cached at all (it would evict everything and still not fit).
Code challengeInteractive · disabled in the PDF
Finish put(key, value, size): remove any old entry for key; skip anything bigger than budget; otherwise add it at the front, store its iterator, add its size to used, and evict from the back until used fits the budget.
A town plans its water pipes between n houses. To avoid waste, the pipes must connect every house with no loops: the network must be a tree. A graph with n vertices is a tree exactly when it has n − 1 edges and none of them closes a cycle. Union-find checks the second part as the edges are added.
Code challengeInteractive · disabled in the PDF
Write isTree(n, pipes): false unless there are exactly n - 1 pipes; then unite each pipe’s ends, returning false if a pipe joins two houses already connected.
C++
1#include<numeric>2#include<utility>3#include<vector>4usingPipes= std::vector<std::pair<int,int>>;56intfind(const std::vector<int>& parent,int x){7while(parent[x]!= x) x = parent[x];8return x;9}10boolisTree(int n,constPipes& pipes){11 std::vector<int>parent(n);12 std::iota(parent.begin(), parent.end(),0);13returnstatic_cast<int>(pipes.size())== n -1;14}
Data structures198 / 201
Module 20 · Mastery challenge
Review it: a heap that is almost right
Find the bugInteractive · disabled in the PDF
This sift-down passes most tests but sometimes breaks the heap. Which line is wrong?
C++
Code challengeInteractive · disabled in the PDF
Write isMinHeap(a): true when every element is greater than or equal to its parent, at index (i - 1) / 2.
Insert 5, 3, 8, 1, 4 into an empty binary search tree, then delete 3 using its successor. What is 5’s left child now?
Insert 5, 3, 8, 1, 4 into an empty binary search tree, then delete 3 using its successor. What is 5’s left child now?
QuizInteractive · disabled in the PDF
A trie holds "tea", "ten" and "to". Separately, a union-find with 5 items runs unite(0, 1), unite(2, 3), unite(1, 3). How many trie nodes (not counting the root) and how many groups?
A trie holds "tea", "ten" and "to". Separately, a union-find with 5 items runs unite(0, 1), unite(2, 3), unite(1, 3). How many trie nodes (not counting the root) and how many groups?
QuizInteractive · disabled in the PDF
A persistent tree’s version 1 has 1,000 nodes. Version 2 is version 1 with one key inserted by path copying, along a path of 10 nodes. Then the variable holding version 1 is destroyed. How many nodes are freed?
A persistent tree’s version 1 has 1,000 nodes. Version 2 is version 1 with one key inserted by path copying, along a path of 10 nodes. Then the variable holding version 1 is destroyed. How many nodes are freed?
Data structures200 / 201
Module 20 · Mastery challenge
Which statements are true?
QuizInteractive · disabled in the PDF
Pick every true statement.
Pick every true statement.
QuizInteractive · disabled in the PDF
Which pairing of structure and cost is wrong?
Which pairing of structure and cost is wrong?
Data structures201 / 201
Module 20 · Mastery challenge
Recap: the whole course
A structure is a choice of which operations are cheap: start from the operations you need.
std::vector and hash tables are the everyday workhorses; most problems start with one of them.
Trees bring order: BSTs and balanced trees (std::map, std::set) for sorted keys, heaps for "best next", tries for prefixes.
Graphs model connections; union-find answers "connected?" as groups merge.
Combining structures (a list plus a map, a map plus a heap) gets the best of each, if you keep them in step.
Ownership is part of the design: unique_ptr for one owner, shared_ptr for shared versions, plain pointers and iterators to look.
Prefix sums and Fenwick trees turn repeated range questions into one or a few lookups.
Persistent structures keep history by sharing; probabilistic ones trade certainty for space.
FlashcardsInteractive · disabled in the PDF
Earliest event next, events arriving any time
A min-heap ordered by time: std::priority_queue with std::greater<>.
Autocomplete
A trie (or a sorted vector with std::lower_bound).
Are these two connected, as links are added?
Union-find.
The n most recently used items, evicting the least recently used
An LRU: std::list of entries plus std::unordered_map of list iterators, with splice.
Range sums with frequent updates
A Fenwick tree, or a segment tree for min and max.
Sorted keys with fast inserts
A balanced search tree (std::set, std::map); a B-tree on disk.
Minimum of a stack in O(1)
A second stack of minimums so far.
Top k of many
A min-heap of size k.
map[key] on a missing key
Inserts it with an empty value. Use find or contains to ask.
Answer key
1. What a data structure is · Data is values kept in memory · QuizThe computer needs the value in box 7 000. What does it do?B. Goes straight to box 7 000 using its addressMemory is built so any box can be reached by its address in the same short time, whichever box it is. That is why it is called random access memory (RAM). Searching is only needed when you know what you want but not where it is, and that is exactly the problem data structures solve.
1. What a data structure is · A data structure is a layout plus the operations it makes cheap · Put in orderPut the steps of choosing a data structure in order.1. List the operations the job needs 2. Work out which of them happen most often 3. Look up what each operation costs in each candidate layout 4. Pick the layout that makes the frequent operations cheap 5. Check it with realistic amounts of dataThe operations come first, the structure last: picking a favourite structure and then fitting the job to it is the usual mistake. The final check matters because real data can surprise you, for example by being far larger than you expected.
1. What a data structure is · Four operations to ask about · QuizA browser checks whether a web address is already in your bookmarks. Which operation is that?B. SearchYou know the value (the address) but not where it is, or whether it is there at all: that is a search. A read is when you already know the position, such as "the first bookmark". The difference matters, because many structures make reading instant but searching slow.
1. What a data structure is · Every value has a type · Fill the blanksGive each variable the right type.1: int, 2: double, 3: bool, 4: std::stringint holds whole numbers and double numbers with a fraction. bool is only true or false. std::string holds text; it needs #include <string> and the std:: in front, because it comes from the standard library rather than the language itself.
1. What a data structure is · Decisions, loops and functions · QuizWhat does the program above print?A. 0, one!, 4The loop runs for i = 0, 1 and 2. When i is 0, twice(0) is 0; when it is 1, the if prints one! instead; when it is 2, twice(2) is 4. It stops before 3 because i < 3 is then false.
1. What a data structure is · Searching a shelf means looking at every book · Find the bugThis search says "not found" for books that are on the shelf. Which line is wrong?Line 5The return -1; inside the loop runs as soon as the first book does not match, so the search gives up after one look. "Not found" can only be decided once the loop has finished, which is why it belongs after it, on line 7.
1. What a data structure is · Try it: count the looks · Code challengeFinish searchShelf(shelf, title): return a Result whose index is the title’s position (or -1) and whose looks is how many books were compared.One solution:
Count before comparing, so the look that finds the book is counted too. The last test is the important one: a missing book is the worst case, because you cannot stop early. On a shelf of a million books that is a million looks, which is what the next slide fixes.
1. What a data structure is · Try it: build the card box · Code challengeFinish byIsbn(books): return a map whose keys are the books’ ISBNs and whose values are the books themselves.One solution:
One pass over the books fills the box: index[book.isbn] = book; files a book under its ISBN. In C++ that line copies the book into the map: the map holds its own books, not a link to yours. Copying is C++’s default for every type, which keeps things simple; later modules show how to share one value without copying it. (auto in the tests lets the compiler work out a variable’s type.)
1. What a data structure is · What a C++ value takes up · QuizWhat does a std::vector<Book> of three books hold, in its own memory?B. The three books themselves, side by sideIn C++ a vector of structs stores the structs themselves, one after another. (Some languages, JavaScript and Java among them, store addresses of objects kept elsewhere instead.) So putting a book in two vectors makes two copies, and changing one does not change the other. When you do want to share one value, C++ has references and pointers, which come later in the course.
2. Arrays · Reading by index is one sum · QuizAn array of doubles starts at address 2000, and each double is 8 bytes. Where is index 5?B. 20402000 + 5 × 8 = 2040. The tempting 2048 counts six slots, as if indexes started at 1. Whatever the numbers, it is one multiplication and one addition: no searching. The compiler writes that sum for you every time you write a[i].
2. Arrays · std::array is a C array that knows its size · QuizWhat is sizeof(std::array<double, 10>), if a double is 8 bytes?C. 80Ten slots of 8 bytes, side by side, with nothing else: 80 bytes. size() would say 10, the number of values; sizeof counts bytes. The values never change the size, because the type alone decides how many bytes each slot takes.
2. Arrays · Passing a vector: a copy, a reference, or a const reference · Fill the blanksOne function reads a playlist, one changes it. Give each the right parameter type, so neither copies it.1: const std::vector<int>&, 2: std::vector<int>&longest only reads, so const std::vector<int>&: no copy, and the compiler stops it from changing the list by mistake. addSong must change the caller’s list, so a plain reference. Passing by value (std::vector<int>) would work for longest but copy every value on each call, and would make addSong change only its own copy.
2. Arrays · Try it: remove from the middle · Code challengeFinish removeAt(items, index) without erase: shift the later values left, drop the last slot with pop_back(), and return the value removed.One solution:
Solution
1#include<string>2#include<vector>34std::stringremoveAt(std::vector<std::string>& items,int index){5 std::string removed = items[index];6for(int i = index; i < items.size()-1; i++){7 items[i]= items[i +1];8}9 items.pop_back();10return removed;11}
Here you walk left to right, the opposite of inserting: each slot copies its right neighbour, so values are read before they are overwritten. The loop stops one short of the end, because the last slot has no right neighbour; pop_back() drops it instead. Removing at index 0 moves every other value, which is why removing from the front of a big vector is slow. (using List = … in the tests gives a long type a short name.)
2. Arrays · A dynamic array grows by doubling · Put in orderPut the steps of a push_back into a full vector in order.1. See that size equals capacity 2. Ask the heap for a block with twice the capacity 3. Copy every value into the new block 4. Put the new value in the next free slot 5. Add one to the size, and give the old block backThe new block must exist before anything can go into it, because the old block has no room, and the old block can only be given back once every value has been copied out of it. Only the size changes on an ordinary push; the capacity changes only when the block is replaced.
2. Arrays · Try it: build a dynamic array · Code challengeFinish push: when size equals capacity, double the capacity, copy the values into a new row (adding to copies), then store the value.One solution:
Solution
1#include<vector>23structDynamicArray{4int capacity =1;5int size =0;6int copies =0;7 std::vector<int> slots = std::vector<int>(1);89voidpush(int value){10if(size == capacity){11 capacity *=2;12 std::vector<int>bigger(capacity);13for(int i =0; i < size; i++) bigger[i]= slots[i];14 copies += size;15 slots.swap(bigger); // keep the new row, drop the old16}17 slots[size]= value;18 size++;19}2021intget(int index)const{return slots[index];}22};
The check comes first: grow, then store. Copying size values each time the row fills gives 1 + 2 + 4 + … + 512 = 1023 copies for 1000 pushes. Growing by a fixed amount instead (say 10 more slots each time) would copy about 50 000 values for the same pushes, because the copies would no longer get rarer. If you run the starter, the second push stops the program with "vector[] index out of bounds": this page’s C++ checks every [], so writing past the end is caught instead of quietly spoiling other memory. (const after get() promises it changes nothing.)
2. Arrays · Which std::vector operations are cheap · QuizWhat does this print, and which lines move the most values?B. cook 3; insert and erase at the front each move every valueinsert at begin() puts shop at the front (moving cook and eat right), push_back adds at the end with no moves, and erase takes shop off the front again (moving the rest left), so cook is first and three tasks remain. With three tasks the moves do not matter; with a million they would.
2. Arrays · A grid is an array of arrays · Find the bugOn a 3 × 4 grid, booking row 1, column 2 marks the wrong seat. Which line is the cause?Line 5: flat[row * cols + col] = 'X';Each row is cols cells long, so row 1 starts cols = 4 cells in: the index is row * cols + col, which is 6. Multiplying by rows (3) gives 5, a seat in the wrong column. On a square grid the bug would hide, because rows and columns are equal.
3. Measuring cost · Count steps, not seconds · QuizAbout how many times does the inner line run, for n guests?C. n × nFor each of the n guests, the inner loop runs n times: n × n in all. Ten guests is 100 greetings; a thousand guests is a million. A loop inside a loop multiplies; two loops one after the other would only add, giving 2 × n.
3. Measuring cost · Big O keeps only how the cost grows · Fill the blanksSimplify each count to its Big O.1: n, 2: n², 3: 1Constants and smaller terms go, because Big O describes the shape of growth. 5n + 100 is still a straight line; 2n² + 3n + 1 is still a curve that is quadrupled when n doubles; 42 never changes.
3. Measuring cost · O(log n): halve the problem each step · QuizWhy does binary search need the array to be sorted?B. So one comparison with the middle tells it which half cannot hold the targetIf the middle value is smaller than the target and the array is sorted, everything to its left is smaller too, so that half can be thrown away. Unsorted, the target could be anywhere, and you are back to linear search.
3. Measuring cost · Try it: count the halvings · Code challengeFinish binarySearch(sorted, target): return a Result with the target’s index (or -1) and how many middle values were compared, looks.One solution:
The two + 1 and - 1 matter: the middle has already been checked, so it is left out of the next half. Writing low = mid instead can loop forever once low and high are next to each other. The last test is the point of the module: 20 looks for a million values, where linear search could take a million.
3. Measuring cost · Loops inside loops multiply · Code challengeFinish hasDuplicate(nums): return true if any number appears twice. Sort the copy it is given and compare neighbours, so a list of 100 000 numbers is quick.One solution:
Solution
1#include<algorithm>2#include<vector>34boolhasDuplicate(std::vector<int> nums){5 std::sort(nums.begin(), nums.end());6for(int i =1; i < nums.size(); i++){7if(nums[i]== nums[i -1])returntrue;8}9returnfalse;10}
std::sort(nums.begin(), nums.end()) (from <algorithm>) sorts the whole vector, smallest first, in O(n log n). The parameter is a plain std::vector<int>, not a reference, on purpose: the function gets its own copy, so sorting it leaves the caller’s list alone, which is what the third test checks. Try the every-pair version: 100 000 numbers means about 5 billion comparisons, and the test runs out of time.
3. Measuring cost · Best, worst and average cases · QuizWhat is the worst case for binary search on n sorted values?B. O(log n)Even when the target is missing, each look halves what is left, so it takes about log₂ n looks before nothing remains. O(1) is its best case: the target happens to be in the middle.
3. Measuring cost · Space cost: the memory it needs · Fill the blanksReverse the array in place: swap the ends, then move both inwards.1: 1, 2: <, 3: ++, 4: --kept holds one value while the other is copied over it: that single variable is all the extra space. The loop stops when the two indexes meet in the middle, so each pair is swapped once: n / 2 swaps, O(n) time, O(1) extra space. The vector comes in by reference (&), so it is the caller’s list that is reversed. (The standard library has this as std::reverse(a.begin(), a.end()), and the three middle lines as std::swap(a[left], a[right]).)
3. Measuring cost · Amortised cost: the average over a whole run · QuizAn array grows by 10 slots each time it is full, instead of doubling. What does push cost, amortised?C. O(n)A copy happens every 10 pushes, and each copy is as big as the array: 10 + 20 + 30 + … up to n is about n² / 20 copies for n pushes. Divided by n pushes, that is O(n) each. Doubling works because the gaps between copies grow with the array.
3. Measuring cost · Recap · Put in orderPut these from the slowest-growing to the fastest-growing.1. O(1) 2. O(log n) 3. O(n) 4. O(n log n) 5. O(n²)At n = 1 000 000 they are roughly 1, 20, a million, 20 million and a million million. Moving one row up this list is often the whole difference between code that copes and code that does not.
4. Strings · A string is a sequence of characters · Quizstd::string s = "bakery"; What is the last character, and how do you read it?B. s[5] or s.back(), which is ybakery has 6 characters, at indexes 0 to 5, so the last is s[5], s[s.size() - 1] or s.back(). s[6] is one past the end: a std::string keeps an invisible null character ('\0', the byte 0) there, a habit from the C language, so you would read that instead of an error. Anything further stops the program here, as with vectors.
4. Strings · A std::string can change in place · Find the bugshout should turn every letter into a capital, but returns the text unchanged. Which line is wrong?Line 5: for (char& c : text) {for (char c : text) gives c a copy of each character, so line 6 changes the copy and the string stays as it was. char& c makes c a reference to the character inside the string, so the change lands there. (std::toupper, from <cctype>, gives the capital of a letter and leaves other characters alone.)
4. Strings · Building a long string piece by piece · QuizWhat does this print?B. bread, milk, eggs, The loop adds ", " after every item, the last one included, so the text ends with a stray comma and space. To put a separator only between items, add it before each item except the first (if (!out.empty()) out += ", ";), or cut the last two characters off at the end.
4. Strings · What common string operations cost · QuizWhat does this print?A. 3 tea 4The first comma is at index 3 (t, e, a are 0 to 2). substr(0, 3) copies three characters, tea, not including the comma. find(',', 4) starts looking at index 4, which is the second comma, right next to the first: the empty text between them is where a CSV has an empty field.
4. Strings · std::string_view looks at text without copying it · QuizA function takes a 1 MB string and returns its first 10 characters. Which costs the least?C. Take std::string_view, return text.substr(0, 10) as a std::string_viewTaking a std::string by value copies the whole megabyte first. A const std::string& avoids that, and copying 10 characters out is cheap. A string_view in and out copies nothing at all: but the returned view is only good while the caller’s string lives, which is the price of free.
4. Strings · Try it: reverse a string · Code challengeFinish reverse(s) with a loop from the last index down to 0, adding each character to out with +=.One solution:
Solution
1#include<string>23std::stringreverse(const std::string& s){4 std::string out;5for(int i = s.size()-1; i >=0; i--){6 out += s[i];7}8return out;9}
The loop starts at s.size() - 1, the last index, and runs while i >= 0, so index 0 is included. Keep i an int: s.size() is a size_t, a whole number that is never negative, so size_t i would make i >= 0 always true, and 0 - 1 would wrap round to a huge number. The library version reverses in place: std::reverse(s.begin(), s.end()) from <algorithm>.
4. Strings · Two pointers walk in from both ends · Code challengeFinish isPalindrome(s), ignoring case, spaces and punctuation. letters already holds only the lower-case letters and digits; compare from both ends.One solution:
Solution
1#include<cctype>2#include<string>34boolisPalindrome(const std::string& s){5 std::string letters;6for(unsignedchar c : s){7if(std::isalnum(c)) letters += std::tolower(c);8}9int left =0;10int right = letters.size()-1;11while(left < right){12if(letters[left]!= letters[right])returnfalse;13 left++;14 right--;15}16returntrue;17}
Return false the moment a pair differs, and true only once the pointers meet, since every pair has then matched. left < right stops at the middle: an odd-length word’s centre character has no partner and cannot spoil it. std::isalnum (is it a letter or digit?) and std::tolower come from <cctype>; they want an unsigned char, which is why the loop reads the bytes that way.
4. Strings · Counting characters with an array of 256 · Code challengeFinish isAnagram(a, b): true when b uses exactly the same letters as a, as many times each. Count a’s letters up, then count b’s down.One solution:
Solution
1#include<array>2#include<string>34boolisAnagram(const std::string& a,const std::string& b){5if(a.size()!= b.size())returnfalse;6 std::array<int,256> counts{};7for(unsignedchar c : a) counts[c]++;8for(unsignedchar c : b){9if(counts[c]==0)returnfalse;10 counts[c]--;11}12returntrue;13}
When b asks for a letter whose count is already 0, it cannot be an anagram. Because the sizes match, if every letter of b found a count to take from, every count ends at 0. Read the bytes as unsigned char: a plain char may be signed (−128 to 127), so a byte above 127, such as part of é, would be a negative index and stop the program. Sorting both strings and comparing also works, but costs O(n log n) instead of O(n).
4. Strings · Characters, bytes and UTF-8 · QuizWhat goes wrong with reverse("ok👍") from the earlier slide?B. The emoji’s four bytes come out in the wrong order, giving broken textIndexing walks bytes, so the four bytes of 👍 come out backwards and no longer make a character: the text is no longer valid UTF-8, and is shown as �. Reversing text people can read means first splitting it into characters, which the standard library does not do for you.
5. Linked lists · A pointer holds an address · QuizWhat does this print?B. 2 1b = *p copies the value 1 into b, which is its own box. Then *p = 2 writes through the pointer into a. So a is 2 and b stays 1. A pointer is like a reference you can re-aim and that can be empty: a reference always names one variable, for good.
5. Linked lists · A node is a small struct · QuizWith the nodes above, what is head->next->next->value?C. "Outro"head points at Intro, head->next at Waves, and its next at Outro. One more ->next would be nullptr, and reading ->value through that would dereference nullptr: undefined behaviour, not an answer. To reach the k-th node you follow k pointers: reading by position is O(n), not O(1) as in an array.
5. Linked lists · Walking the list from the head · Fill the blanksCount the nodes.1: head, 2: nullptr, 3: ->The same three parts as every traversal: start at the head, stop at nullptr, step with ->next (-> because node is a pointer). That is why list classes usually keep a size member instead: counting is O(n), reading a stored number is O(1).
5. Linked lists · Nodes live on the heap, until someone frees them · Find the bugThis frees every node, but reads memory it has already given back. Which line is wrong?Line 5: head = next;After delete head, the node is gone, so head->next reads freed memory: undefined behaviour, which may work today and fail tomorrow. Line 3 already saved the next pointer for exactly this reason; line 5 should use it. Bugs like this are why modern C++ rarely writes delete by hand, as the next slides show.
5. Linked lists · A class looks after its own parts · Put in orderPut what the program above prints in order.1. made Road trip 2. 385 3. made Short 4. gone Short 5. end of main 6. gone Road tripquick lives only inside its own { }, so it is made and destroyed before end of main. trip lives until main ends, after its last line: its destructor runs then. Destructors always run when a variable’s scope ends, which is what makes RAII dependable.
5. Linked lists · std::unique_ptr owns what it points at · QuizWhich of these does not compile?B. std::unique_ptr<Node> b = a;Copying would leave two owners, and both would delete the same node. So a unique_ptr has no copy: you must say std::move, after which a is empty. A plain Node* from .get() is fine, because a plain pointer never deletes anything; it just must not outlive the owner.
5. Linked lists · Try it: a list class with a tail · Code challengeFinish append(value) on LinkedList: add a node at the end in O(1) using tail, handling the empty list, and add 1 to size.One solution:
Keep a plain pointer to the new node (added) before moving it: after std::move(node), node is empty. Then link the old tail to it before moving tail, or you lose your only way to reach the old tail. In an empty list, tail is nullptr, so the new node becomes the head instead. prepend shows the mirror case: the first node added is also the tail. The destructor frees the nodes in a loop, as the last slide explained.
5. Linked lists · Removing a node means changing the one before it · Find the bugIn a list class with a tail, removing the last node leaves things broken. Which line is missing a step?Line 4: if (gone == tail) { tail = previous; } previous->next = std::move(gone->next);If gone was the tail, tail still points at the removed node, which line 4 has just freed: a dangling pointer. The next append writes into freed memory, which is undefined behaviour. Whenever a node is removed, ask whether it was the head or the tail.
5. Linked lists · Try it: remove every match · Code challengeFinish removeAll(head, value): remove every node holding value. Walk from the sentinel with previous, unlinking as you go.One solution:
Only move previous forward when you did not remove: after a removal, its new next has not been checked yet, and it might match too (the "side by side" test). Handing sentinel.next back to head at the end matters when the head itself was removed. The sentinel lives on the stack, so it vanishes when the function returns, with no delete.
5. Linked lists · A doubly linked list points both ways · QuizYou hold a pointer to a node in the middle of a long list. Removing it is O(1) in which list?B. Doubly linkedRemoving means changing the node before it. A doubly linked node knows it (prev); a singly linked one does not, so you would walk from the head to find it: O(n). This is why the LRU cache later in the course uses a doubly linked list.
6. Stacks · A vector’s end is a ready-made top · QuizWhat does this print?A. 1 3Push 1, push 2, pop takes the 2, push 3, push 4, pop takes the 4: 1 3 is left, with 3 on top. Each pop takes whatever was pushed most recently, not what was pushed first.
6. Stacks · A template writes code once, for any type · Fill the blanksMake Box hold one value of any type.1: typename, 2: T, 3: inttemplate <typename T> opens the type; inside, T is used wherever the type goes. Box<int> chooses int for T. (class works in place of typename there too; they mean the same.) Templates are why the standard library’s containers hold any type, and still run as fast as code written by hand for one.
6. Stacks · Try it: a stack class · Code challengeFinish the class template Stack<T> on a private vector: push, pop and peek (both give std::nullopt when empty), size() and empty().One solution:
Check items.empty() before back(): on an empty vector, back() has nothing to give, and this page stops the program (elsewhere it is undefined behaviour). Copy the top out beforepop_back(), because afterwards it is gone. Using the vector’s front with insert and erase would also make a stack, but every operation would move the whole vector.
6. Stacks · A stack on a linked list · Fill the blanksPush onto, and pop from, the head of a list.1: top, 2: nullptr, 3: nextThe new node takes over the old top as its next, then becomes the top. Popping copies the value, then moves the top down one node; the old node loses its owner and is deleted there and then. Check for an empty stack first, or top->value follows a null pointer.
6. Stacks · The call stack keeps track of running functions · Put in ordercheckout calls total, which calls tax. Put what happens to the call stack in order.1. Push a frame for checkout 2. Push a frame for total 3. Push a frame for tax 4. Pop tax’s frame as it returns 5. Pop total’s frame as it returns 6. Pop checkout’s frameThe last function called is the first to finish: LIFO. That is why a debugger’s stack trace (or backtrace) lists the functions innermost first: it is a printout of the frames on the call stack at that moment.
6. Stacks · Undo and redo are two stacks · Find the bugAfter undo then a new edit, redo brings back text that no longer fits. Which line is missing a step?Line 2: undos.push_back(text); redos.clear();A new edit starts a new history, so the redo stack must be emptied. Otherwise redo would replace the new text with a state from the abandoned branch. redos.clear() empties a vector in place (its capacity stays, ready for reuse).
6. Stacks · Matching brackets with a stack · Code challengeFinish isBalanced(text): true when every ), ] and } closes the most recent unclosed opener of its kind, and nothing is left open. Other characters are ignored. partnerOf gives a closer’s opener.One solution:
open.empty() || must come first: || stops as soon as one side is true, so top() is never called on an empty stack, which would be undefined behaviour. The final open.empty() catches openers never closed. Counting each kind of bracket is the tempting shortcut, and ([]{)} shows why it fails: the counts match, the order does not. Compilers check your code’s brackets this way.
7. Queues · A queue: first in, first out · QuizEnqueue Ada, Bo and Cy; dequeue once; enqueue Di; dequeue once. Who is at the front now?C. CyThe first dequeue serves Ada, the second serves Bo, so Cy is at the front with Di behind. A stack would have served the most recent arrivals instead: Cy, then Di.
7. Queues · Try it: a queue with a head index · Code challengeFinish Queue<T>: dequeue() returns the front value (or std::nullopt when empty) by moving head, without erase; size() counts what is left.One solution:
Solution
1#include<optional>2#include<vector>34template<typenameT>5classQueue{6public:7voidenqueue(constT& value){ items.push_back(value);}89 std::optional<T>dequeue(){10if(head == items.size())return std::nullopt;11T value = items[head];12 head++;13return value;14}1516intsize()const{return items.size()- head;}1718private:19 std::vector<T> items;20int head =0;21};
The queue is empty when head has caught up with items.size(); checking that first stops head running past the end, which this page would stop with an error. The starter’s erase gives the right answers but moves every value on each call: 200 000 people means about 20 billion moves, and the last test runs out of time. Dropping dead slots, as the previous slide says, is the next step for a queue that runs for ever.
7. Queues · A ring buffer reuses its slots · Fill the blanksWork out where the next value goes in a ring of capacity 5.1: %, 2: 03 + 2 = 5, and 5 divided by 5 leaves a remainder of 0, so the next value wraps round to slot 0. % keeps any index between 0 and capacity - 1. Advancing the head on dequeue wraps the same way: head = (head + 1) % capacity.
7. Queues · Try it: a ring buffer · Code challengeFinish enqueue (put the value at (head + count) % capacity; return false if full) and dequeue (take from head, wrap it, or std::nullopt if empty).One solution:
Keeping count rather than a second "tail" index avoids the classic ambiguity: with only head and tail, head == tail could mean empty or full. The constructor makes all the slots at once, slots(capacity), and the ring never grows. A fixed size known while writing the code can even be a template parameter, template <typename T, int N> with a std::array<T, N> inside, so the ring needs no heap at all.
7. Queues · A deque works at both ends · QuizA music app keeps your last 50 songs: each new song goes at the front, and the oldest falls off when there are more than 50. What fits best?B. A std::dequeYou add at one end and drop at the other, while also reading the newest: a deque does all of it in O(1). The vector works, but inserting at the front moves all 50 songs each time. A stack only works at one end, so it cannot drop the oldest.
7. Queues · Queues everywhere · Put in orderIn a program with an event loop, a button is clicked while a long function is running. Put what happens in order.1. The long function keeps running 2. The click waits in the event queue 3. The function returns to the event loop 4. The loop dequeues the click 5. The click handler runsAn event loop handles one event at a time. A click cannot interrupt running code; it joins the queue and is taken, first in first out, when the loop is free again. That is why a long calculation freezes a window: nothing in the queue can run.
8. Hash tables · A simple hash function for strings · QuizWhy would std::rand() % size, a random number, be a useless hash function?C. The same key would land in a different bucket each time, so get could not find what set storedRandom numbers spread evenly, but a hash function’s first job is to be deterministic: the look-up must recompute the bucket the value was stored in. Evenness is the second job.
8. Hash tables · Try it: a hash map with chaining · Code challengeFinish set and get. Both find the key’s bucket with hash(key, buckets.size()) and search only that bucket.One solution:
Comparing the key inside the bucket is essential: 100 keys in 8 buckets means about 12 per bucket, so the bucket alone does not say which entry is yours. auto& bucket is a reference to the bucket inside the table, so push_back adds to the table and not to a copy; in get, const auto& reads without copying. Forgetting to check for an existing key in set leaves two entries for tea, which the third test catches.
8. Hash tables · Deleting an entry · Code challengeFinish erase(key): remove the key’s entry from its bucket (copy the last entry over it, then pop_back()), lower size, and return true; return false if the key was not there.One solution:
This table uses the standard library’s hash function, std::hash<std::string>{}(key) (from <functional>): it makes a hasher with {} and calls it. bucketFor returns a reference (std::vector<Entry>&) to the bucket inside the table, so changes through it change the table. Copying the last entry over the one to delete, then pop_back(), is O(1) after the search; when the entry is the last one, it is copied onto itself and then dropped, which still works. Lowering size only when the key was found keeps it honest: deleting a missing key must change nothing.
8. Hash tables · Deleting an entry · Find the bugThis table grows when it gets full. Afterwards, get misses keys that are there. Which line is wrong?Line 12: int i = hash(key, buckets.size());After grow, there are 16 buckets and keys were rehashed into them, but get still computes the bucket for 8. Any key whose bucket changed is looked for in the wrong place. Every operation must hash with the current number of buckets.
8. Hash tables · Load factor and resizing · QuizResizing copies every entry: O(n). Why is set still called O(1)?B. Because the table doubles, resizes get rarer as it grows, so the cost is O(1) amortisedIt is the vector argument again: doubling means n inserts cause at most about 2n rehashes in total. Each one is O(1) amortised. Every key must be rehashed, because its bucket depends on the number of buckets.
8. Hash tables · Average O(1), worst case O(n) · QuizWith this hash, where do the keys tea, jam and bun go?B. All into bucket 3All three keys are 3 letters long, so all land in bucket 3, and the table is just a slow list. A hash must look at the content of the key, not only its size: that is what multiplying in each byte does.
9. Maps and sets · Four containers for looking things up · QuizA leaderboard must list players by name, A to Z, and find any player quickly. Which fits?B. std::map<std::string, int>You need a value per player (the score), so a map, and you need names in order, so the sorted one: std::map walks its keys A to Z, and finds any name in O(log n), about 20 steps for a million players. The unordered map finds faster, but its order is a jumble.
9. Maps and sets · std::unordered_map: a ready-made hash table · Fill the blanksRecord a sale: take one scone out of stock, and drop the entry when none are left.1: find, 2: end, 3: erasefind hashes once and hands back where the entry is, so the rest uses it without looking the key up again. Comparing with end() is how C++ says "found". erase accepts the iterator itself. stock["scone"]-- would also work, but it would create a scone entry (at 0, then −1) if there was none, as the next slide shows.
9. Maps and sets · m[key] adds the key if it is missing · QuizWhat does this print?B. no pie, no jam, 2prices["pie"] added pie with the price 0, which is why the test said "no pie", and left it in the map. count("jam") only asked. So there are two entries: tea and pie. In a real shop, that stray pie would now appear on the menu at no cost.
9. Maps and sets · Sets: each value once · Code challengeFinish inBoth(a, b): the titles on both reading lists, in a’s order, each once. Put b in an unordered_set so each check is O(1), and use another set to skip titles already added.One solution:
With std::find(b.begin(), b.end(), title) instead of a set, each check would scan all of b: O(n × m) for the whole thing, over a billion steps for the last test. A set makes it O(n + m). added.insert(title).second does two jobs at once: it records the title, and says whether it was new. && only runs it when the title is in b.
9. Maps and sets · Grouping and counting with a map · Find the bugEvery count comes out one too high. Which line is wrong?Line 5: if (!counts.contains(word)) counts[word] = 0;A new word is set to 1, then line 6 adds another 1, so it starts at 2. Since counts[word] already starts a missing word at 0, the whole if can go: counts[word]++ alone is the usual way to count in C++.
9. Maps and sets · Grouping and counting with a map · Code challengeFinish byAuthor(books): a std::map from each author to their titles, in the order the books came.One solution:
The first time an author is seen, groups[book.author] makes an empty vector for them; every time, push_back adds the title to the vector inside the map, because [] returns a reference to it. Being a std::map, the authors come out sorted for free. With std::unordered_map the groups would be the same, in a jumbled order.
9. Maps and sets · A sorted map answers "what comes next?" · QuizWhat does this print?B. 11 BoThere is no booking at 10, and lower_bound does not need one: it finds the first key that is not less than 10, which is 11. lower_bound(15) would give bookings.end(), meaning "none", so check that before reading ->first. An unordered_map has no lower_bound at all: its keys are in no order to search.
10. Trees · Depth, height and subtrees · QuizA Leek recipe gets two variations as its children: Leek and potato, Leek and pea. What is the cookbook’s height now?B. 3The new nodes sit one edge below Leek, which was at depth 2, so they are at depth 3, and the deepest leaf sets the height: 3. Seven is the number of nodes before the change, a different measure.
10. Trees · Trees are everywhere · QuizWhich of these is not a tree?C. Roads between towns, where you can drive round in a loopIn a tree, each node has one parent, so there is only one path between any two nodes and never a loop. Roads form loops and have no root: that is a graph, the more general shape covered later in the course. Every tree is a graph; not every graph is a tree.
10. Trees · A tree node holds a vector of children · Fill the blanksAdd a Pancakes recipe under Baking, and count Baking’s recipes.1: children, 2: push_back, 3: sizeChildren are an ordinary vector, so you index into it and push_back onto it. The & matters: TreeNode baking = … would copy Baking and its whole subtree, and Pancakes would be added to the copy, leaving the cookbook as it was.
10. Trees · Recursion: solve it for the children, then combine · Find the bugThis always returns 1. Which line is wrong?Line 4: total += countNodes(child);The recursive call works out each child’s count and returns it, and line 4 throws it away. A recursive call is an ordinary function call: its answer has to be used. Each call has its own total in its own frame, so the children cannot add to the parent’s by themselves.
10. Trees · Try it: leaves and height · Code challengeFinish countLeaves(node): a leaf counts 1; any other node’s count is the sum of its children’s counts.One solution:
Solution
1#include<string>2#include<vector>34structTreeNode{5 std::string value;6 std::vector<TreeNode> children;7};89intcountLeaves(constTreeNode& node){10if(node.children.empty())return1;11int total =0;12for(constTreeNode& child : node.children){13 total +=countLeaves(child);14}15return total;16}
Here the base case has to be written out, because a leaf’s answer (1) is not what the loop would give for no children (0). The parent counts nothing for itself, unlike countNodes: it only gathers its children’s answers.
10. Trees · Try it: leaves and height · Code challengeFinish height(node): the number of edges on the longest path down to a leaf. A leaf has height 0; any other node is 1 more than its tallest child. std::max(a, b) gives the larger of two.One solution:
Starting tallest at -1 makes a leaf come out as -1 + 1 = 0 with no special case; if (node.children.empty()) return 0; is just as good. Taking the maximum, not the sum, is the point: height is about the longest single path, not how many nodes there are.
10. Trees · Depth-first: preorder and postorder · Put in orderPut the cookbook’s nodes in preorder: root first, then each subtree left to right.1. Cookbook 2. Baking 3. Scones 4. Bread 5. Soups 6. Leek 7. IndexPreorder finishes all of Baking’s subtree (Scones, Bread) before moving to Soups. In postorder the root comes last: Scones, Bread, Baking, Leek, Soups, Index, Cookbook.
10. Trees · Breadth-first: level by level, with a queue · Code challengeFinish levels(root): a vector per depth, each holding that level’s values left to right. Work one level at a time: the next level is all the current level’s children.One solution:
Each pass of the loop handles one whole level, so the queue is replaced by "this level" and "the next". The inner loop gathers every node’s children into next, in order. When a level has no children at all, next is empty and the loop ends. This is breadth-first search with the levels kept apart, the shape used to find shortest paths in graphs. The pointers stay valid because nothing changes the tree while they are in use.
10. Trees · When a tree is too deep for recursion · QuizIn the loop above, back() and pop_back() take the most recently added node. Which traversal does that give?B. Depth-firstA stack is last in, first out, so the children just pushed are visited before anything older: the walk dives deep first. A queue is first in, first out, so older nodes (shallower ones) go first: breadth-first. The only difference between the two traversals is the structure holding the waiting nodes.
11. Binary search trees · Smaller keys go left, larger keys go right · QuizA tree has root 50, whose left child is 30, and 30’s right child is 55. Is it a BST?B. No: 55 is in 50’s left subtree, so it must be smaller than 50The rule is about whole subtrees, not just parent and child. 55 sits below 50 on the left, so it must be smaller than 50, and it is not. Checking only each parent with its children is the classic mistake; you will meet it again at the end of this module.
11. Binary search trees · A node owns its two children · QuizThe variable root above goes out of scope while holding a tree of 7 nodes. What happens?B. All 7 nodes are freed: each unique_ptr deletes the node it owns, which destroys that node’s childrenDestroying a unique_ptr deletes its node, and deleting a node destroys its members, including its own two unique_ptrs. That chain reaches every node. delete root would not even compile: root is a unique_ptr, not a raw pointer.
11. Binary search trees · Insert walks down to an empty spot · Fill the blanksComplete the insert so smaller keys go left and larger ones go right.1: &, 2: make_unique, 3: <, 4: left, 5: rightThe & is the whole trick: nodeis the parent’s left or right, not a copy, so filling it links the new node into the tree. Without it the code does not even compile, because a unique_ptr cannot be copied: there can only be one owner.
11. Binary search trees · An in-order walk visits the keys in sorted order · Code challengeFinish collect(node, out): push every key of the tree onto out, smallest first. Visit the left subtree, then the node, then the right subtree. Node and insert are ready for you.One solution:
The empty node is the base case that stops the recursion. Each call adds its left subtree’s keys, then its own, then its right subtree’s. Pushing node->key first gives a pre-order list instead, which is not sorted. Passing one out vector by reference down every call means no vector is ever copied; returning a fresh vector from each call and joining them would copy keys over and over.
11. Binary search trees · The smallest key is the leftmost, the largest the rightmost · Code challengeWrite floorKey(node, budget): the largest key less than or equal to budget, or std::nullopt if every key is larger. Walk down like a search, remembering the best key so far.One solution:
When a key fits the budget it is a candidate, and anything better must be larger, so you go right. When a key is too dear, only smaller keys can help, so you go left. The last candidate you saw is the answer. The tempting mistake is to stop at the first key that fits: for a budget of 65 that would be 50, but 60 is further down on the right. std::optional<int> holds an int or nothing, which says "no floor" more honestly than a made-up -1.
11. Binary search trees · Deleting a key has three cases · Put in orderPut the steps for deleting a node with two children in order.1. Search down the tree to the node with the key 2. Step once into its right subtree 3. Keep going left to find the smallest key there, the successor 4. Copy the successor’s key into the node being deleted 5. Delete the successor from the right subtree (it has at most one child)The successor is "one step right, then all the way left". Copying its key keeps the node and all its links in place, so only the successor’s old node really disappears, and that one is an easy case. The predecessor (largest on the left) works just as well.
11. Binary search trees · Delete in code · Find the bugThis version of the two-children case breaks the ordering rule. Which line is wrong?Line 2: while (next->right) next = next->right.get();Starting from the left child is fine, but then you want the largest key there, the predecessor, found by going right. Going left finds the smallest key of the whole left side, and copying it into the node leaves larger keys to its left. Either use the predecessor (left, then all the way right) or the successor (right, then all the way left).
11. Binary search trees · Sorted input makes a tree as slow as a list · QuizYou insert the titles of a bookshop’s catalogue, already in alphabetical order, into a plain BST. How tall is the tree for 1,000 books?C. 1,000Each new title is larger than every title before it, so it always goes right of the last one: a single chain of 1,000 nodes. Random order would give a height in the low 20s (about twice log₂ 1000 ≈ 10, because random trees are not perfectly balanced). Real data is often sorted, which is why balancing matters.
11. Binary search trees · Check a tree really is a BST · Code challengeFinish isBst(node, low, high): true when every key is strictly between low and high, and both subtrees are valid with tightened limits. tree and leaf build trees by hand for the tests.One solution:
The limits carry what every ancestor demands. 55 under 30 passes a parent-and-child check, but it inherited the upper limit 50 from the root, so it fails here. Strict <= and >= reject duplicates; a tree that allows them must pick one side for equal keys and loosen that test. (using Ptr = … just gives the long type a short name.)
12. Heaps and priority queues · Each parent is smaller than its children, and that is all · QuizWhich of these is true of the min-heap above?B. The second-smallest item is always a child of the rootThe second smallest must be smaller than everything but the root, so it can only sit directly under the root. Level by level gives 1, 3, 8, 5, 4, 9: not sorted. And the largest item is somewhere among the leaves, but not necessarily the last one. A heap is only "a little sorted", which is exactly why it is cheap to keep.
12. Heaps and priority queues · A complete tree fits in a vector with no gaps · Fill the blanksWrite the index arithmetic for a heap stored from index 0.1: 1, 2: 1, 3: 2Each level doubles, so the children of i are at 2i + 1 and 2i + 2, and going back up undoes that: subtract one, halve. Dividing two whole numbers in C++ drops the remainder, so / 2 already rounds down. (std::size_t is the unsigned whole-number type the standard library uses for sizes and indexes.) Some books store the heap from index 1, where the formulas become 2i, 2i + 1 and ⌊i / 2⌋; mixing the two conventions is a common off-by-one bug.
12. Heaps and priority queues · Push adds at the end and sifts up · QuizThe heap is [1, 3, 8, 5]. What is it after heapPush(heap, 2)?C. [1, 2, 8, 5, 3]2 goes in at index 4, whose parent is index 1 (value 3). 2 is smaller, so they swap: [1, 2, 8, 5, 3]. Now at index 1, its parent is the root, 1, which is smaller, so it stops. The heap is not sorted, and it does not need to be.
12. Heaps and priority queues · Push adds at the end and sifts up · Code challengeFinish heapPush: after adding value at the end, sift it up until its parent is smaller or equal. Use std::swap.One solution:
Solution
1#include<utility>2#include<vector>34voidheapPush(std::vector<int>& heap,int value){5 heap.push_back(value);6 std::size_t i = heap.size()-1;7while(i >0){8 std::size_t parent =(i -1)/2;9if(heap[parent]<= heap[i])break;10 std::swap(heap[parent], heap[i]);11 i = parent;12}13}
Each pass compares the value with one parent and either stops or moves up a level, so a heap of a million items needs at most about 20 swaps. Using < instead of <= in the stop test also works, but swaps equal values for nothing. Forgetting i = parent stops after one swap: the next pass compares the same two places, now in order, and breaks, so a new value never climbs more than one level (the first two tests catch it). And watch the std::size_t: it cannot go below 0, so a loop written as i >= 0 would never end.
12. Heaps and priority queues · Pop takes the root and sifts the last item down · Put in orderPop from the heap [1, 3, 8, 5, 4]. Put what happens in order.1. Remember the root, 1, to return it 2. Take the last item, 4, off the end and put it at the root: [4, 3, 8, 5] 3. Its children are 3 and 8; 3 is smaller and less than 4, so swap: [3, 4, 8, 5] 4. At index 1, its only child is 5, which is larger: stop 5. Return 1The shape stays complete because only the last slot disappears. Swapping with the smaller child matters: swapping 4 with 8 would put 8 above 3, breaking the heap property. Like push, it is one swap per level at most: O(log n).
12. Heaps and priority queues · Pop takes the root and sifts the last item down · Code challengeFinish siftDown(heap, i): while the item at i is larger than its smaller child, swap them and continue from the child. heapPop uses it.One solution:
Solution
1#include<optional>2#include<utility>3#include<vector>45voidsiftDown(std::vector<int>& heap, std::size_t i){6while(true){7 std::size_t l =2* i +1, r = l +1, small = i;8if(l < heap.size()&& heap[l]< heap[small]) small = l;9if(r < heap.size()&& heap[r]< heap[small]) small = r;10if(small == i)return;11 std::swap(heap[i], heap[small]);12 i = small;13}14}15std::optional<int>heapPop(std::vector<int>& heap){16if(heap.empty())return std::nullopt;17int top = heap[0];18 heap[0]= heap.back();19 heap.pop_back();20if(!heap.empty())siftDown(heap,0);21return top;22}
small starts as the node itself and becomes whichever child is smaller still; when neither child beats it, the item has found its level. The l < heap.size() checks matter in C++: reading heap[l] past the end is not a harmless "nothing", it is a bug that this runner stops with "index out of bounds" (and that an unchecked build would silently read garbage for). Popping every item in turn gives them sorted: that is heap sort.
12. Heaps and priority queues · Heaps schedule work and find the top k · QuizTo keep the 10 largest values of a long stream, which heap do you keep, and of what size?B. A min-heap of size 10The root of a size-10 min-heap is the weakest of your current top 10, the one to throw out when something better comes. Each new value is compared with it in O(1) and, if larger, replaces it in O(log 10). A max-heap of everything works but costs O(n) memory; a max-heap of size 10 puts the wrong item at the root.
12. Heaps and priority queues · The standard library has a heap: std::priority_queue · QuizWhat does this print?C. 7With no comparison given it is a max-heap: the top was 9, pop threw it away, and the next largest is 7. Answering 4 is reading it as a min-heap (pop the 2, then 4 is smallest); for that you need std::greater<>. And pop returns nothing in C++, so you read top() first if you want the item.
12. Heaps and priority queues · Top k with the standard heap · Code challengeFinish topK(values, k): the k largest values, largest first. Keep a min-heap best of at most k items: push each value, and pop whenever it holds more than k.One solution:
The heap never holds more than k + 1 items, so each push and pop costs O(log k), and the whole thing O(n log k). The item popped is always the smallest of those kept, the one that just lost its place. A max-heap here would pop the largest and keep the worst values. Emptying a min-heap gives smallest first, so the result is reversed at the end. (static_cast<int> turns the unsigned size() into an int before comparing it with k.)
13. Graphs · Edges can have a direction and a weight · QuizWeb pages link to other pages. What kind of graph is that?B. Directed and unweightedA link goes from one page to another; the other page does not link back unless someone added a second link. So edges are directed. A plain link has no number on it, so the graph is unweighted (a search engine may add weights, such as how important a link is).
13. Graphs · An adjacency list stores each vertex’s neighbours · QuizIn an undirected graph, the edges are Ada–Ben, Ada–Cleo, Ben–Cleo and Cleo–Dev. What is the degree of Cleo, and how many entries are in all the adjacency lists together?A. Degree 3; 8 entriesCleo touches three edges (Ada, Ben, Dev), so her degree is 3. Each of the 4 edges is stored from both ends: 8 entries. That is a handy check: the degrees always add up to twice the number of edges.
13. Graphs · An adjacency matrix is a grid of yes and no · QuizA social network has 10 million people, each with about 200 friends. Roughly how many cells would a matrix need, compared with entries in adjacency lists?B. Matrix: 10¹⁴ cells; lists: about 2 × 10⁹ entriesThe matrix has (10⁷)² = 10¹⁴ cells, almost all zero: even at one bit per cell that is over 12 terabytes. The lists hold 10⁷ × 200 = 2 × 10⁹ ints, about 8 GB. Most real graphs are sparse (few edges compared with all possible pairs), and adjacency lists win. Matrices suit small or dense graphs.
13. Graphs · Try it: a friends network · Code challengeFinish Friends: addFriendship(a, b) joins two people both ways (adding them if new), and degree(p) is how many friends p has (0 for someone unknown, without adding them).One solution:
Solution
1#include<string>2#include<unordered_map>3#include<unordered_set>45structFriends{6 std::unordered_map<std::string,7 std::unordered_set<std::string>> adj;89voidaddFriendship(const std::string& a,const std::string& b){10 adj[a].insert(b);11 adj[b].insert(a);12}13intdegree(const std::string& p)const{14auto it = adj.find(p);15return it == adj.end()?0: it->second.size();16}17};
adj[a] creates an empty set for a new person, then insert adds the friend; a set ignores one that is already there, so Ben–Ada after Ada–Ben changes nothing. In degree, adj[p] would be the trap: operator[]inserts a missing key, so asking about Zoe would add her, and it does not even compile in a const function. find asks without changing anything; it->second is the set it found.
13. Graphs · Directed and weighted graphs in code · Fill the blanksStore a one-way flight with its price, and read a price back.1: Edge, 2: from, 3: to, 4: priceThe outer vector goes from a city to its outgoing flights; each Edge holds a destination and a price, and {to, price} builds one in place. Only from gets the edge, because the edge is directed: flights[1] is still empty. For an undirected weighted graph, you would add both directions with the same weight.
13. Graphs · Directed and weighted graphs in code · Find the bugThis friends graph says Ben has no friends after befriend(0, 1) (Ada, Ben). Which line is the bug?Line 5: adj[b].push_back(a);Line 5 adds Ada to her own list (a self-loop) instead of adding Ada to Ben’s. The graph has quietly become directed. A good test for any undirected graph: after adding a–b, both hasEdge(a, b) and hasEdge(b, a) are true, and no vertex is its own neighbour.
13. Graphs · Who can I reach? A first graph search · Code challengeFinish reachable(graph, start): mark in seen every vertex reachable from start, including start. Use the std::queue (from the queues module): take a vertex from the front, and push each unseen neighbour, marking it as you push it.One solution:
Solution
1#include<queue>2#include<vector>3usingGraph= std::vector<std::vector<int>>;45std::vector<int>reachable(constGraph& graph,int start){6 std::vector<bool>seen(graph.size(),false);7 std::queue<int> queue;8 seen[start]=true;9 queue.push(start);10while(!queue.empty()){11int v = queue.front();12 queue.pop();13for(int next : graph[v]){14if(seen[next])continue;15 seen[next]=true;16 queue.push(next);17}18}19 std::vector<int> out;20for(std::size_t v =0; v < graph.size(); v++)21if(seen[v]) out.push_back(v);22return out;23}
Marking a vertex seen when it is queued, not when it is taken out, means it can never be queued twice. Every vertex and every edge is handled once: O(V + E). The groups found this way are the graph’s connected components; the disjoint sets module finds them another way. (using Graph = … gives the long type a short name.)
14. Tries: trees of prefixes · A node is 26 child slots and an end flag · QuizStarting from an empty trie, you insert "car", "cart" and "cat". How many nodes are there, not counting the root?B. 5"car" makes c, a, r (3). "cart" reuses c, a, r and adds t (4). "cat" reuses c, a and adds a second t under a (5). Ten letters in all, but only five nodes: the shared prefix "ca" is stored once.
14. Tries: trees of prefixes · has and startsWith differ only at the last step · Code challengeFinish find(root, text) (the node at the end of text, or nullptr), then use it in has (a whole word) and startsWith (any word begins with it). The tests build a trie with the insert from the last slide.One solution:
The early return nullptr matters in C++: once a letter is missing, node is nullptr, and reading node->children on the next letter would dereference it: undefined behaviour. startsWith(t, "") is true, since the root is the empty prefix. In has, node && node->end checks the pointer first, and && stops there if it is nullptr. Forgetting the end check in has is the classic trie bug: every prefix would count as a word.
14. Tries: trees of prefixes · Collect every word below a prefix · Put in orderPut the steps of wordsWithPrefix(root, "ca") in order.1. Walk from the root down "c" then "a" 2. If a letter was missing, return an empty list 3. Start a depth-first visit at the "a" node, with the text "ca" 4. At each node, if its end flag is set, add its text to the results 5. For each child, visit it with the text plus the child’s letterFinding the prefix costs its length; the visit then costs the size of that part of the trie, which is about the size of the answer. The rest of the dictionary is never touched. A search box usually stops after the first 10 words found.
14. Tries: trees of prefixes · Collect every word below a prefix · Code challengeFinish collect(node, text, out): push text onto out if the node ends a word, then collect from each child that exists, with its letter (char('a' + i)) added to text. The tests’ wordsWithPrefix walks to the prefix and calls it.One solution:
Solution
1#include<array>2#include<memory>3#include<string>4#include<vector>5structNode{6 std::array<std::unique_ptr<Node>,26> children;7bool end =false;8};9voidcollect(constNode* node, std::string text,10 std::vector<std::string>& out){11if(node->end) out.push_back(text);12for(int i =0; i <26; i++)13if(node->children[i])14collect(node->children[i].get(), text +char('a'+ i), out);15}
The text grows by one letter per level, so each word is built exactly when its last node is reached. Because the slots go from a to z, suggestions come out in alphabetical order with no sorting: a gift of the array layout. Passing text by value copies it at every node; the faster habit is one shared std::string& that you push_back a letter onto before the call and pop_back after it.
14. Tries: trees of prefixes · The price is memory · QuizWhich job does a trie do clearly better than a std::unordered_set of words?B. Suggesting completions for what someone has typed so farExact lookups and counting distinct words are what a hash set is for: O(L) on average and less memory. The trie earns its memory only when you ask about prefixes. Choosing a structure means asking which operations you need.
14. Tries: trees of prefixes · Counting through a trie · Find the bugAfter inserting car, cart and cat, countPrefix(root, "car") should be 2, but it says 1. Which line is the bug?Line 6: next->count++;Line 6 counts the node being left, not the one being entered. The root gets counted for every word (harmless), but the last letter of each word never is: the "r" of car is only counted when cart passes through it on the way to "t", so it reads 1. Counting next counts every node on the path, including the last. An off-by-one level is the usual trie bug; test with a word that is a prefix of another.
15. Balanced trees · Height and the balance factor · Code challengeWrite height(node) (0 for nullptr, otherwise 1 plus the taller child’s height) and isBalanced(node): true when every node’s subtrees differ in height by at most 1. std::max and std::abs help.One solution:
Checking only the root is the tempting shortcut, and the last test catches it: the root’s sides are 3 and 2 tall, but node 3 has a left side two taller than its empty right. This version recomputes heights again and again, O(n²) in the worst case. Returning the height and the verdict together from one traversal makes it O(n), and AVL trees go further: each node stores its height, updated as the tree changes.
15. Balanced trees · A rotation lifts one side and lowers the other · QuizAn in-order traversal of the tree before the rotation gives 20, 30, 40, 50, 60. What does it give after?B. 20, 30, 40, 50, 60A rotation never changes the sorted order, only the shape. 40 was right of 30 and left of 50 before, and it still is: that is why it moves across. This is what lets a search tree reshape itself without breaking the ordering rule.
15. Balanced trees · Try it: rotate left · Code challengeWrite rotateLeft(top): lift top->right, move its left subtree across to top->right, put the old top under it on the left, and make top own the lifted node.One solution:
The order of the moves matters: hand lifted->left to top->rightbefore overwriting lifted->left, or assigning over it deletes 40 and everything under it. Moving top into lifted->left empties top for a moment, so the last line refills it with the new top. Get the order wrong in the other direction and you read through an empty pointer: undefined behaviour. The nodes themselves never move in memory: only who owns them changes.
15. Balanced trees · AVL insertion: insert, then rotate on the way up · QuizYou insert 10, 20 and 30, in that order, into an empty AVL tree. What happens?B. A left rotation at 10, leaving 20 at the root with 10 and 30 under itAfter 30 goes in, 10 has an empty left and a right side of height 2: balance factor −2, a right-right case. One left rotation at 10 lifts 20. A plain BST would have stayed a chain; an AVL tree never lets it get that far.
15. Balanced trees · Red-black trees balance more loosely · QuizA map is changed constantly and read about as often. Why do standard libraries often pick a red-black tree over an AVL tree for it?B. Red-black trees do less restructuring per change, for lookups that are only a little slowerBoth are O(log n) for everything. Red-black trees accept a somewhat taller tree in exchange for fewer rotations per update, a good deal for general-purpose maps. AVL trees are shorter, which pays off when reads dominate. Neither is "better"; they make different trade-offs.
15. Balanced trees · std::set and std::map are balanced trees you can use today · Code challengeWrite floorKey(prices, budget) with the set’s own search: the largest price ≤ budget, or std::nullopt. Find the first price above the budget with upper_bound; the floor, if any, is just before it.One solution:
This is the floor from the binary search trees module, done by a tree that cannot go skewed. upper_bound skips equal keys, so the step back lands on the budget itself when it is there. The begin() check is the edge case that matters: stepping back from the first position is undefined, a crash or garbage rather than an error. lower_bound is the tempting choice, but it finds 60 for a budget of 60 and 70 for 65, so you would need two different cases.
15. Balanced trees · B-trees: wide and shallow, for disks · QuizWhy does a database not use a binary tree for its index on disk?A. A binary tree of a billion keys is about 30 levels deep, and each level could be a separate slow disk readThe cost that matters on disk is the number of pages read, not comparisons. Thirty levels means up to thirty reads; a B-tree needs three or four, each read bringing in hundreds of keys to compare in memory. The same idea, shallow and wide, helps in memory too: a node of a std::set is a separate allocation, so each level can be a cache miss, which is why in-memory B-tree maps (with a few dozen keys per node, side by side) often beat std::map.
16. Disjoint sets: union-find · Which group is this in, and are these two together? · QuizWhich of these can a plain union-find do?A. Merge two groups, and tell whether two items share a groupUnion-find only merges and compares. It cannot undo a union (splitting needs other structures), and it knows nothing about the route between two items, only that one exists. That narrow job is what makes it so fast.
16. Disjoint sets: union-find · Find climbs to the root; union links two roots · QuizWith the struct above and 4 items, what are parent[3] and find(3) after these calls?B. parent[3] is 2; find(3) is 0The first two unions make 1 a child of 0 and 3 a child of 2. The third finds the roots of 1 and 3 (0 and 2) and hangs 2 under 0. Only roots move, so parent[3] is still 2, and find(3) climbs 3 → 2 → 0. That climb is what the next slides make short.
16. Disjoint sets: union-find · Try it: count friend circles · Code challengeFinish countCircles(n, friendships): for each pair, find both roots; if they differ, link one under the other and count one group fewer. Return how many groups are left.One solution:
The ra == rb check is the heart of it: the third test closes a loop (2 is already with 0), and counting it would give 0 circles. Linking parent[b] = a instead of the roots is the classic slip: it can cut b away from its old group. Always link root to root. (auto [a, b] is a structured binding: it unpacks each std::pair into two names.)
16. Disjoint sets: union-find · Path compression: point straight at the root · Fill the blanksMake find compress the path: each item on the way gets the root as its parent.1: find, 2: parent, 3: parentThe recursive call returns the root, and on the way back each item stores it as its parent. return parent[x] then returns the root for roots and non-roots alike. A loop version does two passes: one to find the root, one to repoint everything. The recursion goes as deep as the path is long, which is fine once paths are kept short: the next slide shows how to keep every path under log₂ n.
16. Disjoint sets: union-find · Union by size: hang the small tree under the big one · Find the bugThis union by size builds tall trees. Which line is wrong?Line 4: if (size[ra] < size[rb]) std::swap(ra, rb);Line 5 hangs rb under ra, so ra must be the larger root. Line 4 swaps when ra is larger, putting the big tree under the small one: the worst choice. With <, the swap makes ra the larger, and line 6 adds the absorbed group’s size to it. Only roots’ sizes need to be right.
16. Disjoint sets: union-find · Union by size: hang the small tree under the big one · Put in orderPut the steps of a union by size with path compression in order.1. Find the root of a, compressing its path 2. Find the root of b, compressing its path 3. If the roots are the same, stop: already together 4. Make the root of the larger group the one that stays on top 5. Set the smaller root’s parent to the larger root 6. Add the smaller group’s size to the larger root’s sizeBoth finds come first because only roots may be linked. The size is updated last, on the root that stays: a root that has been absorbed never needs its size again.
16. Disjoint sets: union-find · Union-find for pixels, networks and cycles · Code challengeWrite firstCycle(n, cables): cables join computers one at a time. Return the index of the first cable that joins two computers already connected, or -1 if none does. UnionFind is ready, with both tricks.One solution:
unite already does the work: it returns false exactly when both ends share a root, which means a path between them existed before this cable. This only works for undirected connections; cycles in a directed graph (a → b → c → a) need a graph search instead. The loop counts with an index, rather than a range-for, because the answer is the index.
17. Caches: combining structures · A cache keeps answers you may need again · QuizA cache answers 900 of 1,000 requests itself. A hit takes 1 ms and a miss 100 ms. What is the average?B. About 11 ms900 × 1 + 100 × 100 = 10,900 ms over 1,000 requests: 10.9 ms each. The misses dominate: raising the hit rate from 90% to 99% would bring it down to about 2 ms. That is why the choice of what to evict matters so much.
17. Caches: combining structures · Which entry goes when the cache is full? · QuizA music app caches songs on the phone. People replay the songs they have been listening to lately. Which policy fits best?B. LRURecent listening predicts what comes next, which is exactly LRU’s bet. FIFO would delete a song you play every day because you first downloaded it long ago. LRU is the default choice for most caches, and the rest of this module builds it.
17. Caches: combining structures · The classic LRU: a hash map plus a doubly linked list · Put in orderPut the steps of get(key) in the map-plus-list LRU in order.1. Look the key up in the map 2. If it is missing, return nothing (std::nullopt) 3. Unlink its node from where it is in the list 4. Link the node in at the front of the list 5. Return the node’s valueEach step is O(1): the map gives the node directly, and a doubly linked node knows both neighbours, so unlinking needs no search. A singly linked list could not do the third step without walking from the front to find the previous node.
17. Caches: combining structures · std::list moves a node in O(1) with splice · QuizWhy does the classic C++ LRU keep its entries in a std::list and store list iterators in the map, rather than keeping entries in a std::vector and storing indexes?B. Moving an entry to the front of a vector shifts all the others, O(n), and changes their indexes; a list splices in O(1) and its iterators stay validAn LRU moves entries on every hit. In a vector that means shifting everything in between, and every shifted entry’s index in the map is now wrong. A list node never moves in memory, so an iterator to it stays good however the list is reordered. Lists use more memory (two pointers per node), the price of that guarantee.
17. Caches: combining structures · Try it: the map-plus-list LRU · Code challengeFinish get and put. get splices a hit to the front. put updates and splices an existing key; otherwise it adds the entry at the front, stores its iterator in index, and, over capacity, evicts order.back() from both structures.One solution:
The two structures must always agree: every key in index has exactly one node in order. On eviction the node goes from both, which is why each entry stores its own key: read order.back().keybeforepop_back frees the node, or you read freed memory. Forgetting the update branch in put adds a second node for the key and overwrites the map’s iterator, leaving a ghost node that is later "evicted" and erases the new key from the map. Lru c{2} sets capacity to 2 and leaves the list and map empty.
17. Caches: combining structures · Memoising: a cache in front of a function · Find the bugThis memoised cost keeps recomputing for some arguments, slowly. Which line is the bug?Line 4: if (cache.contains(n)) return cache[n];cache[n] on a new key inserts a 0, so "0" has to mean "not computed yet". When cost(n) really is 0, every call looks like a miss and computes it again. Ask whether the key is there (contains, or find), not whether its value looks empty. The same trap hides in any cache that uses a special value, like -1, to mean "missing".
17. Caches: combining structures · Expiry: entries that go stale · Code challengeFinish TtlCache: set stores the value with expires = now + ttl; get returns it only while now is before expires, and erases it once it has expired.One solution:
Storing the expiry time, not the time it was stored, makes the check one comparison. map.erase(found) erases through the iterator you already have, with no second lookup; do not use found after it, since erasing invalidates it. Lazy expiry only frees memory when an expired key is read again; real caches combine it with an LRU limit or an occasional sweep. Taking now as an argument, instead of reading the clock inside, is what lets the tests jump an hour ahead in a microsecond.
18. Range queries · Totals over a range, asked again and again · QuizA dashboard shows 10,000 range totals over 1,000,000 data points, each range about half the data. About how many additions does the loop above do?C. About 5,000,000,00010,000 questions × 500,000 additions each is 5 × 10⁹: seconds of work even for compiled C++, every time the page loads. The rest of this module answers each question in one subtraction, after one pass to prepare.
18. Range queries · Prefix sums: subtract two running totals · QuizWith the same prefix, what is sum(0, 5), and what is sum(3, 3)?A. 23 and 1sum(0, 5) is prefix[6] - prefix[0], 23 − 0, the whole list. sum(3, 3) is prefix[4] - prefix[3], 9 − 8 = 1: a range of one day is just that day’s value. The leading 0 is what makes from = 0 work without an if.
18. Range queries · Try it: a range-sum struct · Find the bugsum(1, 3) should be 1 + 4 + 1 = 6 but gives 5. Which line is the bug?Line 4: return prefix[to + 1] - prefix[from];prefix[i] holds the first i values, so the total through index to is prefix[to + 1]. prefix[to] stops one short and drops the last day of the range. Mixing "sum of the first i" with "sum through index i" is the classic off-by-one with prefix sums; pick one meaning and test a range of length one.
18. Range queries · Try it: a range-sum struct · Code challengeFinish RangeSum: fill prefix in the constructor, then make sum(from, to) return the total of values[from] through values[to] in O(1).One solution:
Solution
1#include<vector>23structRangeSum{4 std::vector<longlong> prefix;56explicitRangeSum(const std::vector<int>& values)7:prefix(values.size()+1,0){8for(std::size_t i =0; i < values.size(); i++)9 prefix[i +1]= prefix[i]+ values[i];10}11longlongsum(int from,int to)const{12return prefix[to +1]- prefix[from];13}14};
The constructor does O(n) work once, and each sum is two reads and a subtraction. The last test is a trillion: an int total would overflow long before, which is why prefix is long long and why prefix[i] + values[i] adds in long long (the int is widened first). The same trick works for anything you can "subtract": counts, and even XOR. It does not work for a minimum, because knowing the minimum of the first 10 and of the first 4 tells you nothing about positions 4 to 9. The standard library’s std::partial_sum (in <numeric>) builds running totals too.
18. Range queries · Prefix sums in two dimensions · Fill the blanksComplete the build, and the query for rows r1–r2 and columns c1–c2, both inclusive.1: -, 2: -, 3: -, 4: +Adding "above" and "left" counts the overlapping corner twice, so it is subtracted once. The query is the mirror: take the big rectangle to the bottom-right corner, cut off the strip above and the strip to the left, and add back the corner piece that was cut off twice. Adding and removing overlaps like this is called inclusion-exclusion.
18. Range queries · Try it: trees in a rectangle · Code challengeThe constructor builds P. Write count(r1, c1, r2, c2): the total of the rectangle from row r1, column c1 to row r2, column c2, both corners included.One solution:
Solution
1#include<vector>2usingGrid= std::vector<std::vector<int>>;34structGridSum{5 std::vector<std::vector<longlong>> P;6explicitGridSum(constGrid& grid)7:P(grid.size()+1,8 std::vector<longlong>(grid[0].size()+1,0)){9for(std::size_t r =0; r < grid.size(); r++)10for(std::size_t c =0; c < grid[0].size(); c++)11 P[r +1][c +1]= grid[r][c]+ P[r][c +1]+ P[r +1][c]12- P[r][c];13}14longlongcount(int r1,int c1,int r2,int c2)const{15return P[r2 +1][c2 +1]- P[r1][c2 +1]- P[r2 +1][c1]+16 P[r1][c1];17}18};
Four reads and three additions, whatever the rectangle’s size. The extra row and column of zeros make the edges work: a rectangle starting in row 0 subtracts P[0][…], which is 0. Forgetting to add the corner back is the usual mistake; the middle-rectangle test catches it. Image software uses the same table (an "integral image") to blur and detect faces quickly.
18. Range queries · When values change, prefix sums fall behind · QuizA live scoreboard changes a score 5,000 times a second and is asked for range totals 5,000 times a second, over 100,000 players. Which fits?C. A Fenwick treeRebuilding prefix sums costs 100,000 steps per change; summing a range costs up to 100,000 steps per question. A Fenwick tree does each in about 17 steps (log₂ 100,000). When both operations are frequent, pick the structure that is never slow at either.
18. Range queries · A Fenwick tree stores sums of power-of-two blocks · QuizWhich cells does the sum of values 1 to 6 read?A. 6 and 46 is 0110, so cell 6 holds 2 values (5–6); 6 − 2 = 4. 4 is 0100, so cell 4 holds values 1–4; 4 − 4 = 0, stop. Two cells cover six values. Each jump removes the lowest 1 bit, so a number with b bits takes at most b jumps.
18. Range queries · A Fenwick tree in code · Code challengeadd is written. Finish prefix(i) (the sum of values 1 to i), then range(from, to): the sum of values from to to, both included.One solution:
Solution
1#include<vector>23structFenwick{4 std::vector<longlong> tree;5explicitFenwick(int n):tree(n +1,0){}67voidadd(int i,longlong delta){8for(; i <static_cast<int>(tree.size()); i += i &-i)9 tree[i]+= delta;10}11longlongprefix(int i)const{12longlong sum =0;13for(; i >0; i -= i &-i) sum += tree[i];14return sum;15}16longlongrange(int from,int to)const{17returnprefix(to)-prefix(from -1);18}19};
range is the prefix-sums trick again, one block down: everything up to to, minus everything before from. Because indexes start at 1, prefix(0) is 0 and needs no special case. To set a value rather than change it by an amount, add the difference: add(i, newValue - oldValue), keeping the old values in a plain vector alongside.
19. Persistent structures · Changing data in place forgets the past · QuizWhat does this print?A. 1C++ copies values: push_back(shelf) stores a whole copy of the vector, so the first saved version keeps its one book. History works, but every save copies every book: O(n) time and memory per version. Persistent structures keep the history without the copies.
19. Persistent structures · Shared cells need shared owners: std::shared_ptr · QuizVersion 1 is a list of three cells. Versions 2 and 3 each put one new cell in front of version 1. What is the reference count of version 1’s first cell?C. 3Three shared_ptrs point at it: the variable holding version 1, and the tail of each of the two new cells. Drop the version 1 variable and the cell stays alive, count 2, because versions 2 and 3 still need it. That is ownership following sharing, with no one deciding who frees what.
19. Persistent structures · Try it: change one item, share the rest · Code challengeWrite update(list, index, value): a new list with value at index, sharing every cell after it with the old list. If the list is too short, return the very same list. Recurse: at index 0 make one cell in front of the old tail; otherwise update the tail, and copy this cell only if the tail came back different.One solution:
Each call copies one cell on the way back up, until index 0, where the new cell points straight at the old tail: from there on, everything is shared. == on shared_ptrs checks identity, the same cell, which is how you test sharing; copying the whole list would print the same titles but fail that test. Without the rest == list->tail check, a too-short list would come back as a full copy instead of the same list.
19. Persistent structures · Path copying: a new tree in O(log n) · Put in orderIn what order are the new nodes finished when a recursive persistent insert adds 40?1. The new leaf 40 is made 2. A copy of 30 is made, with right child 40 3. A copy of 50 is made, with left child the new 30 4. The new root is returned as the new versionThe recursion goes down first, then builds on the way back up: each level can only make its copy once it has the new child from below. So the leaf is finished first and the root last. Nothing old is ever changed, so a reader holding the old root sees a complete, valid tree the whole time.
19. Persistent structures · Try it: a persistent insert · Code challengeWrite insert(tree, key) for an immutable BST: return a new tree with key added, copying only the nodes on the path (node(left, key, right) makes one). If the key is already there, return the very same tree, not a copy.One solution:
Solution
1#include<memory>2structNode;3usingTree= std::shared_ptr<constNode>;4structNode{5int key;6Tree left, right;7};8Treenode(Tree left,int key,Tree right){9return std::make_shared<Node>(Node{key, left, right});10}1112Treeinsert(constTree& t,int key){13if(!t)returnnode(nullptr, key,nullptr);14if(key < t->key){15Tree left =insert(t->left, key);16return left == t->left ? t :node(left, t->key, t->right);17}18if(key > t->key){19Tree right =insert(t->right, key);20return right == t->right ? t :node(t->left, t->key, right);21}22return t;23}
Each copied node takes the one new child and the sameshared_ptr for the other side, so that whole subtree is shared (its count goes up by one). The last test is the subtle one: copying on the way back up even when nothing below changed would make a new root for a key that was already there. Checking left == t->left returns the old node when its child came back unchanged, so callers can notice "nothing changed" with one pointer comparison.
19. Persistent structures · Same pointer, same contents: change detection for free · Find the bugHere the cart is held by std::shared_ptr<Cart>, not const. After addToCart, the old state has the new book too. Which line is the bug?Line 2: auto cart = std::make_shared<Cart>(*state.cart);Copying a shared_ptr copies the pointer, not the cart: both states now own the same cart, and the push changes it for both. Undo is broken, and next.cart == state.cart is true, so anything checking for changes skips the cart. Copy the object (*state.cart) into a new one. Holding shared data through shared_ptr<const Cart> makes the compiler refuse this code (line 2 cannot drop the const, and the cart cannot be changed through it), which is why persistent code uses const everywhere.
19. Persistent structures · What persistence costs · QuizWhich job gains most from a persistent structure?B. A drawing app that keeps every step so the user can undo any number of timesUndo history is the textbook case: each step is a version, sharing almost everything with the last. The other two never look back and need raw speed, where a mutable vector is the right choice. As always, the operations you need choose the structure.
19. Persistent structures · Bonus: a Bloom filter trades certainty for space · QuizA sign-up form checks a Bloom filter of taken usernames before asking the database. What can the filter’s answer tell you for sure?B. A "no" means the name is certainly freeA Bloom filter has false positives but never false negatives: a name that was added always finds its bits set. So "no" is final and skips the database; only a "maybe" needs the real check. Databases such as Cassandra and RocksDB use them the same way, to skip disk reads for keys that are not there. Items cannot be removed, since clearing a bit could erase another item.
20. Mastery challenge · How this challenge works · QuizWhat does this print?C. out of dune, 2stock["dune"] finds no "dune", so it inserts one with the value 0 and returns that: the test is true and the map now has two keys. Asking with [] changes the map; find or contains only look. The same trap breaks caches and memo tables. (Hash tables; caches.)
20. Mastery challenge · Choose a structure by the operations you need · QuizA calendar app receives reminders in any order, each with a time. It must always fire the earliest pending reminder next, and new reminders keep arriving. Which structure?B. A min-heap ordered by timeThe operations are "add anywhere in time" and "take the earliest": a priority queue. A heap does both in O(log n); in C++ that is a std::priority_queue with std::greater<>, since the default puts the largest on top. A sorted vector makes each add O(n); a queue would fire in arrival order, not time order; a hash map has no notion of "earliest". (Heaps.)
20. Mastery challenge · Choose a structure by the operations you need · Put in orderPut the steps of choosing a structure for a new feature in order.1. List the operations the feature needs 2. Estimate how often each runs, and how big the data gets 3. Compare what each candidate structure costs for those operations 4. Pick the simplest one that is fast enough 5. Measure with realistic data, and change only if it is too slowThe operations decide, not the structure’s fame. "How often" and "how big" separate an O(n) that is fine (a few hundred items) from one that is not (a million, every keystroke). Simple first: a std::vector or a std::unordered_map often wins until measurements say otherwise, because contiguous memory is fast in ways Big O does not show.
20. Mastery challenge · Build it: a stack that knows its minimum · Code challengeFinish MinStack: keep mins in step with items, so min() returns the smallest value on the stack (or std::nullopt when empty) in O(1), including after pops.One solution:
Each level of mins answers "what is the smallest at or below here?", which never changes while that level exists. So a pop just discards one level of each. Storing only a single min number fails the second test: after popping 3, nothing remembers 5. mins.back() on an empty vector is undefined (this runner stops the program), which is why push checks empty() first. A version that only pushes onto mins when the value is smaller saves memory but must handle equal values, which the third test checks. (Stacks.)
20. Mastery challenge · Build it: the k most frequent words · Code challengeWrite topWords(words, k): count each word, keep heap (a min-heap of {count, word} pairs) at most k entries big, and return the words most frequent first.One solution:
Here counts[w]++ is the right use of []: a new word starts at 0 and becomes 1. Counting is O(n); the heap work is O(m log k) for m distinct words, better than sorting all m when k is small. The heap pops the least frequent first, so the drained list is reversed at the end. The mistakes to avoid: the default std::priority_queue (a max-heap) would pop the most frequent and keep the worst, and a heap of everything is correct but O(m) memory. (Hash tables; heaps.)
20. Mastery challenge · Build it: an LRU cache with a size budget · Code challengeFinish put(key, value, size): remove any old entry for key; skip anything bigger than budget; otherwise add it at the front, store its iterator, add its size to used, and evict from the back until used fits the budget.One solution:
Routing every removal through remove keeps used honest, which is the real difficulty: the second test fails if replacing a adds 6 without taking away 4. The size check must come before inserting, or one huge image would empty the cache on its way through. The loop copies the oldest key before removing it: order.back().key is a string inside the node that remove frees, and passing a reference to it would leave key pointing at freed memory midway through. The while loop always ends, because once only the new entry is left, used is its size, which fits. (Caches.)
20. Mastery challenge · Build it: is this network a tree? · Code challengeWrite isTree(n, pipes): false unless there are exactly n - 1 pipes; then unite each pipe’s ends, returning false if a pipe joins two houses already connected.One solution:
Solution
1#include<numeric>2#include<utility>3#include<vector>4usingPipes= std::vector<std::pair<int,int>>;56intfind(const std::vector<int>& parent,int x){7while(parent[x]!= x) x = parent[x];8return x;9}10boolisTree(int n,constPipes& pipes){11 std::vector<int>parent(n);12 std::iota(parent.begin(), parent.end(),0);13if(static_cast<int>(pipes.size())!= n -1)returnfalse;14for(auto[a, b]: pipes){15int ra =find(parent, a), rb =find(parent, b);16if(ra == rb)returnfalse;17 parent[ra]= rb;18}19returntrue;20}
Counting alone is not enough: the third test has three pipes for four houses, but they form a loop and house 3 is left out. Why are the two checks together enough? Each pipe that closes no cycle merges two groups, so n − 1 such pipes merge n groups into one: everything is connected. The cast keeps the comparison between two ints: pipes.size() is unsigned, and comparing it with a signed n - 1 would turn that into an unsigned number too, so for n = 0 its −1 would become a huge number. Here the answer happens to come out the same either way, but mixing signed and unsigned in a comparison is a classic source of bugs, and compilers warn about it. (Graphs; disjoint sets.)
20. Mastery challenge · Review it: a heap that is almost right · Find the bugThis sift-down passes most tests but sometimes breaks the heap. Which line is wrong?Line 5: if (r < heap.size() && heap[r] < heap[small]) small = r;Line 5 compares the right child with the left child instead of with the best so far. Take a parent of 2 with children 9 and 5: the parent is already the smallest, but line 5 sees 5 < 9 and picks the right child anyway, so 2 and 5 swap and 5 ends up above 2. Comparing with heap[small] means "smaller than everything checked so far", which includes the parent. (Heaps.)
20. Mastery challenge · Review it: a heap that is almost right · Code challengeWrite isMinHeap(a): true when every element is greater than or equal to its parent, at index (i - 1) / 2.One solution:
Solution
1#include<vector>23boolisMinHeap(const std::vector<int>& a){4for(std::size_t i =1; i < a.size(); i++)5if(a[(i -1)/2]> a[i])returnfalse;6returntrue;7}
Checking each element against its one parent covers every parent-child pair exactly once: O(n). A check like this is worth keeping in a heap’s tests, run after every push and pop, to catch bugs like the one above; the standard library has one too, std::is_heap (for max-heaps, unless given std::greater<>()). Starting the loop at 1 skips the root, which has no parent, and keeps i - 1 from wrapping around below zero. (Heaps.)
20. Mastery challenge · Predict what the structures do · QuizInsert 5, 3, 8, 1, 4 into an empty binary search tree, then delete 3 using its successor. What is 5’s left child now?B. 43 has two children, 1 and 4. Its successor is the smallest key in its right subtree: 4, a leaf. 4 is copied into 3’s place and the old 4 removed, so 5’s left child is 4, with 1 below it on the left. The ordering rule still holds. (Binary search trees.)
20. Mastery challenge · Predict what the structures do · QuizA trie holds "tea", "ten" and "to". Separately, a union-find with 5 items runs unite(0, 1), unite(2, 3), unite(1, 3). How many trie nodes (not counting the root) and how many groups?B. 5 nodes and 2 groupsThe trie shares "t" and "te": t, e, a, n, o, five nodes for eight letters. The unions merge {0, 1} and {2, 3}, then join those two; item 4 is still alone, so 2 groups. (Tries; disjoint sets.)
20. Mastery challenge · Predict what the structures do · QuizA persistent tree’s version 1 has 1,000 nodes. Version 2 is version 1 with one key inserted by path copying, along a path of 10 nodes. Then the variable holding version 1 is destroyed. How many nodes are freed?B. 10Version 2 shares 990 of version 1’s nodes, so their shared_ptr counts stay above 0. Only the 10 old nodes on the copied path were owned by version 1 alone: their counts reach 0 and they are deleted. Reference counting frees exactly what no version uses any more. (Persistent structures.)
20. Mastery challenge · Which statements are true? · QuizPick every true statement.B. An in-order traversal of any binary search tree visits the keys in sorted order.; D. A trie’s lookup cost depends on the length of the word, not on how many words it holds.A heap is only ordered parent over child, not sorted (heaps). Union-find only knows whether two items are connected, not how (disjoint sets). A Bloom filter has false positives but never false negatives (persistent structures, bonus). The true ones are the defining property of BSTs and the reason tries exist.
20. Mastery challenge · Which statements are true? · QuizWhich pairing of structure and cost is wrong?B. Plain binary search tree built from sorted input: search O(log n)Sorted input makes a plain BST a chain, so search is O(n). A balanced tree, like the red-black tree inside std::set, guarantees O(log n) whatever the input order. The other three are exactly what their modules showed. (Binary search trees; balanced trees.)