Lesson 42Phase 4: Collections and algorithms
Searching and sorting algorithms
Linear and binary search, then selection, insertion and merge sort traced step by step, and what Java's own sort does.
By BasithUpdated 60 min
- linear search
- binary search
- selection sort
- insertion sort
- merge sort
- divide and conquer
Two jobs turn up in almost every program: finding one item in a pile of data, and putting the pile in order. The contact list on your phone does both, keeping names in alphabetical order and jumping to the one you type. This lesson builds the classic algorithms for each job by hand, traces them one step at a time, and measures them with Big-O. Then it opens up the sorts that come with Java, which are the ones you will call in real code.
- Search an array with linear search, and say when it is the only choice.
- Run binary search on a sorted array, following
lo,midandhi, and explain why the data must be sorted. - Read what
Arrays.binarySearchreturns, including the negative number it gives for a missing value. - Trace selection sort, insertion sort and merge sort by hand, and give the Big-O of each.
- Say what makes a sort stable, and which of these sorts are.
- Explain what
Arrays.sortandList.sortrun inside, and why you call them instead of writing your own.
Linear search: check every item#
Start with the plainest way to find something. A teacher has a row of test scores in no particular order and wants to know where the 88 is. The only sure method is to look at the first score, then the next, then the next, until 88 turns up or the scores run out. That is linear search: walk the array from the front and compare each item with the target, the value you are looking for.
The method below returns the index where it finds the target, or -1 if the target is not there. No real index is negative, so -1 can never be mistaken for a position. Press Run and watch the arrow i walk along the array.
public class Main { public static void main(String[] args) { int[] scores = {72, 95, 61, 88, 79, 54}; System.out.println(indexOf(scores, 88)); System.out.println(indexOf(scores, 100)); } static int indexOf(int[] values, int target) { for (int i = 0; i < values.length; i++) { if (values[i] == target) { return i; } } return -1; }}This prints 3 and then -1. Finding 88 took four comparisons, because 88 sits at index 3. Proving that 100 is missing took six: the loop had to check every score before it could give up.
In the worst case linear search compares all n items, so it is O(n): double the number of scores and you double the worst case. In return it asks nothing of the data. The items can be in any order, and the search can start straight away. That makes it the right tool for small arrays, for data you search only once, and for data in no order at all. ArrayList's indexOf method works exactly this way, -1 and all.
Binary search: halve what is left#
Try a guessing game with a friend. They think of a whole number from 1 to 100 and answer each guess with “higher” or “lower”. Guessing 1, 2, 3 and so on could take 100 guesses. Guess 50 first, though, and whatever they answer, half the numbers are gone. Keep guessing the middle of what is left and you never need more than 7 guesses.
Binary search plays that game on a sorted array. Two variables, lo and hi, mark the stretch of the array where the target could still be. Each round looks at the item halfway between them, at index mid, and compares it with the target:
- If the middle item is the target, the search is over.
- If the middle item is smaller than the target, the target can only be to its right, so
lomoves tomid + 1. - If the middle item is bigger, the target can only be to its left, so
himoves tomid - 1.
When lo passes hi, nothing is left to search, and the target is not in the array. Here it is on a sorted row of locker numbers. Open the trace: the arrows lo and hi under the array close in, and the call stack shows mid jumping between them.
public class Main { public static void main(String[] args) { int[] lockers = {3, 8, 12, 17, 21, 25, 30, 34, 41}; System.out.println(search(lockers, 30)); System.out.println(search(lockers, 5)); } static int search(int[] sorted, int target) { int lo = 0; int hi = sorted.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (sorted[mid] == target) { return mid; } else if (sorted[mid] < target) { lo = mid + 1; } else { hi = mid - 1; } } return -1; }}It prints 6, then -1. Finding 30 takes two looks. The middle of all nine lockers is 21, at index 4, which is too small, so lo jumps to 5. The middle of indexes 5 to 8 is index 6, which holds 30. Searching for 5 takes three looks, and then hi drops below lo and the method gives up with -1.
Now drive it yourself. Pick a target, press Next look, and watch the count of numbers still in play shrink. Try a number that is missing, too.
Or tap any number in the row to search for it.
Looking for 64. All 16 numbers are in play: lo is 0 and hi is 15. Press “Next look”.
Key ideaBinary search throws away half of what is left with every look. That makes it O(log n): even a million sorted items need at most 20 looks. The price is that the data must be sorted first.
Why the data must be sorted#
Every step rests on one assumption: everything left of the middle is smaller and everything right of it is bigger. That is what lets binary search drop half the array without looking at it. When the data is not sorted, the assumption is false, and the search can throw away the half that holds the answer. Here is the same method on numbers in no order.
public class Main { public static void main(String[] args) { int[] mixed = {30, 3, 41, 8, 21, 12, 34}; System.out.println(search(mixed, 30)); } static int search(int[] sorted, int target) { int lo = 0; int hi = sorted.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (sorted[mid] == target) { return mid; } else if (sorted[mid] < target) { lo = mid + 1; } else { hi = mid - 1; } } return -1; }}30 is right there at index 0, yet the method prints -1. The first look lands on 8, in the middle. 8 is smaller than 30, so in a sorted array 30 would have to be further right, and the search drops the whole left side, 30 included. Nothing crashes and no warning appears; the method simply returns a wrong answer.
How many looks?#
Each look cuts the part still in play roughly in half, so the number of looks is about the number of times you can halve n before one item is left. That count is log n, the logarithm from the Big-O lesson, and it grows very slowly:
| Sorted items | Linear search, worst case | Binary search, worst case |
|---|---|---|
| 16 | 16 looks | 5 looks |
| 1,000 | 1,000 looks | 10 looks |
| 1,000,000 | 1,000,000 looks | 20 looks |
| 1,000,000,000 | 1,000,000,000 looks | 30 looks |
Going from a thousand items to a million makes the worst case of linear search a thousand times longer. Binary search needs 10 more looks.
Finding the middle safely#
Look closely at how the code finds the middle: it computes lo + (hi - lo) / 2. The obvious way is the average, (lo + hi) / 2, and for every array in this course the two give the same number. The difference is how big the numbers get along the way. An int holds whole numbers only up to 2,147,483,647. Add past that and the result wraps around to a large negative number, which is called overflow. Here are both formulas on two big indexes:
public class Main { public static void main(String[] args) { int lo = 1_500_000_000; int hi = 2_000_000_000; System.out.println((lo + hi) / 2); System.out.println(lo + (hi - lo) / 2); }}The average prints -397483648. lo + hi is 3.5 billion, too big for an int, so it wrapped around before the division ever happened. The safe version prints 1750000000, the true middle. It never adds the two indexes: it halves the distance between them and steps that far from lo, so every number along the way stays in range. (The _ marks in 1_500_000_000 only make the number easier to read. Java ignores them.)
Before moving on, rebuild binary search yourself.
Tap a line, then tap where it should go. Or use the arrows.
Java's binary search: Arrays.binarySearch#
You will rarely need to write binary search, because java.util.Arrays already has one. Arrays.binarySearch(array, target)searches a sorted array and returns the target's index when it finds it. The interesting part is what comes back when the target is missing.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] lockers = {3, 8, 12, 17, 21, 25, 30, 34, 41}; System.out.println(Arrays.binarySearch(lockers, 30)); System.out.println(Arrays.binarySearch(lockers, 20)); int result = Arrays.binarySearch(lockers, 20); int insertAt = -result - 1; System.out.println("20 belongs at index " + insertAt); }}This prints 6, then -5, then 20 belongs at index 4. For 30, Java agrees with our method: index 6. For the missing 20, where our method would return -1, Java returns -5. That negative number carries extra information.
Reading a negative result#
When the target is missing, Java first works out the insertion point: the index where the target would go if you added it and kept the array sorted. That is the index of the first item bigger than the target, or the length of the array if every item is smaller. For 20, the first bigger item is 21, at index 4, so the insertion point is 4. Java then returns -(insertion point) - 1, which is -(4) - 1, or -5.
The extra minus one is there because an insertion point can be 0. If Java returned plain -(insertion point), a target that belongs at the very front would come back as 0, and 0already means “found at index 0”. Subtracting one more keeps every “missing” answer at -1 or below. So reading the result takes one check: 0 or more means found at that index, and anything negative means missing.
To get the insertion point back, undo the formula: -result - 1. The last lines of the program do that, which is useful when you want to add a value to sorted data and keep it sorted.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] shelf = {10, 20, 30, 40}; System.out.println(Arrays.binarySearch(shelf, 30)); System.out.println(Arrays.binarySearch(shelf, 25)); System.out.println(Arrays.binarySearch(shelf, 99)); }}Three searches on a sorted shelf of four numbers. Pick what the program prints, then run it.
- Lists get the same method:
Collections.binarySearch(list, target)searches a sortedListand answers the same way. - If the target appears more than once, Java does not promise which copy it finds.
- On data that is not sorted, the answer means nothing, as you saw above.
Sorting, and what stable means#
Binary search needs sorted data, so sorting comes next. People have invented dozens of sorting algorithms. This lesson traces three classics, each built on a different idea: selection sort, insertion sort and merge sort.
You have met one sort already: bubble sort, which keeps swapping side-by-side items that are out of order until none are left. It is the sort everybody hears about first. Donald Knuth, whose books are the standard reference on sorting, wrote that it has little going for it apart from its catchy name. Bubble sort is O(n²) and does more swapping than any sort in this lesson, so real code does not use it.
Stable and unstable sorts#
One more idea before the algorithms. Sometimes two items are equal by whatever you are sorting on. A sort that always keeps equal items in the order they started in is called stable. You met the word in Comparable, Comparator and sorting; here is what it looks like.
Picture a game's player list in alphabetical order: Ana, Ben, Cleo, Dev, Eli. You sort it by level, and Ana and Cleo are both on level 3. A stable sort keeps Ana in front of Cleo, so players on the same level stay alphabetical. An unstable sort is allowed to put Cleo first. With plain numbers you could never tell the difference, since one 3 looks exactly like another. With objects that carry more data, you can.
Selection sort: pick the smallest, again and again#
Suppose you are lining up friends by height. You scan the whole line, find the shortest person, and swap them into first place. Then you scan everyone except that first person, find the shortest of the rest, and swap them into second place. You keep going until you reach the end. That is selection sort: each pass selects the smallest item in the unsorted part of the array and swaps it to the front of that part.
As it runs, the array has two parts: a sorted part on the left that grows by one item each pass, and an unsorted part on the right that shrinks. Once an item joins the sorted part, it is in its final place and never moves again. Here are the passes on six heights:
| Pass | Smallest in the unsorted part | Sorted part | Unsorted part |
|---|---|---|---|
| 1 | 10, swapped with 29 | 10 | 29, 14, 37, 13, 22 |
| 2 | 13, swapped with 29 | 10, 13 | 14, 37, 29, 22 |
| 3 | 14, already in place | 10, 13, 14 | 37, 29, 22 |
| 4 | 22, swapped with 37 | 10, 13, 14, 22 | 29, 37 |
| 5 | 29, already in place | 10, 13, 14, 22, 29 | 37 |
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] heights = {29, 10, 14, 37, 13, 22}; selectionSort(heights); } static void selectionSort(int[] a) { for (int i = 0; i < a.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < a.length; j++) { if (a[j] < a[minIndex]) { minIndex = j; } } int temp = a[i]; a[i] = a[minIndex]; a[minIndex] = temp; System.out.println("pass " + (i + 1) + ": " + Arrays.toString(a)); } }}The output shows the array after each pass, matching the table. In the trace, i marks the front of the unsorted part and j scans the rest of it. minIndex remembers where the smallest value so far sits, so it holds an index rather than a value. The three lines after the inner loop are the same swap you used in bubble sort. In passes 3 and 5 the smallest item is already at the front, so it swaps with itself and nothing changes.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] a = {5, 2, 8, 1, 9}; int minIndex = 0; for (int j = 1; j < a.length; j++) { if (a[j] < a[minIndex]) { minIndex = j; } } int temp = a[0]; a[0] = a[minIndex]; a[minIndex] = temp; System.out.println(Arrays.toString(a)); }}This is the first pass of selection sort on its own. What does the array look like afterwards?
How fast is selection sort?#
Count the comparisons for those six heights. The first pass compares 5 pairs, the next 4, then 3, 2 and 1, which is 15 in all. For n items it is n(n - 1) / 2, about half of n², so selection sort is O(n²). It makes exactly that many comparisons on every input, even one that is already sorted, because the only way to be sure which item is smallest is to look at all of them.
Its one strength is that it moves very little. Each pass ends in a single swap, so the whole sort makes only n - 1 swaps. That helps when writing to memory is slow, but it rarely matters in everyday code.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] a = {4, 3, 2, 1}; for (int i = 0; i < a.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < a.length; j++) { if (a[j] < a[minIndex]) { minIndex = j; } } int temp = a[i]; a[i] = a[minIndex]; a[minIndex] = temp; System.out.println(Arrays.toString(a)); } }}Selection sort on an array that starts backwards, printing after every pass. Which output is right?
Is selection sort stable?#
No. Its long-distance swap can jump an item over an equal one. Here are three players sorted by level with exactly the selection sort above, now comparing levels.
public class Main { public static void main(String[] args) { Player[] players = { new Player("Ana", 3), new Player("Cleo", 3), new Player("Ben", 1) }; for (int i = 0; i < players.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < players.length; j++) { if (players[j].level() < players[minIndex].level()) { minIndex = j; } } Player temp = players[i]; players[i] = players[minIndex]; players[minIndex] = temp; } for (Player p : players) { System.out.println(p.level() + " " + p.name()); } }} record Player(String name, int level) {}It prints 1 Ben, 3 Cleo, 3 Ana. Ana starts in front of Cleo, and both are on level 3, yet Cleo comes out first. In the first pass the smallest level is Ben's, at the back. Swapping Ben to the front sends Ana to Ben's old place, behind Cleo, and nothing later moves her back. A stable sort would have kept Ana first.
Insertion sort: like sorting a hand of cards#
Think about how you sort playing cards as you pick them up one at a time. The cards already in your hand are in order. You take the next card, slide it left past every card bigger than it, and drop it into the gap. That is insertion sort: the left part of the array is a sorted hand, and each step inserts the next item into its place in that hand.
In code, sliding a card left takes a few lines. The new item waits in a variable called current. Every bigger item in the hand shifts one place to the right, which opens a gap. When the item on the left is not bigger, the gap is in the right spot, and current drops into it.
| Step | Pick up | Shifted right | Sorted hand after | Not looked at yet |
|---|---|---|---|---|
| 1 | 3 | 7 | 3, 7 | 9, 2, 8, 5 |
| 2 | 9 | nothing | 3, 7, 9 | 2, 8, 5 |
| 3 | 2 | 9, 7, 3 | 2, 3, 7, 9 | 8, 5 |
| 4 | 8 | 9 | 2, 3, 7, 8, 9 | 5 |
| 5 | 5 | 9, 8, 7 | 2, 3, 5, 7, 8, 9 | nothing |
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] cards = {7, 3, 9, 2, 8, 5}; insertionSort(cards); } static void insertionSort(int[] a) { for (int i = 1; i < a.length; i++) { int current = a[i]; int j = i - 1; while (j >= 0 && a[j] > current) { a[j + 1] = a[j]; j--; } a[j + 1] = current; System.out.println("place " + current + ": " + Arrays.toString(a)); } }}Watch j start just left of i and walk further left while it finds bigger cards, copying each of them one slot to the right. After the first shift the array briefly holds one value twice and seems to have lost the card you picked up. It has not: the card is safe in current, and the line after the loop writes it into the gap. The output shows the array after each card is placed, matching the table.
How fast is insertion sort?#
It depends on the input, which sets insertion sort apart from selection sort. On a backwards array every new card slides all the way to the front: 1 shift, then 2, then 3, up to n - 1, which adds up to about n² / 2. That is O(n²). On an array that is already sorted, each new card is compared once with the card to its left, found to be in place, and left alone: n - 1 comparisons and no shifts at all, which is O(n).
Real data is often nearly sorted, such as a list that was sorted yesterday and has had a few entries added since, and insertion sort gets through that kind of input in close to O(n) time. Its loop is also so simple that it beats cleverer sorts on small arrays. Those two strengths are why the sorts built into Java hand small pieces of work to insertion sort, as you will see at the end of this lesson.
Is insertion sort stable?#
Yes. A card only slides past cards that are strictly bigger, because the loop tests a[j] > current. It stops at an equal card, so equal items never pass each other. Change that > to >= and the array still ends up sorted, but equal items now hop over each other and the sort is no longer stable.
insertionSort so it puts city names into alphabetical order, using insertion sort. main and check are done: they sort five arrays, including an empty one and one with repeated names, and print each result.Merge sort: divide and conquer#
Selection sort and insertion sort both inch through the array one item at a time, and both are O(n²) in the worst case. Merge sort reaches O(n log n) with a different plan, called divide and conquer: split a problem into smaller copies of itself, solve the copies, then combine their answers. If that sounds like recursion, it is. Merge sort is a recursive method, and its plan has four steps:
- 1If the array has 0 or 1 items, it is already sorted, so stop. This is the base case.
- 2Otherwise, split it into a left half and a right half.
- 3Sort each half by calling merge sort on it.
- 4Merge the two sorted halves into one sorted array.
Step 3 can look like cheating: to sort an array, you sort its halves. It works for the reason all recursion works. Each call gets a smaller array, the splitting ends at arrays of one item, and those are sorted already. Every comparison between items happens in step 4. Here is the whole process on eight numbers:
| Step | The pieces |
|---|---|
| Start | [52, 19, 36, 7, 44, 23, 61, 15] |
| Split | [52, 19, 36, 7] [44, 23, 61, 15] |
| Split | [52, 19] [36, 7] [44, 23] [61, 15] |
| Split | [52] [19] [36] [7] [44] [23] [61] [15] |
| Merge | [19, 52] [7, 36] [23, 44] [15, 61] |
| Merge | [7, 19, 36, 52] [15, 23, 44, 61] |
| Merge | [7, 15, 19, 23, 36, 44, 52, 61] |
The merge step#
Merging is easiest to picture with two piles of cards, each sorted with the smallest on top. Compare the two top cards, take the smaller one, and add it to a new pile. Repeat. When one pile runs out, move the rest of the other pile across in one go. Every comparison places one card, so merging piles with n cards between them takes at most n - 1 comparisons.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] left = {7, 19, 36, 52}; int[] right = {15, 23, 44, 61}; System.out.println(Arrays.toString(merge(left, right))); } static int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; int i = 0; int j = 0; int k = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result[k] = left[i]; i++; } else { result[k] = right[j]; j++; } k++; } while (i < left.length) { result[k] = left[i]; i++; k++; } while (j < right.length) { result[k] = right[j]; j++; k++; } return result; }}This merges the last two piles from the table and prints [7, 15, 19, 23, 36, 44, 52, 61]. In the trace, i is the top of the left pile, j the top of the right pile, and k the next free slot in result. The first loop runs while both piles have cards. The last two loops copy whatever is left, and only one of them ever runs, because by then one pile is empty.
Look at the test in the first loop: left[i] <= right[j]. On a tie it takes from the left pile. Every item in the left half started out in front of every item in the right half, so taking the left one first keeps equal items in their original order. That is what makes merge sort stable. With < instead, a tie would take from the right pile and swap the two equal items.
The whole sort#
Now the recursive method, following the four steps. It splits with Arrays.copyOfRange(a, from, to), which copies the items from index from up to, but not including, index to into a new array. The merge also prints each merge it makes, so the output is a log of the work.
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] prices = {52, 19, 36, 7, 44, 23, 61, 15}; System.out.println(Arrays.toString(mergeSort(prices))); } static int[] mergeSort(int[] a) { if (a.length <= 1) { return a; } int mid = a.length / 2; int[] left = mergeSort(Arrays.copyOfRange(a, 0, mid)); int[] right = mergeSort(Arrays.copyOfRange(a, mid, a.length)); return merge(left, right); } static int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; int i = 0; int j = 0; int k = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result[k] = left[i]; i++; } else { result[k] = right[j]; j++; } k++; } while (i < left.length) { result[k] = left[i]; i++; k++; } while (j < right.length) { result[k] = right[j]; j++; k++; } System.out.println("merge " + Arrays.toString(left) + " and " + Arrays.toString(right) + " into " + Arrays.toString(result)); return result; }}The merges come out in a different order from the table. The table works level by level, but the program finishes the whole left half, splitting all the way down and merging all the way back up, before it touches the right half. Step through it with the call stack in view: it grows as the calls split the array and shrinks as each merge returns. The last line printed is the sorted array, [7, 15, 19, 23, 36, 44, 52, 61].
How fast is merge sort?#
Look at the table again. Each round of merging handles every item once, so one round costs about n steps. The number of rounds is the number of times n can be halved before the pieces have one item each, which is log n. Multiply the two and merge sort is O(n log n), on every input, sorted or not.
The gap from O(n²) is huge once n is large. To sort a million items, selection sort makes about 500 billion comparisons. Merge sort needs fewer than 20 million.
The price is memory. Selection sort and insertion sort rearrange the array they are given and need only a few extra variables, which is O(1) extra space. Merge sort copies items into new arrays as it works, so it needs extra space that grows with n: O(n).
Key ideaDivide and conquer: split the problem in half, solve each half the same way, and combine the answers. Merge sort splits log n times and does about n steps of merging per level, which makes it O(n log n).
The three sorts side by side#
Here are all three on the same twelve bars. Choose a sort, press Play or step one move at a time, and watch the two counters. A comparison is one “which is smaller?” question, and a move is one value written into a slot.
Row now: 66, 10, 57, 98, 96, 34, 64, 74, 46, 99, 49, 52.
Each pass finds the smallest value in the unsorted part and swaps it to the front of that part.
Run each sort on the same numbers, then press New numbers and run them again. Selection sort makes 66 comparisons every time, which is 12 × 11 / 2. Insertion sort's count changes with the numbers. Merge sort never needs more than 33.
| Sort | Best case | Worst case | Extra memory | Stable |
|---|---|---|---|---|
| Selection sort | O(n²) | O(n²) | O(1) | No |
| Insertion sort | O(n), on sorted input | O(n²) | O(1) | Yes |
| Merge sort | O(n log n) | O(n log n) | O(n) | Yes |
| Bubble sort, as written in arrays-1d | O(n²) | O(n²) | O(1) | Yes |
What Java does when you call sort#
In real programs you sort with one line. Arrays.sort sorts an array in place. List.sort sorts a list, and Collections.sort(list) is an older way to call the same thing.
import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List; public class Main { public static void main(String[] args) { int[] scores = {72, 95, 61, 88, 79, 54}; Arrays.sort(scores); System.out.println(Arrays.toString(scores)); String[] names = {"Zara", "Leo", "Mia", "Adam"}; Arrays.sort(names); System.out.println(Arrays.toString(names)); List<Integer> ages = new ArrayList<>(List.of(31, 19, 25, 19)); Collections.sort(ages); System.out.println(ages); }}This prints [54, 61, 72, 79, 88, 95], then [Adam, Leo, Mia, Zara], then [19, 19, 25, 31]. Each comes out in its natural order: numbers from smallest to largest, and strings alphabetically.
Two algorithms behind one name#
The calls look alike, but Java picks a different algorithm depending on what you are sorting. Both arrived in Java 7.
- For arrays of primitives such as
int[]anddouble[],Arrays.sortuses dual-pivot quicksort, by Vladimir Yaroslavskiy, Jon Bentley and Joshua Bloch. Quicksort is another divide and conquer sort. It picks an item called a pivot, splits the array into the items smaller than the pivot and the items bigger, then sorts each part. The dual-pivot version picks two pivots and splits into three parts. Java's version promises O(n log n) on every input: if the splits keep coming out lopsided, it switches to another algorithm, heapsort, to keep that promise. - For arrays of objects such as
String[], and forList.sortandCollections.sort, Java uses TimSort, adapted from the sort Tim Peters wrote for Python. It is a merge sort at heart, so it is O(n log n) and stable, and it is tuned for real data. It looks for runs that are already in order, forwards or backwards, and merges those, so on a nearly sorted list it needs only about n comparisons.
Both switch to insertion sort for small pieces of a few dozen items, because its simple loop beats the heavier machinery when there is little to do. TimSort uses a version called binary insertion sort, which finds each item's place with a binary search.
Why objects get a stable sort#
A sort of primitives has no need to be stable: two equal ints are the same number, and nobody can tell which 3 came first. Objects carry other data, so the order of equal objects shows, and every object sort in Java guarantees stability. Here is the player list again, sorted by level with List.sort and a comparator.
import java.util.ArrayList;import java.util.Comparator;import java.util.List; public class Main { public static void main(String[] args) { List<Player> players = new ArrayList<>(List.of( new Player("Ana", 3), new Player("Ben", 1), new Player("Cleo", 3), new Player("Dev", 2), new Player("Eli", 1) )); players.sort(Comparator.comparing(Player::level)); for (Player p : players) { System.out.println(p.level() + " " + p.name()); } }} record Player(String name, int level) {}The output is 1 Ben, 1 Eli, 2 Dev, 3 Ana, 3 Cleo. Ben and Eli share level 1 and keep their original order, and so do Ana and Cleo on level 3. The selection sort earlier flipped Ana and Cleo; List.sort never will.
List.sort. How are each customer's orders arranged in the final list?Why you almost always call the library#
So the advice for real code is simple: call Arrays.sort or List.sort, and search sorted arrays with Arrays.binarySearch. The library sorts are faster than anything you would write in an afternoon, they take advantage of data that is partly sorted already, the object sort is guaranteed stable, and they have been tested on far more inputs than any course exercise. A sort of your own is one more place for a bug like the midpoint overflow to hide.
Writing the classics yourself is still worth doing once. It is how you learn what O(n log n) buys and what a stable sort promises, and writing binary search or merge sort from memory is a popular job interview question.
Key ideaIn real code, sort with
Arrays.sortorList.sortand search withArrays.binarySearch. Writing your own sort is how you learn what those calls do for you.
Arrays.binarySearch(array, target) assumes the array is in natural, ascending order. If you sorted an array of objects with a comparator, such as longest word first, pass the same comparator to the search: Arrays.binarySearch(array, target, comparator). Searching with a different order breaks it the same way the unsorted array did.Try it
Answer each one in your head or on paper first, then open the answer.
{4, 9, 15, 22, 31, 40, 57}. Write down lo, hi and mid for every look, and the value returned.Arrays.binarySearch returns -4. Is the target in the array? If not, at which index would it go?{1, 2, 3, 4, 5, 6}, which is already sorted? How many would insertion sort make?merge, what changes if left[i] <= right[j] becomes left[i] < right[j]?Practice
Work through these on your own. Each one builds on this page.
search(int[] a, int target, int lo, int hi), with no loop. What is the base case, and what does each recursive call pass along?{2, 4, 4, 4, 7, 9}, write firstIndexOf, which returns the index of the first copy of the target (1 for the target 4), still in O(log n).ArrayList<Integer> of scores. For each new score, call Collections.binarySearch to find where it belongs, turn a negative result into an insertion point, and add the score there with list.add(index, score). Print the list after each insert.boolean that records whether a pass swapped anything, and a break when it did not. What is its best case now, and on what input does it happen?Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email