SEN1002 · Week 13 · Searching and Sorting (Selection, Insertion, Merge Sort)

Searching and sorting: linear, binary, selection, insertion, merge sort

Two search and three sorting algorithms, the state of the array after each pass, and a Big-O comparison. The exam asks 'what does the array look like after pass 2'.

linear searchbinary searchselection sortinsertion sortmerge sortpassswapdivide and conquerBig O notationbest caseworst casecomparisonArrays.sortArrays.binarySearch

Searching and sorting are the first stop of algorithm analysis. This week you trace two search and three sorting algorithms step by step. Typical exam questions: “what does the array look like after the second pass when selection sort is applied to this array”, “which indexes does binary search look at”, and “what is its Big-O, and why”. Instead of memorizing the code, know the one-sentence idea of each algorithm; the code follows from that idea.

Golden rules:

  1. Binary search works only on a sorted array. It halves the range at each step.
  2. Selection sort: find the smallest of the rest and put it at the front.
  3. Insertion sort: shift the next element into its place in the sorted part on the left.
  4. Merge sort: split in two, sort the two halves, merge.
  5. Big-O: linear O(n)O(n), binary O(log⁡n)O(\log n), selection and insertion O(n2)O(n^2), merge O(nlog⁡n)O(n \log n).

Searching

Walk through the array one by one from the start, and return the index when you see the searched value. The array does not have to be sorted.

static int linearSearch(int[] a, int key) {
    for (int i = 0; i < a.length; i++)
        if (a[i] == key) return i;
    return -1;
}
int[] data = {42, 7, 19, 3, 25, 7};

Three searches, also counting the comparisons:

1 comps=2
4 comps=5
-1 comps=6

(When 7 is searched, the first 7 is found, index 1; 5 comparisons for 25; 99 is not there, the whole array was looked at.)

If the array is sorted, look at the middle element. If the searched value is larger, you can eliminate the left half entirely; if smaller, the right half. Last week we wrote it recursively; the loop version:

static int binarySearch(int[] a, int key) {
    int low = 0, high = a.length - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (a[mid] == key) return mid;
        if (a[mid] < key) low = mid + 1;    // right half
        else high = mid - 1;                // left half
    }
    return -1;
}
int[] sorted = {2, 5, 9, 14, 20, 27, 33, 41, 48, 56, 63};
binarySearch(sorted, 41);
binarySearch(sorted, 4);
  low=0 high=10 mid=5 a[mid]=27
  low=6 high=10 mid=8 a[mid]=48
  low=6 high=7 mid=6 a[mid]=33
  low=7 high=7 mid=7 a[mid]=41
index 7 comps=4
  low=0 high=10 mid=5 a[mid]=27
  low=0 high=4 mid=2 a[mid]=9
  low=0 high=1 mid=0 a[mid]=2
  low=1 high=1 mid=1 a[mid]=5
index -1 comps=4

When 4 is searched, at the last step low = 1, high = 1, and since a[1] = 5 > 4, high = 0; low > high and the loop ends: −1.

PITFALL · low < high. If you write the condition with <, when the range shrinks to a single element that element is never checked. On the array {2, 5, 9} this faulty version found 5 but returned −1 for 9 and 2.

Java’s built-in version: Arrays.binarySearch(sorted, 27) → 5. It also expects the array to be sorted; if it is not sorted, the result is meaningless.

Sorting

We will trace the three algorithms on the same array: {29, 10, 14, 37, 13}.

Selection sort

The idea: find the smallest element in the unsorted part of the array and swap it with the start of that part. In each pass, one more element from the front settles into its final place.

static void selectionSort(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        int min = i;
        for (int j = i + 1; j < a.length; j++)
            if (a[j] < a[min]) min = j;
        int tmp = a[i]; a[i] = a[min]; a[min] = tmp;   // swap
    }
}

The array after each pass:

  pass 1: [10, 29, 14, 37, 13]
  pass 2: [10, 13, 14, 37, 29]
  pass 3: [10, 13, 14, 37, 29]
  pass 4: [10, 13, 14, 29, 37]
comps=10
pass 1: [29 10 14 37 13]  smallest 10 (index 1) ↔ index 0    → [10 | 29 14 37 13]
pass 2: [10 | 29 14 37 13] smallest 13 (index 4) ↔ index 1   → [10 13 | 14 37 29]
pass 3: smallest 14 is already at index 2                    → [10 13 14 | 37 29]
pass 4: smallest 29 (index 4) ↔ index 3                      → [10 13 14 29 | 37]

The number of comparisons is the same for every input: (n−1)+(n−2)+⋯+1=n(n−1)2(n-1) + (n-2) + \dots + 1 = \frac{n(n-1)}{2}. For n = 5 it is 10. That is why selection sort is O(n2)O(n^2) in every case.

Insertion sort

The idea: like sorting cards in your hand. The part on the left is always sorted. Take the next element (key), shift the ones larger than it one place to the right, and put the key in the gap that opens.

static void insertionSort(int[] a) {
    for (int i = 1; i < a.length; i++) {
        int key = a[i];
        int j = i - 1;
        while (j >= 0 && a[j] > key) {
            a[j + 1] = a[j];     // shift right
            j--;
        }
        a[j + 1] = key;
    }
}
  pass 1: [10, 29, 14, 37, 13]
  pass 2: [10, 14, 29, 37, 13]
  pass 3: [10, 14, 29, 37, 13]
  pass 4: [10, 13, 14, 29, 37]
comps=8

Compare the passes of the two algorithms: in selection sort the elements at the front are final (after pass 2, 10 and 13 never change again). In insertion sort the front part is sorted but not final: in pass 3, [10, 14, 29, 37] is sorted but 13 slipped in between later.

Insertion sort’s comparison count depends on the input:

InputInsertionSelection
{1, 2, 3, 4, 5} (sorted)410
{5, 4, 3, 2, 1} (reverse)1010
{29, 10, 14, 37, 13}810

On sorted input, insertion sort makes one comparison per element and stops: the best case is O(n)O(n). On reverse-ordered input each element slides all the way to the front: the worst case is O(n2)O(n^2).

Merge sort

The idea: divide and conquer. Split the array in two, sort the two halves recursively, then merge the two sorted halves into a single sorted array (merge). An array with one element is already sorted: the base case.

static void mergeSort(int[] a, int low, int high) {
    if (low >= high) return;                 // 0 or 1 elements
    int mid = (low + high) / 2;
    mergeSort(a, low, mid);                  // left half
    mergeSort(a, mid + 1, high);             // right half
    merge(a, low, mid, high);                // merge
}
static void merge(int[] a, int low, int mid, int high) {
    int[] tmp = new int[high - low + 1];
    int i = low, j = mid + 1, k = 0;
    while (i <= mid && j <= high)
        tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];   // take the smaller one
    while (i <= mid) tmp[k++] = a[i++];                // the leftovers
    while (j <= high) tmp[k++] = a[j++];
    System.arraycopy(tmp, 0, a, low, tmp.length);
}

For {38, 27, 43, 3, 9, 82, 10}, the state of that range after each merge:

  merge [0..1]: [27, 38]
  merge [2..3]: [3, 43]
  merge [0..3]: [3, 27, 38, 43]
  merge [4..5]: [9, 82]
  merge [4..6]: [9, 10, 82]
  merge [0..6]: [3, 9, 10, 27, 38, 43, 82]
[3, 9, 10, 27, 38, 43, 82] comps=14
                 [38 27 43 3 9 82 10]
            [38 27 43 3]          [9 82 10]
         [38 27]   [43 3]       [9 82]   [10]
        [38] [27] [43] [3]     [9] [82]
         └─┬─┘     └─┬─┘        └─┬─┘
        [27 38]   [3 43]       [9 82]
           └────┬────┘            └──┬──┘
        [3 27 38 43]            [9 10 82]
                └──────────┬──────────┘
              [3 9 10 27 38 43 82]

The splitting takes log⁡2n\log_2 n levels, and at each level a total of n elements are merged: O(nlog⁡n)O(n \log n), independent of the input. The cost: extra O(n)O(n) memory for the temporary array.

Big-O comparison

AlgorithmBestAverageWorstExtra memoryNote
Linear searchO(1)O(1)O(n)O(n)O(n)O(n)noneWorks on an unsorted array too
Binary searchO(1)O(1)O(log⁡n)O(\log n)O(log⁡n)O(\log n)noneSorted array required
Selection sortO(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)noneAt most n − 1 swaps
Insertion sortO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)noneFast on nearly sorted data, stable
Merge sortO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(n)O(n)Preferred for big data

The practical counterpart of the theory: comparisons counted on reverse-ordered arrays.

nSelection sortMerge sort
10004995004932
2000199900010864
4000799800023728

When n doubles, selection sort’s work quadruples (n2n^2), while merge sort’s grows a little more than double (nlog⁡nn \log n).

TIP · in real life. You do not write your own sort: Arrays.sort(array) and Collections.sort(list) are ready and O(nlog⁡n)O(n \log n). In the exam, however, you are asked to trace and write the algorithm by hand; the built-in method is not accepted.

Common mistakes

Wrong ideaThe truth
Binary search works on every arrayA sorted array is required
while (low < high) is enoughIt must be <=, otherwise the last remaining element is skipped
Selection sort is fast on a sorted arrayThe comparison count is always n(n−1)/2
The passes of insertion sort and selection sort are the sameSelection finalizes from the front; insertion keeps the front part sorted but elements can slip in
Merge sort merges the right half firstThe left half finishes completely, then the right
Merge sort needs no extra memoryA temporary array: O(n)
The difference between O(n log n) and O(n²) is smallAt n = 4000, 23728 versus 7998000
n passes are neededn − 1 passes are enough, the last element is in place by itself

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1medium

Show the array after each pass of selection sort (smallest to the front).

int[] a = {29, 10, 14, 37, 13};
Show answer
[10,29,14,37,13] / [10,13,14,37,29] / [10,13,14,37,29] / [10,13,14,29,37]

In each pass the smallest of the unsorted part is found and swapped with the start of that part. Pass 1: the smallest, 10, swaps with 29. Pass 2: among the remaining [29,14,37,13] the smallest, 13, swaps with 29. Pass 3: the remaining is [14,37,29], and the smallest, 14, is already in place (it swaps with itself, the array is the same). Pass 4: [37,29] → 29 to the front. For n = 5 there are 4 passes.

Question 2medium

Show the array after each pass of insertion sort.

int[] a = {29, 10, 14, 37, 13};
Show answer
[10,29,14,37,13] / [10,14,29,37,13] / [10,14,29,37,13] / [10,13,14,29,37]

In each pass the next element is shifted into its correct place in the sorted part on the left. Pass 1: 10 goes in front of 29. Pass 2: 14 goes in front of 29 (larger than 10). Pass 3: 37 is already the largest, nothing changes. Pass 4: 13 shifts three elements to the right and goes in after 10.

Question 3medium

Binary search for 41 in {2, 5, 9, 14, 20, 27, 33, 41, 48, 56, 63} with mid = (low + high) / 2. List the mid values checked.

Show answer
5, 8, 6, 7 (found, index 7)

(0,10) → mid 5, a[5] = 27 < 41, low = 6. (6,10) → mid 8, a[8] = 48 > 41, high = 7. (6,7) → mid 6, a[6] = 33 < 41, low = 7. (7,7) → mid 7, a[7] = 41: found. 4 comparisons; for an 11-element array at most 4 are needed.

Question 4easy

How many comparisons does linear search make to find 25 in {42, 7, 19, 3, 25, 7}, and to search for 99?

Show answer
5 and 6

Linear search looks one by one from the start: 42, 7, 19, 3, 25 → it finds it at the 5th comparison (index 4). For a value that is not there it looks at the whole array: 6 comparisons, returns −1. If 7 were searched, it would find the first 7 (index 1) and stop: 2 comparisons.

Question 5easy

What is the Big-O (worst case) of each algorithm? linear search, binary search, selection sort, insertion sort, merge sort

Show answer
O(n), O(log n), O(n²), O(n²), O(n log n)

Linear may look at every element: O(n)O(n). Binary halves the range at each step: O(log⁡n)O(\log n). Selection and insertion have two nested loops: O(n2)O(n^2). Merge sort divides into log⁡n\log n levels, and at each level n elements are merged: O(nlog⁡n)O(n \log n).

Question 6medium

Insertion sort makes 4 comparisons on {1, 2, 3, 4, 5} but 10 on {5, 4, 3, 2, 1}. Selection sort makes 10 on both. Why?

Show answer
Insertion sort's best case is O(n), selection sort is O(n²) on every input

On sorted input, insertion sort makes a single comparison per element and stops: n − 1 = 4. On reverse-ordered input, each element slides all the way to the front: 1 + 2 + 3 + 4 = 10. To find the smallest, selection sort always looks at the whole remaining part: n(n − 1)/2 = 10, regardless of the input.

Question 7hard

Merge sort sorts {38, 27, 43, 3, 9, 82, 10} with mid = (low + high) / 2, left half first. What is the result of the third merge?

Show answer
[3, 27, 38, 43]

The order of merges: [0..1] → [27, 38], [2..3] → [3, 43], then these two [0..3] → [3, 27, 38, 43]. It does not move to the right half until the left half is completely done. Then [4..5] → [9, 82], [4..6] → [9, 10, 82], and last [0..6] → [3, 9, 10, 27, 38, 43, 82].

Question 8medium

Find the bug: the search below misses some elements of {2, 5, 9}.

static int search(int[] a, int key) {
    int low = 0, high = a.length - 1;
    while (low < high) {
        int mid = (low + high) / 2;
        if (a[mid] == key) return mid;
        if (a[mid] < key) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}
Show answer
The condition must be low <= high; it returns -1 for 9 and 2

When the range shrinks to a single element (low == high), the loop ends without checking that element. In the test run 5 was found (1) but −1 was returned for 9 and 2. The correct form is while (low <= high).

Question 9easy

Which statement is TRUE?

  1. Binary search works on any array
  2. Selection sort does fewer comparisons on an already sorted array
  3. Merge sort needs an extra array proportional to n
  4. Insertion sort is O(n log n) in the worst case
Show answer
Merge sort needs an extra array proportional to n

The merge step merges the two halves into a temporary array: extra memory O(n)O(n). Binary search works only on a sorted array. Selection sort's comparison count is independent of the input. Insertion sort's worst case is O(n2)O(n^2).

Question 10medium

For n = 1000, 2000 and 4000 (reverse-ordered input), selection sort made 499500, 1999000 and 7998000 comparisons; merge sort made 4932, 10864 and 23728. What happens to each when n doubles?

Show answer
Selection grows about 4 times, merge sort a little more than 2 times

O(n2)O(n^2): when n doubles, (2n)2=4n2(2n)^2 = 4n^2, four times (499500 → 1999000 → 7998000). O(nlog⁡n)O(n \log n): 2nlog⁡(2n)2n \log(2n), a little more than double (4932 → 10864 → 23728). As n grows, the gap becomes a chasm; that is why merge sort (and Arrays.sort) is used for big data.

Question 11medium

How many comparisons does binary search need at most for a sorted array of 1,000,000 elements?

Show answer
20

Each comparison halves the range. 220=1048576≥1062^{20} = 1048576 \geq 10^6, so at most 20 steps. Linear search on the same array makes 1,000,000 comparisons in the worst case.

Question 12hard

Write a method sortByLength(String[] words) that sorts the array by word length (shortest first) using insertion sort. Words with equal length must keep their original order. {"kiwi", "fig", "banana", "apple", "date"} becomes [fig, kiwi, date, apple, banana].

Show answer
Insertion sort, comparison words[j].length() > key.length()

Insertion sort is stable: it does not shift equal elements, so their order is preserved. For this the comparison must be >, not >=:

public static void sortByLength(String[] words) {
    for (int i = 1; i < words.length; i++) {
        String key = words[i];
        int j = i - 1;
        while (j >= 0 && words[j].length() > key.length()) {
            words[j + 1] = words[j];   // shift right
            j--;
        }
        words[j + 1] = key;
    }
}
kiwi and date both have 4 letters; kiwi came first, so it is first in the result too: [fig, kiwi, date, apple, banana].

Verification: All traces, comparison counts and outputs were run with JDK 26 (java verify/SEN1002/W13.java, W13b.java). The growth table is the comparisons counted in the same file for n = 1000, 2000, 4000.

Review card

Linear search: from start to end, works on an unsorted array too. O(n). Returns -1 if not found. Binary search: a SORTED array is required. mid=(low+high)/2; if key is larger low=mid+1, if smaller high=mid-1. Loop condition while (low <= high) (with < the last remaining element is not checked). O(log n). 1,000,000 elements → at most 20 steps (2^20 ≈ 10^6). Selection: in each pass find the SMALLEST of the rest, swap to the front. n-1 passes. n(n-1)/2 comparisons on any input. O(n²). Insertion: place the next element into the sorted part on the left by SHIFTING. Best O(n) (sorted), worst O(n²). Merge: split in two, sort the two halves recursively, merge. O(n log n) in every case, extra O(n) memory. Merge order: the left half finishes completely, then the right half, then the two merge. Doubling n: O(n²) → 4 times, O(n log n) → a little more than 2 times, O(log n) → +1 step. Pass question: selection → the smallest ones at the front are final; insertion → the front part is sorted but not final. Built-in: Arrays.sort(a), Arrays.binarySearch(a, key) (sorted array), Collections.sort(list).