Midterm Prep.
Proofs, recurrences, choosing a data structure, AVL trees, and heaps.
Lucas P. Cordova, Ph.D.
Willamette University | Fall 2026
Outline
- The exam: what is on it and how it runs
- How to prove something: a framework
- Recurrences: a recipe that solves almost all of them
- Choosing a data structure: the question type that matters most
- AVL trees: review
- Heaps: review
- Sorts: review
- Wrap-up: how to use the week

The exam
What is on it and how it runs
The midterm at a glance
| When | Wednesday, October 14, in class |
| Covers | The start of the course through heaps and heap sort |
| Not on it | Greedy algorithms, minimum spanning trees, shortest paths |
| How you answer | Handwritten, on the paper exam. No computers or other devices. |
| You may use | Printed copies of your own homework submissions and feedback, and of code you wrote for this course |
| You may not use | Any device, LLMs or AI assistants of any kind, the internet, another person |
Short analyses, traces by hand, one or two short proofs, and choose-a-data-structure problems, which carry the most weight.

How to prove something
A framework
What a proof is
- A proof is an argument a skeptical reader can check, one step at a time.
- Every step is either something given, a definition, or follows from earlier steps by a rule you can name.
- Examples are not proofs. Checking n = 1, 2, 3 shows the claim is plausible, not that it is true.
- You do not need to be clever. You need to pick the right template and fill it in.
- Write the claim precisely.
- Pick the technique from the shape of the claim.
- Fill in that technique's template.
- End by restating what you showed.

Step 2: the shape of the claim picks the technique
| If the claim looks like | Use | Template |
|---|---|---|
| “For every n ≥ 0, …” or anything about a structure of size n | Induction | base case, hypothesis, step |
| “This loop computes …” | Loop invariant | initialization, maintenance, termination |
| “No … exists”, “… is always at least …”, “… must be …” | Contradiction | assume the opposite, derive something false |
| “This runs in O(…)” | Count the work | a sum for loops, a recurrence for recursion |
| “This claim is false” | Counterexample | one specific input, shown to fail |
Name the technique in your first sentence: “We prove this by induction on h.” It tells the grader what to look for and it tells you what to write next.

Induction: the template
- State P(n). Write the claim as a statement about one number n. Say what n measures (size, height, number of inserts).
- Base case. Show P holds for the smallest n. Actually check it.
- Inductive hypothesis. “Assume P(k) holds for some k ≥ base.” (Strong form: assume P(j) for every j ≤ k.)
- Inductive step. Show P(k + 1). Break the size k + 1 object into smaller pieces, apply the hypothesis to the pieces, and combine.
- Conclude. “By induction, P(n) holds for all n ≥ base.”
Point to the exact line where you used the hypothesis. If you cannot find one, it is not an induction proof yet.

Worked proof 1: induction
A perfect binary tree of height h has exactly 2h+1 − 1 nodes.
- P(h): a perfect binary tree of height h has 2h+1 − 1 nodes. Induction on h.
- Base, h = 0: a single node. 21 − 1 = 1. True.
- Hypothesis: assume a perfect tree of height k has 2k+1 − 1 nodes.
- Step: a perfect tree of height k + 1 is a root plus two perfect subtrees of height k. By the hypothesis each has 2k+1 − 1 nodes. Total: 1 + 2(2k+1 − 1) = 2k+2 − 1.
- Conclude: by induction, P(h) holds for every h ≥ 0. ■
HEIGHT 2: A ROOT PLUS TWO PERFECT TREES OF HEIGHT 1. 1 + 2(3) = 7.

Loop invariants: the template
A loop invariant is a statement that is true before every iteration of a loop. It describes what the loop has accomplished so far.
- State the invariant. “At the start of iteration i, …”
- Initialization. It is true before the first iteration.
- Maintenance. If it is true before an iteration, it is true before the next one.
- Termination. When the loop ends, the invariant plus the exit condition give you what you wanted.
Initialization is the base case. Maintenance is the inductive step. Termination is the conclusion, with one extra idea: plug in the value the loop variable has when the loop stops.

Worked proof 2: a loop invariant
1 def insertion_sort(A): 2 for i in range(1, len(A)): 3 key = A[i] 4 j = i - 1 5 while j >= 0 and A[j] > key: 6 A[j + 1] = A[j] 7 j -= 1 8 A[j + 1] = key
At the start of iteration i, A[0:i] holds the original first i items, in sorted order.
- Initialization, i = 1: A[0:1] is one item. One item is sorted.
- Maintenance: assume A[0:i] is sorted. The inner loop shifts every item larger than key one place right and drops key into the gap. Now A[0:i+1] is sorted, which is the invariant for i + 1.
- Termination: the loop stops when i = n. The invariant says A[0:n] is sorted. That is the whole array. ■

Worked proof 3: contradiction
In a max-heap with distinct keys, the largest key is at the root.
- Assume the opposite: the largest key sits at some node x that is not the root.
- x is not the root, so x has a parent p.
- The max-heap property says key(p) ≥ key(x). Keys are distinct, so key(p) > key(x).
- But key(x) is the largest key in the heap. Contradiction.
- So the assumption was false: the largest key is at the root. ■
Assume the opposite. Use definitions to push forward until two statements cannot both be true. Conclude the original claim.

Five ways proofs fall short
- Assuming what you are proving. If your step starts from P(k + 1), you are going backwards.
- No base case, or a base case that is asserted and not checked.
- The hypothesis is never used. Then the step is either wrong or not induction.
- Proof by example. “It works for n = 4” proves the claim for n = 4.
- Undefined letters. Say what n, k, and h are before you use them.
A correct template with a gap in the step earns most of the points. A page of algebra with no stated claim earns few. Write the skeleton first.

Recurrences
A recipe that solves almost all of them
Where a recurrence comes from
1 def merge_sort(A): 2 if len(A) <= 1: 3 return A 4 mid = len(A) // 2 5 L = merge_sort(A[:mid]) # one call, size n/2 6 R = merge_sort(A[mid:]) # one call, size n/2 7 return merge(L, R) # O(n) work here
T(n) = 2 T(n / 2) + n
| Symbol | Meaning | Here |
|---|---|---|
| a | how many recursive calls one call makes | 2 |
| b | how much smaller each subproblem is: size n / b | 2 |
| f(n) | work done in this call, outside the recursive calls | n |
- Count the calls: that is a.
- Look at the argument: that is n / b.
- Everything else in the function body is f(n).

The recipe: three steps
For T(n) = a T(n / b) + nd (a ≥ 1, b > 1):
- Read off a, b, and d (the exponent of the work term).
- Compute the critical exponent c = logb a.
- Compare d with c.
| Compare | Who does the most work | Answer |
|---|---|---|
| d < c | the leaves | Θ(nc) = Θ(nlogb a) |
| d = c | every level the same | Θ(nd log n) |
| d > c | the root | Θ(nd) |
One question: going down the tree, does the work grow (more subproblems) or shrink (smaller subproblems)?

Who does most of the work?
For T(n) = a T(n / b) + Θ(nd), the comparison is between two exponents.
| Measures | In words | |
|---|---|---|
| c = logb a | how fast the number of subproblems grows | growth caused by the recursion |
| d | how expensive the work at each node is, outside the recursive calls | growth caused by the work at each node |
As I move down the recursion tree, does the total work per level get bigger, stay the same, or get smaller?
Bigger: the leaves do most of the work. The same: every level contributes equally. Smaller: the root does most of the work.

The recursion tree: root, levels, leaves
1 def find_max(A, lo, hi): # max of A[lo:hi] 2 if hi - lo == 1: 3 return A[lo] 4 mid = (lo + hi) // 2 5 left = find_max(A, lo, mid) # size n/2 6 right = find_max(A, mid, hi) # size n/2 7 return max(left, right) # O(1) work
- Every box is one execution of the function, labelled with its range.
- Root: the call you made, find_max(A, 0, 8).
- Levels: the calls created by lines 5 and 6.
- Leaves: calls on one item. They return on line 3 and make no calls.
- Own work: line 7 (or line 3), 1 unit per call. So work per level = calls per level: 1, 2, 4, 8.
find_max(A, 0, 8). TEAL: THE ROOT. AMBER: THE LEAVES.

Leaves do most of the work: d < c
1 def find_max(A, lo, hi): # max of A[lo:hi] 2 if hi - lo == 1: 3 return A[lo] 4 mid = (lo + hi) // 2 5 left = find_max(A, lo, mid) # size n/2 6 right = find_max(A, mid, hi) # size n/2 7 return max(left, right) # O(1) work
Finding the maximum by halves: T(n) = 2 T(n / 2) + 1
- Two calls, so a = 2. Each on half, so b = 2. One comparison outside the calls, so d = 0. c = log2 2 = 1. d < c.
- Root: 1 call of size n. Its own work: 1.
- Level 1: 2 calls, 1 unit each. Total 2. Level 2: 4 calls. Total 4.
- Leaves: n calls on single items, 1 unit each. Total n.
- Each level does twice the work of the one above.
Each call does almost nothing, but the calls multiply. The n leaves do more than everything above them: T(n) = Θ(nc) = Θ(n).

Every level does the same work: d = c
1 def merge_sort(A): 2 if len(A) <= 1: 3 return A 4 mid = len(A) // 2 5 L = merge_sort(A[:mid]) # size n/2 6 R = merge_sort(A[mid:]) # size n/2 7 return merge(L, R) # O(n) work here
Merge sort: T(n) = 2 T(n / 2) + n
- Two calls, so a = 2. Each on half, so b = 2. The merge is linear, so d = 1. c = log2 2 = 1. d = c.
- Root: 1 call of size n. Its own work: n (the final merge).
- Level 1: 2 calls, each merging n / 2 items. Total n.
- Level 2: 4 calls, each merging n / 4 items. Total n.
- Leaves: n calls on single items. Total n.
Twice as many calls, each doing half the work: it cancels exactly. No level wins, so count the levels: n work × log2 n levels = Θ(n log n).

The root does most of the work: d > c
1 def search(A, k): # binary search 2 if not A: 3 return False 4 mid = len(A) // 2 5 if A[mid] == k: 6 return True 7 if k < A[mid]: 8 return search(A[:mid], k) # copies n/2 9 return search(A[mid + 1:], k)
Binary search that slices: T(n) = T(n / 2) + n
- One call runs, so a = 1. On half, so b = 2. The slice copies about n / 2 items, so d = 1. c = log2 1 = 0. d > c.
- Root: 1 call of size n. Its own work: n.
- Level 1: 1 call of size n / 2. Work n / 2. Level 2: work n / 4.
- Leaf: 1 call on an empty or one-item list. Work 1.
- n + n/2 + n/4 + … is less than 2n. The root alone is more than half of it.
The work shrinks and the calls do not multiply. The root wins: T(n) = Θ(nd) = Θ(n). Passing lo and hi with no slicing gives Θ(log n).

From code to a recurrence, step by step
1 def blend(A, lo, hi): 2 n = hi - lo 3 if n <= 1: 4 return A[lo] if n == 1 else 0 5 h = n // 2 6 q = h // 2 7 x = blend(A, lo, lo + h) # left half 8 y = blend(A, lo + h, hi) # right half 9 z = blend(A, lo + q, lo + q + h) # middle half 10 s = 0 11 for i in range(lo, hi): # one pass 12 s += A[i] 13 return max(x, y, z, s)
- What is n? The size of the input this call works on. Here n = hi − lo.
- Base case. When n ≤ 1 the function returns at once: T(1) = Θ(1).
- Count the recursive calls that run in one execution. Three: x, y, z. So a = 3.
- How big is each call's input? Each range has length h = n / 2. So b = 2.
- Everything else is f(n). Cross out the recursive calls. What is left is a loop of n iterations plus constant work. So f(n) = n, and d = 1.
T(n) = 3 T(n / 2) + n

Then solve it with the recipe
1 def blend(A, lo, hi): 2 n = hi - lo 3 if n <= 1: 4 return A[lo] if n == 1 else 0 5 h = n // 2 6 q = h // 2 7 x = blend(A, lo, lo + h) # left half 8 y = blend(A, lo + h, hi) # right half 9 z = blend(A, lo + q, lo + q + h) # middle half 10 s = 0 11 for i in range(lo, hi): # one pass 12 s += A[i] 13 return max(x, y, z, s)
T(n) = 3 T(n / 2) + n so a = 3, b = 2, d = 1.
- Critical exponent: c = logb a = log2 3.
- Estimate it with the definition of a log: 21 = 2 and 22 = 4, and 3 sits between them. So 1 < c < 2 (it is about 1.58).
- Compare: d = 1 is less than c. The leaves do most of the work.
- Answer: T(n) = Θ(nlog2 3), about n1.58.
Two calls of size n / 2 with linear work would be n log n. A third call makes it worse, but it stays below n2. An answer between n log n and n2 is believable.

Two traps when reading code
Trap 1: calls that are written but do not all run.
1 def search(A, lo, hi, k): 2 if lo >= hi: 3 return -1 4 mid = (lo + hi) // 2 5 if A[mid] == k: 6 return mid 7 if k < A[mid]: 8 return search(A, lo, mid, k) 9 return search(A, mid + 1, hi, k)
- Two calls appear, but one execution makes one of them. a = 1, not 2.
- T(n) = T(n / 2) + 1 = Θ(log n).
Trap 2: work hidden inside one line.
1 def search(A, k): 2 if not A: 3 return False 4 mid = len(A) // 2 5 if A[mid] == k: 6 return True 7 if k < A[mid]: 8 return search(A[:mid], k) 9 return search(A[mid + 1:], k)
- A slice copies its elements: A[:mid] costs Θ(n).
- T(n) = T(n / 2) + n = Θ(n). The same algorithm, no longer logarithmic.

Six recurrences
| Recurrence | a | b | d | c = logb a | Compare | T(n) | Seen in |
|---|---|---|---|---|---|---|---|
| 2T(n/2) + n | 2 | 2 | 1 | 1 | d = c | Θ(n log n) | merge sort |
| T(n/2) + 1 | 1 | 2 | 0 | 0 | d = c | Θ(log n) | binary search |
| 2T(n/2) + 1 | 2 | 2 | 0 | 1 | d < c | Θ(n) | tree traversal |
| 4T(n/2) + n | 4 | 2 | 1 | 2 | d < c | Θ(n2) | |
| 3T(n/3) + n2 | 3 | 3 | 2 | 1 | d > c | Θ(n2) | |
| 2T(n/4) + √n | 2 | 4 | 1/2 | 1/2 | d = c | Θ(√n log n) |
- T(n) = 8 T(n / 2) + n2
- T(n) = T(n / 3) + n.

Logarithm rules
| Rule | Statement | Use it to |
|---|---|---|
| Definition | logb x = y means by = x | compute c: log2 8 = 3 because 23 = 8 |
| Product | log(xy) = log x + log y | split log(n · n) into 2 log n |
| Quotient | log(x / y) = log x − log y | simplify log(n / 2) = log n − 1 |
| Power | log(xk) = k log x | log(n3) = 3 log n = Θ(log n) |
| Change of base | logb x = log x / log b | log4 2 = 1/2; bases differ by a constant |
| Inverse | blogb x = x | 2log2 n = n |
| Swap | alogb n = nlogb a | count leaves: 2log4 n = n1/2 = √n |
The base of a log does not matter inside Θ. And log(n!) = Θ(n log n), which is where the sorting lower bound comes from.

When the recipe does not apply
The recipe needs subproblems of size n / b. If the size shrinks by subtraction, unroll or draw the tree.
| Recurrence | Unroll it | T(n) |
|---|---|---|
| T(n − 1) + 1 | 1 + 1 + … + 1, n times | Θ(n) |
| T(n − 1) + n | n + (n − 1) + … + 1 = n(n + 1) / 2 | Θ(n2) |
| 2T(n − 1) + 1 | 1 + 2 + 4 + … + 2n | Θ(2n) |
- Recursion tree, in three questions: how much work on level i? How many levels? What do the levels sum to?
- Geometric sums: shrinking terms sum to the first term, growing terms to the last term, equal terms to (one term) × (number of levels).
- Arithmetic sum: 1 + 2 + … + n = n(n + 1) / 2 = Θ(n2).

Choosing a data structure
The question type that matters most
The method: five steps
- List the operations the problem needs. Underline the ones that happen most often.
- Classify each operation. Is it by key, by position, by order (next, previous, range, smallest), or only the extreme (max or min)?
- Write down the candidates. Usually three: one cheap and simple, one hash-based, one tree-based.
- Fill in the cost table. One row per candidate, one column per operation.
- Choose and justify. Name the operation that rules out each loser, and state the trade-off you accepted.
This is the decision you make in real code. Nobody asks you to rotate a tree at work. They ask why the lookup is slow.

Step 2: the questions that decide it
| Ask | If yes | Why |
|---|---|---|
| Do I only ever look up by exact key? | hash table | expected O(1), but no order at all |
| Do I need order: next larger, previous, range, sorted output? | balanced BST (AVL) | O(log n) for everything, order included |
| Do I only need the largest (or smallest), repeatedly? | binary heap | O(1) peek, O(log n) insert and remove, tiny and simple |
| Do I need items by position: the i-th, insert in the middle? | array, or AVL as a sequence | array: O(1) access, O(n) middle insert; AVL: O(log n) both |
| Is the data static after loading? | sorted array | O(log n) search by binary search, nothing to maintain |
| Do I need a worst-case guarantee? | avoid hashing and plain BSTs | hashing is expected-case; a plain BST can be O(n) tall |

The cost table to know cold
| Structure | find(k) | insert | delete | max or min | next larger |
|---|---|---|---|---|---|
| Unsorted array | n | 1 (a) | n | n | n |
| Sorted array | log n | n | n | 1 | log n |
| Hash table | 1 (e) | 1 (e, a) | 1 (e) | n | n |
| AVL tree | log n | log n | log n | log n | log n |
| Binary heap | n | log n (a) | log n (the max) | 1 | n |
(a) amortized (e) expected All others are worst case.
Hash table: fastest lookups, no order. AVL: nothing is fast, nothing is slow. Heap: one question answered very well.

Worked example: the print shop
A print shop keeps a set of job numbers. It needs add(k), cancel(k), and next_after(k): the smallest job number larger than k. Which structure?
- Operations: add, cancel, next_after. All three are frequent.
- Classify: add and cancel are by key. next_after is by order.
| Candidate | add | cancel | next_after |
|---|---|---|---|
| Hash table | 1 (e) | 1 (e) | n |
| Sorted array | n | n | log n |
| AVL tree | log n | log n | log n |
AVL tree. The hash table loses on next_after (no order, so it must scan). The sorted array loses on add and cancel (shifting). Trade-off accepted: O(log n) updates in place of the hash table's O(1).

Three quick ones
| The problem needs | Choose | The deciding fact |
|---|---|---|
| An emergency room queue: add a patient with a priority, treat the most urgent next. Nothing else. | Binary heap | Only the maximum is ever asked for. O(log n) both ways, O(1) to peek. |
| A course roster: look up a student by ID, enroll, drop. Never listed in order. | Hash table | Exact-key lookups only. Expected O(1), and order is not needed. |
| An event log: record an event with a timestamp, delete one, list every event between two times. | AVL tree | A range query is an order operation. Find the start in O(log n), then walk in order. |
| A text editor line buffer: jump to line i, insert a line in the middle, delete a line. | AVL as a sequence | By position, with middle inserts. An array pays O(n) per insert. |
The roster now also needs “print all students in ID order” once a semester. Does the answer change?

How to write the answer
- Name the structure in the first sentence.
- Give the cost of every operation the problem asked for, with (a) or (e) where it applies.
- Rule out the alternatives, one sentence each, naming the operation that sinks them.
- State the trade-off you accepted, if there is one.
- If the problem asks for it, describe how each operation works in two or three lines. Pseudocode or precise prose.
Choosing a hash table when the problem needs order. Forgetting that hashing is expected-case. Quoting O(log n) for a plain BST, which is only true if it is balanced.

AVL trees
Review: the facts, the cases, one full insert
AVL trees: what to know
- Height: a leaf has height 0, an empty subtree −1. Skew(x) = h(right) − h(left).
- AVL property: every node has skew −1, 0, or +1. It forces h ≤ 1.44 log2 n, so every operation is O(log n).
- Insert or delete as in a BST, then walk up recomputing heights. At the lowest node with skew ±2, rotate.
| skew(x) | heavy child's skew | Shape | Fix |
|---|---|---|---|
| +2 | 0 or +1 | same side | single: rotate_left(x) |
| +2 | −1 | zigzag | double: rotate_right(x.right), then rotate_left(x) |
| −2 | 0 or −1 | same side | single: rotate_right(x) |
| −2 | +1 | zigzag | double: rotate_left(x.left), then rotate_right(x) |
An insert needs at most one fix (single or double). A delete can need a fix at every level on the way up.

One full insert: add 22
- The tree holds 30, 20, 40, 10, 25. Insert 22: it goes left of 25.
- Walk up from the new leaf, recomputing heights.
- skew(25) = −1. skew(20) = +1. skew(30) = −2.
- Lowest unbalanced node: 30.
- Its left child 20 has skew +1, the opposite sign. That is the zigzag shape.
- Fix:
rotate_left(20), thenrotate_right(30).
LABELS: HEIGHT (SKEW). MAGENTA: UNBALANCED. AMBER: JUST INSERTED.

After the double rotation
- 25 was lifted to the root. 20 and 30 are its children.
- 22 changed parents: it was 25's left child and is now 20's right child.
- In-order before and after: 10, 20, 22, 25, 30, 40. Rotations never reorder keys.
- Height went from 3 back to 2.
The lowest unbalanced node. Its skew. The shape (same side or zigzag). The rotation or rotations, by name. The final tree.
TEAL: THE NODE THE ROTATION LIFTED INTO PLACE.

Heaps
Review: the array, the two moves, build
Binary heaps: what to know
- A complete binary tree stored in an array, level by level. No pointers.
parent(i) = (i - 1) // 2left(i) = 2i + 1,right(i) = 2i + 2- Max-heap property: every parent is at least its children. The max is at index 0.
- It is not a BST: there is no order between siblings.
- Height is ⌊log2 n⌋, always.
THE ARRAY [9, 7, 8, 2, 5, 4] AS A TREE. LABELS ARE INDICES.
insert and delete_max: O(log n) each. build: O(n). Heap sort: O(n log n), in place.

Build and delete_max, by hand
build on [2, 9, 4, 7, 5, 8]: sink down at index 2, then 1, then 0.
| Step | What happens | Array |
|---|---|---|
| start | 2 9 4 7 5 8 | |
| i = 2 | 4 has one child, 8. Swap. | 2 9 8 7 5 4 |
| i = 1 | 9 beats 7 and 5. Nothing. | 2 9 8 7 5 4 |
| i = 0 | 2 swaps with 9, then with 7. | 9 7 8 2 5 4 |
- delete_max: swap 9 with the last item (4), pop the 9, sink the 4. It swaps with 8. Result: [8, 7, 4, 2, 5].
- Sink down swaps with the larger child. Swapping with the smaller one breaks the property.
Sinking costs a node's height. Half the nodes have height 0, a quarter have height 1, and so on. The total is O(n).

Sorts
Review: one table, then which one when
Every sort so far, in one table
| Sort | Best | Worst | In place | Stable | One-line idea |
|---|---|---|---|---|---|
| Insertion | n | n2 | yes | yes | grow a sorted prefix, one item at a time |
| Selection | n2 | n2 | yes | no | repeatedly move the max to the end |
| Merge | n log n | n log n | no | yes | sort two halves, merge them |
| Heap | n log n | n log n | yes | no | build a heap, delete_max n times |
| AVL | n log n | n log n | no | yes | insert everything, traverse in order |
| Counting | n + k | n + k | no | yes | keys in 0..k: count, then place |
| Radix | n logn k | n logn k | no | yes | counting sort on each base-n digit |
Any sort that only compares keys needs Ω(n log n) comparisons in the worst case. Counting and radix sort are not comparison sorts: they use keys as array indices.

Which sort when
| Situation | Reach for | Because |
|---|---|---|
| Small input, or nearly sorted | insertion sort | O(n) on sorted input, O(nk) when items are at most k places off |
| Need a worst-case guarantee and no extra memory | heap sort | O(n log n) always, in place |
| Need stability (equal keys keep their order) | merge sort | stable and O(n log n), at the price of O(n) extra space |
| Integer keys in a small range | counting or radix sort | linear time, by not comparing |
| Data arrives over time and you query in between | AVL tree | it is a sort you can keep inserting into |
Sort n integers, each between 0 and n3 − 1, in O(n). Which sort, and why does it not break the lower bound?

Wrap-up
How to use the week
Between now and the exam
- Work the study guide's sample problems on paper, timed, about 12 minutes each.
- Rebuild the cost table for the five structures from memory, with a reason for each cell.
- Write out the three templates once: induction, loop invariant, contradiction.
- Redo one homework problem of each kind from a blank page, then compare with the posted solution.
- Trace by hand: an AVL insert with a double rotation, a heap build, one recurrence from each of the three cases.
Templates for proofs, a recipe for recurrences, a five-step method for choosing a structure. The exam rewards method over memory.

References
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms, 4th ed. MIT Press, 2022. Section 2.1 (loop invariants), Sections 4.3 to 4.5 (recurrences and the master method), Chapter 6 (heapsort), Chapter 8 (sorting in linear time), Chapters 11 to 13 (hash tables and search trees).
- The CS 351 Midterm Study Guide: topics, what you may use, and sample problems.

CS 351 F26: Midterm Prep
By Lucas Cordova
CS 351 F26: Midterm Prep
Midterm preparation for Fall 2026