Cairn
algorithms · sorting · O(d·(n+k)) · non-comparison, in-place, unstable, MSD

back to Non-Comparison Sorts

American Flag Sort

Radix sort, on this site, works from the least significant digit up, and leans on every pass being stable: sort by the ones digit, then the tens digit, and stability guarantees the ones-digit order survives underneath. That trick buys correctness without ever looking more than one digit at a time, but it costs an O(n+k) auxiliary output array, rebuilt fresh every pass. American flag sort takes the opposite route: work from the most significant digit down, and instead of writing to a fresh array, count how many values belong in each digit-bucket first, fix each bucket's final index range in advance, then permute the array in place — swapping every value directly into its bucket, following the swap chain until everything lands. Ties within one bucket aren't resolved yet, so it isn't stable; it recurses into the next (less significant) digit within each multi-element bucket to finish the job, the same divide-and-conquer shape as quicksort's partition, just ten-way instead of two-way.

Try it

Enter a comma-separated list of non-negative integers, each 0–99 (this demo fixes the width at two digits — tens, then ones — and caps it at 10 values). Step through: for the active range, each value is counted into a digit bucket, those counts become each bucket's start/end boundary, then the range is permuted in place — bucket by bucket, following each swap chain until every slot in that bucket holds a value that belongs there. Any bucket left with more than one element then recurses on the ones digit. The shaded region is the active range (reusing quicksort's .partition window); the bordered bar is the index currently being examined; the solid bar is its swap partner; settled bars are done for good.

range [0..7] — tens digit
digit 0–9: count / start / end
Press Load, then Step through the sort.

Why it works

The counting pass over a range [lo..hi] tells you exactly how many values have each digit 09 at the current place, before anything moves. Turning those counts into a running total, offset by lo, gives every bucket b a fixed [starts[b], ends[b]) — the exact index range it will occupy once the permutation is done, decided in advance rather than discovered by appending to a growing output. That's what makes the in-place trick possible: since the destination range is already known, each value can be swapped directly to a slot inside its own bucket's range instead of a separate array. The permutation walks each bucket b with a cursor starting at starts[b]: if the value sitting there already belongs in bucket b, advance the cursor; otherwise swap it with whatever sits at its own bucket's cursor, and advance that bucket's cursor instead. Crucially, the current index is not advanced after a foreign swap — the value that just arrived might belong somewhere else too, and has to be checked again before moving on. Follow that chain of swaps far enough and it always closes: every swap places at least one more value in its final bucket, so after at most hi-lo+1 swaps every index in the range holds a value whose current digit matches its bucket.

Grouping by one digit isn't the same as being sorted by it, though — unlike radix sort's stable passes, nothing here preserves relative order within a bucket, so a bucket with more than one element is grouped correctly but internally unordered. That's why the recursion is not optional the way an LSD pass's later stability is: any bucket holding two or more values must recurse on the next digit down before the range counts as sorted. Once the least significant digit (ones, for this fixed two-digit demo) has been processed for a range, every value in it has been distinguished by both digits, so there's nothing left to break a tie — the range is done.

Reference implementation

Fixed two-digit width (base 10), matching the demo above exactly. place starts at 10 (tens) and divides by 10 each recursion until it drops below 1:

function americanFlagSort(arr, lo = 0, hi = arr.length - 1, place = 10) {
  if (hi - lo < 1 || place < 1) return arr;        // 0 or 1 elements, or no digits left

  const counts = new Array(10).fill(0);
  for (let i = lo; i <= hi; i++) counts[digit(arr[i], place)]++;

  const starts = new Array(10);
  const ends = new Array(10);
  let acc = lo;
  for (let b = 0; b < 10; b++) {
    starts[b] = acc;
    acc += counts[b];
    ends[b] = acc;                                  // exclusive
  }

  const next = starts.slice();                       // per-bucket write cursor
  for (let b = 0; b < 10; b++) {
    while (next[b] < ends[b]) {
      const idx = next[b];
      const d = digit(arr[idx], place);
      if (d === b) { next[b]++; continue; }           // already in its bucket
      [arr[idx], arr[next[d]]] = [arr[next[d]], arr[idx]]; // swap into its real bucket
      next[d]++;
    }
  }

  if (place > 1) {
    for (let b = 0; b < 10; b++) americanFlagSort(arr, starts[b], ends[b] - 1, place / 10);
  }
  return arr;
}

function digit(v, place) {
  return Math.floor(v / place) % 10;
}

Pitfalls

The in-place permutation must follow each swap chain, not just walk the range once. It's tempting to write the permutation as a plain for loop over each bucket's index range — advance idx unconditionally after handling it, the way a normal array scan works. That breaks it: when arr[idx] gets swapped out for a foreign value pulled from later in the array, that newly-arrived value might also not belong in bucket b, and a plain loop that always moves on to idx+1 never checks it again. Run for real on [75, 86, 61, 25]: the correct algorithm above produces [25, 61, 75, 86]; the same code with the cursor-based while replaced by a fixed for (idx = starts[b]; idx < ends[b]; idx++) loop produces [61, 25, 75, 86] — still grouped into the right tens-buckets, but 61 and 25 never got swapped past each other because the second one arrived at an index the loop had already moved past. Checked broadly, not just this one case: over 5,000 seeded random trials, the fixed-loop version disagrees with a correct sort on 2,054 of them (roughly 41%) — not a rare edge case, a coin flip.

Skipping the recursion into the next digit leaves buckets grouped but not sorted. The demo's own default array, [70, 91, 45, 24, 92, 12, 75, 3], groups correctly into tens-buckets after just the tens-digit pass — but bucket 9 holds 91 and 92 in whatever order the swap chain happened to leave them, and bucket 7 holds 70 and 75 the same way. Running only the tens-digit pass and stopping there (no ones-digit recursion) on this exact array produces [3, 12, 24, 45, 70, 75, 92, 91] — every other value in its right place, but 91 and 92 swapped, because nothing after the first pass ever compared them by their ones digit. Checked broadly: over the same 5,000 seeded trials, stopping after one pass disagrees with a correct sort on 2,379 of them (roughly 48%). Unlike radix sort, where every pass is required by construction (each digit position must be visited once, in order, or the stability chain breaks), here the recursion depth genuinely varies per bucket — a bucket that happens to land with 0 or 1 elements is already done and never recurses at all, which is real, exploitable savings when the input is already partially sorted by its leading digits, not just an implementation detail to skip carelessly.

Only works directly on fixed-width, non-negative keys, same restriction as radix sort. This demo fixes every key at exactly two digits (0–99) so digit(v, 10) and digit(v, 1) always land in [0, 9]; variable-width keys need padding to a common width first (extra leading zero digits, conceptually), or the shorter keys sort as if their missing high digits are 0 — which is usually what's wanted, but has to be deliberate, not assumed. Negative values need the same offset trick radix sort's own Pitfalls section describes.

Complexity

Time: O(d·(n+k)) — the same shape as radix sort: d digit levels, each level's total work across all its buckets bounded by O(n+k) (one full range scanned for counting, plus one full range permuted, plus k=10 buckets' worth of bookkeeping). Space: the real difference from radix sort — O(k) per active recursion level (just the counts, starts, ends and cursor arrays, ten entries each) instead of a fresh O(n+k) output array per pass, because every swap happens directly inside the input array. Recursion depth is at most d, so total extra space is O(k·d), a constant for fixed-width keys — genuinely smaller than radix sort's per-pass O(n) buffer, paid for by giving up stability and, per the first Pitfall above, a permutation step that's easy to get subtly wrong.

See Choosing a Non-Comparison Sort for how this compares against the site's other nine Non-Comparison Sorts entries — short version: this one when memory is tighter than time and nothing depends on tie order surviving.