Sorting and Algorithm Bounds Reading

Since sorting is such a common operation, it’s important for Java developers to optimize for real-world runtime in addition to asymptotic time complexity.

Why doesn't Java use in-place partitioning quicksort to sort arrays of objects since it is faster than merge sort?

In-place partitioning is unstable, so in-place quicksort is also unstable. Stability is important for arrays of reference types (e.g. objects) but not important for arrays of primitive types (e.g. integers).

In 20091, the Java unstable sorting algorithm was updated to use two pivot items in an algorithm called dual-pivot quicksort. If classic quicksort is analogous to binary search trees, then dual-pivot quicksort is analogous to 3-trees: search trees where each node has 2 keys and 3 non-null children.

The invariant of classical Quicksort is:

[ ≤ p | > p ]

There are several modifications of the schema:

[ < p | = p | > p ]  or  [ = p | < p | > p | = p ]

But all of them use one pivot.

The new Dual-Pivot Quicksort uses two pivots elements in this manner:

1. Pick an elements P1, P2, called pivots from the array.
2. Assume that P1 ≤ P2, otherwise swap it.
3. Reorder the array into three parts: those less than the smaller pivot, those larger than the larger pivot, and in between are those elements between (or equal to) the two pivots.
4. Recursively sort the sub-arrays.

The invariant of the Dual-Pivot Quicksort is:

[ < P1 | P1 ≤ & ≤ P2 | > P2 ]
Give the order of growth of the runtime for dual-pivot quicksort on a random input.

Still linearithmic (order $$N \log N$$) runtime, but the height of the 3-tree (recursive depth) will be smaller than previous versions of quicksort.

Although quicksort was originally developed in 1959, it continues to be improved. How much further can we improve our sorting algorithms?

One way to improve the runtime of a sorting algorithm is to make it adaptive, or change its behavior based on the input. Java unstable sort switches from dual-pivot quicksort to insertion sort if the array is tiny (fewer than 47 items) since insertion sort is the fastest sorting algorithm on small arrays.

Java’s merge sort implementation is also adaptive but goes one step further by inspecting the input data. Real-world data is often not totally random; it’s often biased. This bias can take the form of natural runs in the data: subsequences of items in ascending or descending order.

a[0] <= a[1] <= a[2] <= ...

Instead of recursively merging perfectly-sized halves, adaptive merge sort merges natural runs in the data to form longer and longer runs. This data-oriented optimization is so effective that Java unstable sort switches to adaptive merge sort if the data contains fewer than 67 runs.

Give a tight asymptotic runtime bound for Java merge sort on a sorted array.

Linear time since the entire input array is an ascending (sorted) run.

Theoretical limits

While adaptive merge sort has a best-case linear runtime, its worst-case runtime is in $$\Theta(N \log N)$$.2 We will show that it’s not possible to improve the worst-case asymptotic runtime for a comparison sorting algorithm through analyzing the theoretically-optimal decision tree sort.

In the worst case, it takes 3 comparisons to fully determine the sorted arrangement of 3 items (a, b, c).

In the worst case, how many comparisons are needed to definitively sort 4 items (a, b, c, d)?

Decision tree sort needs to ask binary comparisons to decide between all possible permutations of the items. We can insert the fourth item (d) in any of 4 possible positions (e.g. dabc, adbc, abdc, or abcd) for each of the 6 possible possible permutations of 3 items (a, b, c). In general, there are $$N!$$ permutations of $$N$$ items.

The minimum number of levels in a 4-item decision tree needs to discern between $$4! = 24$$ items, so we need to ask 5 questions in the worst case.

Decision tree sort would be perfect if only we knew what the decision tree looked like for any array of size $$N$$. Computer scientists have only found the optimal decision tree sort for $$N = 1 \ldots 15$$ and $$N = 22$$ items. Even though the optimal decision tree sort doesn’t exist for every $$N$$, we can use the bound on decision tree sort to decide between all possible permutations of $$N$$ items.

Goal
Find a tight asymptotic complexity bound for $$\log_2 N!$$, the worst-case number of questions needed to find the correct sort among permutations of $$N$$ items.
Upper bound
1. $$N! = 1 \cdot 2 \cdot 3 \cdots N$$
2. $$N^N = N \cdot N \cdot N \cdots N$$, so $$N! < N^N$$
3. $$\log N! < \log N^N$$, so $$\log N! < N \log N$$
4. Therefore, $$\log N! \in O(N \log N)$$
Lower bound
1. Asymptotically, $$(N / 2)^{N / 2} < N!$$
2. $$\log{(N / 2)^{N / 2}} < \log N!$$
3. $$(N / 2) \log{N / 2} < \log N!$$
4. Therefore, $$\log N! \in \Omega(N \log N)$$

Since the worst-case runtime for the optimal decision tree sort is in $$\Theta(N \log N)$$, the worst-case runtime for any comparison-based sorting algorithm must be in $$\Omega(N \log N)$$.

Counting sorts

However, as we saw with duplicate-finding algorithms, we can do better if we change our approach. Rather than use binary comparisons, counting-based sorting algorithms sort the input via hashing by using the key itself as an array index.

How can we address duplicate (and potentially non-integer) keys?

In order to know the exact index for an item, we need to count the number of duplicates for each type of key. The index of the first instance of a key depends on the number of keys preceding it in the input.

How might we use counting to sort strings if each string can be arbitrarily long? Critically, how do we achieve this without comparing the characters in each string?

The number of digits $$R$$ in the alphabet, e.g. A–Z has a radix of 26.