Finding a slot is different from making room.
Binary search can find an insertion position in O(log n), but inserting into an array may still shift O(n) items. Across all insertions, the worst-case shifting work is Θ(n²).
a · the original arrayc · temporary vector for this pair onlyBefore copying: which front does this if choose?
void merge_sortBU(Item a[], size_t left, size_t right) { for (auto size = 1; size <= right - left; size *= 2) for (auto i = left; i <= right - size; i += 2 * size) merge(a, i, i + size, std::min(i + 2 * size, right));} // merge_sortBU()Line 4 calls this helper. It gets its own variables and returns after merging this pair.
void merge(Item a[], size_t left, size_t mid, size_t right) { auto n = right - left; vector<Item> c(n); for (auto i = left, j = mid, k = 0; k < n; ++k) { if (i == mid) c[k] = a[j++]; else if (j == right) c[k] = a[i++]; else c[k] = (a[i] <= a[j]) ? a[i++] : a[j++]; } // for copy(begin(c), end(c), &a[left]);} // merge()void merge_sort(Item a[], size_t left, size_t right) { if (left + 1 >= right) // base case: 0 or 1 item(s) return; auto mid = left + (right - left) / 2; merge_sort(a, left, mid); // [left, mid) merge_sort(a, mid, right); // [mid, right) merge(a, left, mid, right);} // merge_sort()Line 7 calls this helper. It gets its own variables and returns after merging this pair.
void merge(Item a[], size_t left, size_t mid, size_t right) { auto n = right - left; vector<Item> c(n); for (auto i = left, j = mid, k = 0; k < n; ++k) { if (i == mid) c[k] = a[j++]; else if (j == right) c[k] = a[i++]; else c[k] = (a[i] <= a[j]) ? a[i++] : a[j++]; } // for copy(begin(c), end(c), &a[left]);} // merge()Lecture 12 · your slide code
Choose a version. Read the highlighted line, then press Next to see what it does.
Top-down sorts the left half, then the right half, then merges them. A one-item group returns immediately.
A copied number stays in a. The faded tile means “already read.” After this pair is merged, copy(begin(c), end(c), &a[left]) copies it back into the same part of a. The next call gets a new c.
Green = executing line. Orange = a parent waiting for its call to return. The same function code is reused by many separate frames.
This is the slide code, with spacing restored. The green bar marks the active driver line. While merge() runs, its calling line stays marked.
Each frame keeps its own local variables and return address. All calls use the same array a; recursion does not copy the whole array into each frame. Only merge() creates a temporary c.
Execution order · most recent events
Begin with two numbers to see one complete call-and-return story. Then choose the slide’s eight-number example. The C++ stays the same.
Separate execution from cost. The stack shows which call runs next. Running time counts the work performed by all those calls, especially the items copied during merges.
| Merge-sort level | Completed merges | Items handled |
|---|
Why not n²? A level has many calls, but their disjoint ranges together contain only n items. You do not merge all n items in every small call.
Why log n levels? Halving 8 takes 8 → 4 → 2 → 1: three splits. There are three merge levels on the way back up. Generally T(n) = 2T(n/2) + Θ(n), for equal halves.
Calls and memory: eight items make 15 merge_sort calls and 7 merge calls. Those 22 calls are not all on the stack at once: this example needs at most 4 active frames including the helper. Recursive stack depth is O(log n); the largest temporary c uses O(n) space. Total auxiliary space is O(n).
Every outlined group is sorted internally. Separate groups can still be out of order with one another.
The slide uses Item as a placeholder type. For this integer example, define using Item = int; and include vector and algorithm with the appropriate std names. In the helper, spell the declaration size_t i = left, j = mid, k = 0;: its printed multi-variable auto declaration mixes size_t initializers with the int literal 0. The animation preserves the printed notation and follows its intended index operations.
Read this first: the slide’s size is the size of one input group. With size = 2, merge two groups of 2 into a group of up to 4. The driver’s i marks the start of a pair.
a[i] means “the number at index i,” not the number i. Indexes start at 0. c[k] = a[i++] copies that number into temporary slot k, then increases the helper’s i. The helper’s variables are local to that call.
Outer loop: “Try size 1, then 2, then 4…” size is the length of ONE input group. Keep it fixed until all pairs for that pass are handled.
Inner loop: “Start a pair at i; merge it; skip 2 * size positions to the next pair.” For size = 2, calls start at i = 0, then 4. The groups are [0, 2) with [2, 4), then [4, 6) with [6, 8).
Function call: merge(a, i, i + size, min(i + 2 * size, right)) passes the start, middle, and ending boundary. For size = 2 and i = 4, that becomes merge(a, 4, 6, 8).
Helper: compare the two unread fronts. Copy the smaller to c; move only the chosen input pointer. If one group runs out, copy from the other. Copy the completed c back into a starting at left.
Why it works: each input group is already sorted. Everything behind its front is at least that large. Therefore the smaller front is the smallest unread number in the pair. Each merge produces a larger sorted group.
The slide’s <= matters: for 8 items, size = 8 still runs. It merges the whole sorted array with an empty right group, copying it once more. size then becomes 16 and the condition fails. An optimized version could stop earlier; this animation follows the printed slide.
Cost: each pass handles O(n) items; there are O(log n) passes. Total time is Θ(n log n), with O(n) peak temporary space. Picking the left on equal keys preserves stability.
Compared with top-down: top-down recursively splits before merging. Bottom-up uses the two driver loops to schedule merges, starting with one-number sorted groups. The merge helper does the same job.
Why recursion works: a one-item range is sorted already. If both smaller halves return sorted, merging them gives a sorted larger range. Repeating this reasoning reaches the original full array.
Cost: there are Θ(log n) levels, with Θ(n) total merge work per level: Θ(n log n) time. The temporary vectors need O(n) peak space, with O(log n) additional call-stack depth.
This is Lecture 12: Merge Sort, 18 slides. It is one lecture, not the entire EECS 281 course. The source is the actual saved deck; the .txt file is only a text extraction.
Open the original 18-slide PDF · Open the PowerPoint
The interactive execution follows slides 10–11 and 14–15. The stack cards and live cost table are added teaching aids; they are not diagrams copied from the lecture.
Visualization controls inspired by Coddy’s merge-sort visualizer. This lesson keeps numbered array boxes and the EECS 281 slide implementations.
A visual guide · start with the moving pieces
Learn what the whole algorithm does, follow every check and move, and see why the final row is sorted. Start with Bubble sort, then compare the other methods using the same five numbers.
Go in lecture order. In the lab, press Step one time at a time. Orange values are being checked or moved; green values are settled. Use the quiz map below to see what each lecture contributes. Open a C++ example when the visual rule starts making sense.
Start here · one complete sorting job
Every lesson starts with 3, 1, 5, 2, 4. We finish only when the entire row is 1, 2, 3, 4, 5. A single swap or pass is one part of that job.
Compare means ask which number is smaller. Swap means exchange two numbers. A pass means one trip through the relevant part of the row.
Continue to the other sorting lessons ↓
Read the whole algorithm below, then choose Teach the full run. Pause and use Step to examine any decision.
What one round accomplishes:
When the entire algorithm stops:
Interactive example · all seven sorts
One click shows one check or one move. The sentence below tells you what comes next.
A comparison checks the numbers. A move slides the tiles. Replay move shows the same move again without advancing.
A sorted prefix or run is not necessarily in its final global position. Temporary copies and merge buffers are shown explicitly above.
Logarithmic: log₂(n) counts roughly how many times you can halve n before reaching one. For 8 items, that is 3 halvings: 8 → 4 → 2 → 1. Θ describes how work grows with input size; O gives an upper bound.
Notation: n means the number of items. Linear work grows like n; quadratic work grows like n². “Worst case” means the most work the algorithm needs for inputs of that size.
Stable means equal keys keep their original order. If two records have key 2, record 2A stays before record 2B when it started that way. In place means the algorithm rearranges the original array with only constant extra storage; recursive stack space still needs to be counted.
Your turn · no score
You can retry or revisit any lesson. A useful goal is explaining a move in your own words.
Lectures 8–12, the quiz topic map, and color-coded C++ examples. Use these after trying the small lessons, or jump straight to a topic you need.
Your Lab 05 quiz · what to study
You need Lectures 8, 10, 11, and 12, plus the binary-search and set-operation ideas from Lecture 9. Give 10 and 11 the closest attention.
Heap construction, in-place sorting, stability, worst-case time, and recognizing heap-sort snapshots.
Binary search inside insertion sort, combining two ranges, and scanning sorted data with a few pointers.
Bubble, selection, insertion, comparisons versus moves, nearly sorted input, and intermediate snapshots.
Valid partitions with equal keys, pivot choices, recursion, linked-list feasibility, and tracing the exact lecture partition code.
Sorted runs, singly-linked lists, sequential access, stability, and extra memory.
Counting sort: lower priority for this quiz’s listed questions. For the handwritten 0/1/2 problem, the single-pass and constant-memory restrictions point to three-way partitioning; ordinary counting sort does not meet those restrictions as written.
Some mathematical expressions disappeared from the pasted quiz. The lecture mapping is clear; those missing expressions still matter when choosing exact multiple-choice options.
Each C++ example is expandable. Follow the comments to see why each step exists. The examples use zero-based indexes: the first item is at index 0.
whilelanguage rule
inttype of value
swap()function call
0number
// whyexplanation
i in the vector.i by one. --i decreases it.left; stop before index right.These function examples assume #include <vector>, #include <algorithm>, #include <iterator>, and using namespace std;.
Five lectures, one question: how do items reach their places?
A heap is a tree-shaped way to organize an array. It does not fully sort values; it keeps enough order to make the largest easy to grab.
In a max heap, every parent is at least as large as its children. So the root is the maximum. The other values can be in many orders.
Heapsort moves the root to the far right, shrinks the heap, then lets the new root sink until the parent-child rule is restored.
Think: “Take the current biggest and park it at the end.” Repeat to fill the array from right to left.
Heap property · parent ≥ child
Stored level by level using the slides’ convention: root index 1; children of index k are 2*k and 2*k+1.
void heapsort(Item heap[], size_t n) { heapify(heap, n); for (size_t i = n; i >= 2; --i) { swap(heap[i], heap[1]); fixDown(heap, i - 1, 1); } // for i} // heapsort() void fixDown(Item heap[], size_t heapsize, size_t k) { while (2 * k <= heapsize) { size_t j = 2 * k; // start with left child if (j < heapsize && heap[j] < heap[j + 1]) ++j; if (heap[k] >= heap[j]) break; // heap restored swap(heap[k], heap[j]); k = j; // move down } // while} // fixDown()Worst-case time Θ(n log n) · extra space O(1) · not stable. This example uses zero-based indexing; the slides also show a one-based version.
This lecture is about what you can do after sorting. Sorted ranges let you search and combine without checking every pair.
To find 9 in [1, 3, 5, 7, 9, 11, 13], check the middle. The first middle is 7 at index 3. Since 9 is larger than 7, ignore that value and the left half. Check the middle of what remains, then narrow again.
Each check halves the range. That’s why binary search takes O(log n), instead of looking at all n values.
binary_search: does the value exist?lower_bound: first value ≥ target.upper_bound: first value > target.Try it · search a sorted row
A duplicate-key example
In [1, 3, 3, 3, 7], searching for 3 gives lower_bound = index 1 and upper_bound = index 4. The range between them contains all three copies.
Binary search needs sorted data and takes O(log n) checks.
Set operations walk both left to right, taking the smaller next value. Usually O(m+n).
vector<int> a = {1, 3, 3, 3, 7};int target = 3; // These all require sorted input.bool exists = binary_search(a.begin(), a.end(), target);auto low = lower_bound(a.begin(), a.end(), target); // index 1auto high = upper_bound(a.begin(), a.end(), target); // index 4 // Two sorted inputs can be merged into a sorted union.vector<int> b = {2, 3, 8};vector<int> result;set_union(a.begin(), a.end(), b.begin(), b.end(), back_inserter(result)); // result uses extra storage. For an O(1)-memory display problem,// scan the sorted inputs and print results as they are found.Bubble, selection, and insertion sort all use comparisons. They differ in what each pass accomplishes. Choose one and step through the same list below.
Look at adjacent values. If backward, swap them. In this course version, a small value bubbles left. An early-stop check lets already sorted input finish in one pass.
Scan the unsorted part, remember its smallest value, then swap it into the next open position. It still scans sorted input, but makes only O(n) swaps.
Treat the left side as sorted. Pick the next value and slide bigger values right until there’s a gap. Like arranging cards in your hand. Nearly sorted input needs little shifting.
Same input · after the first pass
Stable, in-place, useful on nearly sorted data. Worst case Θ(n²).
Θ(n²) comparisons even on sorted input, but only O(n) swaps.
void bubble(Item a[], size_t left, size_t right) { for (auto i = left; i < right - 1; ++i) { bool swapped = false; for (auto j = right - 1; j > i; --j) { if (a[j] < a[j - 1]) { swapped = true; swap(a[j - 1], a[j]); } // if } // for j if (!swapped) break; } // for i} // bubble()Adaptive version: Θ(n) on already sorted input; Θ(n²) worst case.
void selection(Item a[], size_t left, size_t right) { for (auto i = left; i < right - 1; ++i) { auto minIndex = i; for (auto j = i + 1; j < right; ++j) if (a[j] < a[minIndex]) minIndex = j; swap(a[i], a[minIndex]); } // for} // selection()Θ(n²) comparisons in every case; at most n−1 swaps.
void insertion(Item a[], size_t left, size_t right) { for (auto i = left + 1; i < right; ++i) { Item v = a[i]; auto j = i; while ((j > left) && (v < a[j - 1])) { a[j] = a[j - 1]; --j; } // while a[j] = v; } // for i} // insertion()Adaptive: Θ(n) best case; Θ(n²) worst case. Stable.
The slide version sorts full Student records by their category key. First count each category. Next turn frequencies into starting offsets. Finally scan the original records in order and copy each into its next free result position. There are no pairwise key comparisons.
For input [2, 1, 2, 0, 3, 2], the buckets below mean one zero, one one, three twos, and one three. Output becomes [0, 1, 2, 2, 2, 3].
The key range must be manageable. This version needs both counts and a separate output vector, so it is not the single-pass, constant-memory handwritten solution.
Count by key
These are pass 1 frequencies. Pass 2 changes them to starting offsets [0, 1, 2, 5]. Pass 3 copies original records into those category blocks.
void counting_sort(vector<Student> &students) { vector<Student> result(students.size()); vector<size_t> counts(CLASSES); size_t temp, sum = 0; for (auto &s : students) // Pass 1: count each category ++counts[s.classStanding()]; for (size_t i = 0; i < CLASSES; ++i) { // Pass 2: calculate start points temp = counts[i]; counts[i] = sum; sum += temp; } // for i for (auto &s : students) { // Pass 3: sort result[counts[s.classStanding()]] = s; ++counts[s.classStanding()]; } // for swap(students, result);} // counting_sort()O(n+k) time and space, where k is the number of possible keys. For stable sorting of records, use offsets and preserve the records when placing them into output.
Exact slide 14 code. This version does not stop early.
void bubble(Item a[], size_t left, size_t right) { for (auto i = left; i < right - 1; ++i) for (auto j = right - 1; j > i; --j) if (a[j] < a[j - 1]) swap(a[j - 1], a[j]);} // bubble()Exact slide 24 code. The added if skips a swap when minIndex already equals i.
void selection(Item a[], size_t left, size_t right) { for (auto i = left; i < right - 1; ++i) { auto minIndex = i; for (auto j = i + 1; j < right; ++j) if (a[j] < a[minIndex]) minIndex = j; if (minIndex != i) swap(a[i], a[minIndex]); } // for} // selection()Exact slide 31 code. The inner loop still visits every earlier neighbor.
void insertion(Item a[], size_t left, size_t right) { for (auto i = left + 1; i < right; ++i) for (auto j = i; j > left; --j) if (a[j] < a[j - 1]) swap(a[j - 1], a[j]);} // insertion()Exact slide 39 code. The minimum at a[left] stops the while loop, so it omits j > left.
void insertion(Item a[], size_t left, size_t right) { for (auto i = right - 1; i > left; --i) // find min item, if (a[i] < a[i - 1]) // put in a[left] swap(a[i - 1], a[i]); // as sentinel for (auto i = left + 2; i < right; ++i) { Item v = a[i]; auto j = i; // v is new item while (v < a[j - 1]) { // v in wrong spot a[j] = a[j - 1]; // copy/overwrite --j; } // while a[j] = v; } // for i} // insertion()Quicksort partitions the range: smaller values go left, larger values go right. Then it repeats on each side.
In the slide 9 partition, values left of pivot 4 are strictly less than 4; values right of it are at least 4. Those sides aren’t sorted yet, but sorting each finishes the job.
Balanced sides give Θ(n log n). If each pivot leaves one huge side and one tiny side, it can degrade to Θ(n²).
Think: Quicksort arranges during the split. No separate merge at the end.
Partition around pivot 4
One valid partition:
Groups are on the correct side; each group still needs sorting.
size_t partition(Item a[], size_t left, size_t right) { auto pivot = --right; // beware right == 0 while (true) { while (a[left] < a[pivot]) ++left; while (left < right && a[right - 1] >= a[pivot]) --right; if (left >= right) break; swap(a[left], a[right - 1]); } // while swap(a[left], a[pivot]); return left;} // partition()One partition call is O(n). It separates groups; it does not sort each group.
void quicksort(Item a[], size_t left, size_t right) { if (left + 1 >= right) return; auto pivot = partition(a, left, right); quicksort(a, left, pivot); quicksort(a, pivot + 1, right);} // quicksort()Average Θ(n log n), worst Θ(n²). Additional recursion stack: typically O(log n), potentially O(n) for this basic implementation.
Merge sort divides the array until each piece has one item. Then it merges small sorted pieces into larger sorted pieces.
Suppose sorted halves are [2, 5, 8] and [1, 4, 7]. Compare their front values, take the smaller one, and advance only that half.
Each merge pass touches every item once. There are about log₂(n) levels, so time is Θ(n log n), even if input is already sorted or reversed.
Think: Quicksort splits values into sides. Merge sort splits by position, then sorts while merging.
Merge two sorted halves
Take the smaller front each time:
Result: [1, 2, 4, 5, 7, 8]
void merge_sort(Item a[], size_t left, size_t right) { if (left + 1 >= right) // base case: 0 or 1 item(s) return; auto mid = left + (right - left) / 2; merge_sort(a, left, mid); // [left, mid) merge_sort(a, mid, right); // [mid, right) merge(a, left, mid, right);} // merge_sort() void merge(Item a[], size_t left, size_t mid, size_t right) { auto n = right - left; vector<Item> c(n); for (auto i = left, j = mid, k = 0; k < n; ++k) { if (i == mid) c[k] = a[j++]; else if (j == right) c[k] = a[i++]; else c[k] = (a[i] <= a[j]) ? a[i++] : a[j++]; } // for copy(begin(c), end(c), &a[left]);} // merge()Standard array version: Θ(n log n) time, O(n) auxiliary array. Equal keys keep their order because the left side wins ties.
Connect the lectures · quiz reasoning
Use the data structure, the memory limit, and the kind of work being counted to decide which algorithm fits.
Binary search can find an insertion position in O(log n), but inserting into an array may still shift O(n) items. Across all insertions, the worst-case shifting work is Θ(n²).
A merge can advance through list nodes without jumping to an arbitrary index. That makes merge sort a natural Θ(n log n) choice for linked lists. An array heap depends on fast parent/child indexing, which ordinary lists do not provide.
For a closest-sum pair, sort first, then keep one pointer at each end. A sum that is too small means moving the left pointer right; a sum that is too large means moving the right pointer left. The scan is linear. Count the sorting cost too.
If total extra space must be O(1), heapsort meets that array-sorting restriction. A basic recursive quicksort also needs stack space.
Sort both ranges, then compare their current items. Output the smaller unmatched item. Skip a match and advance both. Treat duplicate counts according to the question’s definition; set membership and multiset counts are different.
Merge sort forms sorted runs of sizes 1, 2, 4, … in its bottom-up version. Selection settles a minimum on each pass. Insertion grows a sorted prefix. Bubble moves an extreme value across neighbors. Heapsort first forms a heap, then grows a sorted suffix.
Sorting [2A, 1, 2B] stably gives [1, 2A, 2B]. It preserves which equal-key record came first. Stability does not mean a runtime guarantee. Standard bubble, insertion, and merge sort are stable; standard selection, heap, and in-place quicksort are not.
Balanced partitions give about log n levels of linear partitioning work. Repeated 0-versus-(n−1) splits give quadratic work. Sorted input alone does not determine quicksort’s time; the pivot rule and partition behavior matter.
For general partition questions, equal keys may appear on either side if no stricter rule is specified. When tracing supplied code, obey its exact < and >= conditions.
For comparison sorting, the best possible general worst-case growth is Ω(n log n) comparisons. Elementary selection sort makes Θ(n²) comparisons but only O(n) swaps. If copying large records dominates, that difference matters.
Maintain four regions: zeros, ones, unknown values, twos. The scan pointer looks at the first unknown item. A 0 goes to the left region; a 2 goes to the right region; a 1 is already in its region. After swapping a 2 rightward, inspect the incoming value before advancing the scan.
This is the three-way partition idea. The quicksort trace earlier uses the separate two-way lecture implementation because that is the one quoted in the quiz.
Heap: remove the biggest. Bubble: swap neighbors. Selection: find next smallest. Insertion: slide into sorted part. Quick: partition around a pivot. Merge: weave sorted halves. Binary search: halve a sorted range.