Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Priority Queue / Heap

Builds on Read-Write Pointers, which you haven’t finished yet.

P2MediumMental model~5 min speedrun

Quickselect (Kth Largest Element)

The problem

Kth Largest Element in an Array (LC 215)(opens leetcode.com)

nums is an integer array and k is an integer. Find the value that would sit in position k if the array were sorted from largest to smallest — this counts every occurrence of a duplicate value separately, not just distinct values, so a repeated value can fill more than one of those ranked positions.

Bonus challenge: can you find it without fully sorting the array?

Example 1
Input:nums = [3,2,1,5,6,4], k = 2
Output:5
From largest to smallest: 6, 5, 4, 3, 2, 1. The 2nd is 5.

Quickselect

Spot it when: You need the value at one rank of an unsorted array (the k-th largest, the k-th smallest, the median, the k-th closest), or the k items up to that rank in any order, and you need it once, with every value already in memory.
Not this one if…
  • Values arrive one at a time and the k-th largest is asked after every arrival, as in Kth Largest Element in a Stream (LC 703): quickselect needs every value at once and would start over for each question. See Top-K Heap
  • The values you rank are counts, which can never exceed the array's length, as in Top K Frequent Elements (LC 347): a range that small and known in advance is the signal for a pass that is linear every time, with no random pivot. See Bucket Sort
Quickselect
Python
1import random
2 
3 
4def find_kth_largest(nums: list[int], k: int) -> int:
5 """Kth Largest Element in an Array: quickselect with a random pivot and a three-way partition.
6 Time: O(N) average, O(N^2) worst case | Space: O(1)
7 """
8 target = len(nums) - k # The k-th largest sits at index n - k once sorted ascending
9 lo, hi = 0, len(nums) - 1 # target always stays inside nums[lo..hi]
10 
11 while True:
12 # 1. Pick a Random Pivot:
13 # A fixed nums[hi] is always the largest value on a sorted input, so a random pick means no input can force the worst case.
14 pivot = nums[random.randint(lo, hi)] # Random pivot: no fixed input can force the worst case
15 lt, i, gt = lo, lo, hi # < pivot: nums[lo..lt-1] | == pivot: nums[lt..i-1] | > pivot: nums[gt+1..hi]
16 
17 # 2. Three-Way Partition Around the Pivot:
18 # Every value lands in exactly one of three blocks; a value equal to the pivot only moves i, since it already sits in the middle block.
19 while i <= gt:
20 if nums[i] < pivot:
21 nums[lt], nums[i] = nums[i], nums[lt] # Grow the smaller block
22 lt += 1
23 i += 1
24 elif nums[i] > pivot:
25 nums[i], nums[gt] = nums[gt], nums[i] # Grow the larger block
26 gt -= 1 # The value swapped into nums[i] is unread: i stays
27 else:
28 i += 1 # Equal to pivot: it stays in the middle block
29 
30 # 3. Keep Only the Side That Can Hold target:
31 # The partition put every copy of pivot exactly where sorting would, so target's rank decides which single side to keep.
32 if target < lt:
33 hi = lt - 1 # The answer is among the smaller values
34 elif target > gt:
35 lo = gt + 1 # The answer is among the larger values
36 else:
37 return pivot # nums[lt..gt] all equal pivot, and target is one of them
Invariant: target always lies in nums[lo..hi], and after each partition nums[lo..lt-1] is smaller than pivot, nums[gt+1..hi] is larger, and nums[lt..gt] holds the copies of pivot at their sorted places.

Quickselect finds the value that would sit at index target = len(nums) - k if nums were sorted, without sorting it: pick a pivot, partition nums[lo..hi] into the values smaller than pivot (nums[lo..lt-1]), equal to it (nums[lt..gt]) and larger (nums[gt+1..hi]), then keep only the block that holds target. This is correct because a partition puts every copy of pivot exactly where sorting would: nothing before lt is larger and nothing after gt is smaller. So if lt <= target <= gt the answer is pivot; otherwise the answer's rank is decided inside one side alone, and hi = lt - 1 or lo = gt + 1 throws the other side away for good. The trap is the pivot choice: a fixed nums[hi] (or nums[lo]) is always the largest (or smallest) value of a sorted input, so while target lies below it each pass removes a single value: asking a sorted input for its smallest value (k = n) rescans almost the whole array n times; pivot = nums[random.randint(lo, hi)] means no input can force that.

See it run

Quickselect, step by step

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 0, 0, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 0, 1, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 1, 2, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 1, 3, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 2, 4, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 7

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 6

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 5

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 4

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 4

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 5, 8

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 5, 7

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 7

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 6

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 5

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 5 · answer 4

Each box is one entry of nums, with its index above it. Entries without a tag are not read yet, or were discarded by lo and hi.

  • pivot the value picked at random
  • smaller a value below the pivot
  • equal a copy of the pivot
  • i the value being read
  • larger a value above the pivot

Start: target = 5, lo = 0, hi = 8. The random pick is nums[0], so pivot = 3.

nums[0] = 3 equals the pivot 3, so only i moves. Then i = 1.

nums[1] = 2 is smaller than 3, so swap it with nums[lt = 0]. Then lt = 1 and i = 2.

nums[2] = 3 equals the pivot 3, so only i moves. Then i = 3.

nums[3] = 1 is smaller than 3, so swap it with nums[lt = 1]. Then lt = 2 and i = 4.

nums[4] = 2 is smaller than 3, so swap it with nums[lt = 2]. Then lt = 3 and i = 5.

nums[5] = 4 is larger than 3, so swap it with nums[gt = 8]. Then gt = 7, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 6 is larger than 3, so swap it with nums[gt = 7]. Then gt = 6, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 5 is larger than 3, so swap it with nums[gt = 6]. Then gt = 5, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 5 is larger than 3, so swap it with nums[gt = 5]. Then gt = 4, and i stays at 5, because the swapped-in value is not read yet.

The partition is done, and nums[3..4] holds 2 copies of 3. target = 5 is above gt = 4, so lo becomes 5.

target = 5, lo = 5, hi = 8. The random pick is nums[8], so pivot = 4.

nums[5] = 5 is larger than 4, so swap it with nums[gt = 8]. Then gt = 7, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 4 equals the pivot 4, so only i moves. Then i = 6.

nums[6] = 5 is larger than 4, so swap it with nums[gt = 7]. Then gt = 6, and i stays at 6, because the swapped-in value is not read yet.

nums[6] = 6 is larger than 4, so swap it with nums[gt = 6]. Then gt = 5, and i stays at 6, because the swapped-in value is not read yet.

Done: target = 5 lies in nums[5..5], which holds only 4. Return 4, the 4-th largest.

03pivot
12
23
31
42
54
65
75
86
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Using the fixed pivot nums[hi] instead of a random one

    Mistake
    pivot = nums[hi]
    Fix
    pivot = nums[random.randint(lo, hi)]

    What happens: On a sorted input, nums[hi] is always the largest value. If target lies below it, each pass removes only that one value. With 10^5 values and k = 105, the passes scan about 5 * 10^9 values in total. The answer stays right, so this mistake has no example. A random pivot means no input can force this.

  2. 2

    Gotcha 2: Setting target = k - 1 for the k-th largest

    Mistake
    target = k - 1
    Fix
    target = len(nums) - k

    What happens: In ascending order, the largest value sits at index n - 1. So the k-th largest sits at index n - k. Index k - 1 holds the k-th smallest value instead.

    Example: Kth Largest Element in an Array (LC 215): nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 2Fix: returns 5
  3. 3

    Gotcha 3: Putting values equal to pivot into the smaller block

    Mistake
    if nums[i] <= pivot:
    Fix
    if nums[i] < pivot:

    What happens: With <=, every copy of pivot joins the smaller block. The middle block nums[lt..gt] is then empty, so lt is gt + 1. The else branch that returns pivot needs lt <= target <= gt, so it can never run. The loop never ends.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: the loop never endsFix: returns 5
  4. 4

    Gotcha 4: Moving i forward after swapping with nums[gt]

    Mistake
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1
    i += 1
    Fix
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1

    What happens: The value swapped in from nums[gt] has not been read yet. Moving i forward skips it. It stays in the middle block, even when it is not equal to pivot. The partition is then wrong, and so is the answer. Sort Colors (LC 75) uses the same rule.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 3Fix: returns 5

Deep Dive

Why one partition fixes the pivot's place

Take Kth Largest Element in an Array (LC 215) with nums = [3, 2, 3, 1, 2, 4, 5, 5, 6] and k = 4. There are 9 values, so target = len(nums) - k = 5. Index 5 is where the answer would sit if nums were sorted.

The first random pick is nums[0], so pivot = 3. The three-way partition reads nums[lo..hi] once. Afterwards nums is [2, 1, 2, 3, 3, 5, 5, 6, 4]. Indexes 0 to 2 hold the values smaller than 3, indexes 3 and 4 hold the two copies of 3, and indexes 5 to 8 hold the larger values.

In the sorted array, the three values smaller than 3 come first, and the two copies of 3 follow. So the two copies of 3 sit at indexes 3 and 4 in the sorted array too. The partition placed them correctly without sorting anything else.

Now target = 5 is above gt = 4. The answer is among the larger values, so lo becomes 5. The values at indexes 0 to 4 are never read again. The second random pick is nums[8], so pivot = 4. After the partition, nums[5..5] holds the pivot, and lt = gt = 5 = target. The answer is 4. In the sorted array [1, 2, 2, 3, 3, 4, 5, 5, 6], index 5 holds 4.

Why the average cost is O(n) and the worst case is O(n^2)

A pass over nums[lo..hi] costs one read per value. The next pass works on one side only. If the pivot is close to the middle in rank, the kept side is about half of the values, and the reads add up to about n + n/2 + n/4 + ..., which is about 2n. A random pivot lands in the middle half of the ranks half of the time, so the kept side shrinks by a constant fraction every two passes on average. The total is O(n) on average.

The numbers from a test agree. On a shuffled array of 10000 values with k = 5000, the code read about 34000 values on average over 50 runs, which is about 3.4 times n. Sorting the same array would cost about n log2 n, which is about 133000 comparisons.

The worst case happens when every pivot is the smallest or the largest value of the range. With the fixed pivot nums[hi] on a sorted array and k = n, each pass removes one value. For n = 10000, the code made 10000 passes and read 50005000 values, which is exactly n * (n + 1) / 2. With a random pivot, 20 runs on the same input read about 19700 values on average, about 2n.

Randomness does not remove the worst case. It removes the input that causes it: no fixed array can make the random choices bad every time. A deterministic O(n) worst case exists (the median-of-medians pivot rule), but the constant is large, and interviews rarely ask for it.

Equal values, and what is left in nums

The partition has three blocks, not two. A value equal to pivot stays in the middle block, and lt..gt is never empty after a pass, because the pivot itself is in it. The else branch returns as soon as target falls inside nums[lt..gt].

This makes many equal values cheap. For an array of 1000 copies of 7, the first pass puts all of them in the middle block, and the loop returns after 1 pass. A two-way split that sends the equal values to the smaller side leaves the middle block empty, and the loop can never return.

The code changes nums. When it returns, nums[target:] holds the k largest values in any order. In a test of 5000 random arrays, sorted(nums[target:]) equalled the last k values of sorted(original) every time. This is why the trigger says you can also take the k items up to that rank. If the caller needs the original order, copy nums first, which costs O(n) extra space.

The median is the same question. For an odd length, the median is the k-th largest with k = (len(nums) + 1) // 2.

Quickselect or the Top-K Heap

Both answer "which values are the largest". The Top-K Heap costs O(n log k) time and O(k) space, and it works on a stream, because it needs only the next value. Quickselect costs O(n) on average and O(1) extra space, but it needs every value in memory at once and it reorders nums.

For Kth Largest Element in a Stream (LC 703), Quickselect would restart for every new number. For Top K Frequent Elements (LC 347), the values are counts, which are never bigger than len(nums). A pass of buckets does that in O(n) every time, with no random choice.

Choose Quickselect when the array is given once, fits in memory, and you want one rank or the group of values up to that rank.

60-Second Pitch

I need the k-th largest value of an array that is fully in memory, once. Sorting works in O(n log n), but I can do better.

I use Quickselect. The k-th largest is at index target = n - k in sorted order. I pick a random pivot and partition the range into smaller, equal and larger values with a three-way split. The equal block sits at its final indexes.

If target is inside the equal block, I return the pivot. Otherwise I keep only the side that holds target, and the other side is never read again.

The average time is O(n), the worst case is O(n^2), and the random pivot means no input can force it. The space is O(1), and the array is reordered. For a stream, or if I must not change the input, I use the Top-K Heap instead.

Common Questions

Why is the average cost O(n) and not O(n log n)?

Quickselect follows one side of the pivot, where sorting follows both. The sizes shrink by a constant fraction on average, so the reads add up to a constant times n: about 3.4 times n in a test with k = n / 2.

Why a random pivot?

A fixed pivot such as nums[hi] is the largest value of a sorted array, so each pass removes one value. A random pivot means no fixed input can force that. The worst case is still possible, but its chance is tiny.

Why a three-way partition?

Copies of pivot go to a middle block that is already in its final place. With many equal values, one pass finishes, and the loop is guaranteed to return when target is in that block.

Quickselect or a heap?

Quickselect is O(n) on average and O(1) extra space, but it needs all the values and reorders them. The Top-K Heap is O(n log k), works on a stream and does not change the input.

How do you get the median or the k smallest?

For an odd length, the median is the k-th largest with k = (len(nums) + 1) // 2. For the k-th smallest, use target = k - 1.

SWE Reasoning

Spot it

  • The question asks for one rank of an unsorted array: the k-th largest, the k-th smallest or the median.
  • The whole array is in memory, and no stream follows.
  • The answer may be given as a value, or as the group of the k values up to that rank in any order.
  • Sorting costs O(n log n), and the question hints at O(n).

The invariant, step by step

  1. target = len(nums) - k is the index of the answer in the sorted array. lo and hi start at the ends of nums.
  2. A random pivot is taken from nums[lo..hi]. The three-way partition sorts the range into smaller values, copies of pivot and larger values.
  3. After the partition, nums[lt..gt] holds the copies of pivot at the indexes where sorting would put them.
  4. If target is below lt, the answer is among the smaller values, so hi = lt - 1. If target is above gt, lo = gt + 1. The other side is discarded for good.
  5. If lt <= target <= gt, the answer is pivot, and the loop returns.

Traps

  • A fixed pivot nums[hi] on a sorted array removes one value per pass, so n = 10000 reads 50005000 values.
  • target = k - 1 gives the k-th smallest. The k-th largest is at len(nums) - k.
  • Sending values equal to pivot to the smaller side makes the middle block empty, so the loop never returns.
  • Moving i after a swap with nums[gt] skips a value that was never read. After that swap, only gt moves.

Practice

Canonical LeetCode:Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)Find the Kth Largest Integer in the Array (LC 1985)Find Subsequence of Length K With the Largest Sum (LC 2099)
Theory Context•Priority Queue / Heap

Builds on Read-Write Pointers, which you haven’t finished yet.

P2MediumMental model~5 min speedrun

Quickselect (Kth Largest Element)

The problem

Kth Largest Element in an Array (LC 215)(opens leetcode.com)

nums is an integer array and k is an integer. Find the value that would sit in position k if the array were sorted from largest to smallest — this counts every occurrence of a duplicate value separately, not just distinct values, so a repeated value can fill more than one of those ranked positions.

Bonus challenge: can you find it without fully sorting the array?

Example 1
Input:nums = [3,2,1,5,6,4], k = 2
Output:5
From largest to smallest: 6, 5, 4, 3, 2, 1. The 2nd is 5.

Quickselect

Spot it when: You need the value at one rank of an unsorted array (the k-th largest, the k-th smallest, the median, the k-th closest), or the k items up to that rank in any order, and you need it once, with every value already in memory.
Not this one if…
  • Values arrive one at a time and the k-th largest is asked after every arrival, as in Kth Largest Element in a Stream (LC 703): quickselect needs every value at once and would start over for each question. See Top-K Heap
  • The values you rank are counts, which can never exceed the array's length, as in Top K Frequent Elements (LC 347): a range that small and known in advance is the signal for a pass that is linear every time, with no random pivot. See Bucket Sort
Quickselect
Python
1import random
2 
3 
4def find_kth_largest(nums: list[int], k: int) -> int:
5 """Kth Largest Element in an Array: quickselect with a random pivot and a three-way partition.
6 Time: O(N) average, O(N^2) worst case | Space: O(1)
7 """
8 target = len(nums) - k # The k-th largest sits at index n - k once sorted ascending
9 lo, hi = 0, len(nums) - 1 # target always stays inside nums[lo..hi]
10 
11 while True:
12 # 1. Pick a Random Pivot:
13 # A fixed nums[hi] is always the largest value on a sorted input, so a random pick means no input can force the worst case.
14 pivot = nums[random.randint(lo, hi)] # Random pivot: no fixed input can force the worst case
15 lt, i, gt = lo, lo, hi # < pivot: nums[lo..lt-1] | == pivot: nums[lt..i-1] | > pivot: nums[gt+1..hi]
16 
17 # 2. Three-Way Partition Around the Pivot:
18 # Every value lands in exactly one of three blocks; a value equal to the pivot only moves i, since it already sits in the middle block.
19 while i <= gt:
20 if nums[i] < pivot:
21 nums[lt], nums[i] = nums[i], nums[lt] # Grow the smaller block
22 lt += 1
23 i += 1
24 elif nums[i] > pivot:
25 nums[i], nums[gt] = nums[gt], nums[i] # Grow the larger block
26 gt -= 1 # The value swapped into nums[i] is unread: i stays
27 else:
28 i += 1 # Equal to pivot: it stays in the middle block
29 
30 # 3. Keep Only the Side That Can Hold target:
31 # The partition put every copy of pivot exactly where sorting would, so target's rank decides which single side to keep.
32 if target < lt:
33 hi = lt - 1 # The answer is among the smaller values
34 elif target > gt:
35 lo = gt + 1 # The answer is among the larger values
36 else:
37 return pivot # nums[lt..gt] all equal pivot, and target is one of them
Invariant: target always lies in nums[lo..hi], and after each partition nums[lo..lt-1] is smaller than pivot, nums[gt+1..hi] is larger, and nums[lt..gt] holds the copies of pivot at their sorted places.

Quickselect finds the value that would sit at index target = len(nums) - k if nums were sorted, without sorting it: pick a pivot, partition nums[lo..hi] into the values smaller than pivot (nums[lo..lt-1]), equal to it (nums[lt..gt]) and larger (nums[gt+1..hi]), then keep only the block that holds target. This is correct because a partition puts every copy of pivot exactly where sorting would: nothing before lt is larger and nothing after gt is smaller. So if lt <= target <= gt the answer is pivot; otherwise the answer's rank is decided inside one side alone, and hi = lt - 1 or lo = gt + 1 throws the other side away for good. The trap is the pivot choice: a fixed nums[hi] (or nums[lo]) is always the largest (or smallest) value of a sorted input, so while target lies below it each pass removes a single value: asking a sorted input for its smallest value (k = n) rescans almost the whole array n times; pivot = nums[random.randint(lo, hi)] means no input can force that.

See it run

Quickselect, step by step

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 0, 0, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 0, 1, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 1, 2, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 1, 3, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 2, 4, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 8

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 7

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 6

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 5

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 4

target 5 · lo, hi 0, 8 · pivot 3 · lt, i, gt 3, 5, 4

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 5, 8

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 5, 7

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 7

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 6

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 5

target 5 · lo, hi 5, 8 · pivot 4 · lt, i, gt 5, 6, 5 · answer 4

Each box is one entry of nums, with its index above it. Entries without a tag are not read yet, or were discarded by lo and hi.

  • pivot the value picked at random
  • smaller a value below the pivot
  • equal a copy of the pivot
  • i the value being read
  • larger a value above the pivot

Start: target = 5, lo = 0, hi = 8. The random pick is nums[0], so pivot = 3.

nums[0] = 3 equals the pivot 3, so only i moves. Then i = 1.

nums[1] = 2 is smaller than 3, so swap it with nums[lt = 0]. Then lt = 1 and i = 2.

nums[2] = 3 equals the pivot 3, so only i moves. Then i = 3.

nums[3] = 1 is smaller than 3, so swap it with nums[lt = 1]. Then lt = 2 and i = 4.

nums[4] = 2 is smaller than 3, so swap it with nums[lt = 2]. Then lt = 3 and i = 5.

nums[5] = 4 is larger than 3, so swap it with nums[gt = 8]. Then gt = 7, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 6 is larger than 3, so swap it with nums[gt = 7]. Then gt = 6, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 5 is larger than 3, so swap it with nums[gt = 6]. Then gt = 5, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 5 is larger than 3, so swap it with nums[gt = 5]. Then gt = 4, and i stays at 5, because the swapped-in value is not read yet.

The partition is done, and nums[3..4] holds 2 copies of 3. target = 5 is above gt = 4, so lo becomes 5.

target = 5, lo = 5, hi = 8. The random pick is nums[8], so pivot = 4.

nums[5] = 5 is larger than 4, so swap it with nums[gt = 8]. Then gt = 7, and i stays at 5, because the swapped-in value is not read yet.

nums[5] = 4 equals the pivot 4, so only i moves. Then i = 6.

nums[6] = 5 is larger than 4, so swap it with nums[gt = 7]. Then gt = 6, and i stays at 6, because the swapped-in value is not read yet.

nums[6] = 6 is larger than 4, so swap it with nums[gt = 6]. Then gt = 5, and i stays at 6, because the swapped-in value is not read yet.

Done: target = 5 lies in nums[5..5], which holds only 4. Return 4, the 4-th largest.

03pivot
12
23
31
42
54
65
75
86
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Using the fixed pivot nums[hi] instead of a random one

    Mistake
    pivot = nums[hi]
    Fix
    pivot = nums[random.randint(lo, hi)]

    What happens: On a sorted input, nums[hi] is always the largest value. If target lies below it, each pass removes only that one value. With 10^5 values and k = 105, the passes scan about 5 * 10^9 values in total. The answer stays right, so this mistake has no example. A random pivot means no input can force this.

  2. 2

    Gotcha 2: Setting target = k - 1 for the k-th largest

    Mistake
    target = k - 1
    Fix
    target = len(nums) - k

    What happens: In ascending order, the largest value sits at index n - 1. So the k-th largest sits at index n - k. Index k - 1 holds the k-th smallest value instead.

    Example: Kth Largest Element in an Array (LC 215): nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 2Fix: returns 5
  3. 3

    Gotcha 3: Putting values equal to pivot into the smaller block

    Mistake
    if nums[i] <= pivot:
    Fix
    if nums[i] < pivot:

    What happens: With <=, every copy of pivot joins the smaller block. The middle block nums[lt..gt] is then empty, so lt is gt + 1. The else branch that returns pivot needs lt <= target <= gt, so it can never run. The loop never ends.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: the loop never endsFix: returns 5
  4. 4

    Gotcha 4: Moving i forward after swapping with nums[gt]

    Mistake
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1
    i += 1
    Fix
    nums[i], nums[gt] = nums[gt], nums[i]
    gt -= 1

    What happens: The value swapped in from nums[gt] has not been read yet. Moving i forward skips it. It stays in the middle block, even when it is not equal to pivot. The partition is then wrong, and so is the answer. Sort Colors (LC 75) uses the same rule.

    Example: nums = [3, 2, 1, 5, 6, 4], k = 2
    Mistake: returns 3Fix: returns 5

Deep Dive

Why one partition fixes the pivot's place

Take Kth Largest Element in an Array (LC 215) with nums = [3, 2, 3, 1, 2, 4, 5, 5, 6] and k = 4. There are 9 values, so target = len(nums) - k = 5. Index 5 is where the answer would sit if nums were sorted.

The first random pick is nums[0], so pivot = 3. The three-way partition reads nums[lo..hi] once. Afterwards nums is [2, 1, 2, 3, 3, 5, 5, 6, 4]. Indexes 0 to 2 hold the values smaller than 3, indexes 3 and 4 hold the two copies of 3, and indexes 5 to 8 hold the larger values.

In the sorted array, the three values smaller than 3 come first, and the two copies of 3 follow. So the two copies of 3 sit at indexes 3 and 4 in the sorted array too. The partition placed them correctly without sorting anything else.

Now target = 5 is above gt = 4. The answer is among the larger values, so lo becomes 5. The values at indexes 0 to 4 are never read again. The second random pick is nums[8], so pivot = 4. After the partition, nums[5..5] holds the pivot, and lt = gt = 5 = target. The answer is 4. In the sorted array [1, 2, 2, 3, 3, 4, 5, 5, 6], index 5 holds 4.

Why the average cost is O(n) and the worst case is O(n^2)

A pass over nums[lo..hi] costs one read per value. The next pass works on one side only. If the pivot is close to the middle in rank, the kept side is about half of the values, and the reads add up to about n + n/2 + n/4 + ..., which is about 2n. A random pivot lands in the middle half of the ranks half of the time, so the kept side shrinks by a constant fraction every two passes on average. The total is O(n) on average.

The numbers from a test agree. On a shuffled array of 10000 values with k = 5000, the code read about 34000 values on average over 50 runs, which is about 3.4 times n. Sorting the same array would cost about n log2 n, which is about 133000 comparisons.

The worst case happens when every pivot is the smallest or the largest value of the range. With the fixed pivot nums[hi] on a sorted array and k = n, each pass removes one value. For n = 10000, the code made 10000 passes and read 50005000 values, which is exactly n * (n + 1) / 2. With a random pivot, 20 runs on the same input read about 19700 values on average, about 2n.

Randomness does not remove the worst case. It removes the input that causes it: no fixed array can make the random choices bad every time. A deterministic O(n) worst case exists (the median-of-medians pivot rule), but the constant is large, and interviews rarely ask for it.

Equal values, and what is left in nums

The partition has three blocks, not two. A value equal to pivot stays in the middle block, and lt..gt is never empty after a pass, because the pivot itself is in it. The else branch returns as soon as target falls inside nums[lt..gt].

This makes many equal values cheap. For an array of 1000 copies of 7, the first pass puts all of them in the middle block, and the loop returns after 1 pass. A two-way split that sends the equal values to the smaller side leaves the middle block empty, and the loop can never return.

The code changes nums. When it returns, nums[target:] holds the k largest values in any order. In a test of 5000 random arrays, sorted(nums[target:]) equalled the last k values of sorted(original) every time. This is why the trigger says you can also take the k items up to that rank. If the caller needs the original order, copy nums first, which costs O(n) extra space.

The median is the same question. For an odd length, the median is the k-th largest with k = (len(nums) + 1) // 2.

Quickselect or the Top-K Heap

Both answer "which values are the largest". The Top-K Heap costs O(n log k) time and O(k) space, and it works on a stream, because it needs only the next value. Quickselect costs O(n) on average and O(1) extra space, but it needs every value in memory at once and it reorders nums.

For Kth Largest Element in a Stream (LC 703), Quickselect would restart for every new number. For Top K Frequent Elements (LC 347), the values are counts, which are never bigger than len(nums). A pass of buckets does that in O(n) every time, with no random choice.

Choose Quickselect when the array is given once, fits in memory, and you want one rank or the group of values up to that rank.

60-Second Pitch

I need the k-th largest value of an array that is fully in memory, once. Sorting works in O(n log n), but I can do better.

I use Quickselect. The k-th largest is at index target = n - k in sorted order. I pick a random pivot and partition the range into smaller, equal and larger values with a three-way split. The equal block sits at its final indexes.

If target is inside the equal block, I return the pivot. Otherwise I keep only the side that holds target, and the other side is never read again.

The average time is O(n), the worst case is O(n^2), and the random pivot means no input can force it. The space is O(1), and the array is reordered. For a stream, or if I must not change the input, I use the Top-K Heap instead.

Common Questions

Why is the average cost O(n) and not O(n log n)?

Quickselect follows one side of the pivot, where sorting follows both. The sizes shrink by a constant fraction on average, so the reads add up to a constant times n: about 3.4 times n in a test with k = n / 2.

Why a random pivot?

A fixed pivot such as nums[hi] is the largest value of a sorted array, so each pass removes one value. A random pivot means no fixed input can force that. The worst case is still possible, but its chance is tiny.

Why a three-way partition?

Copies of pivot go to a middle block that is already in its final place. With many equal values, one pass finishes, and the loop is guaranteed to return when target is in that block.

Quickselect or a heap?

Quickselect is O(n) on average and O(1) extra space, but it needs all the values and reorders them. The Top-K Heap is O(n log k), works on a stream and does not change the input.

How do you get the median or the k smallest?

For an odd length, the median is the k-th largest with k = (len(nums) + 1) // 2. For the k-th smallest, use target = k - 1.

SWE Reasoning

Spot it

  • The question asks for one rank of an unsorted array: the k-th largest, the k-th smallest or the median.
  • The whole array is in memory, and no stream follows.
  • The answer may be given as a value, or as the group of the k values up to that rank in any order.
  • Sorting costs O(n log n), and the question hints at O(n).

The invariant, step by step

  1. target = len(nums) - k is the index of the answer in the sorted array. lo and hi start at the ends of nums.
  2. A random pivot is taken from nums[lo..hi]. The three-way partition sorts the range into smaller values, copies of pivot and larger values.
  3. After the partition, nums[lt..gt] holds the copies of pivot at the indexes where sorting would put them.
  4. If target is below lt, the answer is among the smaller values, so hi = lt - 1. If target is above gt, lo = gt + 1. The other side is discarded for good.
  5. If lt <= target <= gt, the answer is pivot, and the loop returns.

Traps

  • A fixed pivot nums[hi] on a sorted array removes one value per pass, so n = 10000 reads 50005000 values.
  • target = k - 1 gives the k-th smallest. The k-th largest is at len(nums) - k.
  • Sending values equal to pivot to the smaller side makes the middle block empty, so the loop never returns.
  • Moving i after a swap with nums[gt] skips a value that was never read. After that swap, only gt moves.

Practice

Canonical LeetCode:Kth Largest Element in an Array (LC 215)K Closest Points to Origin (LC 973)Find the Kth Largest Integer in the Array (LC 1985)Find Subsequence of Length K With the Largest Sum (LC 2099)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•