Skip to content
dsa450for five year olds

Searching & Sorting · 36 of 36

Partitioning and Sorting Arrays with Many Repeated Entries

Sort a big pile of cards with only a few different numbers by splitting it into smaller, equal and bigger piles, and never touching the equal pile again.

Simple way
time O(n^2)
room O(n)
Clever way
time O(n log k)
room O(log n)

The problem, simply

You get an array with lots of repeated numbers, for example a million numbers but only a handful of different values. Sort it fast.

Plain quicksort can get very slow here. The fix is to split the array into three parts around a chosen value, the pivot: smaller than the pivot, equal to it, and bigger than it. The equal part is already in its final place, so only the other two parts need more work.

A story to remember it

Picture it

array
01234567equal to the pivot: never touched again11444499

After one three way split with pivot 4: the smaller part, the equal part (finished), and the bigger part.

  • checking
  • seen
  • done

Examples

Many repeats

In
[4, 9, 4, 4, 1, 9, 4, 4, 9, 4, 4, 1, 4]
Out
[1, 1, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9]

Thirteen numbers, three different values. With pivot 4, eight of them are finished after the first pass.

Three colors

In
[2, 0, 2, 1, 1, 0]
Out
[0, 0, 1, 1, 2, 2]

With only 0, 1 and 2, a single split around the pivot 1 sorts everything. This is the Dutch national flag problem.

All the same

In
[5, 5, 5, 5]
Out
[5, 5, 5, 5]

Everything equals the pivot. One pass, and both other parts are empty. Plain quicksort is at its worst here.

One number

In
[7]
Out
[7]

Nothing to do.

Way 1: the simple way

Use plain quicksort with a two way split. Pick the last number as the pivot. Move everything smaller than or equal to it to the left, put the pivot after them, and sort both sides.

  1. pivot = arr[hi], and i marks where the "small" part ends.
  2. For each j from lo to hi - 1: if arr[j] <= pivot, swap it into the small part.
  3. Put the pivot right after the small part, then sort the left side and the right side.
java
class Solution {
    public void sort(int[] arr) {
        quick(arr, 0, arr.length - 1);
    }

    private void quick(int[] arr, int lo, int hi) {
        if (lo >= hi) return;
        int pivot = arr[hi], i = lo;
        for (int j = lo; j < hi; j++) {
            if (arr[j] <= pivot) {            // equal numbers all go left
                int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
                i++;
            }
        }
        int t = arr[i]; arr[i] = arr[hi]; arr[hi] = t;   // pivot into place
        quick(arr, lo, i - 1);
        quick(arr, i + 1, hi);
    }
}

How long it takes: on random distinct numbers about O(n log n). But when many numbers are equal, all of them land on one side, so each split only removes one number. On [5, 5, 5, ..., 5] that is O(n^2). Extra room: the calls can go n deep, O(n), which can overflow the stack on big inputs.

Way 2: the clever way

Split into three parts in one pass, with three fingers:

  • Everything left of lt is smaller than the pivot.
  • Everything from lt to i - 1 equals the pivot.
  • Everything from i to gt is not looked at yet.
  • Everything right of gt is bigger than the pivot.

Look at arr[i]:

  1. Smaller: swap it with arr[lt], then move both lt and i right.
  2. Equal: just move i right.
  3. Bigger: swap it with arr[gt] and move gt left. Do not move i, because the number that came from gt has not been looked at yet.

Stop when i passes gt. Then sort only lo..lt-1 and gt+1..hi.

Three fingers split around the pivot 4

01 / 13

array
0123456794149414gtilt
stack
sort(0..7)top

The middle number, at index 3, is 4, so the pivot is 4. Nothing is looked at yet: lt and i start at 0, gt at 7.

pivot
4
  • looking now
  • checking
  • seen
  • done
  • answer
java
class Solution {
    public void sort(int[] arr) {
        quick3(arr, 0, arr.length - 1);
    }

    private void quick3(int[] arr, int lo, int hi) {
        if (lo >= hi) return;
        int pivot = arr[lo + (hi - lo) / 2];
        int lt = lo, i = lo, gt = hi;
        while (i <= gt) {
            if (arr[i] < pivot) {
                swap(arr, lt++, i++);         // smaller: to the left part
            } else if (arr[i] > pivot) {
                swap(arr, i, gt--);           // bigger: to the right part, look at i again
            } else {
                i++;                          // equal: stays in the middle
            }
        }
        quick3(arr, lo, lt - 1);              // the equal part lt..gt is finished
        quick3(arr, gt + 1, hi);
    }

    private void swap(int[] arr, int a, int b) {
        int t = arr[a]; arr[a] = arr[b]; arr[b] = t;
    }
}

How long it takes: every split finishes one whole value, so with k different values and good pivots there are about log k levels of O(n) work, O(n log k). When all values are different that is the usual O(n log n), and when all are equal it is one pass, O(n). A bad pivot every time can still cause O(n^2), so real libraries pick the pivot at random. Extra room: the call depth, O(log n) with good pivots.

Watch out for

  • The loop condition is i <= gt, not i < gt. The number at gt is also unseen.
  • Recursing into lo..gt or lt..hi instead of skipping the equal part throws away the whole benefit.
  • Picking the first or last number as the pivot makes already sorted input slow. The middle or a random one is safer.
  • Quicksort is not stable: equal numbers may change their order. That only matters when they carry other data.

What interviewers ask next

  • Sort an array of only 0s, 1s and 2s in one pass. That is this exact split with pivot 1, the Dutch national flag problem.
  • If the values are small whole numbers, counting sort is O(n + range). Count each value, then write them back.
  • If there are few distinct values but they are huge or not numbers, count them in a TreeMap, which costs O(n log k) and writes them back in order.
  • Why does Java's Arrays.sort for int[] handle repeats well? It uses dual pivot quicksort, which also groups numbers equal to the pivots.
  • Partition around a value without sorting, for example "all negatives first". That is one pass of the same three finger loop.

Remember this

Grown-up words: the three piles are three way partitioning, the chosen card is the pivot, the whole method is 3 way quicksort (Dijkstra's version), and the 0, 1, 2 case is the Dutch national flag problem.