Chapter 3 · Lectures

Data structures, ADTs, Big-O, sorting

3.1 Representing Information as Bits

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:

::: center Range (dec) Range (hex) Meaning


0—31 0x000x1F Control characters (non-printing) 32 0x20 Space 33—47 0x210x2F Punctuation: ! " # $ % & ’ ( ) * + , - . / 48—57 0x300x39 Digits ’0’’9’ 58—64 0x3A0x40 More punctuation: : ; < = > ? @ 65—90 0x410x5A Uppercase ’A’’Z’ 91—96 0x5B0x60 [ \ ] ^ _ ‘ 97—122 0x610x7A Lowercase ’a’’z’ 123—126 0x7B0x7E { | } ~ 127 0x7F DEL (control) :::

What ASCII can’t do: everything else

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 10FFFF1610FFFF_{16} (1.1\sim 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 Structure Access Insert mid Good for


Array / vector O(1)O(1) O(n)O(n) Random access, cache-friendly Linked list O(n)O(n) O(1)O(1)^*^ Frequent insert/delete Hash table O(1)O(1) avg O(1)O(1) avg Fast keyed lookup Binary search tree O(logn)O(\log n) O(logn)O(\log n) Sorted iteration Heap --- O(logn)O(\log n) Always know the max/min Graph --- varies Relationships, networks :::

^*^Given a pointer to the insertion point. Finding the point is still O(n)O(n).

Balanced only. A degenerate BST is O(n)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 Concept C++ 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)O(1) amortized O(1)O(1) with tail pointer Insert at front O(n)O(n) (shift all) O(1)O(1) (new head) Random access a[i] O(1)O(1) O(i)O(i) (walk from head) Insert in middle (given iterator) O(n)O(n) shift O(1)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-kk 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)O(1); if the new candidate beats it, pop and push. Total O(nlogk)O(n \log k) 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)O(k) per candidate that beats the minimum.

  • BST / std::set. Overkill here, but useful if kk 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 ADT What the contract says Usual implementations


List Ordered sequence; append, remove, search, traverse. Array, linked list Dynamic array Ordered with O(1)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 \to 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++ name Default structure


List std::list, std::forward_list Doubly / singly linked list Dynamic array std::vector Contiguous array Stack std::stack std::deque Queue std::queue std::deque Deque std::deque Chunked array Set std::set, std::unordered_set BST / hash table Priority queue std::priority_queue Binary heap over std::vector Map std::map, std::unordered_map BST / hash table :::

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

::: center Language Library ADTs you get


C++ Standard Template Library (STL) vector, list, deque, stack, queue, priority_queue, set, map, unordered_set, unordered_map Python Built-ins + collections list, tuple, set, dict, deque, defaultdict, Counter, heapq Java Java Collections Framework (JCF) ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, ArrayDeque, PriorityQueue :::

Why CS 300 still has you build them by hand

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 shape ADT


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

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 — nn comparisons.

  • Average case. On uniformly random hit positions, roughly n/2n/2 comparisons. When the key is absent, always nn.

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)O(N)" starts to feel slow surprises people the first time they see it. Suppose one comparison takes 1 microsecond (10610^{-6} s).

::: center NN Worst-case comparisons Wall time @ 1 s each


10310^3 1,000 0.001 s 10610^6 1,000,000 1 s 10710^7 10710^7 10 s 2×1082 \times 10^8 (Amazon’s catalog, ish) 2×1082 \times 10^8 200 s \approx 3 min 20 s 10910^9 10910^9 about 17 min :::

Ignoring constants: why we say “on the order of NN

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 NN 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 data Algorithm Worst case


Nothing; unsorted sequence Linear search O(N)O(N) Array is sorted Binary search O(logN)O(\log N) Data lives in a hash table keyed by what you’re searching for Hash lookup O(1)O(1) expected Data lives in a balanced BST keyed by the search value BST lookup O(logN)O(\log N) :::

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)O(\log n)

Why \log n is so dramatic

The concrete comparison with linear search is what sells binary search. One comparison at 1 s:

::: center NN Linear worst Binary worst Ratio


10310^3 1,000 ops (1 ms) 10 ops (10 s) 100×\times 10610^6 10610^6 ops (1 s) 20 ops (20 s) 50,000×\times 2×1082 \times 10^8 2×1082 \times 10^8 (3+ min) 28 ops (28 s) \approx 7 \times 10^6$$\times 10910^9 10910^9 ops (\approx17 min) 30 ops (30 s) \approx 3 \times 10^7$$\times :::

The bigger lesson

3.9 Constant-Time Operations

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)O(1) on a modern processor:

::: center Operation Why 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 Ω\Omega, Θ\Theta). 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)T(N)

For an algorithm running on input of size NN, the number of constant-time operations it performs is a function of NN. We’ll call that function T(N)T(N). Usually T(N)T(N) depends on more than just NN — it depends on which particular input of size NN we’re running on. Hence the standard two-sided view:

Upper and lower bounds

Rather than carrying the exact T(N)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 N2N^2. We care that it’s an N2N^2. That’s what Big-O, Ω\Omega, and Θ\Theta formalize: rate of growth, constants and lower-order terms absorbed.

What c and N_0 buy you

The constant cc absorbs the hardware, the language, and any constant-factor overhead. The threshold N0N_0 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 NN, the following functions are ordered by how fast they grow. Every entry dominates the ones above it.

::: center Class Name Examples


O(1)O(1) Constant Array index, hash lookup O(logN)O(\log N) Logarithmic Binary search, balanced-BST lookup O(N)O(N) Linear Linear search, one pass over an array O(NlogN)O(N \log N) Linearithmic / ”NN log NN” Merge sort, heap sort, good sorts O(N2)O(N^2) Quadratic Bubble sort, insertion sort worst case, nested loops O(N3)O(N^3) Cubic Naive matrix multiply, certain DP algorithms O(2N)O(2^N) Exponential Brute-force subset enumeration O(N!)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 Property What it means


Time complexity Best / average / worst-case count of comparisons and swaps as a function of nn. Space complexity Extra memory beyond the input. In-place sorts use O(1)O(1) extra; merge sort needs O(n)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)O(n^2)

Best case: O(n)O(n) on already-sorted input

Nearly sorted lists

The adaptivity generalizes. Suppose only a constant number cc of elements are out of place. Then

  • ncn - c elements each need 1 comparison to stay put.

  • cc elements each need at most nn comparisons.

Total work: (nc)1+cn=nc+cnO(n)(n - c) \cdot 1 + c \cdot n = n - c + cn \in O(n) (since cc is constant). Insertion sort runs in linear time on nearly-sorted input.

Why this matters in practice

Properties

::: center Property Insertion sort


Best case Θ(n)\Theta(n) — already-sorted input Worst case Θ(n2)\Theta(n^2) — reverse-sorted input Average case Θ(n2)\Theta(n^2) for random input Space O(1)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)\Theta(n^2) comparisons

Where selection sort is better than insertion sort

Selection sort has one redeeming feature: it does at most n1n - 1 swaps total. Insertion sort can do Θ(n2)\Theta(n^2) swaps.

Selection sort vs. insertion sort at a glance

::: center Property Insertion sort Selection sort


Best case Θ(n)\Theta(n) Θ(n2)\Theta(n^2) Worst case Θ(n2)\Theta(n^2) Θ(n2)\Theta(n^2) Space O(1)O(1) O(1)O(1) Stable? Yes No (swap can reorder equals) Adaptive? Yes No Swaps Up to Θ(n2)\Theta(n^2) Exactly n1n - 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)O(n \log n) 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 n2n^2.

Interleaved lists and the “gap”

Shell sort in C++

Choice of gap sequence

::: center Gap sequence Example (n=100n=100) Worst-case runtime


Halving: n/2,n/4,,1n/2, n/4, \ldots, 1 (original Shell) 50, 25, 12, 6, 3, 1 O(n2)O(n^2) Hibbard: 2k12^k - 1 63, 31, 15, 7, 3, 1 O(n3/2)O(n^{3/2}) Sedgewick / Knuth 121, 40, 13, 4, 1 O(n4/3)O(n^{4/3}) Ciura (empirical, best known small-nn) 57, 23, 10, 4, 1 very fast in practice :::

Why this beats plain insertion sort

Properties

::: center Property Shell sort


Best case O(nlogn)O(n \log n) (already sorted, depends on sequence) Worst case O(n2)O(n^2) (naïve halving) down to O(n4/3)O(n^{4/3}) (Sedgewick) Average case Depends on gap sequence; between O(n5/4)O(n^{5/4}) and O(n3/2)O(n^{3/2}) in practice Space O(1)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)O(n \log n) 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 Strategy Behavior


First or last element Disastrously O(n2)O(n^2) on sorted inputs. Don’t. Middle element Good for sorted inputs; still O(n2)O(n^2) on adversarial data. Random element Expected O(nlogn)O(n \log n) 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 2logn2 \log n, switch to heap sort. Guaranteed O(nlogn)O(n \log n) worst case. std::sort does this. :::

Properties

::: center Property Quicksort


Best case Θ(nlogn)\Theta(n \log n) Average case Θ(nlogn)\Theta(n \log n) Worst case Θ(n2)\Theta(n^2) — mitigated by pivot strategy Space O(logn)O(\log n) 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)O(n^2)?

3.19 Merge Sort

Quicksort gets its O(nlogn)O(n \log n) on average and pays a rare O(n2)O(n^2) on bad pivots. Merge sort buys a guaranteed O(nlogn)O(n \log n) worst case plus stability, at the cost of O(n)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)O(n \log n)

Memory cost

Properties

::: center Property Merge sort


Best case Θ(nlogn)\Theta(n \log n) Average case Θ(nlogn)\Theta(n \log n) Worst case Θ(nlogn)\Theta(n \log n) — guaranteed Space O(n)O(n) extra — temporary merge buffer Stable? Yes Adaptive? Not natively (plain merge sort). Variants like Timsort are adaptive. :::

Merge sort vs. quicksort

::: center Criterion Quicksort Merge sort


Best / avg case Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Worst case Θ(n2)\Theta(n^2) Θ(nlogn)\Theta(n \log n) Extra memory O(logn)O(\log n) (stack) O(n)O(n) (buffer) Stable? No Yes Cache behavior Excellent (sequential) Good (streaming) In-place? Yes No Recursion depth O(logn)O(\log n) avg, O(n)O(n) worst O(logn)O(\log n) 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)\Omega(n \log n). Radix sort is our first non-comparison sort — it never compares two elements to each other — and it breaks the nlognn \log n bound, sorting in O(nd)O(n \cdot d) where dd is the number of digits. For fixed-width integer keys, that’s effectively O(n)O(n).

The trick: sort by one digit at a time

The LSD algorithm

Tracing a small example

::: center Step Array


Input 170, 45, 75, 90, 802, 24, 2, 66 After 1’s pass 170, 90, 802, 2, 24, 45, 75, 66 After 10’s pass 802, 2, 24, 45, 66, 170, 75, 90 After 100’s pass 2, 24, 45, 66, 75, 90, 170, 802 :::

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 Property Radix sort (LSD)


Time (best / avg / worst) Θ(nd)\Theta(n \cdot d) — all cases the same Space Θ(n+B)\Theta(n + B) where BB 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)O(n + k) time, where kk 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 Algorithm Average case Fast?


Selection sort Θ(n2)\Theta(n^2) No Insertion sort Θ(n2)\Theta(n^2) No Shell sort Θ(n1.5)\Theta(n^{1.5}) (typical sequences) No Quicksort Θ(nlogn)\Theta(n \log n) Yes Merge sort Θ(nlogn)\Theta(n \log n) Yes Heap sort Θ(nlogn)\Theta(n \log n) Yes Radix sort Θ(nd)=O(n)\Theta(n \cdot d) = O(n) for fixed-width Yes :::

The Ω(nlogn)\Omega(n \log n) lower bound for comparison sorts

Comparison vs. non-comparison sorts

::: center Algorithm Comparison sort?


Selection, insertion, shell sort Yes Quicksort, merge sort, heap sort Yes Radix sort (and counting, bucket sort) No :::

Best / average / worst, side by side

Average case is the headline number, but the other two cases matter when you care about guarantees (worst) or optimized input (best).

::: center Algorithm Best Average Worst


Quicksort Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Θ(n2)\Theta(n^2) Merge sort Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Heap sort Θ(n)\Theta(n) * / Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Θ(nlogn)\Theta(n \log n) Radix sort Θ(n)\Theta(n) Θ(n)\Theta(n) Θ(n)\Theta(n) :::

Picking a fast sort: a mini decision tree

The extended comparison table

Everything from the chapter, in one place:

::: center Algorithm Best Average Worst Space Stable? Adaptive?


Selection n2n^2 n2n^2 n2n^2 11 No No Insertion nn n2n^2 n2n^2 11 Yes Yes Shell nlognn \log n n1.5n^{1.5} n1.52n^{1.5\text{--}2} 11 No Some Quicksort nlognn \log n nlognn \log n n2n^2 logn\log n No No Merge nlognn \log n nlognn \log n nlognn \log n nn Yes No Heap nlognn \log n nlognn \log n nlognn \log n 11 No No Radix nn nn nn nn Yes No :::

What this connects to

Companion materials. Compact notes: chapters/ch_3/notes.tex (includes the full sort comparison table). Practice prompts (problem \to pseudocode \to C++): chapters/ch_3/practice.md.

interactive mode active