Dutch National Flag Algorithm Explained — Sort 3 Values in One Pass
- DNF is the optimal solution for 3-way partitioning problems (like QuickSort's 3-way partition optimization).
- It maintains a strict loop invariant across four virtual zones in a single array.
- The algorithm is O(n) time and O(1) space, making it a favorite for 'Sort Colors' interview questions.
- Three pointers: low (0s boundary), mid (scan pointer), high (2s boundary)
- Invariant: arr[0..low-1]=0s, arr[low..mid-1]=1s, arr[mid..high]=unknown, arr[high+1..n-1]=2s
- Time: O(n). Space: O(1). Single pass, in-place.
- Critical rule: never increment mid after swapping with high
- Used as 3-way partition in QuickSort for arrays with many duplicates — turns O(n^2) into O(n)
- Cache-friendly: single pass, no auxiliary array
- Incrementing mid after swapping with high. The swapped element is unexamined — it could be 0, 1, or 2.
2s appear in the middle of the sorted array.
Add trace: System.out.println("swap2: mid=" + mid + " high=" + high + " arr=" + Arrays.toString(arr))Verify mid is NOT incremented after the case 2 swapLast element is unsorted.
Print mid and high at loop exit: System.out.println("exit: mid=" + mid + " high=" + high)If mid == high + 1 and the last element is wrong, the loop exited too earlyArrayIndexOutOfBoundsException on empty array.
Print arr.length at entryIf arr.length == 0, high = -1, and any array access crashesPartition is correct but performance is worse than expected.
Count total swaps: add a counter incremented in swap()Compare swap count to n. DNF should do at most n swaps.Production Incident
Production Debug GuideSymptom-first investigation path for DNF partition bugs.
Sorting an array with only three distinct values is a constraint that makes generic comparison sorts wasteful. Counting sort works but requires two passes. QuickSort is O(n log n) and overkill. The Dutch National Flag algorithm solves it in a single pass — O(n) time, O(1) space — by maintaining three pointers that carve the array into four live regions as they converge.
The algorithm was proposed by Edsger Dijkstra. It is the foundation of 3-way QuickSort, which handles duplicate-heavy arrays efficiently. The classic problem is LeetCode 75 'Sort Colors', where values are 0, 1, 2 representing red, white, blue.
The critical invariant: at every step, arr[0..low-1] contains only 0s, arr[low..mid-1] contains only 1s, arr[high+1..n-1] contains only 2s, and arr[mid..high] is unexamined. The mid pointer scans forward; low and high converge inward. The single rule that causes 90% of bugs: never increment mid after swapping with high.
How Dutch National Flag Works — Plain English
The Dutch National Flag algorithm sorts an array of three distinct values (0, 1, 2) in O(n) time, O(1) space, single pass.
Three-way partition invariant — three pointers divide the array: arr[0..low-1] = 0s. arr[low..mid-1] = 1s. arr[mid..high] = unknown. arr[high+1..n-1] = 2s.
Step-by-step: 1. Initialize low=0, mid=0, high=n-1. 2. While mid <= high: a. arr[mid]==0: swap arr[low]↔arr[mid], low++, mid++. b. arr[mid]==1: mid++ (already correct region). c. arr[mid]==2: swap arr[mid]↔arr[high], high--. Do NOT advance mid.
Worked example on [2,0,2,1,1,0]: low=0,mid=0,high=5. arr[0]=2: swap arr[0]↔arr[5]→[0,0,2,1,1,2]. high=4. arr[0]=0: swap arr[0]↔arr[0]. low=1,mid=1. arr[1]=0: swap arr[1]↔arr[1]. low=2,mid=2. arr[2]=2: swap arr[2]↔arr[4]→[0,0,1,1,2,2]. high=3. arr[2]=1: mid=3. arr[3]=1: mid=4. mid>high. Done. Result: [0,0,1,1,2,2].
package io.thecodeforge.algo; import java.util.Arrays; public class DutchNationalFlag { /** * Sorts an array of three distinct values (0, 1, 2) in-place. * O(n) time, O(1) space, single pass. */ public static void sortColors(int[] arr) { if (arr == null || arr.length <= 1) return; int low = 0; int mid = 0; int high = arr.length - 1; while (mid <= high) { switch (arr[mid]) { case 0: swap(arr, low, mid); low++; mid++; break; case 1: mid++; break; case 2: swap(arr, mid, high); high--; // Do NOT increment mid — swapped element is unexamined break; } } } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args) { int[] arr = {2, 0, 2, 1, 1, 0}; System.out.println("Before: " + Arrays.toString(arr)); sortColors(arr); System.out.println("After: " + Arrays.toString(arr)); // Edge cases sortColors(new int[]{2, 2, 2, 2}); // all 2s sortColors(new int[]{0, 0, 0, 0}); // all 0s sortColors(new int[]{1, 1, 1, 1}); // all 1s sortColors(new int[]{2, 0, 1}); // already one of each sortColors(new int[]{}); // empty sortColors(new int[]{1}); // single element } }
After: [0, 0, 1, 1, 2, 2]
- arr[0..low-1]: all 0s. Sorted. Never touched again.
- arr[low..mid-1]: all 1s. Sorted. Never touched again.
- arr[mid..high]: unknown. This is where mid scans.
- arr[high+1..n-1]: all 2s. Sorted. Never touched again.
- Loop ends when mid > high: unknown zone is empty.
The Three-Pointer Partition Strategy
The brilliance of the DNF algorithm lies in its ability to maintain four sections within the array using only three pointers: low, mid, and high.
[0 ... low-1]: Elements smaller than the pivot (0s).[low ... mid-1]: Elements equal to the pivot (1s).[mid ... high]: Elements yet to be explored (Unknown).[high+1 ... N-1]: Elements larger than the pivot (2s).
We iterate using the mid pointer. When we encounter a 0, we swap it with low and advance both. When we see a 1, we just move mid. When we see a 2, we swap it with high and decrement high—but we don't move mid yet, because the element swapped from the end is still unknown.
package io.thecodeforge.algo; /** * Dutch National Flag Algorithm Implementation * Complexity: Time O(n), Space O(1) */ public class SortColors { public void sort(int[] nums) { if (nums == null || nums.length <= 1) return; int low = 0; int mid = 0; int high = nums.length - 1; while (mid <= high) { switch (nums[mid]) { case 0: // Element belongs in the 'Red' zone swap(nums, low++, mid++); break; case 1: // Element belongs in the 'White' (middle) zone mid++; break; case 2: // Element belongs in the 'Blue' zone swap(nums, mid, high--); // Note: We do NOT increment mid here because the // swapped element from 'high' is unexamined. break; } } } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args) { int[] testCase = {2, 0, 2, 1, 1, 0}; new SortColors().sort(testCase); // Result: [0, 0, 1, 1, 2, 2] } }
DNF as 3-Way QuickSort Partition — The Duplicate-Heavy Optimization
Dutch National Flag is the partition step in Dijkstra's 3-way QuickSort. Standard QuickSort partitions into two zones (< pivot, >= pivot). 3-way QuickSort partitions into three zones (< pivot, == pivot, > pivot). This is critical for arrays with many duplicate elements.
Standard QuickSort on an array of all identical elements: O(n^2) because every partition is maximally unbalanced (all elements go to one side). 3-way QuickSort with DNF: O(n) because all elements are grouped into the == pivot zone in a single pass, and only the < and > zones are recursed on.
The DNF partition in QuickSort uses the pivot value as the 'middle' value. Elements less than pivot go to the low zone, elements equal to pivot go to the mid zone, elements greater than pivot go to the high zone. The mid zone is already sorted and requires no further recursion.
package io.thecodeforge.algo; import java.util.Arrays; public class ThreeWayQuickSort { /** * 3-way QuickSort using DNF partition. * O(n log n) average, O(n) for duplicate-heavy arrays. */ public static void sort(int[] arr) { if (arr == null || arr.length <= 1) return; quickSort(arr, 0, arr.length - 1); } private static void quickSort(int[] arr, int lo, int hi) { if (lo >= hi) return; // DNF partition around arr[lo] as pivot int lt = lo; // arr[lo..lt-1] < pivot int gt = hi; // arr[gt+1..hi] > pivot int mid = lo; // arr[lt..mid-1] == pivot int pivot = arr[lo]; while (mid <= gt) { if (arr[mid] < pivot) { swap(arr, lt++, mid++); } else if (arr[mid] > pivot) { swap(arr, mid, gt--); } else { mid++; } } // Recurse only on < and > zones. The == zone is done. quickSort(arr, lo, lt - 1); quickSort(arr, gt + 1, hi); } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static void main(String[] args) { // Duplicate-heavy array: 90% identical elements int[] arr = {5, 5, 5, 5, 5, 3, 3, 3, 7, 7, 7, 7, 5, 5, 5}; System.out.println("Before: " + Arrays.toString(arr)); sort(arr); System.out.println("After: " + Arrays.toString(arr)); // All identical: O(n) with 3-way, O(n^2) with standard QuickSort int[] allSame = {4, 4, 4, 4, 4, 4, 4, 4}; sort(allSame); System.out.println("All same: " + Arrays.toString(allSame)); } }
After: [3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 5, 7, 7, 7, 7]
All same: [4, 4, 4, 4, 4, 4, 4, 4]
- Standard QuickSort: 2-way partition (< pivot, >= pivot). O(n^2) on all-identical arrays.
- 3-way QuickSort: 3-way partition (< pivot, == pivot, > pivot). O(n) on all-identical arrays.
- DNF is the partition step in 3-way QuickSort.
- Java's Arrays.sort for primitives uses a dual-pivot QuickSort variant that incorporates 3-way partitioning.
- Use 3-way QuickSort when duplicates are expected. Use standard QuickSort when all elements are distinct.
Generalizing DNF to N Values — When 3 Pivots Aren't Enough
DNF is specialized for exactly 3 values. For 4 or more values, you need a generalized approach. The options are: counting sort (O(n+k) time, O(k) space), multiple DNF passes, or a bucket-based partition.
Counting sort is the simplest generalization: count occurrences of each value, then overwrite the array. It requires O(k) space and two passes. For k=4 or k=5, this is usually fine. For large k, the space cost dominates.
Multiple DNF passes: partition into 3 zones, then recursively apply DNF to each zone. This is essentially 3-way QuickSort with a fixed set of values. It works but adds complexity.
The practical recommendation: use counting sort for k <= 10. Use DNF (as 3-way QuickSort partition) for arbitrary values with expected duplicates. Use standard comparison sort for general-purpose sorting.
package io.thecodeforge.algo; import java.util.Arrays; public class CountingSortKValues { /** * Sorts an array of k distinct values using counting sort. * O(n + k) time, O(k) space. * Use when k is small (k <= 10) and you need a simple solution. */ public static void sort(int[] arr, int k) { if (arr == null || arr.length <= 1) return; int[] count = new int[k]; for (int val : arr) { count[val]++; } int idx = 0; for (int val = 0; val < k; val++) { for (int j = 0; j < count[val]; j++) { arr[idx++] = val; } } } public static void main(String[] args) { // Sort 5 values (0-4) int[] arr = {3, 0, 4, 1, 2, 3, 0, 1, 4, 2}; System.out.println("Before: " + Arrays.toString(arr)); sort(arr, 5); System.out.println("After: " + Arrays.toString(arr)); } }
After: [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
- k=3: DNF. O(n) time, O(1) space. Single pass. Cache-friendly.
- k=4-10: Counting sort. O(n+k) time, O(k) space. Two passes. Simple.
- k > 10 with duplicates: 3-way QuickSort. O(n log n) average. DNF as partition.
- k > 10, no duplicates: Standard QuickSort or
Arrays.sort(). O(n log n). - Decision rule: if k is known and small, counting sort. If k=3, DNF. Otherwise, comparison sort.
| Algorithm | Time Complexity | Space Complexity | Best Use Case |
|---|---|---|---|
| Counting Sort | O(2n) / Two Passes | O(k) where k=3 | Simple to implement if 2 passes are okay |
| QuickSort / MergeSort | O(n log n) | O(log n) to O(n) | General sorting for many unique values |
| Dutch National Flag | O(n) / One Pass | O(1) | Sorting 3 distinct values with minimal overhead |
| 3-Way QuickSort | O(n log n) avg, O(n) dup-heavy | O(log n) stack | Arbitrary values with many duplicates |
| Counting Sort (k values) | O(n + k) | O(k) | Small k (4-10), known value range |
| Bucket Sort | O(n + k) avg | O(n + k) | Uniformly distributed values, known range |
🎯 Key Takeaways
- DNF is the optimal solution for 3-way partitioning problems (like QuickSort's 3-way partition optimization).
- It maintains a strict loop invariant across four virtual zones in a single array.
- The algorithm is O(n) time and O(1) space, making it a favorite for 'Sort Colors' interview questions.
- Practice the logic on a whiteboard: the key is moving the 'mid' pointer only when you are certain of the current element's final position.
- Never increment mid after swapping with high. The swapped element is unexamined and must be re-checked.
- DNF is the partition step in 3-way QuickSort. It turns O(n^2) duplicate-heavy sorting into O(n).
- For k > 3 distinct values, use counting sort (O(n+k) time, O(k) space). Don't over-generalize DNF.
- Always test with homogeneous arrays (all 0s, all 1s, all 2s) to expose the mid-increment bug.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QLeetCode 75: Given an array with n objects colored red, white, or blue, sort them in-place so that objects of the same color are adjacent, with the colors in the order red, white, and blue.
- QWhiteboard Challenge: Can you explain the 'loop invariant' of the DNF algorithm and how it remains true after a swap with the 'high' pointer?
- QAdvanced: How is the Dutch National Flag logic used to improve the performance of QuickSort when the input array contains many duplicate elements?
- QOptimization: Why is a single-pass O(n) algorithm like DNF preferred over a two-pass counting sort for data that is too large to fit in the CPU cache?
- QEdge Case: How does your implementation handle an array consisting only of 1s and 2s? What about an empty array?
- QExtension: How would you generalize DNF to sort 4 distinct values? What is the time and space complexity?
- QProof: Prove that the DNF loop invariant is maintained after each of the three cases (0, 1, 2). What happens to each zone after a swap?
- QProduction: Describe a real-world scenario where DNF is used in production. What edge cases did you test?
- QStability: Is DNF stable? If not, can you make it stable? What is the cost?
- QComparison: When would you choose DNF over counting sort, and when would you choose counting sort over DNF?
Frequently Asked Questions
Why is it called the Dutch National Flag algorithm?
The algorithm was proposed by Edsger Dijkstra. He used the problem of sorting pebbles of three colors (red, white, and blue) to represent the three bands of the Dutch national flag.
Can I use this algorithm to sort 4 or 5 colors?
Technically, the 3-way partition is specialized for three values. For more values, you would need a more generalized approach like a full Counting Sort or multiple passes of the DNF logic, which usually makes standard sorting algorithms more attractive.
Is DNF stable?
No, the Dutch National Flag algorithm is generally not stable. Swapping elements across large distances in the array can change the relative order of identical elements.
Why don't we increment mid after swapping a 2 to the high end?
The swapped element arrived from the unknown region — we haven't examined it yet. We must inspect arr[mid] again before moving mid forward. Prematurely advancing mid would skip it, potentially misplacing a 0.
Is this the same as the three-way partition in quick sort?
Yes — Dutch National Flag is exactly the three-way partition used in Dijkstra's three-way quicksort. It is optimal for arrays with many duplicate elements, turning O(n^2) pivot-equal duplicates into O(n) moves.
How does DNF compare to counting sort for 3 values?
DNF is O(n) time, O(1) space, single pass. Counting sort is O(n) time, O(3) space, two passes. DNF wins on cache efficiency (single pass, no auxiliary array) and space (O(1) vs O(3)). For exactly 3 values, DNF is strictly better.
What happens if the array has fewer than 3 distinct values (e.g., only 0s and 2s)?
DNF still works correctly. If there are no 1s, low stays at 0 and mid scans through the array, swapping 2s to high. The 0s zone grows from the left, the 2s zone grows from the right, and the middle (where 1s would be) is empty. The algorithm terminates correctly.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.