Link

Quicksort Reading

Complete the Reading Quiz by 3:00pm before lecture.

Quicksort and merge sort are examples of divide-and-conquer algorithms: algorithms that work by breaking down a problem into two or more smaller related subproblems, recursively solving them, and then combining the results to solve the original problem.

Merge sort:

  1. Recursively merge sort the left half.
  2. Recursively merge sort the right half.
  3. Merge the two halves.

Quicksort:

  1. Partition around a pivot item.
  2. Recursively quicksort the left half.
  3. Recursively quicksort the right half.

The runtime of quicksort depends on two parts: partitioning and choosing a pivot item. To partition an array a[] around pivot item x = a[i] is to rearrange the items in a[] so that the following three conditions hold.

  • Pivot item x is in its sorted, final position in the array labeled j.
  • All entries to the left of x are <= x.
  • All entries to the right of x are >= x.

Quicksort initial partitioning array

There are many possible partitions for the above array.

Quicksort partitioning options

Which of the above partitions are valid?

Options A, B, and C are valid partitions. Option D is invalid because the entry to the immediate right of x is 4, which is less than x.

Note that the relative order of entries to the left of x does not need to stay the same as the input. The same applies to the relative order of entries to the right of x.

Given an array containing N items and a particular pivot item, how can we efficiently partition around the pivot item? How do we know if an algorithm for partition is efficient? It’s helpful to define lower and upper bounds for the partition problem.

How can we solve the partition problem in O(N log N) time?

The partition problem reduces to sorting! Sorting an array will place the pivot item in its sorted, final position—while also ensuring that all entries to the left are less-than-or-equal-to it and that all entries to the right are greater-than-or-equal-to it. We can use merge sort to sort an array in order of N log N time.

While we could use sorting to solve the partition problem, it feels too strong to use it in quicksort since there’s no point in recursively quicksorting the left and right halves.

static int[] quicksort(int[] a) {
    return mergeSort(a);
}

Our goal is to solve the partition problem in strictly better than order N log N runtime.

One way to do this is to create another array and populate it using 3 passes across the original array. Scan across the input array and add all the less-than-pivot items to the output array. Then, scan and add the pivot item(s) to the output array. Finally, scan across the input array again and add all the greater-than-pivot items to the output array. This partitioning algorithm maintains the relative order of items less-than-pivot and items greater-than-pivot, but this is not necessarily true for all partitioning algorithms. You can see a demo of this algorithm here.

If we always pick the leftmost item as the pivot, this results in what we will call Naive Quicksort. For a variety of reasons, naive quicksort is slower than merge sort in practice. In lecture, we’ll discuss how to optimize naive quicksort by improving our pivot choices and partitioning algorithm.


Reading Quiz