ma'am audrey's lessons

ITC 120 › Final › Sorting

Sorting algorithms · Lesson 2 of 2

Advanced Sorting: Merge, Quick, and Heap Sort

This lesson teaches:Algorithm

Three 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:

  1. explain divide and conquer, and use recursion (base case, recursive case, call stack) to follow the calls of merge sort and quick sort;
  2. trace merge sort, quick sort, and heap sort by hand on a small array;
  3. read the three sorts in Java, find common bugs, and complete the merge step;
  4. 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 , 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:

Sorting 8 quiz papers as a teamThe SAME rule for everyone:1. Only 1 paper? It is sorted. Give it back.2. More? Split it in half; a helper takes each half.3. Merge the two sorted halves you get back.8442222TeacherHelpers with 2 papers split once more, into single papers.split, pass a half onsorted pile back
Nobody sorts the whole pile alone. Each person splits the job, and small piles are easy.

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:

SortBig ideaReal-life picture
Merge sortsplit in half until each part has one value, then merge sorted partsthe team of helpers above
Quick sortpick a pivot; smaller values go left, the rest go right; repeat on each sidetwo tables: "below 20" and "20 or more"
Heap sortbuild a heap (biggest on top), then take the biggest out again and againa sports fest bracket: the champion is on top

Where you see fast sorting in IT

  • Java's own Arrays.sort is 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:

PartMeaningWith the papers
base casea 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 casethe 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.

Call stack(newest call on top)(empty: every call has finished)Output:

main calls printHalves(20). Java starts a new call of the method with its own n, and puts it on the call stack.

Step 1 of 15
 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
}
Call stack(newest call on top)printHalves(0)runningprintHalves(0)waitingprintHalves(1)waitingprintHalves(2)waitingprintHalves(5)waitingprintHalves(10)waitingprintHalves(20)waiting… thousands more, until StackOverflowErrorOutput: 20 10 5 2 1 0 0 0 0 …
0 / 2 is 0, so 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.

merge two pilescomparisons: 0100141292133374arrleft halfright halfijtemp01234k

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.

Step 1 of 7

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.

Merge sort on paper: split down, then merge upstart2910143713

Start with the whole array: 29, 10, 14, 37, 13.

Step 1 of 7

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 .

Walkthrough: mergeSort on 29, 10, 14

Use Next to follow the calls. The array is short so you can see every call.

290101142arrCall stack (newest on top)mergeSort(0, 2)running

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.

Step 1 of 9
 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.

comparisons: 0100141292133374arrleft halfright halftemp01234

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.

Step 1 of 11
 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:

CodeRead 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];
}
10013114229304arrtemp1001311422930437 never reached temp; temp[4] was still 0
The main loop stops when the left half is used up, so 37 is never copied.

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.

Pick one paper as the pivot: 20The pile3112258154020pivot12815smaller than 2031254020 or more20its finalplaceThen do the same with each table, using a new pivot.The tables never need to be merged.
After one round, the pivot is in its final place, and each table is a smaller sorting job.

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:

  1. The pivot is the last value. Put a marker i at the first index: the next small value goes there.
  2. Walk j from the first index to the one before the pivot. Each time arr[j] is smaller than the pivot, swap it into index i and move i one step right.
  3. At the end, swap the pivot into index i. It is now in its final place.
  4. 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.

comparisons: 0swaps: 0290101142373134

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.

Step 1 of 15

Each pivot splits its part into "smaller" and "bigger", like a tree. On paper, you can draw it like this:

Each pivot splits its part in two2910143713pivot 13smallerbigger10one value13in place143729pivot 29142937Read the bottom from left to right:1013142937
Pivot 13, then pivot 29. Parts with one value need no work.

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.

comparisons: 0swaps: 0290101142373134working on index 0 to 4ipivot = 13

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.

Step 1 of 8
 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?

Already sorted + last value as pivot10131429375 left4 comparisons10131429374 left3 comparisons10131429373 left2 comparisons10131429372 left1 comparison10131429371 leftEach pivot is the biggest: the "bigger" side is empty,so each part is only one value shorter: 4 + 3 + 2 + 1.
Each partition only removes the pivot itself, so it does as much work as bubble sort.

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:

Barangay sports fest: arm wrestling8Ana5Ben9Cris6Dan8Ana9Cris9Cris, championNumbers = strength. Every winner sits abovethe ones they beat, so the strongest is on top.Take the champion out: a short replay finds the next one.
After the champion leaves, only the matches on the champion's path need a replay, not the whole fest.

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:

370291142103134arr370291142103134Parent at index i: children at 2i + 1 and 2i + 2index 1 (29): children at 2·1 + 1 = 3 and 2·1 + 2 = 4
The same five values as a tree (top) and as an array (bottom).
For the node at index iIndexCheck with index 1
left child2 * i + 13
right child2 * i + 24
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.

✓ a max-heap402531825✗ not a max-heap40253183030 > its parent 25each parent ≥ its children
Only parent and child are compared. 31 may be bigger than 25 on the other side.

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:

  1. Build a max-heap. Sift down every parent, starting from the last parent (index n / 2 - 1) back to the root.
  2. 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.

the array as a treecomparisons: 0swaps: 0290101142373134arr290101142373134

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

Step 1 of 22

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.

130291142103374arr130291142103374sorted part →

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.

Step 1 of 7
 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 ).

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:

Step 0 of 0
comparing now just moved left half / smaller right half / bigger pivot / sifting sorted or in place

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:

ArrayBubbleInsertionMergeQuickHeap
quiz scores1088612
already sorted10471012
reversed101051010

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):

ValuesBubble / selectionInsertionMergeQuickHeap
104529242141
1004,9502,5025347131,040
1,000499,500249,0688,70310,30316,900
10,00049,995,00025,122,311120,378176,467235,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:

8 values: 3 levels of halvingstartlevel 1level 2level 3Merging back up: 3 levels × 8 values each= about 24 steps. Doubling to 16 values adds only 1 level.
The number of levels is how many times you can halve n until you reach 1.

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 sortQuick sortHeap sort
Usual workabout n × log₂ nabout n × log₂ n, often the fastest in practiceabout n × log₂ n
Worst casestill about n × log₂ nabout n × n / 2 (bad pivots, like sorted data with a last-value pivot)still about n × log₂ n
Extra memorya helper array of up to n valuesnone for values (only the call stack)none
Stable?yesnono

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

Part 8 · Choosing

Stability, choosing, and Java's own sort

Which ones are stable?

Remember ? 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:

SortResult (by grade)Ana still before Cris?
start (A to Z)Ana 85, Ben 80, Cris 85
Merge sortBen 80, Ana 85, Cris 85✓ yes (stable)
Quick sortBen 80, Cris 85, Ana 85✗ no
Heap sortBen 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:

SituationGood choice
Big data, sorted in memory, fastest on averageQuick 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 listMerge sort (it only reads each part from front to back)
Speed must be guaranteed and there is no room for an extra arrayHeap sort
Small or almost-sorted dataInsertion sort (fast sorts use it for small parts too)
A real Java programArrays.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.

  1. Merge sort. Draw the split rows and the merge rows. How many comparisons does the last merge make?
  2. Quick sort. Do the first partition (pivot = last value). Write the array after it, and the pivot's index.
  3. Heap sort. Build the max-heap. Write the array after step 1, and draw it as a tree.
  4. Recursion. What does printHalves(13) print ( )? How many calls are made in total?
  5. 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.

Merge sort on 8, 3, 5, 1, 9, 2start835192split835192split835192split835192merge385192merge358129merge123589

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

comparisons: 5swaps: 2102152839435working on index 0 to 5ipivot = 2after the first partition

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.

comparisons: 7swaps: 2908152133425908152133425

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.

  1. What does every recursive method need so that it stops?

  2. Merge sort merges 3, 8 with 1, 9. What is the result, and how many comparisons does the merge make?

  3. Quick sort (pivot = last value) partitions {7, 2, 9, 4}. Where does the pivot 4 end up?

  4. In a max-heap stored in an array, where are the children of index 2?

  5. Which sort needs an extra array as big as the data?

  6. When is our quick sort (pivot = last value) at its slowest?

  7. Which of the three advanced sorts is stable?

  8. About how many comparisons does merge sort need for 1,000,000 values?

Score: 0 / 8

Key takeaways

  1. Recursion: a method calls itself on a smaller job. The base case stops it; the call stack keeps track of the calls.
  2. 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.
  3. 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.
  4. 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.
  5. All three need about n × log₂ n comparisons (quick sort: usually), far fewer than the basic sorts' n × n / 2 on big data.
  6. 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

  1. 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
  2. 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).
  3. A. Bhargava, Grokking Algorithms. Manning, 2016, Chapters 3 (recursion) and 4 (divide and conquer, quicksort).
  4. Oracle, "Class Arrays," Java SE 21 API (sort: dual-pivot quicksort for primitives; stable, adaptive mergesort for objects). docs.oracle.com
  5. 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.