Skip to main content
Engineering LibreTexts

11.1: Comparison-Based Sorting

  • Page ID
    8481
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    In this section, we present three sorting algorithms: merge-sort, quicksort, and heap-sort. Each of these algorithms takes an input array \(\mathtt{a}\) and sorts the elements of \(\mathtt{a}\) into non-decreasing order in \(O(\mathtt{n}\log \mathtt{n})\) (expected) time. These algorithms are all comparison-based. Their second argument, \(\mathtt{c}\), is a Comparator that implements the \(\mathtt{compare(a,b)}\) method. These algorithms don't care what type of data is being sorted; the only operation they do on the data is comparisons using the \(\mathtt{compare(a,b)}\) method. Recall, from Section 1.2.4, that \(\mathtt{compare(a,b)}\) returns a negative value if \(\mathtt{a}<\mathtt{b}\), a positive value if \(\mathtt{a}>\mathtt{b}\), and zero if \(\mathtt{a}=\mathtt{b}\).

    \(\PageIndex{1}\) Merge-Sort

    The merge-sort algorithm is a classic example of recursive divide and conquer: If the length of \(\mathtt{a}\) is at most 1, then \(\mathtt{a}\) is already sorted, so we do nothing. Otherwise, we split \(\mathtt{a}\) into two halves, \(\mathtt{a0}=\mathtt{a[0]},\ldots,\mathtt{a[n/2-1]}\) and \(\mathtt{a1}=\mathtt{a[n/2]},\ldots,\mathtt{a[n-1]}\). We recursively sort \(\mathtt{a0}\) and \(\mathtt{a1}\), and then we merge (the now sorted) \(\mathtt{a0}\) and \(\mathtt{a1}\) to get our fully sorted array \(\mathtt{a}\):

        <T> void mergeSort(T[] a, Comparator<T> c) {
            if (a.length <= 1) return;
            T[] a0 = Arrays.copyOfRange(a, 0, a.length/2);
            T[] a1 = Arrays.copyOfRange(a, a.length/2, a.length);
            mergeSort(a0, c);
            mergeSort(a1, c);
            merge(a0, a1, a, c);
        }
    

    An example is shown in Figure \(\PageIndex{1}\).

    mergesort.png
    Figure \(\PageIndex{1}\): The execution of \(\mathtt{mergeSort(a,c)}\)

    Compared to sorting, merging the two sorted arrays \(\mathtt{a0}\) and \(\mathtt{a1}\) is fairly easy. We add elements to \(\mathtt{a}\) one at a time. If \(\mathtt{a0}\) or \(\mathtt{a1}\) is empty, then we add the next elements from the other (non-empty) array. Otherwise, we take the minimum of the next element in \(\mathtt{a0}\) and the next element in \(\mathtt{a1}\) and add it to \(\mathtt{a}\):

        <T> void merge(T[] a0, T[] a1, T[] a, Comparator<T> c) {
            int i0 = 0, i1 = 0;
            for (int i = 0; i < a.length; i++) {
                if (i0 == a0.length)
                    a[i] = a1[i1++];
                else if (i1 == a1.length)
                    a[i] = a0[i0++];
                else if (compare(a0[i0], a1[i1]) < 0)
                    a[i] = a0[i0++];
                else 
                    a[i] = a1[i1++];
            }
        }
    

    Notice that the \(\mathtt{merge(a0,a1,a,c)}\) algorithm performs at most \(\mathtt{n}-1\) comparisons before running out of elements in one of \(\mathtt{a0}\) or \(\mathtt{a1}\).

    To understand the running-time of merge-sort, it is easiest to think of it in terms of its recursion tree. Suppose for now that \(\mathtt{n}\) is a power of two, so that \(\mathtt{n}=2^{\log \mathtt{n}}\), and \(\log \mathtt{n}\) is an integer. Refer to Figure \(\PageIndex{2}\). Merge-sort turns the problem of sorting \(\mathtt{n}\) elements into two problems, each of sorting \(\mathtt{n}/2\) elements. These two subproblem are then turned into two problems each, for a total of four subproblems, each of size \(\mathtt{n}/4\). These four subproblems become eight subproblems, each of size \(\mathtt{n}/8\), and so on. At the bottom of this process, \(\mathtt{n}/2\) subproblems, each of size two, are converted into \(\mathtt{n}\) problems, each of size one. For each subproblem of size \(\mathtt{n}/2^{i}\), the time spent merging and copying data is \(O(\mathtt{n}/2^i)\). Since there are \(2^i\) subproblems of size \(\mathtt{n}/2^i\), the total time spent working on problems of size \(2^i\), not counting recursive calls, is

    \[ 2^i\times O(\mathtt{n}/2^i) = O(\mathtt{n}) \enspace . \nonumber\]

    Therefore, the total amount of time taken by merge-sort is

    \[ \sum_{i=0}^{\log \mathtt{n}} O(\mathtt{n}) = O(\mathtt{n}\log \mathtt{n}) \enspace . \nonumber\]

    mergesort-recursion.png
    Figure \(\PageIndex{2}\): The merge-sort recursion tree.

    The proof of the following theorem is based on preceding analysis, but has to be a little more careful to deal with the cases where \(\mathtt{n}\) is not a power of 2.

    Theorem \(\PageIndex{1}\).

    The \(\mathtt{mergeSort(a,c)}\) algorithm runs in \(O(\mathtt{n}\log \mathtt{n})\) time and performs at most \(\mathtt{n}\log \mathtt{n}\) comparisons.

    Proof. The proof is by induction on \(\mathtt{n}\). The base case, in which \(\mathtt{n}=1\), is trivial; when presented with an array of length 0 or 1 the algorithm simply returns without performing any comparisons.

    Merging two sorted lists of total length \(\mathtt{n}\) requires at most \(\mathtt{n}-1\) comparisons. Let \(C(\mathtt{n})\) denote the maximum number of comparisons performed by \(\mathtt{mergeSort(a,c)}\) on an array \(\mathtt{a}\) of length \(\mathtt{n}\). If \(\mathtt{n}\) is even, then we apply the inductive hypothesis to the two subproblems and obtain

    \[\begin{align}
    C(\mathtt{n})
    &\le \mathtt{n}-1 + 2C(\mathtt{n}/2)\nonumber\\
    &\le \mathtt{n}-1 + 2((\mathtt{n}/2)\log(\mathtt{n}/2))\nonumber\\
    &= \mathtt{n}-1 + \mathtt{n}\log(\mathtt{n}/2)\nonumber\\
    &= \mathtt{n}-1 + \mathtt{n}\log \mathtt{n}-\mathtt{n}\nonumber\\
    &< \mathtt{n}\log \mathtt{n} \enspace .\nonumber
    \end{align}\nonumber\]

    The case where \(\mathtt{n}\) is odd is slightly more complicated. For this case, we use two inequalities that are easy to verify:

    \[\log(x+1) \le \log(x) + 1 \enspace , \label{log-ineq-a}\]

    for all \(x\ge 1\) and

    \[\log(x+1/2) + \log(x-1/2) \le 2\log(x) \enspace , \label{log-ineq-b}\]

    for all \(x\ge 1/2\). Inequality (\(\ref{log-ineq-a}\)) comes from the fact that \(\log(x)+1 = \log(2x)\) while (\(\ref{log-ineq-b}\)) follows from the fact that \(\log\) is a concave function. With these tools in hand we have, for odd \(\mathtt{n}\),

    \[\begin{align}
    C(\mathtt{n})
    &\le \mathtt{n}-1 + C(\lceil \mathtt{n}/2 \rceil) + C(\lfloor \mathtt{n}/2 \rfloor)\nonumber\\
    &\le \mathtt{n}-1 + \lceil \mathtt{n}/2 \rceil \log \lceil \mathtt{n} / 2\rceil+\lfloor \mathtt{n}/2 \rfloor\log \lfloor \mathtt{n}/2 \rfloor\nonumber\\
    &= \mathtt{n}-1 + (\mathtt{n}/2 + 1/2)\log (\mathtt{n} / 2+1/2) + (\mathtt{n}/2 - 1/2) \log (\mathtt{n}/2-1/2)\nonumber\\
    &\le \mathtt{n}-1 + \mathtt{n}\log( (\mathtt{n} / 2)+(1 / 2)(\log (\mathtt{n}/2+1/2) - \log (\mathtt{n}/2-1/2))\nonumber\\
    &\le \mathtt{n}-1 + \mathtt{n}\log(\mathtt{n}/2) + 1/2\nonumber\\
    &< \mathtt{n} + \mathtt{n}\log(\mathtt{n}/2)\nonumber\\
    &= \mathtt{n} + \mathtt{n}(\log\mathtt{n}-1)\nonumber\\
    &= \mathtt{n}\log\mathtt{n} \enspace . \nonumber
    \end{align}\nonumber\]

    $ \qedsymbol$

    \(\PageIndex{2}\) Quicksort

    The quicksort algorithm is another classic divide and conquer algorithm. Unlike merge-sort, which does merging after solving the two subproblems, quicksort does all of its work upfront.

    Quicksort is simple to describe: Pick a random pivot element, \(\mathtt{x}\), from \(\mathtt{a}\); partition \(\mathtt{a}\) into the set of elements less than \(\mathtt{x}\), the set of elements equal to \(\mathtt{x}\), and the set of elements greater than \(\mathtt{x}\); and, finally, recursively sort the first and third sets in this partition. An example is shown in Figure \(\PageIndex{3}\).

        <T> void quickSort(T[] a, Comparator<T> c) {
            quickSort(a, 0, a.length, c);
        }
        <T> void quickSort(T[] a, int i, int n, Comparator<T> c) {
            if (n <= 1) return;
            T x = a[i + rand.nextInt(n)];
            int p = i-1, j = i, q = i+n;
            // a[i..p]<x,  a[p+1..q-1]??x, a[q..i+n-1]>x 
            while (j < q) {
                int comp = compare(a[j], x);
                if (comp < 0) {       // move to beginning of array
                    swap(a, j++, ++p);
                } else if (comp > 0) {
                    swap(a, j, --q);  // move to end of array
                } else {
                    j++;              // keep in the middle
                }
            }
            // a[i..p]<x,  a[p+1..q-1]=x, a[q..i+n-1]>x 
            quickSort(a, i, p-i+1, c);
            quickSort(a, q, n-(q-i), c);
        }
    
    quicksort.png
    Figure \(\PageIndex{3}\): An example execution of \(\mathtt{quickSort(a,0,14,c)}\)

    All of this is done in place, so that instead of making copies of subarrays being sorted, the \(\mathtt{quickSort(a,i,n,c)}\) method only sorts the subarray \(\mathtt{a[i]},\ldots,\mathtt{a[i+n-1]}\). Initially, this method is invoked with the arguments \(\mathtt{quickSort(a,0,a.length,c)}\).

    At the heart of the quicksort algorithm is the in-place partitioning algorithm. This algorithm, without using any extra space, swaps elements in \(\mathtt{a}\) and computes indices \(\mathtt{p}\) and \(\mathtt{q}\) so that

    \[ \mathtt{a[i]} \begin{cases}
    {}< \mathtt{x} & \mbox{if $0 \leq \mathtt{i} \leq \mathtt{p}$}\\
    {}=\mathtt{x} & \mbox{if $\mathtt{p} < \mathtt{i} < \mathtt{q}$} \\
    {}>\mathtt{x}&\mbox{if $\mathtt{q}\le \mathtt{i} \le \mathtt{n}-1$}
    \end{cases}\nonumber\]

    This partitioning, which is done by the \(\mathtt{while}\) loop in the code, works by iteratively increasing \(\mathtt{p}\) and decreasing \(\mathtt{q}\) while maintaining the first and last of these conditions. At each step, the element at position \(\mathtt{j}\) is either moved to the front, left where it is, or moved to the back. In the first two cases, \(\mathtt{j}\) is incremented, while in the last case, \(\mathtt{j}\) is not incremented since the new element at position \(\mathtt{j}\) has not yet been processed.

    Quicksort is very closely related to the random binary search trees studied in Section 7.1. In fact, if the input to quicksort consists of \(\mathtt{n}\) distinct elements, then the quicksort recursion tree is a random binary search tree. To see this, recall that when constructing a random binary search tree the first thing we do is pick a random element \(\mathtt{x}\) and make it the root of the tree. After this, every element will eventually be compared to \(\mathtt{x}\), with smaller elements going into the left subtree and larger elements into the right.

    In quicksort, we select a random element \(\mathtt{x}\) and immediately compare everything to \(\mathtt{x}\), putting the smaller elements at the beginning of the array and larger elements at the end of the array. Quicksort then recursively sorts the beginning of the array and the end of the array, while the random binary search tree recursively inserts smaller elements in the left subtree of the root and larger elements in the right subtree of the root.

    The above correspondence between random binary search trees and quicksort means that we can translate Lemma 7.1.1 to a statement about quicksort:

    Lemma \(\PageIndex{1}\).

    When quicksort is called to sort an array containing the integers \(0,\ldots,\mathtt{n}-1\), the expected number of times element \(\mathtt{i}\) is compared to a pivot element is at most \(H_{\mathtt{i}+1} + H_{\mathtt{n}-\mathtt{i}}\).

    A little summing up of harmonic numbers gives us the following theorem about the running time of quicksort:

    Theorem \(\PageIndex{2}\).

    When quicksort is called to sort an array containing \(\mathtt{n}\) distinct elements, the expected number of comparisons performed is at most \(2\mathtt{n}\ln \mathtt{n} + O(\mathtt{n})\).

    Proof. Let \(T\) be the number of comparisons performed by quicksort when sorting \(\mathtt{n}\) distinct elements. Using Lemma \(\PageIndex{1}\) and linearity of expectation, we have:

    \[\begin{align}
    \mathrm{E}[T]
    &= \sum_{i=0}^{\mathtt{n}-1}(H_{\mathtt{i}+1}+H_{\mathtt{n}-\mathtt{i}})\nonumber\\
    &= 2\sum_{i=1}^{\mathtt{n}}H_i\nonumber\\
    &\le 2\sum_{i=1}^{\mathtt{n}}H_{\mathtt{n}}\nonumber\\
    &\le 2\mathtt{n}\ln\mathtt{n} + 2\mathtt{n} = 2\mathtt{n}\ln \mathtt{n} + O(\mathtt{n})\nonumber
    \end{align}\nonumber\]

    $ \qedsymbol$

    Theorem \(\PageIndex{3}\) describes the case where the elements being sorted are all distinct. When the input array, \(\mathtt{a}\), contains duplicate elements, the expected running time of quicksort is no worse, and can be even better; any time a duplicate element \(\mathtt{x}\) is chosen as a pivot, all occurrences of \(\mathtt{x}\) get grouped together and do not take part in either of the two subproblems.

    Theorem \(\PageIndex{3}\).

    The \(\mathtt{quickSort(a,c)}\) method runs in \(O(\mathtt{n}\log \mathtt{n})\) expected time and the expected number of comparisons it performs is at most \(2\mathtt{n}\ln \mathtt{n} +O(\mathtt{n})\).

    \(\PageIndex{3}\) Heap-sort

    The heap-sort algorithm is another in-place sorting algorithm. Heap-sort uses the binary heaps discussed in Section 10.1. Recall that the BinaryHeap data structure represents a heap using a single array. The heap-sort algorithm converts the input array \(\mathtt{a}\) into a heap and then repeatedly extracts the minimum value.

    More specifically, a heap stores \(\mathtt{n}\) elements in an array, \(\mathtt{a}\), at array locations \(\mathtt{a[0]},\ldots,\mathtt{a[n-1]}\) with the smallest value stored at the root, \(\mathtt{a[0]}\). After transforming \(\mathtt{a}\) into a BinaryHeap, the heap-sort algorithm repeatedly swaps \(\mathtt{a[0]}\) and \(\mathtt{a[n-1]}\), decrements \(\mathtt{n}\), and calls \(\mathtt{trickleDown(0)}\) so that \(\mathtt{a[0]},\ldots,\mathtt{a[n-2]}\) once again are a valid heap representation. When this process ends (because \(\mathtt{n}=0\)) the elements of \(\mathtt{a}\) are stored in decreasing order, so \(\mathtt{a}\) is reversed to obtain the final sorted order.1 Figure \(\PageIndex{4}\) shows an example of the execution of \(\mathtt{heapSort(a,c)}\).

    heapsort.png
    Figure \(\PageIndex{4}\): A snapshot of the execution of \(\mathtt{heapSort(a,c)}\). The shaded part of the array is already sorted. The unshaded part is a BinaryHeap. During the next iteration, element \(5\) will be placed into array location \(8\).
        <T> void sort(T[] a, Comparator<T> c) {
            BinaryHeap<T> h = new BinaryHeap<T>(a, c);
            while (h.n > 1) {
                h.swap(--h.n, 0);
                h.trickleDown(0);
            }
            Collections.reverse(Arrays.asList(a));
        }
    

    A key subroutine in heap sort is the constructor for turning an unsorted array \(\mathtt{a}\) into a heap. It would be easy to do this in \(O(\mathtt{n}\log\mathtt{n})\) time by repeatedly calling the BinaryHeap \(\mathtt{add(x)}\) method, but we can do better by using a bottom-up algorithm. Recall that, in a binary heap, the children of \(\mathtt{a[i]}\) are stored at positions \(\mathtt{a[2i+1]}\) and \(\mathtt{a[2i+2]}\). This implies that the elements \(\mathtt{a}[\lfloor\mathtt{n}/2\rfloor],\ldots,\mathtt{a[n-1]}\) have no children. In other words, each of \(\mathtt{a}[\lfloor\mathtt{n}/2\rfloor],\ldots,\mathtt{a[n-1]}\) is a sub-heap of size 1. Now, working backwards, we can call \(\mathtt{trickleDown(i)}\) for each \(\mathtt{i}\in\{\lfloor \mathtt{n}/2\rfloor-1,\ldots,0\}\). This works, because by the time we call \(\mathtt{trickleDown(i)}\), each of the two children of \(\mathtt{a[i]}\) are the root of a sub-heap, so calling \(\mathtt{trickleDown(i)}\) makes \(\mathtt{a[i]}\) into the root of its own subheap.

        BinaryHeap(T[] a, Comparator<T> c) {
            this.c = c;
            this.a = a;
            n = a.length;
            for (int i = n/2-1; i >= 0; i--) {
                trickleDown(i);
            }
        }
    

    The interesting thing about this bottom-up strategy is that it is more efficient than calling \(\mathtt{add(x)}\) \(\mathtt{n}\) times. To see this, notice that, for \(\mathtt{n}/2\) elements, we do no work at all, for \(\mathtt{n}/4\) elements, we call \(\mathtt{trickleDown(i)}\) on a subheap rooted at \(\mathtt{a[i]}\) and whose height is one, for \(\mathtt{n}/8\) elements, we call \(\mathtt{trickleDown(i)}\) on a subheap whose height is two, and so on. Since the work done by \(\mathtt{trickleDown(i)}\) is proportional to the height of the sub-heap rooted at \(\mathtt{a[i]}\), this means that the total work done is at most

    \[ \sum_{i=1}^{\log\mathtt{n}} O((i-1)\mathtt{n} / 2^{i})
    \leq \sum_{i=1}^{\infty} O(i \mathtt{n} / 2^{i})
    =O(\mathtt{n}) \sum_{i=1}^{\infty} i/2^{i}
    = O(2\mathtt{n}) = O(\mathtt{n}) \enspace . \nonumber\]

    The second-last equality follows by recognizing that the sum \(\sum_{i=1}^{\infty} i/2^{i}\) is equal, by definition of expected value, to the expected number of times we toss a coin up to and including the first time the coin comes up as heads and applying Lemma 4.4.1.

    The following theorem describes the performance of \(\mathtt{heapSort(a,c)}\).

    Theorem \(\PageIndex{4}\).

    The \(\mathtt{heapSort(a,c)}\) method runs in \(O(\mathtt{n}\log \mathtt{n})\) time and performs at most \(2\mathtt{n}\log \mathtt{n} + O(\mathtt{n})\) comparisons.

    Proof. The algorithm runs in three steps: (1) transforming \(\mathtt{a}\) into a heap, (2) repeatedly extracting the minimum element from \(\mathtt{a}\), and (3) reversing the elements in \(\mathtt{a}\). We have just argued that step 1 takes \(O(\mathtt{n})\) time and performs \(O(\mathtt{n})\) comparisons. Step 3 takes \(O(\mathtt{n})\) time and performs no comparisons. Step 2 performs \(\mathtt{n}\) calls to \(\mathtt{trickleDown(0)}\). The \(i\)th such call operates on a heap of size \(\mathtt{n}-i\) and performs at most \(2\log(\mathtt{n}-i)\) comparisons. Summing this over \(i\) gives

    \[ \sum_{i=0}^{\mathtt{n}-i} 2\log(\mathtt{n}-i)
    \leq \sum_{i=0}^{\mathtt{n}-i} 2 \log \mathtt{n}
    = 2\mathtt{n}\log \mathtt{n} \nonumber\]

    Adding the number of comparisons performed in each of the three steps completes the proof. $ \qedsymbol$

    \(\PageIndex{4}\) A Lower-Bound for Comparison-Based Sorting

    We have now seen three comparison-based sorting algorithms that each run in \(O(\mathtt{n}\log \mathtt{n})\) time. By now, we should be wondering if faster algorithms exist. The short answer to this question is no. If the only operations allowed on the elements of \(\mathtt{a}\) are comparisons, then no algorithm can avoid doing roughly \(\mathtt{n}\log \mathtt{n}\) comparisons. This is not difficult to prove, but requires a little imagination. Ultimately, it follows from the fact that

    \[ \log(\mathtt{n}!)
    = \log \mathtt{n} + \log (\mathtt{n}-1)+\cdots+\log (1)
    = \mathtt{n}\log \mathtt{n} - O(\mathtt{n}) \enspace . \nonumber\]

    (Proving this fact is left as Exercise 11.3.11.)

    We will start by focusing our attention on deterministic algorithms like merge-sort and heap-sort and on a particular fixed value of \(\mathtt{n}\). Imagine such an algorithm is being used to sort \(\mathtt{n}\) distinct elements. The key to proving the lower-bound is to observe that, for a deterministic algorithm with a fixed value of \(\mathtt{n}\), the first pair of elements that are compared is always the same. For example, in \(\mathtt{heapSort(a,c)}\), when \(\mathtt{n}\) is even, the first call to \(\mathtt{trickleDown(i)}\) is with \(\mathtt{i=n/2-1}\) and the first comparison is between elements \(\mathtt{a[n/2-1]}\) and \(\mathtt{a[n-1]}\).

    Since all input elements are distinct, this first comparison has only two possible outcomes. The second comparison done by the algorithm may depend on the outcome of the first comparison. The third comparison may depend on the results of the first two, and so on. In this way, any deterministic comparison-based sorting algorithm can be viewed as a rooted binary comparison tree. Each internal node, \(\mathtt{u}\), of this tree is labelled with a pair of indices \(\texttt{u.i}\) and \(\texttt{u.j}\). If \(\texttt{a[u.i]}<\texttt{a[u.j]}\) the algorithm proceeds to the left subtree, otherwise it proceeds to the right subtree. Each leaf \(\mathtt{w}\) of this tree is labelled with a permutation \(\texttt{w.p[0]},\ldots,\mathtt{w\texttt{.}p[n-1]}\) of \(0,\ldots,\mathtt{n}-1\). This permutation represents the one that is required to sort \(\mathtt{a}\) if the comparison tree reaches this leaf. That is,

    \[ \texttt{a[w.p[0]]}<\texttt{a[w.p[1]]}<\cdots<\mathtt{a[w\texttt{.}p[n-1]]} \enspace . \nonumber\]

    An example of a comparison tree for an array of size \(\mathtt{n=3}\) is shown in Figure \(\PageIndex{5}\).

    comparison-tree.png
    Figure \(\PageIndex{5}\): A comparison tree for sorting an array \(\mathtt{a[0]},\mathtt{a[1]},\mathtt{a[2]}\) of length \(\mathtt{n=3}\).

    The comparison tree for a sorting algorithm tells us everything about the algorithm. It tells us exactly the sequence of comparisons that will be performed for any input array, \(\mathtt{a}\), having \(\mathtt{n}\) distinct elements and it tells us how the algorithm will reorder \(\mathtt{a}\) in order to sort it. Consequently, the comparison tree must have at least \(\mathtt{n}!\) leaves; if not, then there are two distinct permutations that lead to the same leaf; therefore, the algorithm does not correctly sort at least one of these permutations.

    For example, the comparison tree in Figure \(\PageIndex{6}\) has only \(4< 3!=6\) leaves. Inspecting this tree, we see that the two input arrays \(3,1,2\) and \(3,2,1\) both lead to the rightmost leaf. On the input \(3,1,2\) this leaf correctly outputs \(\mathtt{a[1]}=1,\mathtt{a[2]}=2,\mathtt{a[0]}=3\). However, on the input \(3,2,1\), this node incorrectly outputs \(\mathtt{a[1]}=2,\mathtt{a[2]}=1,\mathtt{a[0]}=3\). This discussion leads to the primary lower-bound for comparison-based algorithms.

    comparison-tree-b.png
    Figure \(\PageIndex{6}\): A comparison tree that does not correctly sort every input permutation.

    Theorem \(\PageIndex{5}\).

    For any deterministic comparison-based sorting algorithm \(\mathcal{A}\) and any integer \(\mathtt{n}\ge 1\), there exists an input array \(\mathtt{a}\) of length \(\mathtt{n}\) such that \(\mathcal{A}\) performs at least \(\log(\mathtt{n}!) = \mathtt{n}\log\mathtt{n}-O(\mathtt{n})\) comparisons when sorting \(\mathtt{a}\).

    Proof. By the preceding discussion, the comparison tree defined by \(\mathcal{A}\) must have at least \(\mathtt{n}!\) leaves. An easy inductive proof shows that any binary tree with \(k\) leaves has a height of at least \(\log k\). Therefore, the comparison tree for \(\mathcal{A}\) has a leaf, \(\mathtt{w}\), with a depth of at least \(\log(\mathtt{n}!)\) and there is an input array \(\mathtt{a}\) that leads to this leaf. The input array \(\mathtt{a}\) is an input for which \(\mathcal{A}\) does at least \(\log(\mathtt{n}!)\) comparisons. $ \qedsymbol$

    Theorem \(\PageIndex{5}\) deals with deterministic algorithms like merge-sort and heap-sort, but doesn't tell us anything about randomized algorithms like quicksort. Could a randomized algorithm beat the \(\log(\mathtt{n}!)\) lower bound on the number of comparisons? The answer, again, is no. Again, the way to prove it is to think differently about what a randomized algorithm is.

    In the following discussion, we will assume that our decision trees have been "cleaned up" in the following way: Any node that can not be reached by some input array \(\mathtt{a}\) is removed. This cleaning up implies that the tree has exactly \(\mathtt{n}!\) leaves. It has at least \(\mathtt{n}!\) leaves because, otherwise, it could not sort correctly. It has at most \(\mathtt{n}!\) leaves since each of the possible \(\mathtt{n}!\) permutation of \(\mathtt{n}\) distinct elements follows exactly one root to leaf path in the decision tree.

    We can think of a randomized sorting algorithm, \(\mathcal{R}\), as a deterministic algorithm that takes two inputs: The input array \(\mathtt{a}\) that should be sorted and a long sequence \(b=b_1,b_2,b_3,\ldots,b_m\) of random real numbers in the range \([0,1]\). The random numbers provide the randomization for the algorithm. When the algorithm wants to toss a coin or make a random choice, it does so by using some element from \(b\). For example, to compute the index of the first pivot in quicksort, the algorithm could use the formula \(\lfloor n b_1\rfloor\).

    Now, notice that if we fix \(b\) to some particular sequence \(\hat{b}\) then \(\mathcal{R}\) becomes a deterministic sorting algorithm, \(\mathcal{R}(\hat{b})\), that has an associated comparison tree, \(\mathcal{T}(\hat{b})\). Next, notice that if we select \(\mathtt{a}\) to be a random permutation of \(\{1,\ldots,\mathtt{n}\}\), then this is equivalent to selecting a random leaf, \(\mathtt{w}\), from the \(\mathtt{n}!\) leaves of \(\mathcal{T}(\hat{b})\).

    Exercise 11.3.13 asks you to prove that, if we select a random leaf from any binary tree with \(k\) leaves, then the expected depth of that leaf is at least \(\log k\). Therefore, the expected number of comparisons performed by the (deterministic) algorithm \(\mathcal{R}(\hat{b})\) when given an input array containing a random permutation of \(\{1,\ldots,n\}\) is at least \(\log(\mathtt{n}!)\). Finally, notice that this is true for every choice of \(\hat{b}\), therefore it holds even for \(\mathcal{R}\). This completes the proof of the lower-bound for randomized algorithms.

    Theorem \(\PageIndex{6}\).

    For any integer \(n\ge 1\) and any (deterministic or randomized) comparison-based sorting algorithm \(\mathcal{A}\), the expected number of comparisons done by \(\mathcal{A}\) when sorting a random permutation of \(\{1,\ldots,n\}\) is at least \(\log(\mathtt{n}!) = \mathtt{n}\log\mathtt{n}-O(\mathtt{n})\).


    Footnotes

    1The algorithm could alternatively redefine the \(\mathtt{compare(x,y)}\) function so that the heap sort algorithm stores the elements directly in ascending order.


    This page titled 11.1: Comparison-Based Sorting is shared under a CC BY license and was authored, remixed, and/or curated by Pat Morin (Athabasca University Press) .