Understanding the Dutch National Flag algorithm

Table of contents

Introduction๐Ÿ”—

In computer science, the Dutch National Flag problem asks: can you take an array of numbers and a "pivot" value, and sort the array in-place so that:

This problem shows up in quicksort, of which itโ€™s the only really complicated part in my opinion.

There's an elegant algorithm to solve this problem. It makes a single pass over the array and uses constant extra space! The implementation is easy enough to understand, but I had trouble understanding why it worked at all. That is, until I looked at it from the perspective of maintaing invariants.

How the algorithm works๐Ÿ”—

The key idea is to define a set of invariants and ensure that they are all true at the beginning of every loop iteration, and also after all iterations are completed. In other words, we must ensure they're all true before any iterations have occurred, before the second iteration starts, before the third iteration starts... and finally when all iterations have completed.

What should our invariants look like? Well, they follow from the problem statement. Let's use three indices i, j, k to carve up our array into four sections, and choose our invariants to be:

  1. Items less than the pivot are at indices < i
  2. Items equal to the pivot are at indices >= i and < j
  3. Unclassified items are at indices >= j and < k
  4. Items greater than the pivot are at indices >= k

At the end of our loop iterations, no element should remain unclassified, which means section 3 should be empty, which means our terminating condition is j == k.

Knowing that we want our invariants . Before the iteration starts, assuming the array is 0-indexed, the indices should be initialized as follows:

The hard part is to figure out what goes in the loop body. Remember, whatever we do in a single loop iteration, we must end with all invariants remaining true. At the same time, we want to make progress. In our formulation, progress means either incrementing j or decrementing i.

In this article we'll skip over how to derive the algorithm from first principles, and instead directly present it. Here it is, in Rust:

fn dnf<T: PartialOrd>(array: &mut [T], pivot: T) {
    if array.is_empty() {
        return;  // Nothing to do here.
    }

    let mut i = 0usize;
    let mut j = 0usize;

    // We checked for an empty array earlier, so this statement is guaranteed to
    // not underflow the `usize` type.
    let mut k: usize = array.len() - 1;

    while j < k {
        if array[j] < pivot {
            array.swap(i, j);
            i += 1;
            j += 1;
        } else if array[j] == pivot {
            j += 1;
        } else {  // array[j] > pivot
            k -= 1;
            array.swap(j, k);
        }
    }
}

The if conditions that make up the body of the loop are clear. But how do we know which indices to update inside the if conditions? The underlying idea is: any time it's safe to "advance" an index (increment in the case of i and j, decrement in the case of k), do it.

Let's analyze this case by case!

array[j] < pivot๐Ÿ”—

After incrementing indices i and j, and swapping the elements at those incremented indices:

array[j] == pivot๐Ÿ”—

After incrementing j:

array[j] > pivot๐Ÿ”—

We first decrement k and then swap the elements at indices j and k. This preserves invariant 4.

When the iteration ends, all invariants still hold. Additionally, the "unclassified" group empty (because j == k), and so all elements are sorted.

If you find this sort of invariant-based analysis interesting, consider reading up on Program Derivation, in particular the book Programming: The Derivation of Algorithms by Anne Kaldewaij. A powerful idea in program derivation is that you can write a program by methodically translating invariants into pseudocode. This is nice because it's typically much easier to formulate invariants for a given problem statement than to directly write a bug-free program.