Algorithms
Patience Sort and the Longest Increasing Subsequence
Deal 100,000 shuffled cards into greedy piles under one rule — never stack a bigger card on a smaller one — and when you finish, the number of piles you built is exactly the length of the longest increasing run hiding in that deck. That single, almost magical observation is the heart of patience sort: a card-game solitaire re-purposed into an O(n log n) algorithm for the Longest Increasing Subsequence (LIS) problem.
Patience sort processes a sequence left to right, placing each element on the leftmost pile whose top is greater-than-or-equal to it (binary search finds that pile), or starting a new pile if none qualifies. The pile tops always stay sorted, so the final pile count equals the LIS length, and back-pointers reconstruct the actual subsequence.
- TypeGreedy / binary-search algorithm for LIS
- Time complexityO(n log n) worst, average, best
- SpaceO(n) for piles + back-pointers
- Named / datedNamed by C.L. Mallows; attributed to A.S.C. Ross, early 1960s
- Key invariantPile-top values stay sorted; #piles = LIS length
- Used indiff/patch, competitive programming, combinatorics (RSK, Young tableaux)
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
The problem it solves
Given a sequence of n numbers, the Longest Increasing Subsequence is the longest list of elements — not necessarily contiguous — that appears in increasing order. In [3, 1, 4, 1, 5, 9, 2, 6] one LIS is [1, 4, 5, 9] (length 4). LIS is a canonical dynamic programming exercise, but the textbook DP is O(n^2): for each element you scan all earlier elements to extend the best run.
Patience sort attacks the same problem through a solitaire metaphor. You deal cards one at a time onto a row of piles under a single constraint borrowed from the card game patience (British for solitaire): a card may only be placed on a pile whose top card is greater than or equal to it. Greedily minimizing the number of piles turns out to compute the LIS length directly, and — with a little bookkeeping — the LIS itself. The payoff is dropping from quadratic to O(n log n), which is the difference between seconds and hours on a million-element input.
How it works, step by step
Maintain a list of piles, represented by their top cards. Process each value x left to right:
- Find the leftmost pile whose top is
≥ x. Because pile tops are always sorted, this is a binary search. - If found, replace that pile's top with
x(x becomes the new, smaller tail). - If none exists (x is bigger than every top), start a new pile on the right.
The array of pile tops is exactly the classic tails array of the O(n log n) LIS DP: tails[k] holds the smallest possible tail value of any increasing subsequence of length k+1. To recover the actual subsequence, store a back-pointer: when x lands on pile k, point it at the current top of pile k-1. At the end, follow pointers from the top of the last pile.
for x in seq:
k = bisect_left(tails, x) # strictly-increasing LIS
if k == len(tails): tails.append(x)
else: tails[k] = x
parent[x_index] = pile_top_index[k-1] if k>0 else -1
return len(tails) # = LIS lengthUse bisect_left for a strictly increasing LIS; bisect_right for non-decreasing.
Complexity and a worked trace
Each of the n elements triggers one binary search over at most n piles: O(log n) each, so O(n log n) total in the best, average, and worst case. Space is O(n) for the tails array plus O(n) for back-pointers if you reconstruct. There is no bad input — the bound is unconditional, unlike quicksort's O(n^2) worst case.
Trace [3, 1, 4, 1, 5, 9, 2, 6], strictly increasing, showing tails after each step:
x=3 -> [3]
x=1 -> [1] # 1 replaces 3 (pile 0)
x=4 -> [1,4] # new pile
x=1 -> [1,4] # 1 replaces top of pile 0
x=5 -> [1,4,5] # new pile
x=9 -> [1,4,5,9] # new pile
x=2 -> [1,2,5,9] # 2 replaces 4 (pile 1)
x=6 -> [1,2,5,6] # 6 replaces 9 (pile 3)
Final length = 4 piles ⇒ LIS length 4. Note the tails array [1,2,5,6] is not itself a valid subsequence of the input — it mixes elements from incompatible positions. That is why back-pointers, not the tails snapshot, must be used to output the real LIS (e.g. [1,4,5,9]).
Where it shows up in real systems
The most famous production use is text diffing. Computing a minimal diff between two files reduces to a Longest Common Subsequence, and the Hunt–Szymanski algorithm converts LCS into an LIS problem, then solves it with patience-sort-style binary search. Variants of this power git diff's patience diff and histogram algorithms, which anchor on unique lines and run patience sort to align the rest — producing cleaner, more human-readable diffs than the classic Myers algorithm on refactored code.
- Version control and merge tools: Git, Bazaar, and IDE three-way merges use patience/LIS matching to reduce spurious hunks.
- Competitive programming and interviews: the default O(n log n) LIS solution.
- Combinatorics and probability: pile counts connect to the RSK correspondence and Young tableaux; Aldous and Diaconis used patience sort to explain why a random permutation's LIS length concentrates near 2√n, with fluctuations following the Tracy–Widom distribution.
- Bioinformatics: chaining collinear seed matches in genome aligners is an LIS problem.
Patience sort vs. the alternatives
Against the quadratic DP, patience sort wins asymptotically (O(n log n) vs O(n^2)) while using the same O(n) space, and it reconstructs the LIS just as easily via parent pointers. The DP is only preferable when n is tiny or when you need per-index "length of LIS ending here" values for a downstream computation.
Do not confuse patience sort with a general-purpose sorting algorithm. Although you can sort by merging the sorted piles (each pile is decreasing, so repeatedly extract the global minimum with a heap), that merge step is what makes real sorting O(n log n); the piling phase alone does not sort — it counts monotone structure. Compared with merge sort, patience sort's real value is not ordering data but revealing the longest monotone run.
- Fenwick/BIT variant: index tails by value instead of length to answer constrained or counting LIS queries.
- vEB trees: shave the log to O(n log log n) only for small integer universes — rarely worth the constants.
Pitfalls, failure modes, and significance
The classic bug is emitting the tails array as the answer. As the trace showed, tails holds minimal tail values from possibly-incompatible positions; its length is correct but its contents are not a subsequence. Always reconstruct via back-pointers.
- Strict vs. non-strict:
bisect_leftgives a strictly increasing LIS;bisect_rightgives longest non-decreasing. Picking the wrong one silently off-by-ones on inputs with duplicates. - Longest decreasing subsequence: negate the values (or reverse the comparator), don't rewrite the loop.
- Comparator correctness: on custom keys, an inconsistent comparator breaks the sorted-tails invariant and the binary search returns garbage.
Historically, patience sorting was named by C.L. Mallows, who credited its invention to A.S.C. Ross in the early 1960s; Hammersley first noted it computes LIS length, and the 1999 survey by Aldous and Diaconis cemented its place linking a children's card game to deep results in random matrix theory. Few algorithms are simultaneously a one-page interview answer and a bridge to the Baik–Deift–Johansson theorem.
| Method | Time | Space | Notes |
|---|---|---|---|
| Quadratic DP (dp[i]=longest ending at i) | O(n^2) | O(n) | Simplest; also yields LIS via parent array |
| Patience sort (tails + binary search) | O(n log n) | O(n) | Greedy leftmost pile; reconstructs LIS with back-pointers |
| Patience sort + Fenwick/BIT (value-indexed) | O(n log n) | O(n) | Handles LIS-with-constraints / counting variants |
| Van Emde Boas / y-fast tries | O(n log log n) | O(n) | Only for small integer universe; large constants |
| Full sorting of the array | O(n log n) | O(n) | Solves nothing about LIS — different problem |
Frequently asked questions
Does patience sort actually sort the array?
Not by itself. The piling phase only computes the length of the longest increasing subsequence (the number of piles). To turn it into a true sort you must additionally merge the piles — each pile is decreasing, so you repeatedly extract the global minimum, typically with a heap, which costs O(n log n). Most people use patience sort for LIS, not for sorting.
Why does the number of piles equal the LIS length?
Two facts combine. First, by the greedy rule, any single increasing subsequence must place each of its elements on a strictly later pile than the previous one, so its length is at most the pile count. Second, back-pointers exhibit an increasing subsequence whose length equals the pile count. Squeezed from both sides, #piles = LIS length (a Dilworth's-theorem argument).
How do I recover the actual subsequence, not just its length?
Store a back-pointer whenever you place an element: point it at the element currently on top of the previous pile. When you finish, take the top of the last pile and follow back-pointers to the front, then reverse. The tails/pile-tops array itself is NOT a valid subsequence, so never output it directly.
What is the difference between using bisect_left and bisect_right?
bisect_left finds the leftmost pile whose top is >= x, producing a strictly increasing LIS (duplicates excluded). bisect_right finds the leftmost top strictly greater than x, producing a longest non-decreasing subsequence (duplicates allowed). Choosing the wrong one gives off-by-one errors precisely on inputs containing equal values.
How does patience sort relate to git's patience diff?
Diffing reduces to Longest Common Subsequence, which the Hunt-Szymanski method converts into an LIS problem solved by patience-sort-style binary search. Git's patience-diff first matches lines that are unique in both files, then runs patience sort to align the rest, yielding cleaner diffs on reordered or refactored code than the default Myers algorithm.
Is the O(n log n) bound truly worst-case, or just average?
It is a hard worst-case bound. Every element performs exactly one O(log n) binary search over the pile tops, so total work is O(n log n) on all inputs. Unlike quicksort, there is no adversarial ordering that degrades it to quadratic; the only way to beat it asymptotically is with integer-universe tricks like van Emde Boas trees.