Builds on Feasible Function, Staircase Search and Lower Bound, which you haven’t finished yet.
Binary Search on Value Range (Kth Smallest in Sorted Matrix)
k.matrix is an n x n grid whose values never decrease from left to right along a row or from top to bottom down a column. Find the k-th smallest of its n² values, where repeated values each take their own place in the count (in [[1, 1], [1, 2]], the third smallest value is still 1). The solution must use less than O(n²) extra memory, so copying all the values and sorting them is ruled out.
1, 5, 9, 10, 11, 12, 13, 13, 15; counting the two 13s separately, the 8th is 13.Kth Smallest
values you can count âž” is the count of values at most x at least k? âž” the first x where it is| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | Â |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | Â |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
k values are at most lo - 1, and at least k values are at most hi, so the k-th smallest value is from lo to hi.The number of values that are at most x never goes down when x goes up. So at_least_k_values(x) is False for small x and True from some x on, and the first True is the k-th smallest value. Run Lower Bound over the values, not over the positions, and let a count answer each question. The count changes only at a value that is in the matrix, so the first x that reaches k is always a value of the matrix. The range runs from lo = matrix[0][0] to hi = matrix[-1][-1] + 1, where the count is the total. The trap is return count == k instead of return count >= k. For k = 7, the count is 6 at x = 12 and 8 at x = 13, because 13 appears twice, so the count is never 7. Then the check is never True, and the call returns 16, which is not a value of the matrix.
See it run
Kth Smallest in a Sorted Matrix: Lower Bound over the values
k 3lo 1hi 7mid none
The top row lists every candidate value x. The count row shows, for each tested mid, how many matrix values are at most mid.
lothe first position that can still be the answerhithe answer is at hi or before it; hi can be one past the endmidthe position the check reads nowlo+midlo and mid are at the same positioncounthow many matrix values are at most this midanswerthe first True, where lo and hi meet
matrix = [[1,5],[2,6]] and k = 3. The candidates are the values from lo = 1 up to hi = 7, which is one past the largest value.
Gotcha 1 of 5
- The trap
Gotcha 1: Asking for "exactly
k" instead of "at leastk"Mistakereturn count == kFixreturn count >= kWhat happens: The count grows by the number of copies of
x, so it can skipk. Fork = 7, the count is 6 atx = 12and 8 atx = 13, because 13 appears twice. The count is never 7, so "exactly 7" is never True. The search then returnshi, which is 16, one past the largest value. The right answer is 13.Example: matrix = [[1, 5, 9], [10, 11, 13], [12, 13, 15]], k = 7Mistake: returns 16Fix: returns 13
- You must return the k smallest items themselves, not only the value of the k-th, as in Find K Pairs with Smallest Sums (LC 373): a count below
midnever lists them. See Best-First Heap - The answer is the smallest setting (a speed, a capacity) that makes a plan work, as in Koko Eating Bananas (LC 875): there is no rank
kto reach. See Feasible Function
Theory
The question we build
What we know
The matrix is sorted along every row and every column. Equal values count separately.
What the problem gives us
The input is a sorted matrix and k.
We return the k-th smallest value.
The question we build
For each value x we ask: "are at least k values at most x?"
We use matrix = [[1, 5], [2, 6]] and k = 3. Sorted, the values are 1, 2, 5, 6, so the third smallest is 5.
The counts for x from 1 to 6 are 1, 2, 2, 2, 3, 4.
The answers are False, False, False, False, True, True.
The first True is x = 5, which is the answer.
The count is 3 first at x = 5. At x = 3 and x = 4 the count stays 2, because no value of the matrix is there.
Narrowing down to the template
The first True is the answer, so the call is binary_search_lower_bound(matrix[0][0], matrix[-1][-1] + 1, at_least_k_values).
Step 1: Sort everything
The k-th smallest value is the value at index k - 1 of the sorted whole.
The sort states this in a few lines.
| 1 | def kth_smallest_sorted(matrix, k): |
| 2 | values = [] |
| 3 | for row in matrix: |
| 4 | for value in row: |
| 5 | values.append(value) |
| 6 | values.sort() |
| 7 | return values[k - 1] |
Cost: O(N log N) time and O(N) memory, where N is the number of values. At LeetCode's limit of n = 300, the copy holds 9 * 104 values.
Step 2: Search the values with a count check
Lower Bound needs a check that reads False, then True. The count gives it: at_least_k_values(x) compares the count of values at most x with k.
The count comes from the staircase walk of Staircase Search, started at the bottom-left corner.
When a cell is at most x, the whole column above it is at most x too, so the walk adds row + 1 and moves right. Otherwise the walk moves up.
The search never builds the sorted list. It only counts, for one value at a time.
| 1 | def kth_smallest(matrix, k): |
| 2 | rows, cols = len(matrix), len(matrix[0]) |
| 3 | Â |
| 4 | def at_least_k_values(x): |
| 5 | count = 0 |
| 6 | row, col = rows - 1, 0 # start at the bottom-left cell |
| 7 | while row >= 0 and col < cols: |
| 8 | current_value = matrix[row][col] |
| 9 | if current_value <= x: |
| 10 | # Every entry from the top down to this row in column col is also at most x. |
| 11 | count += row + 1 |
| 12 | col += 1 # move right to check the next column |
| 13 | else: |
| 14 | row -= 1 # the current value is too large; move up to a smaller value |
| 15 | return count >= k |
| 16 | Â |
| 17 | lo = matrix[0][0] # the smallest value is the top-left cell |
| 18 | hi = matrix[-1][-1] + 1 # one past the largest value, which is the bottom-right cell |
| 19 | return binary_search_lower_bound(lo, hi, at_least_k_values) |
Cost: O((rows + cols) * log(max - min + 2)). Each check is a walk from the bottom-left corner, and it never reads the same cell twice.
| Item | Sort everything | Count check |
|---|---|---|
values.sort() and values[k - 1] | at_least_k_values and one Lower Bound call | The sort builds the whole sorted list. The count check never builds it, and it never reads a cell more than once. |
The shorter version
| 1 | import bisect |
| 2 | Â |
| 3 | def kth_smallest_short(matrix, k): |
| 4 | rows, cols = len(matrix), len(matrix[0]) |
| 5 | Â |
| 6 | def at_least_k_values(x): |
| 7 | count = 0 |
| 8 | row, col = rows - 1, 0 |
| 9 | while row >= 0 and col < cols: |
| 10 | current_value = matrix[row][col] |
| 11 | if current_value <= x: |
| 12 | count += row + 1 |
| 13 | col += 1 |
| 14 | else: |
| 15 | row -= 1 |
| 16 | return count >= k |
| 17 | Â |
| 18 | lo, hi = matrix[0][0], matrix[-1][-1] + 1 |
| 19 | return bisect.bisect_left(range(lo, hi), True, key=at_least_k_values) + lo |
range(lo, hi) holds the candidate values.
bisect_left returns a position in that range, so + lo turns it back into a value.
This form needs Python 3.10 or later, because of key=.
An alternative: one `bisect_right` per row
On one machine, the staircase walk is cheaper: O(rows + cols) for one check, against O(rows * log(cols + 1)) for one bisect_right call per row.
But the walk is one sequential pass over adjacent rows and columns, so it cannot be split across machines.
A count with bisect_right can be split: each row's count comes from one bisect_right call on that row alone. Each row can live on its own machine, and only that row's count travels across the network.
| 1 | import bisect |
| 2 | Â |
| 3 | def kth_smallest_by_row(matrix, k): |
| 4 | def at_least_k_values(x): |
| 5 | count = 0 |
| 6 | for row in matrix: |
| 7 | count += bisect.bisect_right(row, x) # how many values in this row are at most x |
| 8 | return count >= k |
| 9 | Â |
| 10 | lo = matrix[0][0] # the smallest value is the top-left cell |
| 11 | hi = matrix[-1][-1] + 1 # one past the largest value, which is the bottom-right cell |
| 12 | return binary_search_lower_bound(lo, hi, at_least_k_values) |
bisect_right(row, x) is the number of values in that row that are at most x.
At scale: rows spread across machines
When the rows are spread over many machines, every guess x is one round of requests to every machine.
The number of rounds is about log2(max - min), which is about 31 for a range of two billion values.
Each round waits for the slowest machine, so the number of rounds matters more than the cost of one bisect_right call inside a machine.
A percentile of sharded sorted data is computed in this way: each shard counts the values at most x, and a coordinator searches x without merging the shards.
A second check on the same idea: two sorted lists
Two sorted lists, nums1 and nums2, both not empty.
The count is bisect_right(nums1, x) + bisect_right(nums2, x), and the range runs from the smallest value of either list to one past the largest value of either list.
The answer is the same as sorting both lists together. The card Median of Two Sorted Arrays builds on this check.
import bisect def kth_of_two_lists(nums1, nums2, k): def at_least_k_values(x): count = bisect.bisect_right(nums1, x) + bisect.bisect_right(nums2, x) return count >= k lo = min(nums1[0], nums2[0]) # the smallest value of either list hi = max(nums1[-1], nums2[-1]) + 1 # one past the largest value of either list return binary_search_lower_bound(lo, hi, at_least_k_values) nums1 = [1, 3], nums2 = [2, 4] and k = 3: the count is 3 first at x = 3.
Say it in an interview
A rehearsed way to say the whole approach out loud, in about 60 seconds, for Kth Smallest Element in a Sorted Matrix (LC 378):
"I search the values, not the positions, with Lower Bound from
matrix[0][0]tomatrix[-1][-1] + 1. The check asks: are at leastkvalues at mostx? That count never goes down whenxgoes up, so the answers are False and then True, and the first True is the k-th smallest value. I count with a walk from the bottom-left corner, in at mostrows + colssteps. I usecount >= k, nevercount == k, because the count skipskwhen a value appears more than once. The time isO((rows + cols) * log(max - min + 2))and the space isO(1)."
Common Questions
- What is the cost, and when is a heap better?
The time is
O((rows + cols) * log(max - min + 2)). A heap that popsktimes costsO(k log rows). For a smallkthe heap is simpler. For a largek, such as the median of annbynmatrix, the count search is faster.- How do I get the k-th largest value instead of the k-th smallest?
Ask for the smallest value at position
total - k + 1. The k-th largest oftotalvalues is the same value as the(total - k + 1)-th smallest, so the same call works with that rank. For[[1, 5], [2, 6]]and the 2nd largest, usek = 4 - 2 + 1 = 3, and the answer is 5.
Practice
Quantile Bisection was the name of this card before.
Its loop keeps lo and hi as two values, and it counts with sum(...) inside the loop.
This course uses the template with the range [lo, hi), and one check function that counts with a plain loop.
The classic loop
| 1 | import bisect |
| 2 | from typing import List |
| 3 | Â |
| 4 | def kth_smallest_matrix(matrix: List[List[int]], k: int) -> int: |
| 5 | """Kth Smallest Element in a Sorted Matrix: binary search on the value range, counting with bisect per row. |
| 6 | Time: O(R * log(C + 1) * log(max - min + 2)) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = matrix[0][0], matrix[-1][-1] |
| 9 | Â |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | # Count elements <= mid across all rows |
| 13 | count = sum(bisect.bisect_right(row, mid) for row in matrix) |
| 14 | if count >= k: |
| 15 | hi = mid # mid satisfies rank; try smaller |
| 16 | else: |
| 17 | lo = mid + 1 # rank not reached; must increase |
| 18 | Â |
| 19 | return lo |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = matrix[0][0], matrix[-1][-1] | lo = matrix[0][0] and hi = matrix[-1][-1] + 1 | The classic range keeps the largest value as hi. The template's hi is one past the last candidate. |
| count = sum(bisect.bisect_right(row, mid) for row in matrix) | at_least_k_values(x), with a plain loop over the rows | The same count, written in a check function with a plain loop. |
| if count >= k: hi = mid else: lo = mid + 1 | binary_search_lower_bound(lo, hi, at_least_k_values) | The same loop, written once as the template. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it. It is the same loop. The template only gives the loop a name.
Gotchas of the classic loop
Gotcha 1 of 5
- The trap
Gotcha 1: Writing
count > kinstead ofcount >= kMistakeif count > k:Fixif count >= k:What happens: When exactly
kvalues are at mostmid,midcan be the answer.count > kis False there, so the loop setslo = mid + 1and moves past the answer. The result is too large.Example: matrix = [[1, 5], [10, 11]], k = 1Mistake: returns 5Fix: returns 1
Earlier signals
You need the k-th smallest value (or the median) of many numbers that are too costly to sort, such as a matrix, a multiplication table or every pair of an array, but for any guess mid you can quickly count how many of them are at most mid.
Earlier invariant
The k-th smallest value is always in [lo, hi]: at least k elements are at most hi, so hi is big enough, and fewer than k elements are at most lo - 1, so every value below lo is too small.
Earlier direction
multi-sequence / row-wise matrix âž” numeric domain bisection [smallest element, largest element] âž” rank counting
Earlier description
Don't look for the k-th smallest by its position; guess its value. Binary search the values from matrix[0][0] to matrix[-1][-1], and for each guess mid, count how many elements are at most mid. If count >= k, the k-th smallest is mid or smaller, so keep mid with hi = mid; otherwise it is bigger than mid, so lo = mid + 1. This works because count only grows as mid grows, so the loop stops on the smallest value whose count reaches k, and that value is always a real element: its count reaches k but the count of the value just below it does not, so at least one element equals it exactly. The trap is writing count > k: when exactly k elements are at most the answer, which is the usual case, the search moves past the answer and returns a value that is too big.
Earlier template
| 1 | import bisect |
| 2 | from typing import Callable, List |
| 3 | Â |
| 4 | def kth_smallest(lo: int, hi: int, k: int, count_at_most: Callable[[int], int]) -> int: |
| 5 | """Quantile Bisection: the k-th smallest of many whole numbers, found by binary search on the value. |
| 6 | count_at_most(mid) returns how many of the numbers are less than or equal to mid. |
| 7 | Precondition: lo is at most the smallest number, hi is at least the largest number, and 1 <= k <= how many numbers there are. |
| 8 | R = hi - lo, the size of the value range. T = the cost of one count_at_most call. |
| 9 | Time: O(T * log R) | Space: O(1) |
| 10 | """ |
| 11 | while lo < hi: |
| 12 | mid = lo + (hi - lo) // 2 |
| 13 | count = count_at_most(mid) |
| 14 | if count >= k: |
| 15 | hi = mid # mid satisfies rank; try smaller |
| 16 | else: |
| 17 | lo = mid + 1 # rank not reached; must increase |
| 18 | Â |
| 19 | return lo # The smallest value whose count reaches k |
| 20 | Â |
| 21 | Â |
| 22 | # Kth Smallest Element in a Sorted Matrix (LC 378): the template above, with lo, hi = matrix[0][0], matrix[-1][-1] and count_at_most(mid) = one bisect_right per row. The step debugger runs this code. |
| 23 | def kth_smallest_matrix(matrix: List[List[int]], k: int) -> int: |
| 24 | """Kth Smallest Element in a Sorted Matrix: binary search on the value range, counting with bisect per row. |
| 25 | Time: O(R * log C * log(max - min)) | Space: O(1) |
| 26 | """ |
| 27 | lo, hi = matrix[0][0], matrix[-1][-1] |
| 28 | Â |
| 29 | while lo < hi: |
| 30 | mid = lo + (hi - lo) // 2 |
| 31 | # Count elements <= mid across all rows |
| 32 | count = sum(bisect.bisect_right(row, mid) for row in matrix) |
| 33 | if count >= k: |
| 34 | hi = mid # mid satisfies rank; try smaller |
| 35 | else: |
| 36 | lo = mid + 1 # rank not reached; must increase |
| 37 | Â |
| 38 | return lo |
Earlier gotchas
Writing count > k instead of count >= k
When exactly k values are at most mid, mid can be the answer. count > k is False there, so the loop sets lo = mid + 1 and moves past the answer. The result is too large.
Searching positions instead of values
The loop compares mid with the values in the matrix. So lo and hi must be values, from the smallest to the largest. With positions, the search runs over numbers that may not be values in the matrix at all.
Counting with bisect_left but keeping the same loop
bisect_right(row, mid) counts the values less than or equal to mid. bisect_left(row, mid) counts the values strictly less than mid. With bisect_left, this loop finds the smallest mid that has k values below it. That mid is larger than the answer, and it may not be in the matrix. To count with bisect_left, search for the largest mid whose count is still less than k.
Counting each row with a full scan
A full scan reads every value for every guess, which is O(R · C) per step. bisect_right reads only O(log C) values in each row. In a matrix sorted by rows and by columns, a staircase walk from the bottom-left corner counts in O(R + C). The answer is the same. Only the time changes.
Using lo = mid + 1 when the values are fractions
lo = mid + 1 is correct only for whole numbers, because no whole number lies between mid and mid + 1. Fractions have values between any two numbers. K-th Smallest Prime Fraction (LC 786) needs the Continuous Bisection loop instead.
Earlier look-alikes
You must return the k smallest items themselves, not only the value of the k-th, as in Find K Pairs with Smallest Sums (LC 373): a count below mid never lists them.
The answer is the smallest setting (a speed, a capacity) that makes a plan work, as in Koko Eating Bananas (LC 875): there is no rank k to reach.
Earlier problems
Kth Smallest Element in a Sorted Matrix
Median of a Row Wise Sorted Matrix
Kth Smallest Number in Multiplication Table
Ugly Number III
K-th Smallest Prime Fraction
Find K-th Smallest Pair Distance
Builds on Feasible Function, Staircase Search and Lower Bound, which you haven’t finished yet.
Binary Search on Value Range (Kth Smallest in Sorted Matrix)
k.matrix is an n x n grid whose values never decrease from left to right along a row or from top to bottom down a column. Find the k-th smallest of its n² values, where repeated values each take their own place in the count (in [[1, 1], [1, 2]], the third smallest value is still 1). The solution must use less than O(n²) extra memory, so copying all the values and sorting them is ruled out.
1, 5, 9, 10, 11, 12, 13, 13, 15; counting the two 13s separately, the 8th is 13.Kth Smallest
values you can count âž” is the count of values at most x at least k? âž” the first x where it is| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | Â |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | Â |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
k values are at most lo - 1, and at least k values are at most hi, so the k-th smallest value is from lo to hi.The number of values that are at most x never goes down when x goes up. So at_least_k_values(x) is False for small x and True from some x on, and the first True is the k-th smallest value. Run Lower Bound over the values, not over the positions, and let a count answer each question. The count changes only at a value that is in the matrix, so the first x that reaches k is always a value of the matrix. The range runs from lo = matrix[0][0] to hi = matrix[-1][-1] + 1, where the count is the total. The trap is return count == k instead of return count >= k. For k = 7, the count is 6 at x = 12 and 8 at x = 13, because 13 appears twice, so the count is never 7. Then the check is never True, and the call returns 16, which is not a value of the matrix.
See it run
Kth Smallest in a Sorted Matrix: Lower Bound over the values
k 3lo 1hi 7mid none
The top row lists every candidate value x. The count row shows, for each tested mid, how many matrix values are at most mid.
lothe first position that can still be the answerhithe answer is at hi or before it; hi can be one past the endmidthe position the check reads nowlo+midlo and mid are at the same positioncounthow many matrix values are at most this midanswerthe first True, where lo and hi meet
matrix = [[1,5],[2,6]] and k = 3. The candidates are the values from lo = 1 up to hi = 7, which is one past the largest value.
Gotcha 1 of 5
- The trap
Gotcha 1: Asking for "exactly
k" instead of "at leastk"Mistakereturn count == kFixreturn count >= kWhat happens: The count grows by the number of copies of
x, so it can skipk. Fork = 7, the count is 6 atx = 12and 8 atx = 13, because 13 appears twice. The count is never 7, so "exactly 7" is never True. The search then returnshi, which is 16, one past the largest value. The right answer is 13.Example: matrix = [[1, 5, 9], [10, 11, 13], [12, 13, 15]], k = 7Mistake: returns 16Fix: returns 13
- You must return the k smallest items themselves, not only the value of the k-th, as in Find K Pairs with Smallest Sums (LC 373): a count below
midnever lists them. See Best-First Heap - The answer is the smallest setting (a speed, a capacity) that makes a plan work, as in Koko Eating Bananas (LC 875): there is no rank
kto reach. See Feasible Function
Theory
The question we build
What we know
The matrix is sorted along every row and every column. Equal values count separately.
What the problem gives us
The input is a sorted matrix and k.
We return the k-th smallest value.
The question we build
For each value x we ask: "are at least k values at most x?"
We use matrix = [[1, 5], [2, 6]] and k = 3. Sorted, the values are 1, 2, 5, 6, so the third smallest is 5.
The counts for x from 1 to 6 are 1, 2, 2, 2, 3, 4.
The answers are False, False, False, False, True, True.
The first True is x = 5, which is the answer.
The count is 3 first at x = 5. At x = 3 and x = 4 the count stays 2, because no value of the matrix is there.
Narrowing down to the template
The first True is the answer, so the call is binary_search_lower_bound(matrix[0][0], matrix[-1][-1] + 1, at_least_k_values).
Step 1: Sort everything
The k-th smallest value is the value at index k - 1 of the sorted whole.
The sort states this in a few lines.
| 1 | def kth_smallest_sorted(matrix, k): |
| 2 | values = [] |
| 3 | for row in matrix: |
| 4 | for value in row: |
| 5 | values.append(value) |
| 6 | values.sort() |
| 7 | return values[k - 1] |
Cost: O(N log N) time and O(N) memory, where N is the number of values. At LeetCode's limit of n = 300, the copy holds 9 * 104 values.
Step 2: Search the values with a count check
Lower Bound needs a check that reads False, then True. The count gives it: at_least_k_values(x) compares the count of values at most x with k.
The count comes from the staircase walk of Staircase Search, started at the bottom-left corner.
When a cell is at most x, the whole column above it is at most x too, so the walk adds row + 1 and moves right. Otherwise the walk moves up.
The search never builds the sorted list. It only counts, for one value at a time.
| 1 | def kth_smallest(matrix, k): |
| 2 | rows, cols = len(matrix), len(matrix[0]) |
| 3 | Â |
| 4 | def at_least_k_values(x): |
| 5 | count = 0 |
| 6 | row, col = rows - 1, 0 # start at the bottom-left cell |
| 7 | while row >= 0 and col < cols: |
| 8 | current_value = matrix[row][col] |
| 9 | if current_value <= x: |
| 10 | # Every entry from the top down to this row in column col is also at most x. |
| 11 | count += row + 1 |
| 12 | col += 1 # move right to check the next column |
| 13 | else: |
| 14 | row -= 1 # the current value is too large; move up to a smaller value |
| 15 | return count >= k |
| 16 | Â |
| 17 | lo = matrix[0][0] # the smallest value is the top-left cell |
| 18 | hi = matrix[-1][-1] + 1 # one past the largest value, which is the bottom-right cell |
| 19 | return binary_search_lower_bound(lo, hi, at_least_k_values) |
Cost: O((rows + cols) * log(max - min + 2)). Each check is a walk from the bottom-left corner, and it never reads the same cell twice.
| Item | Sort everything | Count check |
|---|---|---|
values.sort() and values[k - 1] | at_least_k_values and one Lower Bound call | The sort builds the whole sorted list. The count check never builds it, and it never reads a cell more than once. |
The shorter version
| 1 | import bisect |
| 2 | Â |
| 3 | def kth_smallest_short(matrix, k): |
| 4 | rows, cols = len(matrix), len(matrix[0]) |
| 5 | Â |
| 6 | def at_least_k_values(x): |
| 7 | count = 0 |
| 8 | row, col = rows - 1, 0 |
| 9 | while row >= 0 and col < cols: |
| 10 | current_value = matrix[row][col] |
| 11 | if current_value <= x: |
| 12 | count += row + 1 |
| 13 | col += 1 |
| 14 | else: |
| 15 | row -= 1 |
| 16 | return count >= k |
| 17 | Â |
| 18 | lo, hi = matrix[0][0], matrix[-1][-1] + 1 |
| 19 | return bisect.bisect_left(range(lo, hi), True, key=at_least_k_values) + lo |
range(lo, hi) holds the candidate values.
bisect_left returns a position in that range, so + lo turns it back into a value.
This form needs Python 3.10 or later, because of key=.
An alternative: one `bisect_right` per row
On one machine, the staircase walk is cheaper: O(rows + cols) for one check, against O(rows * log(cols + 1)) for one bisect_right call per row.
But the walk is one sequential pass over adjacent rows and columns, so it cannot be split across machines.
A count with bisect_right can be split: each row's count comes from one bisect_right call on that row alone. Each row can live on its own machine, and only that row's count travels across the network.
| 1 | import bisect |
| 2 | Â |
| 3 | def kth_smallest_by_row(matrix, k): |
| 4 | def at_least_k_values(x): |
| 5 | count = 0 |
| 6 | for row in matrix: |
| 7 | count += bisect.bisect_right(row, x) # how many values in this row are at most x |
| 8 | return count >= k |
| 9 | Â |
| 10 | lo = matrix[0][0] # the smallest value is the top-left cell |
| 11 | hi = matrix[-1][-1] + 1 # one past the largest value, which is the bottom-right cell |
| 12 | return binary_search_lower_bound(lo, hi, at_least_k_values) |
bisect_right(row, x) is the number of values in that row that are at most x.
At scale: rows spread across machines
When the rows are spread over many machines, every guess x is one round of requests to every machine.
The number of rounds is about log2(max - min), which is about 31 for a range of two billion values.
Each round waits for the slowest machine, so the number of rounds matters more than the cost of one bisect_right call inside a machine.
A percentile of sharded sorted data is computed in this way: each shard counts the values at most x, and a coordinator searches x without merging the shards.
A second check on the same idea: two sorted lists
Two sorted lists, nums1 and nums2, both not empty.
The count is bisect_right(nums1, x) + bisect_right(nums2, x), and the range runs from the smallest value of either list to one past the largest value of either list.
The answer is the same as sorting both lists together. The card Median of Two Sorted Arrays builds on this check.
import bisect def kth_of_two_lists(nums1, nums2, k): def at_least_k_values(x): count = bisect.bisect_right(nums1, x) + bisect.bisect_right(nums2, x) return count >= k lo = min(nums1[0], nums2[0]) # the smallest value of either list hi = max(nums1[-1], nums2[-1]) + 1 # one past the largest value of either list return binary_search_lower_bound(lo, hi, at_least_k_values) nums1 = [1, 3], nums2 = [2, 4] and k = 3: the count is 3 first at x = 3.
Say it in an interview
A rehearsed way to say the whole approach out loud, in about 60 seconds, for Kth Smallest Element in a Sorted Matrix (LC 378):
"I search the values, not the positions, with Lower Bound from
matrix[0][0]tomatrix[-1][-1] + 1. The check asks: are at leastkvalues at mostx? That count never goes down whenxgoes up, so the answers are False and then True, and the first True is the k-th smallest value. I count with a walk from the bottom-left corner, in at mostrows + colssteps. I usecount >= k, nevercount == k, because the count skipskwhen a value appears more than once. The time isO((rows + cols) * log(max - min + 2))and the space isO(1)."
Common Questions
- What is the cost, and when is a heap better?
The time is
O((rows + cols) * log(max - min + 2)). A heap that popsktimes costsO(k log rows). For a smallkthe heap is simpler. For a largek, such as the median of annbynmatrix, the count search is faster.- How do I get the k-th largest value instead of the k-th smallest?
Ask for the smallest value at position
total - k + 1. The k-th largest oftotalvalues is the same value as the(total - k + 1)-th smallest, so the same call works with that rank. For[[1, 5], [2, 6]]and the 2nd largest, usek = 4 - 2 + 1 = 3, and the answer is 5.
Practice
Quantile Bisection was the name of this card before.
Its loop keeps lo and hi as two values, and it counts with sum(...) inside the loop.
This course uses the template with the range [lo, hi), and one check function that counts with a plain loop.
The classic loop
| 1 | import bisect |
| 2 | from typing import List |
| 3 | Â |
| 4 | def kth_smallest_matrix(matrix: List[List[int]], k: int) -> int: |
| 5 | """Kth Smallest Element in a Sorted Matrix: binary search on the value range, counting with bisect per row. |
| 6 | Time: O(R * log(C + 1) * log(max - min + 2)) | Space: O(1) |
| 7 | """ |
| 8 | lo, hi = matrix[0][0], matrix[-1][-1] |
| 9 | Â |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | # Count elements <= mid across all rows |
| 13 | count = sum(bisect.bisect_right(row, mid) for row in matrix) |
| 14 | if count >= k: |
| 15 | hi = mid # mid satisfies rank; try smaller |
| 16 | else: |
| 17 | lo = mid + 1 # rank not reached; must increase |
| 18 | Â |
| 19 | return lo |
From the classic loop to our template
| Old Way line | In our template | Why |
|---|---|---|
| lo, hi = matrix[0][0], matrix[-1][-1] | lo = matrix[0][0] and hi = matrix[-1][-1] + 1 | The classic range keeps the largest value as hi. The template's hi is one past the last candidate. |
| count = sum(bisect.bisect_right(row, mid) for row in matrix) | at_least_k_values(x), with a plain loop over the rows | The same count, written in a check function with a plain loop. |
| if count >= k: hi = mid else: lo = mid + 1 | binary_search_lower_bound(lo, hi, at_least_k_values) | The same loop, written once as the template. |
When the classic form is fine
Use it when an interviewer asks for this form, or when the code base already uses it. It is the same loop. The template only gives the loop a name.
Gotchas of the classic loop
Gotcha 1 of 5
- The trap
Gotcha 1: Writing
count > kinstead ofcount >= kMistakeif count > k:Fixif count >= k:What happens: When exactly
kvalues are at mostmid,midcan be the answer.count > kis False there, so the loop setslo = mid + 1and moves past the answer. The result is too large.Example: matrix = [[1, 5], [10, 11]], k = 1Mistake: returns 5Fix: returns 1
Earlier signals
You need the k-th smallest value (or the median) of many numbers that are too costly to sort, such as a matrix, a multiplication table or every pair of an array, but for any guess mid you can quickly count how many of them are at most mid.
Earlier invariant
The k-th smallest value is always in [lo, hi]: at least k elements are at most hi, so hi is big enough, and fewer than k elements are at most lo - 1, so every value below lo is too small.
Earlier direction
multi-sequence / row-wise matrix âž” numeric domain bisection [smallest element, largest element] âž” rank counting
Earlier description
Don't look for the k-th smallest by its position; guess its value. Binary search the values from matrix[0][0] to matrix[-1][-1], and for each guess mid, count how many elements are at most mid. If count >= k, the k-th smallest is mid or smaller, so keep mid with hi = mid; otherwise it is bigger than mid, so lo = mid + 1. This works because count only grows as mid grows, so the loop stops on the smallest value whose count reaches k, and that value is always a real element: its count reaches k but the count of the value just below it does not, so at least one element equals it exactly. The trap is writing count > k: when exactly k elements are at most the answer, which is the usual case, the search moves past the answer and returns a value that is too big.
Earlier template
| 1 | import bisect |
| 2 | from typing import Callable, List |
| 3 | Â |
| 4 | def kth_smallest(lo: int, hi: int, k: int, count_at_most: Callable[[int], int]) -> int: |
| 5 | """Quantile Bisection: the k-th smallest of many whole numbers, found by binary search on the value. |
| 6 | count_at_most(mid) returns how many of the numbers are less than or equal to mid. |
| 7 | Precondition: lo is at most the smallest number, hi is at least the largest number, and 1 <= k <= how many numbers there are. |
| 8 | R = hi - lo, the size of the value range. T = the cost of one count_at_most call. |
| 9 | Time: O(T * log R) | Space: O(1) |
| 10 | """ |
| 11 | while lo < hi: |
| 12 | mid = lo + (hi - lo) // 2 |
| 13 | count = count_at_most(mid) |
| 14 | if count >= k: |
| 15 | hi = mid # mid satisfies rank; try smaller |
| 16 | else: |
| 17 | lo = mid + 1 # rank not reached; must increase |
| 18 | Â |
| 19 | return lo # The smallest value whose count reaches k |
| 20 | Â |
| 21 | Â |
| 22 | # Kth Smallest Element in a Sorted Matrix (LC 378): the template above, with lo, hi = matrix[0][0], matrix[-1][-1] and count_at_most(mid) = one bisect_right per row. The step debugger runs this code. |
| 23 | def kth_smallest_matrix(matrix: List[List[int]], k: int) -> int: |
| 24 | """Kth Smallest Element in a Sorted Matrix: binary search on the value range, counting with bisect per row. |
| 25 | Time: O(R * log C * log(max - min)) | Space: O(1) |
| 26 | """ |
| 27 | lo, hi = matrix[0][0], matrix[-1][-1] |
| 28 | Â |
| 29 | while lo < hi: |
| 30 | mid = lo + (hi - lo) // 2 |
| 31 | # Count elements <= mid across all rows |
| 32 | count = sum(bisect.bisect_right(row, mid) for row in matrix) |
| 33 | if count >= k: |
| 34 | hi = mid # mid satisfies rank; try smaller |
| 35 | else: |
| 36 | lo = mid + 1 # rank not reached; must increase |
| 37 | Â |
| 38 | return lo |
Earlier gotchas
Writing count > k instead of count >= k
When exactly k values are at most mid, mid can be the answer. count > k is False there, so the loop sets lo = mid + 1 and moves past the answer. The result is too large.
Searching positions instead of values
The loop compares mid with the values in the matrix. So lo and hi must be values, from the smallest to the largest. With positions, the search runs over numbers that may not be values in the matrix at all.
Counting with bisect_left but keeping the same loop
bisect_right(row, mid) counts the values less than or equal to mid. bisect_left(row, mid) counts the values strictly less than mid. With bisect_left, this loop finds the smallest mid that has k values below it. That mid is larger than the answer, and it may not be in the matrix. To count with bisect_left, search for the largest mid whose count is still less than k.
Counting each row with a full scan
A full scan reads every value for every guess, which is O(R · C) per step. bisect_right reads only O(log C) values in each row. In a matrix sorted by rows and by columns, a staircase walk from the bottom-left corner counts in O(R + C). The answer is the same. Only the time changes.
Using lo = mid + 1 when the values are fractions
lo = mid + 1 is correct only for whole numbers, because no whole number lies between mid and mid + 1. Fractions have values between any two numbers. K-th Smallest Prime Fraction (LC 786) needs the Continuous Bisection loop instead.
Earlier look-alikes
You must return the k smallest items themselves, not only the value of the k-th, as in Find K Pairs with Smallest Sums (LC 373): a count below mid never lists them.
The answer is the smallest setting (a speed, a capacity) that makes a plan work, as in Koko Eating Bananas (LC 875): there is no rank k to reach.
Earlier problems
Kth Smallest Element in a Sorted Matrix
Median of a Row Wise Sorted Matrix
Kth Smallest Number in Multiplication Table
Ugly Number III
K-th Smallest Prime Fraction
Find K-th Smallest Pair Distance