Chapter 2 covered algorithms — the instructions that process data. Chapter 3 shifts the view to how data is represented in the first place. At the bottom of every stack, “data” is ultimately a sequence of tiny binary switches. This section is the ground-floor view: what a bit is, what a byte is, and how characters like ’A’ or ’Z’ get mapped onto patterns of bits.
Bits and bytes
Why bits
Every piece of data — an integer, a character, a pixel, a video frame, a neural network’s weights — gets ultimately stored as a pattern of bits. The question isn’t whether data is bits but which encoding the program uses to interpret those bits. Two different encodings can give the same pattern two different meanings:
ASCII: bits that mean characters
A subset of the ASCII table
The full ASCII table is 128 entries. A few useful patterns to commit to memory:
ASCII encodes English. It has no code points for accented letters (é, ñ), no space for non-Latin scripts (Arabic, Chinese, Cyrillic, Tamil), and no emoji. The modern universal encoding is Unicode, which assigns code points up to 10FFFF16 (∼1.1 million characters).
Connecting bits back to data structures
Every data structure you’re about to meet (linked list, stack, queue, tree, hash table) is ultimately a layout of bits in memory, organized so that the operations the structure promises are fast. A linked list stores a value plus a pointer-to-next-node; a vector stores an array of values plus a size and capacity; a hash table stores an array of buckets plus a hash function. The abstractions hide the bits, but understanding that the bits are still there — and that performance ultimately comes from how they’re arranged — is what separates “using” a data structure from choosing one.
3.2 Data Structures
A data structure is a way of organizing data in memory so that a particular set of operations (access, search, insert, delete) can be done efficiently. looked at how individual values are stored as bits; this section starts naming the ways those values get grouped into useful collections. The rest of the course is essentially a detailed tour of the seven structures listed here.
The seven structures you’ll meet this semester
Why there are so many
Every structure makes different operations fast at the cost of others. There is no “best” data structure; the right one depends on the operation mix you actually perform.
::: center
StructureAccessInsert midGood for
Array / vector O(1)O(n) Random access, cache-friendly
Linked list O(n)O(1)^*^ Frequent insert/delete
Hash table O(1) avg O(1) avg Fast keyed lookup
Binary search tree O(logn)O(logn) Sorted iteration
Heap --- O(logn) Always know the max/min
Graph --- varies Relationships, networks
:::
^*^Given a pointer to the insertion point. Finding the point is still O(n).
Balanced only. A degenerate BST is O(n).
A concrete example: the shift problem
The classic case where structure choice matters is inserting in the middle of a collection.
How to pick a data structure
The C++ standard library’s version of this list
Almost every structure named above already ships with the language. Know the mapping so you don’t reinvent:
::: center
ConceptC++ standard type
Array / vector std::vector<T>, std::array<T, N>
Linked list std::list<T> (doubly) or std::forward_list<T>
Hash table std::unordered_map<K, V>, std::unordered_set<T>
Balanced BST std::map<K, V>, std::set<T>
Heap / priority queue std::priority_queue<T>
Stack / queue (adapters) std::stack<T>, std::queue<T>
Graph no built-in — build from vector<vector<int>> or similar
:::
3.3 Relation Between Data Structures and Algorithms
Chapter 2 treated algorithms in the abstract; section 3.2 laid out the main data structures. This section is the hinge between them. The relationship runs in two directions, and keeping both in mind is what turns “I know some algorithms” and “I know some structures” into “I can design software.”
Direction 1: Different structure, different algorithm
“Append an item” sounds like one operation, but the code behind it depends entirely on what you’re appending to. Compare appending to a linked list against appending to an array.
Insert-at-front is the sharper example
Append hides a lot because both versions usually finish fast. Prepend makes the structure choice visible immediately.
::: center
Operation Array / vector Singly linked list
Append at end O(1) amortized O(1) with tail pointer
Insert at front O(n) (shift all) O(1) (new head)
Random access a[i]O(1)O(i) (walk from head)
Insert in middle (given iterator) O(n) shift O(1) relink
:::
Same operation names, completely different runtimes. The structure you pick is the set of costs you accept.
Direction 2: Algorithms use structures as scratch space
The other direction: a larger algorithm almost always relies on a data structure to hold intermediate results. The structure you pick shapes the algorithm you write.
Example: top-k salespersons. Given a list of salespeople, print the top 5 by sales total.
Different structure, different algorithm
If we used a different structure to hold the running top-5, the algorithm changes shape:
Min-heap of size 5. Peek at the minimum in O(1); if the new candidate beats it, pop and push. Total O(nlogk) but cleaner and faster than re-sorting an array.
Sorted linked list of size 5. Insert-in-order means walking the list to find the right slot — O(k) per candidate that beats the minimum.
BST / std::set. Overkill here, but useful if k were large or you needed dynamic queries.
Same problem, four different implementations, each dictated by the structure you choose.
Putting the two directions together
3.4 Abstract Data Types
The previous section showed that the same operation name on two different data structures hides two different algorithms. That’s a feature, not a bug. The whole point of this section’s central idea — the abstract data type — is to let us name an operation without naming an implementation, so code that uses the operation keeps working no matter which structure sits underneath.
Why the separation matters
Everyday analogy: “printer” is an ADT (print this page, cancel this job). Laser printer, inkjet, thermal, network printer — all different implementations. The program calling print() does not and should not care. Swap the printer, the program still works.
In code, the benefits land as:
Replaceability. Start with an array-backed list; switch to a linked-list-backed list when profiling tells you to. No caller code changes.
Reasoning. The correctness of caller code depends on the ADT’s contract, not on representation details.
Teamwork. One person builds the ADT, another builds against its interface. They negotiate a contract, not a memory layout.
The standard ADTs you should recognize
The textbook lists nine common ADTs. Treat this table as a vocabulary list — each one gets its own chapter later.
::: center
ADTWhat the contract saysUsual implementations
List Ordered sequence; append, remove, search, traverse. Array, linked list
Dynamic array Ordered with O(1) indexed access; grows on demand. Array (with resize)
Stack LIFO; push / pop / peek at the top only. Array, linked list
Queue FIFO; enqueue at back, dequeue from front. Linked list, circular array
Deque Insert/remove at both ends. Doubly linked list, circular array
Bag Unordered collection; duplicates allowed. Array, linked list
Set Unordered; no duplicates. BST, hash table
Priority queue Queue where highest-priority item comes out first. Heap
Dictionary / Map Key → value lookup. Hash table, BST
:::
C++ makes the ADT/structure distinction concrete
The C++ standard library names ADTs via container adaptors that sit on top of underlying containers. This is worth seeing explicitly because it’s the same idea the textbook is describing, except made literal in the type system.
The library’s ADT / structure mapping
::: center
ADT (from textbook)C++ nameDefault structure
(C++ doesn’t ship a “bag” type; a std::vector or std::multiset fills that role.)
Using ADTs to keep caller code honest
3.5 Applications of ADTs
Section 3.4 made the ADT/structure distinction; this section asks what you get from keeping them separate. Two payoffs: abstraction simplifies programming, and standard libraries mean you rarely implement the common ADTs by hand. The catch — which this section also makes explicit — is that abstraction does not free you from thinking about efficiency.
Abstraction as a productivity lever
Abstraction buys three compounding benefits once you’re comfortable with it:
Less code to write. You describe what you want, not how to do it. map[key] = value hides a hash computation, bucket lookup, collision handling, and possible rehash.
Less code to read. A colleague scanning your code sees the ADT’s operations and immediately knows the shape of the data flow — “oh, it’s a queue, so FIFO” — without studying a custom linked-list implementation.
Less code to debug. Bugs in the ADT’s implementation live in one place; bugs in your algorithm live in yours. The two don’t mix.
ADTs in standard libraries
Almost every modern language ships an implementation of the common ADTs as part of its standard library. You rarely implement a queue or a hash map from scratch in production; you use the language’s provided one.
Production code uses the standard library. This course has you implement linked lists, hash tables, heaps, and trees from scratch. That’s deliberate, and for two reasons.
Choosing the right ADT is the actual skill
Because the common ADTs are available off the shelf, the hard part isn’t implementing them — it’s recognizing which one the problem in front of you is asking for.
::: center
Problem shapeADT
“I need the most recent item first.” Stack
“I need the oldest waiting item first.” Queue
“I need the most important item first.” Priority queue
“I need fast lookup by a key.” Map / dictionary
“I need a collection of distinct items, order doesn’t matter.” Set
“I need ordered access by index.” Dynamic array / list
:::
3.6 Searching and Algorithm Runtime
This section restarts the algorithms thread now that we have a vocabulary of data structures to search through. The worked example is linear search, the simplest search algorithm there is. The real payoff of the section, though, is the first careful look at runtime as a function of input size — the idea that turns into Big-O in .
Linear search
Why linear — and when it’s fine
Linear search makes no assumptions about the data. It works on an unsorted array, a linked list, or any sequence you can iterate once. That’s the whole point: it’s the baseline.
Best case. Key is at index 0 — 1 comparison.
Worst case. Key is last, or absent — n comparisons.
Average case. On uniformly random hit positions, roughly n/2 comparisons. When the key is absent, always n.
Runtime as a function of input size
The key idea the textbook introduces here: runtime grows with the size of the input. Counting actual seconds depends on the machine. Counting the number of operations the algorithm does is a machine-independent proxy that predicts the seconds.
Concrete numbers
The scale at which "O(N)" starts to feel slow surprises people the first time they see it. Suppose one comparison takes 1 microsecond (10−6 s).
::: center
NWorst-case comparisonsWall time @ 1 s each
103 1,000 0.001 s
106 1,000,000 1 s
107107 10 s
2×108 (Amazon’s catalog, ish) 2×108 200 s ≈ 3 min 20 s
109109 about 17 min
:::
Ignoring constants: why we say “on the order of N”
If your linear search does 3 operations per element (bounds check, array access, comparison) instead of 1, the constant changes but the shape of the growth does not: doubling N still doubles the work.
Searching, structures, and algorithms together
The textbook frames this section as “algorithms,” but notice how much depends on the structure underneath. Linear search is the universal baseline because it makes no assumptions. If you know more about the structure, you can do much better:
::: center
What you know about the dataAlgorithmWorst case
Nothing; unsorted sequence Linear search O(N)
Array is sorted Binary search O(logN)
Data lives in a hash table keyed by what you’re searching for Hash lookup O(1) expected
Data lives in a balanced BST keyed by the search value BST lookup O(logN)
:::
3.8 Binary Search
Section 3.6 covered linear search — the universal baseline. This section introduces the first algorithm that does substantially better: binary search. The catch, and the whole point, is that it only works if the data is sorted and randomly accessible. That trade-off — accept a precondition on the structure, unlock a much faster algorithm — is a pattern you will see repeatedly for the rest of the course.
The idea
Think about how you find a name in a physical phone book. You don’t read from the beginning. You open near the middle, see whether your target comes before or after, and immediately ignore the half that doesn’t contain it. Repeat.
Binary search in C++
Recursive binary search
Binary search is also the textbook example of a recursive algorithm that calls itself on a shrinking problem.
Why O(logn)
Why \log n is so dramatic
The concrete comparison with linear search is what sells binary search. One comparison at 1 s:
Binary search was the first algorithm where the number of operations mattered more than the speed of the processor. This section makes precise what “one operation” even means for runtime analysis. The short answer: we count constant-time operations — the unit of work the CPU can do in a fixed time independent of input values.
What counts as constant time
The short list of things that are typically O(1) on a modern processor:
::: center
OperationWhy it’s constant
Arithmetic on fixed-size numeric types (int, double) Hardware does it in a bounded number of cycles regardless of values.
Assignment of a fixed-size value (scalar, pointer, reference) Copies a fixed number of bytes.
Comparison of fixed-size values One CPU instruction.
Indexed access to an array element (a[i]) One pointer arithmetic + one memory load.
Push / pop on a std::vector (amortized) Constant on average; occasional resize is paid for across many pushes.
Hash-table lookup (expected) One hash computation + one bucket probe on average.
Dereferencing a pointer One memory load.
Reading the length stored by a container (v.size()) Returns a cached value.
:::
Why fixed size matters
Notice the repeating phrase: “fixed-size integer or floating-point values.” A modern CPU’s adder has a fixed number of bits — 32 or 64 — and its time-per-add doesn’t depend on whether the operands are 3 or 2,147,483,646.
Counting the right things
In runtime analysis we don’t count every line of code. We pick a representative operation and count how many times it runs.
The hidden-loop trap
The rule of thumb
3.10 Growth of Functions and Complexity
Section 3.9 defined the unit of measurement: constant-time operations. This section asks how the count of those operations grows as the input grows. That growth is what we summarize with Big-O (and its cousins Ω, Θ). These are the three core pieces of vocabulary that will structure every runtime claim for the rest of the course.
Best case, worst case, and T(N)
For an algorithm running on input of size N, the number of constant-time operations it performs is a function of N. We’ll call that function T(N). Usually T(N) depends on more than just N — it depends on which particular input of size N we’re running on. Hence the standard two-sided view:
Upper and lower bounds
Rather than carrying the exact T(N) formula around (with all its constants and lower-order terms), we bound it with simpler functions.
Asymptotic notation: strip the constants
For cross-algorithm comparison we don’t care about the constants in front of N2. We care that it’s an N2. That’s what Big-O, Ω, and Θ formalize: rate of growth, constants and lower-order terms absorbed.
What c and N_0 buy you
The constant c absorbs the hardware, the language, and any constant-factor overhead. The threshold N0 lets us ignore small inputs, where lower-order terms and one-time setup costs distort the picture.
How to read a big-O claim
The growth-rate hierarchy
For large N, the following functions are ordered by how fast they grow. Every entry dominates the ones above it.
::: center
ClassNameExamples
O(1) Constant Array index, hash lookup
O(logN) Logarithmic Binary search, balanced-BST lookup
O(N) Linear Linear search, one pass over an array
O(NlogN) Linearithmic / ”N log N” Merge sort, heap sort, good sorts
O(N2) Quadratic Bubble sort, insertion sort worst case, nested loops
O(N3) Cubic Naive matrix multiply, certain DP algorithms
O(2N) Exponential Brute-force subset enumeration
O(N!) Factorial Brute-force permutations (traveling salesman)
:::
In practice
3.14 Sorting: Introduction
Searching (3.6, 3.8) was the first algorithmic problem we analyzed. Sorting is the second, and it’s arguably the more important one: binary search, priority queues, deduplication, many-pass merges, and much of computational geometry all assume data you can sort first. This short section sets the stage; the rest of this chapter walks through insertion, selection, shell, quicksort, merge, and radix sort. (Bubble sort lives in ch_13’s extras chapter, since it is pedagogy-only; heap sort waits for ch_7, once we have heaps.)
Why sorting is its own field
Sorting is the most-studied algorithmic problem in computer science. It looks trivial at small sizes — you could do it by hand — and turns out to be surprisingly deep.
The fundamental constraint
Why isn’t sorting just “put each element in its right slot”? Because the program doesn’t know where the right slot is without doing work to find out.
What makes sorts differ
When we compare two sorting algorithms we ask five questions:
::: center
PropertyWhat it means
Time complexity Best / average / worst-case count of comparisons and swaps as a function of n.
Space complexity Extra memory beyond the input. In-place sorts use O(1) extra; merge sort needs O(n).
Stability Does the sort preserve the original order of equal elements? Useful when you’ve already sorted by a secondary key.
Adaptivity Does it run faster on already-mostly-sorted input?
Access pattern Does it need random access (quicksort, binary search during sort) or sequential only (merge sort)?
:::
Faster-than-comparison sorts
In C++
3.15 Insertion Sort
Insertion sort is the first concrete sorting algorithm we’ll build. It’s the sort most people naturally perform by hand — think about how you’d organize a hand of playing cards — and its quadratic worst-case cost is the price of its simplicity. It’s also surprisingly good when the input is already nearly sorted, and that adaptivity is where it shines.
The idea
Insertion sort in C++
Worst-case analysis: O(n2)
Best case: O(n) on already-sorted input
Nearly sorted lists
The adaptivity generalizes. Suppose only a constant number c of elements are out of place. Then
n−c elements each need 1 comparison to stay put.
c elements each need at most n comparisons.
Total work: (n−c)⋅1+c⋅n=n−c+cn∈O(n) (since c is constant). Insertion sort runs in linear time on nearly-sorted input.
Why this matters in practice
Properties
::: center
PropertyInsertion sort
Best case Θ(n) — already-sorted input
Worst case Θ(n2) — reverse-sorted input
Average case Θ(n2) for random input
Space O(1) extra — in-place
Stable? Yes — equal elements retain original order
Adaptive? Yes — fast on nearly-sorted input
:::
3.16 Selection Sort
Selection sort is insertion sort’s mirror image. Both carve the array into a sorted prefix and an unsorted suffix; both extend the prefix by one element per outer-loop pass. The difference is which element gets placed each pass. Insertion sort takes whatever is next and slides it into position. Selection sort picks the smallest element of the remaining unsorted region and parks it at the end of the sorted region.
Selection sort in C++
Why it always does Θ(n2) comparisons
Where selection sort is better than insertion sort
Selection sort has one redeeming feature: it does at most n−1 swaps total. Insertion sort can do Θ(n2) swaps.
Selection sort vs. insertion sort at a glance
::: center
PropertyInsertion sortSelection sort
Best case Θ(n)Θ(n2)
Worst case Θ(n2)Θ(n2)
Space O(1)O(1)
Stable? Yes No (swap can reorder equals)
Adaptive? Yes No
Swaps Up to Θ(n2) Exactly n−1
:::
When to reach for each
3.17 Shell Sort
Shell sort is the first sort we meet that breaks out of the quadratic class — not by doing something dramatically new, but by applying insertion sort cleverly in multiple passes over interleaved sublists. It’s the bridge between the simple sorts of 3.15—3.16 and the O(nlogn) sorts of the next chapter. Named for Donald Shell, 1959.
The core insight
Insertion sort is slow on unsorted input because each misplaced element can only move one slot at a time. A tiny value at the end of a big array has to be swapped past every larger neighbor, one pair at a time, to reach the front — that’s the n2.
Interleaved lists and the “gap”
Shell sort in C++
Choice of gap sequence
::: center
Gap sequenceExample (n=100)Worst-case runtime
Halving: n/2,n/4,…,1 (original Shell) 50, 25, 12, 6, 3, 1 O(n2)
Hibbard: 2k−1 63, 31, 15, 7, 3, 1 O(n3/2)
Sedgewick / Knuth 121, 40, 13, 4, 1 O(n4/3)
Ciura (empirical, best known small-n) 57, 23, 10, 4, 1 very fast in practice
:::
Why this beats plain insertion sort
Properties
::: center
PropertyShell sort
Best case O(nlogn) (already sorted, depends on sequence)
Worst case O(n2) (naïve halving) down to O(n4/3) (Sedgewick)
Average case Depends on gap sequence; between O(n5/4) and O(n3/2) in practice
Space O(1) extra — in-place
Stable? No
Adaptive? Somewhat — fewer passes on presorted input
:::
When to use it
3.18 Quicksort
Quicksort is the first algorithm we meet that achieves O(nlogn)on average — and it’s the most widely used comparison sort in practice. Its strategy is a textbook example of divide-and-conquer: do a bit of work to partition the problem into two smaller subproblems, recurse on each, and stitch the result together. Invented by Tony Hoare in 1959 while he was a graduate student trying to sort Russian words for machine translation.
The idea: partition and recurse
The Hoare-style partition
The textbook’s partition is a variant of Hoare’s original. Two index pointers walk from the ends toward each other, swapping out-of-place values when they meet misplaced elements.
Runtime: average vs. worst
Pivot selection strategies
::: center
StrategyBehavior
First or last element Disastrously O(n2) on sorted inputs. Don’t.
Middle element Good for sorted inputs; still O(n2) on adversarial data.
Random element Expected O(nlogn) no matter the input pattern. Adversary can’t predict the pivot.
Median-of-three (first, middle, last) Cheap approximation of the true median; very robust in practice. The choice most real implementations make.
Introsort Start with quicksort, but if recursion depth exceeds 2logn, switch to heap sort. Guaranteed O(nlogn) worst case. std::sort does this.
:::
Properties
::: center
PropertyQuicksort
Best case Θ(nlogn)
Average case Θ(nlogn)
Worst case Θ(n2) — mitigated by pivot strategy
Space O(logn) recursion stack (in-place for the data)
Stable? No — partition swaps non-adjacent elements
Adaptive? Not especially — depends on pivot choice
:::
Why is it called quick if it can be O(n2)?
3.19 Merge Sort
Quicksort gets its O(nlogn)on average and pays a rare O(n2) on bad pivots. Merge sort buys a guaranteedO(nlogn) worst case plus stability, at the cost of O(n) extra memory. It’s also divide-and-conquer, but with the work distributed differently: quicksort does the work before recursing (partition then recurse), merge sort does the work after (recurse then merge). Invented by John von Neumann in 1945.
The idea: recurse then merge
The merge step
The merge is where everything happens. Two already-sorted subsequences can be combined into one sorted sequence in linear time by walking both with two pointers and always taking the smaller of the two front elements.
Merge sort in C++
Runtime: why always O(nlogn)
Memory cost
Properties
::: center
PropertyMerge sort
Best case Θ(nlogn)
Average case Θ(nlogn)
Worst case Θ(nlogn) — guaranteed
Space O(n) extra — temporary merge buffer
Stable? Yes
Adaptive? Not natively (plain merge sort). Variants like Timsort are adaptive.
:::
Merge sort vs. quicksort
::: center
CriterionQuicksortMerge sort
Best / avg case Θ(nlogn)Θ(nlogn)
Worst case Θ(n2)Θ(nlogn)
Extra memory O(logn) (stack) O(n) (buffer)
Stable? No Yes
Cache behavior Excellent (sequential) Good (streaming)
In-place? Yes No
Recursion depth O(logn) avg, O(n) worst O(logn) always
:::
Timsort: merge sort’s real-world descendant
3.20 Radix Sort
Every sort we’ve seen so far is comparison-based: it uses < between pairs of elements as its only decision procedure. The lower bound for that class is Ω(nlogn). Radix sort is our first non-comparison sort — it never compares two elements to each other — and it breaks the nlogn bound, sorting in O(n⋅d) where d is the number of digits. For fixed-width integer keys, that’s effectively O(n).
Three passes, because the max value has three digits. After each pass, the prefix of digit-sorted-ness grows by one.
Runtime
Handling negatives and other bases
Properties
::: center
PropertyRadix sort (LSD)
Time (best / avg / worst) Θ(n⋅d) — all cases the same
Space Θ(n+B) where B is the base
Stable? Yes — and correctness depends on it
Adaptive? No — doesn’t notice already-sorted input
Works on? Integer keys (or anything decomposable into fixed-width digits)
:::
When to reach for radix sort
Counting sort: the standalone non-comparison sort
Radix sort uses counting sort once per digit. But counting sort is also a complete sorting algorithm in its own right — the right choice when keys are integers in a known small range and you want O(n+k) time, where k is the size of the range.
3.21 Overview of Fast Sorting Algorithms
The last several sections introduced a parade of sorting algorithms. This section steps back and classifies them. Two questions organize the view: how fast? (average-case complexity) and what kind? (does the algorithm compare elements, or does it exploit more structure in the keys?).
The speed classification
::: center
AlgorithmAverage caseFast?
Selection sort Θ(n2) No
Insertion sort Θ(n2) No
Shell sort Θ(n1.5) (typical sequences) No
Quicksort Θ(nlogn) Yes
Merge sort Θ(nlogn) Yes
Heap sort Θ(nlogn) Yes
Radix sort Θ(n⋅d)=O(n) for fixed-width Yes
:::
::: center
AlgorithmBestAverageWorstSpaceStable?Adaptive?
Selection n2n2n21 No No
Insertion nn2n21 Yes Yes
Shell nlognn1.5n1.5–21 No Some
Quicksort nlognnlognn2logn No No
Merge nlognnlognnlognn Yes No
Heap nlognnlognnlogn1 No No
Radix nnnn Yes No
:::
What this connects to
Companion materials. Compact notes: chapters/ch_3/notes.tex (includes the full sort comparison table). Practice prompts (problem → pseudocode → C++): chapters/ch_3/practice.md.