ITC 120 › Final › Sorting
Sorting algorithms · Lesson 2 of 2
Advanced Sorting: Merge, Quick, and Heap Sort
This lesson teaches:AlgorithmThree faster ways to sort big data, a quick review of the tool they need (recursion), and how to choose among them.
By the end, you can:
- explain divide and conquer, and use recursion (base case, recursive case, call stack) to follow the calls of merge sort and quick sort;
- trace merge sort, quick sort, and heap sort by hand on a small array;
- read the three sorts in Java, find common bugs, and complete the merge step;
- compare all six sorts by work (about n × n / 2 versus about n × log n comparisons), extra memory, and stability, and choose one for a situation.
You need: the Basic sorting lesson (swap, pass, comparisons, stability), the Arrays and Recursion lessons, the Programming refresher (methods, loops, &&), and scratch paper.
Part 1 · The idea
Why we need faster sorts
Bubble, selection, and insertion sort are easy, but slow on big data: their work grows like a staircase , about n × n / 2 comparisons. For the 1,000,000 transactions an e-wallet like GCash handles in a short time, that is about 500 billion comparisons. The sorts in this lesson need about 20 million.
How? Think of a teacher with a big pile of quiz papers and some helpful students:
Divide and conquer
Divide and conquer is a way to solve a problem in three steps: divide it into smaller problems of the same kind, solve each smaller one (the same way), and combine their answers into the answer for the whole.
Merge sort and quick sort are divide-and-conquer sorts. Heap sort uses a different trick: it keeps the values in a special shape, a heap, where the biggest is always easy to find. Here they are at a glance:
| Sort | Big idea | Real-life picture |
|---|---|---|
| Merge sort | split in half until each part has one value, then merge sorted parts | the team of helpers above |
| Quick sort | pick a pivot; smaller values go left, the rest go right; repeat on each side | two tables: "below 20" and "20 or more" |
| Heap sort | build a heap (biggest on top), then take the biggest out again and again | a sports fest bracket: the champion is on top |
Where you see fast sorting in IT
- Java's own
Arrays.sortis a quick sort for numbers and a merge sort for objects (Part 8). - Databases sort millions of records for "ORDER BY" and use merge sort when the data does not fit in memory.
- Online shops and social apps rank thousands of items by price or by time in a fraction of a second.
- Priority lists, like a hospital queue where the most urgent patient goes first, use a heap.
Part 2 · A quick review
Recursion: a method that calls itself
Look at the helpers again. Every helper follows the same rule, only on a smaller pile. In code, that is recursion, which you learned in the Recursion lesson: a method that calls itself with a smaller job. Merge sort and quick sort are written this way. Here is what you need from that lesson. Every recursive method has two parts:
| Part | Meaning | With the papers |
|---|---|---|
| base case | a job so small that the method answers it at once, without calling itself; this is what makes it stop | "Only 1 paper? It is sorted. Give it back." |
| recursive case | the method calls itself on a smaller job | "Split it; a helper takes each half." |
Java keeps track of the calls on the call stack: a new call goes on top, and the call under it waits until the new one is done.
One small Java fact you will need for mid: dividing two ints drops the decimal part.
System.out.println(5 / 2); // 2 (int / int drops the .5)
System.out.println(1 / 2); // 0 Need a refresher? Watch a recursive method on the call stack (optional)
Here is a tiny recursive method on numbers. It prints a number, then calls itself with half of that number, until the number is 0. Green is the base case and blue is the recursive case:
■ base case ■ recursive case
1static void printHalves(int n) {
2 if (n == 0) {
3 return; // base case: stop
4 }
5 System.out.print(n + " ");
6 printHalves(n / 2); // recursive case: a smaller job
7} Call stack
The call stack is Java's list of method calls that have started but not finished. A new call goes on top; the call under it waits. When the top call finishes, it leaves, and Java goes back to the call below it, right where it left off. (It works like the stack of plates in the Stacks lesson.)
Walkthrough: printHalves(20)
Use Next to watch the calls pile up on the stack, then leave it.
main calls printHalves(20). Java starts a new call of the method with its own n, and puts it on the call stack.
1static void printHalves(int n) { 2 if (n == 0) { 3 return; // base case: stop 4 } 5 System.out.print(n + " "); 6 printHalves(n / 2); // recursive case: a smaller job 7} Mistake: no base case
Situation: a student leaves out the if:
static void printHalves(int n) {
System.out.print(n + " ");
printHalves(n / 2); // no base case: it never stops
} printHalves(0) calls printHalves(0) forever.What goes wrong: every call makes another call, and none of them finishes. The call stack fills up memory, and Java stops the program with a StackOverflowError.
Fix: always write the base case first, and make sure each recursive call has a smaller job that reaches it.
Part 3 · Sort 1
Merge sort: split, then merge
Merge sort
Merge sort splits the array into two halves, sorts each half with merge sort (recursion), and then merges the two sorted halves into one sorted array. A part with one value is the base case: it is already sorted.
The key step: merging two sorted piles
Two helpers hand back two sorted piles, face up: 10, 14, 29 and 13, 37. To make one sorted pile, you only ever look at the top paper of each pile: take the smaller one, and repeat. When one pile is empty, put the rest of the other pile at the end. Those are the leftovers, and they need no more comparisons.
Merging 10, 14, 29 with 13, 37, by hand
Use Next to go through the steps.
Now merge the two sorted halves, 10, 14, 29 and 13, 37, into a new array temp. i points at the front of the left half, j at the front of the right half.
Merging two piles with a total of n values takes at most n - 1 comparisons. That is the cheap part.
The whole sort by hand
On paper, write the split rows going down until every part has one value, then the merge rows going back up. When a part has an odd number of values, the left half gets the extra one (like mid in binary search: (low + high) / 2, rounded down).
Merge sort on 29, 10, 14, 37, 13, by hand
Use Next to go through the steps.
Start with the whole array: 29, 10, 14, 37, 13.
Merge sort in Java
The code has two methods. mergeSort does the splitting with recursion; merge does the merging. Both work on a part of the array, from index low to index high, so the first call is mergeSort(arr, 0, arr.length - 1). Both are static void .
Walkthrough: mergeSort on 29, 10, 14
Use Next to follow the calls. The array is short so you can see every call.
low = 0high = 2
To see the recursion, take a short array: 29, 10, 14. main calls mergeSort(arr, 0, 2): sort index 0 to 2, the whole array.
1static void mergeSort(int[] arr, int low, int high) { 2 if (low >= high) { 3 return; // base case: 0 or 1 value 4 } 5 int mid = (low + high) / 2; 6 mergeSort(arr, low, mid); // sort the left half 7 mergeSort(arr, mid + 1, high); // sort the right half 8 merge(arr, low, mid, high); // merge the two sorted halves 9} Now the merge method, on the last merge of the five scores. It is long, but it is just the "two piles" steps from above, in code:
Walkthrough: merge(arr, 0, 2, 4) on 10, 14, 29 | 13, 37
Use Next to go step by step, or tap a line.
low = 0mid = 2high = 4
This is the last merge of the five scores. The left half (index 0 to 2) is already sorted: 10, 14, 29. So is the right half (index 3 to 4): 13, 37. merge must join them.
1static void merge(int[] arr, int low, int mid, int high) { 2 int[] temp = new int[high - low + 1]; 3 int i = low; // next value in the left half 4 int j = mid + 1; // next value in the right half 5 int k = 0; // next empty slot in temp 6 while (i <= mid && j <= high) { 7 if (arr[i] <= arr[j]) { 8 temp[k] = arr[i]; 9 i++;10 } else {11 temp[k] = arr[j];12 j++;13 }14 k++;15 }16 while (i <= mid) { // leftovers from the left half17 temp[k] = arr[i];18 i++;19 k++;20 }21 while (j <= high) { // leftovers from the right half22 temp[k] = arr[j];23 j++;24 k++;25 }26 for (k = 0; k < temp.length; k++) {27 arr[low + k] = temp[k]; // copy back into arr28 }29} Some lines are new. Here is how to read them aloud:
| Code | Read it as |
|---|---|
int[] temp = new int[high - low + 1]; | "Make a new, empty int array called temp, with one slot for each value from low to high." A new int array starts full of 0s. |
while (i <= mid && j <= high) | "While BOTH halves still have values." |
if (arr[i] <= arr[j]) | "If the left front is smaller or equal, take it." On a tie, the left one goes first. |
arr[low + k] = temp[k]; | "Copy slot k of temp back into the matching slot of arr." temp starts at 0, but the part of arr starts at low. |
Mistake: forgetting the leftovers
Situation: a student writes only the main loop and the copy-back loop:
while (i <= mid && j <= high) {
... // the main loop, as before
}
for (k = 0; k < temp.length; k++) { // the two leftover loops are missing
arr[low + k] = temp[k];
} What goes wrong: when one half runs out, the other half still has values. They never reach temp, so its last slot keeps its starting 0, and 37 is replaced by 0.
Fix: after the main loop, copy what is left of the left half, then what is left of the right half. Only one of the two loops will actually copy something.
One cost to remember: merge sort needs the extra array temp. For n values, that is extra memory for up to n values.
Part 4 · Sort 2
Quick sort: pick a pivot
Merge sort does the easy part first (splitting) and the work at the end (merging). Quick sort does it the other way around: the work comes first, so nothing needs merging at the end.
Quick sort, pivot, partition
Quick sort picks one value, the pivot, and partitions the array: it moves every value smaller than the pivot to the pivot's left and every other value to its right. The pivot is then in its final place. Quick sort then sorts the left part and the right part the same way (recursion). A part with 0 or 1 values is the base case.
There are many ways to pick a pivot. In this lesson, the pivot is always the last value of the part. By hand, partitioning goes like this:
- The pivot is the last value. Put a marker
iat the first index: the next small value goes there. - Walk
jfrom the first index to the one before the pivot. Each timearr[j]is smaller than the pivot, swap it into indexiand moveione step right. - At the end, swap the pivot into index
i. It is now in its final place. - Do the same for the part left of the pivot and the part right of it.
Step through it. Gold is the pivot, teal values are smaller than the pivot, blue ones are not, and green values are in their final place:
Quick sort on 29, 10, 14, 37, 13, by hand
Use Next to go through the steps.
Quick sort on 29, 10, 14, 37, 13. It picks one value, the pivot (here: the last value of the part), puts every smaller value to its left and every other value to its right, then sorts the two sides the same way.
Each pivot splits its part into "smaller" and "bigger", like a tree. On paper, you can draw it like this:
Quick sort in Java
quickSort is short, because partition does the work. Compare it with mergeSort: here the work (partition) comes before the two recursive calls.
1static void quickSort(int[] arr, int low, int high) {
2 if (low >= high) {
3 return; // base case: 0 or 1 value
4 }
5 int p = partition(arr, low, high); // the pivot's final index
6 quickSort(arr, low, p - 1); // sort the smaller values
7 quickSort(arr, p + 1, high); // sort the bigger values
8} The first call is quickSort(arr, 0, arr.length - 1). Here is the first partition of the five scores:
Walkthrough: partition(arr, 0, 4) on 29, 10, 14, 37, 13
Use Next to go step by step, or tap a line.
low = 0high = 4pivot = 13
quickSort calls partition(arr, 0, 4) on all five scores. Line 2: the pivot is the last value, arr[4] = 13.
1static int partition(int[] arr, int low, int high) { 2 int pivot = arr[high]; // the last value is the pivot 3 int i = low; // where the next smaller value goes 4 for (int j = low; j < high; j++) { 5 if (arr[j] < pivot) { 6 int temp = arr[i]; // swap arr[i] and arr[j] 7 arr[i] = arr[j]; 8 arr[j] = temp; 9 i++;10 }11 }12 int temp = arr[i]; // put the pivot in its place13 arr[i] = arr[high];14 arr[high] = temp;15 return i;16} partition changes the array and returns a value: the pivot's index, so quickSort knows where the two parts are.
Mistake: including the pivot again
Situation: a student writes the first recursive call with p instead of p - 1:
quickSort(arr, low, p); // should be p - 1
quickSort(arr, p + 1, high); What goes wrong: the pivot is already in place, but it is sorted again. On the part 10, 13, the pivot 13 stays at the end, so quickSort(arr, 0, 1) calls quickSort(arr, 0, 1): the same job, forever. The program ends with a StackOverflowError, just like a missing base case.
Fix: leave the pivot out: low to p - 1 and p + 1 to high. Each call must get a smaller job.
Quick sort's weak spot
Quick sort is fast when each pivot splits its part into two halves of about the same size. But what if the array is already sorted?
With our last-value pivot, a sorted array of 1,000 values takes 499,500 comparisons, the same as bubble sort. Real programs avoid this by picking the middle value or a random value as the pivot (and swapping it to the end first). Then a bad split is very unlikely.
Part 5 · Sort 3
Heap sort: the biggest on top
Selection sort finds the smallest value by checking every value, pass after pass. That is what makes it slow. Heap sort keeps the values in a shape where the biggest is always on top, like a sports fest bracket:
First: a tree of values
The bracket is a tree: values connected from the top down. You studied trees in the Trees lesson (midterm); for heap sort you only need these words:
Tree words
Each circle is a node (it holds one value). The top node is the root. A node directly below another is its child; the one above is its parent. A node with no children is a leaf. In a binary tree, every node has at most 2 children.
A heap is stored in a plain array. Read the array from left to right and fill the tree row by row, from left to right: index 0 is the root, index 1 and 2 are its children, and so on. No links are needed, because a simple rule finds every child:
| For the node at index i | Index | Check with index 1 |
|---|---|---|
| left child | 2 * i + 1 | 3 |
| right child | 2 * i + 2 | 4 |
| parent | (i - 1) / 2 (rounded down) | 0 |
Max-heap
A max-heap is a binary tree, filled row by row from the left, where every parent is greater than or equal to its children. So the biggest value is always at the root, index 0.
Heap sort by hand
The main move is sift down: a value that is too small for its spot swaps with its bigger child, again and again, until it is at least as big as its children (or has none). Heap sort has two steps:
- Build a max-heap. Sift down every parent, starting from the last parent (index n / 2 - 1) back to the root.
- Take the biggest out, again and again. Swap the root (the biggest) with the last value of the heap. That value is now in its final place, at the end, and leaves the heap. Sift the new root down to repair the heap. Repeat until the heap has one value.
It is selection sort's idea ("find the biggest, put it at the end"), but the heap finds the biggest with a few swaps along one path instead of a whole pass. Watch the tree and the array change together:
Heap sort on 29, 10, 14, 37, 13, by hand
Use Next to go through the steps.
Heap sort on 29, 10, 14, 37, 13. First, read the array as a tree: index 0 is the root, and the children of index i are at 2i + 1 and 2i + 2. Step 1 turns it into a max-heap (every parent at least as big as its children).
Heap sort in Java
heapSort follows the two steps. It uses no recursion: the sifting is a while loop.
1static void heapSort(int[] arr) {
2 int n = arr.length;
3 for (int i = n / 2 - 1; i >= 0; i--) {
4 siftDown(arr, i, n); // step 1: build a max-heap
5 }
6 for (int end = n - 1; end > 0; end--) {
7 int temp = arr[0]; // step 2: swap the root (biggest)
8 arr[0] = arr[end]; // with the last value in the heap
9 arr[end] = temp;
10 siftDown(arr, 0, end); // repair the smaller heap
11 }
12} Here is siftDown, right after 37 was swapped to the end (the heap is now index 0 to 3):
Walkthrough: siftDown(arr, 0, 4) on 13, 29, 14, 10 | 37
Use Next to go step by step, or tap a line.
i = 0size = 4
Heap sort just swapped 37 out to index 4. Now 13 sits at the root, and it is too small. siftDown(arr, 0, 4) repairs the heap: start at index 0, and only index 0 to 3 are still in the heap.
1static void siftDown(int[] arr, int i, int size) { 2 while (2 * i + 1 < size) { // while i has a left child 3 int child = 2 * i + 1; // the left child 4 if (child + 1 < size && arr[child + 1] > arr[child]) { 5 child++; // the right child is bigger 6 } 7 if (arr[i] >= arr[child]) { 8 return; // parent is big enough: stop 9 }10 int temp = arr[i]; // swap parent and bigger child11 arr[i] = arr[child];12 arr[child] = temp;13 i = child; // keep going down14 }15} The parameter size matters: values at index size and beyond are already sorted, so siftDown must not treat them as children.
Mistake: the wrong child formula
Situation: a student remembers "children at 2i and 2i + 1" from a book:
int child = 2 * i; // the left child is 2 * i + 1 What goes wrong: that formula is for heaps that start at index 1. In a Java array, the root is at index 0, and 2 × 0 = 0: the root would be its own child, and the tree is read wrong.
Fix: with the root at index 0, use 2 * i + 1 and 2 * i + 2. Check the formula on index 0: its children must be 1 and 2 (see the index picture ).
Good to know: some books store the heap starting at index 1 (children 2i and 2i + 1). Both are correct; just use the formula that matches where the root is. Heaps are also used outside sorting, as priority queues: Java's PriorityQueue is a heap.
Part 6 · Try it
The advanced sort lab
Run all three sorts on the same arrays. Try "8 values" and "already sorted" with each sort, and look at the comparison table at the end. On tiny arrays the advanced sorts win by only a little; Part 7 shows what happens on big ones.
Advanced sort lab
Pick an array, then a sort. Use Next step to watch it work. At the end, the lab compares the comparisons of all six sorts (basic and advanced) on the same array.
Arrays:
Part 7 · Why this matters
How much work do they save?
First, the five quiz scores, counted from the lab (and from running the Java code). Here are the comparisons:
| Array | Bubble | Insertion | Merge | Quick | Heap |
|---|---|---|---|---|---|
| quiz scores | 10 | 8 | 8 | 6 | 12 |
| already sorted | 10 | 4 | 7 | 10 | 12 |
| reversed | 10 | 10 | 5 | 10 | 10 |
With 5 values, there is not much difference. Heap sort even does more. The difference shows with more values. Here are the comparisons on random arrays (the same arrays for every sort):
| Values | Bubble / selection | Insertion | Merge | Quick | Heap |
|---|---|---|---|---|---|
| 10 | 45 | 29 | 24 | 21 | 41 |
| 100 | 4,950 | 2,502 | 534 | 713 | 1,040 |
| 1,000 | 499,500 | 249,068 | 8,703 | 10,303 | 16,900 |
| 10,000 | 49,995,000 | 25,122,311 | 120,378 | 176,467 | 235,383 |
For 10,000 values, bubble sort makes about 415 times as many comparisons as merge sort. Why so big a gap? Look at how merge sort splits:
log₂ n ("log base 2 of n")
log₂ n is the number of times you can halve n until you reach 1. log₂ 8 = 3 (8 → 4 → 2 → 1), log₂ 1,024 = 10, and log₂ 1,000,000 is about 20. It grows very slowly. (You met the same halving in binary search.)
Merge sort has about log₂ n levels, and the merges on each level touch all n values. So its work is about n × log₂ n. For 1,000,000 values: about 1,000,000 × 20 = 20 million, against about 500 billion for bubble sort. Quick sort (with good pivots) and heap sort grow the same way, only with different extra amounts.
Here is the full summary of the three sorts in this lesson:
| Merge sort | Quick sort | Heap sort | |
|---|---|---|---|
| Usual work | about n × log₂ n | about n × log₂ n, often the fastest in practice | about n × log₂ n |
| Worst case | still about n × log₂ n | about n × n / 2 (bad pivots, like sorted data with a last-value pivot) | still about n × log₂ n |
| Extra memory | a helper array of up to n values | none for values (only the call stack) | none |
| Stable? | yes | no | no |
Good to know: books write this growth as O(n log n) ("order n log n"). Merge sort and heap sort are O(n log n) even in the worst case; quick sort is O(n log n) on average and O(n²) in the worst case. You met O(n²) in Basic sorting (the staircase ).
Part 8 · Choosing
Stability, choosing, and Java's own sort
Which ones are stable?
Remember stable sorting ? Here is a short class list, already in A-to-Z order. Ana and Cris tie at 85. Here it is sorted by grade with each sort:
| Sort | Result (by grade) | Ana still before Cris? |
|---|---|---|
| start (A to Z) | Ana 85, Ben 80, Cris 85 | |
| Merge sort | Ben 80, Ana 85, Cris 85 | ✓ yes (stable) |
| Quick sort | Ben 80, Cris 85, Ana 85 | ✗ no |
| Heap sort | Ben 80, Cris 85, Ana 85 | ✗ no |
Merge sort is stable because on a tie it takes from the left half (<=). In quick sort, Ben (smaller than the pivot Cris) swaps with Ana, and then the pivot swaps into Ana's new spot, which throws Ana behind Cris. In heap sort, the root Ana is swapped straight to the end, past Cris. Long jumps like these can carry a value past an equal one.
Which sort when?
Here are all six sorts from the two lessons, by situation:
| Situation | Good choice |
|---|---|
| Big data, sorted in memory, fastest on average | Quick sort (with a middle or random pivot) |
| Records with ties whose order must stay (students by grade) | Merge sort (stable) |
| Data too big for memory, or a linked list | Merge sort (it only reads each part from front to back) |
| Speed must be guaranteed and there is no room for an extra array | Heap sort |
| Small or almost-sorted data | Insertion sort (fast sorts use it for small parts too) |
| A real Java program | Arrays.sort |
What Arrays.sort uses
Java's own sort, which you met in Basic sorting, picks a method by the kind of array:
int[] scores = {29, 10, 14, 37, 13};
Arrays.sort(scores); // ints: a fast quick sort (dual-pivot)
String[] names = {"Mia", "Ana", "Joy", "Ben"};
Arrays.sort(names); // objects: a stable merge sort For numbers (int[], double[]), Java uses a dual-pivot quick sort (two pivots, three parts). Equal numbers look the same, so stability does not matter. For objects (String[], records), it uses a stable merge sort called TimSort, so ties keep their order.
All three in one program
Here are the three sorts in one runnable file. Run it, and check that each line prints the sorted scores:
1import java.util.Arrays;
2
3public class Main {
4 static void mergeSort(int[] arr, int low, int high) {
5 if (low >= high) {
6 return;
7 }
8 int mid = (low + high) / 2;
9 mergeSort(arr, low, mid);
10 mergeSort(arr, mid + 1, high);
11 merge(arr, low, mid, high);
12 }
13
14 static void merge(int[] arr, int low, int mid, int high) {
15 int[] temp = new int[high - low + 1];
16 int i = low;
17 int j = mid + 1;
18 int k = 0;
19 while (i <= mid && j <= high) {
20 if (arr[i] <= arr[j]) {
21 temp[k] = arr[i];
22 i++;
23 } else {
24 temp[k] = arr[j];
25 j++;
26 }
27 k++;
28 }
29 while (i <= mid) {
30 temp[k] = arr[i];
31 i++;
32 k++;
33 }
34 while (j <= high) {
35 temp[k] = arr[j];
36 j++;
37 k++;
38 }
39 for (k = 0; k < temp.length; k++) {
40 arr[low + k] = temp[k];
41 }
42 }
43
44 static void quickSort(int[] arr, int low, int high) {
45 if (low >= high) {
46 return;
47 }
48 int p = partition(arr, low, high);
49 quickSort(arr, low, p - 1);
50 quickSort(arr, p + 1, high);
51 }
52
53 static int partition(int[] arr, int low, int high) {
54 int pivot = arr[high];
55 int i = low;
56 for (int j = low; j < high; j++) {
57 if (arr[j] < pivot) {
58 int temp = arr[i];
59 arr[i] = arr[j];
60 arr[j] = temp;
61 i++;
62 }
63 }
64 int temp = arr[i];
65 arr[i] = arr[high];
66 arr[high] = temp;
67 return i;
68 }
69
70 static void heapSort(int[] arr) {
71 int n = arr.length;
72 for (int i = n / 2 - 1; i >= 0; i--) {
73 siftDown(arr, i, n);
74 }
75 for (int end = n - 1; end > 0; end--) {
76 int temp = arr[0];
77 arr[0] = arr[end];
78 arr[end] = temp;
79 siftDown(arr, 0, end);
80 }
81 }
82
83 static void siftDown(int[] arr, int i, int size) {
84 while (2 * i + 1 < size) {
85 int child = 2 * i + 1;
86 if (child + 1 < size && arr[child + 1] > arr[child]) {
87 child++;
88 }
89 if (arr[i] >= arr[child]) {
90 return;
91 }
92 int temp = arr[i];
93 arr[i] = arr[child];
94 arr[child] = temp;
95 i = child;
96 }
97 }
98
99 public static void main(String[] args) {
100 int[] a = {29, 10, 14, 37, 13};
101 mergeSort(a, 0, a.length - 1);
102 System.out.println(Arrays.toString(a)); // [10, 13, 14, 29, 37]
103
104 int[] b = {29, 10, 14, 37, 13};
105 quickSort(b, 0, b.length - 1);
106 System.out.println(Arrays.toString(b)); // [10, 13, 14, 29, 37]
107
108 int[] c = {29, 10, 14, 37, 13};
109 heapSort(c);
110 System.out.println(Arrays.toString(c)); // [10, 13, 14, 29, 37]
111 }
112} ▶ This program runs as is. Try it in the Programiz online compiler: tap Copy, paste it there, and press Run.
Part 9 · Practice
Practice
Work these out on paper first, then check the answers. Use the array {8, 3, 5, 1, 9, 2} for problems 1 to 3.
- Merge sort. Draw the split rows and the merge rows. How many comparisons does the last merge make?
- Quick sort. Do the first partition (pivot = last value). Write the array after it, and the pivot's index.
- Heap sort. Build the max-heap. Write the array after step 1, and draw it as a tree.
- Recursion. What does
printHalves(13)print (the method from Part 2 )? How many calls are made in total? - Which sort? (a) A registrar sorts 30,000 student records by course, and students in the same course must stay in ID order. (b) A small device has almost no free memory but must sort 50,000 sensor readings with guaranteed speed.
Show the answers
For self-study, not for grading.
1. Here are the rows. The last merge joins 3, 5, 8 with 1, 2, 9: 3 vs 1 (take 1), 3 vs 2 (take 2), 3 vs 9 (take 3), 5 vs 9 (take 5), 8 vs 9 (take 8), then 9 is a leftover: 5 comparisons.
2. Pivot 2. Only 1 is smaller: it swaps into index 0 (with 8). Then the pivot swaps into index 1. The array is 1, 2, 5, 8, 9, 3 and the pivot index is 1 (5 comparisons).
3. Start at the last parent, index 2 (5): its only child is 2, so nothing moves. Index 1 (3): its children are 1 and 9; swap with 9. Index 0 (8): its children are 9 and 5; swap with 9, then 8 is bigger than its new children 1 and 3, so it stops. The max-heap is 9, 8, 5, 1, 3, 2.
4. It prints 13 6 3 1. The calls are printHalves(13), (6), (3), (1), and (0), the base case: 5 calls.
5. (a) Merge sort, because it is stable (or Arrays.sort on objects, which is a stable merge sort). (b) Heap sort: about n × log₂ n even in the worst case, and no extra array.
Coding task: finish merge
The merge method below is missing its two leftover loops. Write them (about 10 short lines), then run the file. All three lines should match the expected output.
1import java.util.Arrays;
2
3public class Main {
4 static void merge(int[] arr, int low, int mid, int high) {
5 int[] temp = new int[high - low + 1];
6 int i = low;
7 int j = mid + 1;
8 int k = 0;
9 while (i <= mid && j <= high) {
10 if (arr[i] <= arr[j]) {
11 temp[k] = arr[i];
12 i++;
13 } else {
14 temp[k] = arr[j];
15 j++;
16 }
17 k++;
18 }
19 // YOUR CODE: copy the leftovers of the left half into temp.
20
21 // YOUR CODE: copy the leftovers of the right half into temp.
22
23 for (k = 0; k < temp.length; k++) {
24 arr[low + k] = temp[k];
25 }
26 }
27
28 public static void main(String[] args) {
29 int[] a = {10, 14, 29, 13, 37};
30 merge(a, 0, 2, 4);
31 System.out.println(Arrays.toString(a)); // expected: [10, 13, 14, 29, 37]
32 int[] b = {40, 50, 60, 1, 2};
33 merge(b, 0, 2, 4);
34 System.out.println(Arrays.toString(b)); // expected: [1, 2, 40, 50, 60]
35 int[] c = {7, 30};
36 merge(c, 0, 0, 1);
37 System.out.println(Arrays.toString(c)); // expected: [7, 30]
38 }
39} ▶ This program runs as is. Try it in the Programiz online compiler: tap Copy, paste it there, and press Run.
Show one possible answer
while (i <= mid) { // leftovers from the left half
temp[k] = arr[i];
i++;
k++;
}
while (j <= high) { // leftovers from the right half
temp[k] = arr[j];
j++;
k++;
} Each test needs a different loop. In test a, the left half runs out first, so 37 is a leftover of the right half. In test b, 1 and 2 go first, so 40, 50, 60 are leftovers of the left half. Test c has one value on each side.
Part 10 · Check yourself
Self-check
Not graded. Tap an answer to see if you got it, and read why.
-
What does every recursive method need so that it stops?
The base case is the situation where the method returns without calling itself again. Without it, the calls never stop and Java throws a
StackOverflowError. -
Merge sort merges
3, 8with1, 9. What is the result, and how many comparisons does the merge make?3 vs 1: take 1. 3 vs 9: take 3. 8 vs 9: take 8. The left half is used up, so 9 is a leftover: copied with no comparison. 3 comparisons.
-
Quick sort (pivot = last value) partitions
{7, 2, 9, 4}. Where does the pivot 4 end up?Only 2 is smaller than 4, so 2 goes to index 0 and the pivot 4 is swapped into index 1. The result is 2, 4, 9, 7: 9 and 7 are on the right, not sorted yet.
-
In a max-heap stored in an array, where are the children of index 2?
Children of index i are at 2i + 1 and 2i + 2: 2 × 2 + 1 = 5 and 2 × 2 + 2 = 6.
-
Which sort needs an extra array as big as the data?
Merge sort merges into
temp, a helper array. Quick sort and heap sort move values around inside the same array (in place). -
When is our quick sort (pivot = last value) at its slowest?
On sorted data, the last value is always the biggest, so one side of every partition is empty. That gives about n × n / 2 comparisons, like bubble sort. A middle or random pivot avoids this.
-
Which of the three advanced sorts is stable?
Merge sort takes from the LEFT half on a tie (
<=), so equal values keep their order. Quick sort and heap sort swap values over long distances, which can carry a value past an equal one. -
About how many comparisons does merge sort need for 1,000,000 values?
1,000,000 values halve about 20 times (2 to the 20th is about a million), and each level touches every value: about 1,000,000 × 20. Bubble sort needs about 500 billion.
Score: 0 / 8
Key takeaways
- Recursion: a method calls itself on a smaller job. The base case stops it; the call stack keeps track of the calls.
- Merge sort: split in half until one value is left, then merge sorted parts by taking the smaller front value. Stable, but needs an extra array.
- Quick sort: partition around a pivot (smaller left, the rest right); the pivot is then in its final place. Fast in practice, but slow with bad pivots.
- Heap sort: build a max-heap (children of i at 2i + 1 and 2i + 2), then swap the biggest to the end and sift down, again and again. No extra array, not stable.
- All three need about n × log₂ n comparisons (quick sort: usually), far fewer than the basic sorts' n × n / 2 on big data.
- In real programs, use
Arrays.sort: quick sort for numbers, stable merge sort for objects.
Next lesson: Hashing and hash tables, the last topic of the course.
References
- R. Sedgewick and K. Wayne, Algorithms, 4th ed. Addison-Wesley, 2011, Sections 2.2 (mergesort), 2.3 (quicksort), and 2.4 (heapsort). algs4.cs.princeton.edu
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms, 3rd ed. MIT Press, 2009, Chapters 2.3, 6, and 7 (the last-value pivot partition is from Chapter 7).
- A. Bhargava, Grokking Algorithms. Manning, 2016, Chapters 3 (recursion) and 4 (divide and conquer, quicksort).
- Oracle, "Class Arrays," Java SE 21 API (sort: dual-pivot quicksort for primitives; stable, adaptive mergesort for objects). docs.oracle.com
- ITC 120 Course Syllabus (Advanced sorting: implement merge, quick, and heap sort; evaluate time and space; assess when each is most effective), Davao Oriental State University.