Skip to main content

Section 26.11 Partitioning

Like merge sort, quick sort is a very simple looking divide and conquer algorithm. In pseudocode, it looks like this:
QuickSort(list, start, end):
  if start >= end:
    return list  // size 0/1 already sorted
  else:
    pivotLocation = Partition(list)
    QuickSort(0 to pivotLocation - 1)  // Sort the first half
    QuickSort(pivotLocation to end)    // Sort the second half
Here, most of the interesting work happens during the partition step, where the list is divided into two parts. This section focuses on that partitioning process. The goal of partitioning is to pick a pivot element and rearrange the list so that all elements less than the pivot come before it, and all elements greater than the pivot come after it.
There are multiple partition algorithms, we will focus on the one known as the Hoare partition scheme. The basic strategy of this algorithm is to create an index that starts at the begining of the range being partitioned (i) and an index that starts at the end of the range being partitioned (j). These two indexes are used to scan the list from both ends towards the middle, swapping elements to ensure that elements less than the pivot end up on the left, and elements greater than the pivot end up on the right.
Simplified pseudocode for the Hoare partitioning algorithm looks like this:
pivot = list.at(start)
i = start + 1
j = end
while i ≀ j:
  Move i right until an element β‰₯ pivot is found
  Move j left until an element ≀ pivot is found
  if i ≀ j:
    swap(list.at(i), list.at(j))
// Done swapping pairs - place pivot in its final position
swap(list.at(start), list.at(j))
Use that pseudocode and the interactive below to practice partitioning a list. Use the instructions in the interactive for guidance.

Activity 26.11.1. Partitioning.

Instructions.

Complete a partition step by step by pressing the various buttons.
Follow this pseudocode as a guide:
While i ≀ j:
  Move i right until i finds a value >= pivot
  Move j left until j finds a value <= pivot
  if i ≀ j:  // if Indexes have not crossed
    // swap the values at i and j
    swap(list.at(i), list.at(j))
Finish by swapping the pivot with the value at j
Reset and redo the interactive until you feel comfortable with the partitioning process. Notice what has to be true when the partitioning is done:
  • All items to the left of the pivot’s final position are less than or equal to the pivot.
  • All items to the right of the pivot’s final position are greater than or equal to the pivot.
  • The pivot is in its final sorted position.

Note 26.11.1.

Real world implementations often select the pivot using a more sophisticated method than β€œthe first element in the partition range”. We will discuss those in the next section.
Implemented to sort a vector in C++, it looks like this:
Listing 26.11.1.
Analyzing the partition algorithm is a little tricky because of the nested loops. Normally, nested loops suggest a quadratic time complexity, but in this case, the inner loops do not run independently for each iteration of the outer loop. Instead, the inner loops collectively scan through the list only once.
Consider this version of the partitioning algorithm:
while (i <= j) {
    if (i <= end && vec.at(i) <= pivotValue) {
        i++;
    }
    if (j >= start && vec.at(j) > pivotValue) {
        j--;
    }
    // Swap items at i and j if i < j
    if (i < j && vec.at(i) > pivotValue && vec.at(j) <= pivotValue) {
        swap(vec.at(i), vec.at(j));
        // advance i and retreat j since those items are fixed
        i++;
        j--;
    }
}
It swaps the inner loops for conditional checks. Each iteration of the loop moves either i, or j, or both closer to the other. The size \(n\) of the vector segment being partitioned is determined by the distance between start and end. Those values also are used to set i and j. So the loop describes iterating over \(n\) items.
Every swap, or comparison, or call to .at is \(O(1)\text{.}\) So the algorithm does \(O(n) \cdot O(1) = O(n)\) operations.
The same total amount of work will be done when the ifs are turned into whiles. In the version with nested loops, each time an inner i or j loop advance more than once it also reduces the number of iterations left for the outer loop.
Partitioning requires \(O(n)\) time, where \(n\) is the number of items being partitioned.

Checkpoint 26.11.1.

Checkpoint 26.11.2.

We do a partition on the list ??, ??, 5, 4, 8, 2, 3, 6, ??, ?? on the range 2-7 (the six numbers shown).
What is the first pair of values that will be swapped?
  • 8 and 3
  • 8 is the first value from the left that is greater than the pivot. 3 is the first value from the right that is less than or equal to the pivot.
  • 4 and 6
  • 4 is less than the pivot value, so i will move past it.
  • 8 and 2
  • j will find a value before 2.
  • 4 and 2
  • 4 is less than the pivot, so the i index will move past it. j will find a value before 2.
  • 5 and 3
  • 5 is the pivot value. We will swap it with j at the very end of the partition.

Checkpoint 26.11.3.

You have attempted of activities on this page.