Builds on Hash Map, which you haven’t finished yet.
Bucket Sort (Top K Frequent, Maximum Gap)
You get an integer array nums. Picture its values sorted from smallest to largest: each pair of values that end up side by side has a difference, and you must return the largest of those differences. If nums holds fewer than two values, there is no such pair, so return 0.
Sorting only defines the answer; it is not a way you may use to find it. Your algorithm has to run in time linear in the length of nums, and it may use extra space that is also linear in it.
1, 3, 6, 9. The differences between neighbours are 2, 3 and 3, so the largest is 3; both 3 → 6 and 6 → 9 reach it.Bucket Sort
min to max), and you need their order, the top k, or the largest gap in linear time, without an O(n log n) sort.- Values arrive one at a time and the answer is needed after every arrival, as in Kth Largest Element in a Stream (LC 703): there is no fixed range to lay the buckets over before the data starts. See Top-K Heap
- Every value that matters is a whole number no larger than the input length, as in H-Index (LC 274). There, any citation count above
ncan be treated asn. One slot for each exact value is enough. No bucket needs to cover a range, and nothing inside a slot needs to be compared. See Counting Sort
| 1 | def maximum_gap(nums: list[int]) -> int: |
| 2 | """Maximum Gap: pigeonhole buckets, compare neighbours across buckets only. |
| 3 | Time: O(N) | Space: O(N) |
| 4 | """ |
| 5 | # 1. Handle Too Few Values: |
| 6 | # With fewer than two values there is no neighbour at all, so no gap can be measured. |
| 7 | n = len(nums) |
| 8 | if n < 2: |
| 9 | return 0 |
| 10 | Â |
| 11 | # 2. Size The Buckets From The Pigeonhole Bound: |
| 12 | # The largest gap is at least the average gap (hi - lo) / (n - 1); max(1, ...) keeps every |
| 13 | # bucket at least one value wide so the later division never hits zero. |
| 14 | lo, hi = min(nums), max(nums) |
| 15 | width = max(1, (hi - lo) // (n - 1)) |
| 16 | count = (hi - lo) // width + 1 # Enough buckets to hold hi as well |
| 17 | bucket_min = [float("inf")] * count # Smallest value that fell in each bucket |
| 18 | bucket_max = [float("-inf")] * count # Largest value that fell in each bucket |
| 19 | Â |
| 20 | # 3. Drop Each Value Into Its Bucket, Keeping Only The Extremes: |
| 21 | # No two values are compared here; because width is at most the smallest possible gap, the |
| 22 | # answer can never sit inside one bucket, so only its smallest and largest value matter. |
| 23 | for x in nums: |
| 24 | b = (x - lo) // width |
| 25 | bucket_min[b] = min(bucket_min[b], x) |
| 26 | bucket_max[b] = max(bucket_max[b], x) |
| 27 | Â |
| 28 | # 4. Walk The Buckets In Order And Measure Cross-Bucket Gaps: |
| 29 | # Skipping empty buckets and carrying prev_max forward means each gap is measured from the |
| 30 | # last non-empty bucket's maximum to the next non-empty bucket's minimum. |
| 31 | best = 0 |
| 32 | prev_max = lo |
| 33 | for b in range(count): |
| 34 | if bucket_min[b] == float("inf"): |
| 35 | continue |
| 36 | best = max(best, bucket_min[b] - prev_max) |
| 37 | prev_max = bucket_max[b] |
| 38 | Â |
| 39 | return best |
b holds exactly the values in [lo + b * width, lo + (b + 1) * width), and width is at most the smallest possible maximum gap.Bucket Sort drops each value straight into the bucket that covers its range, b = (x - lo) // width, so placing a value needs no comparison with other values and the buckets come out in order. The key can be any bounded number: bucket by value range to find gaps, or by count to order items by frequency. For the largest gap between neighbours, keep only bucket_min[b] and bucket_max[b], walk the buckets once, and measure bucket_min[b] - prev_max, where prev_max is the largest value of the last non-empty bucket. That is enough because of the pigeonhole principle: n values from lo to hi leave n - 1 gaps that add up to hi - lo, so the largest gap is at least their average, and being a whole number it is at least width whenever hi > lo. Two values in the same bucket differ by at most width - 1, so the maximum gap is never inside one bucket: it always runs from one bucket's maximum to a later bucket's minimum, and the values in between never matter. The trap is the floor of 1: when hi - lo < n - 1, for example with repeated values, (hi - lo) // (n - 1) is 0 and (x - lo) // width divides by zero, so write width = max(1, (hi - lo) // (n - 1)).
See it run
Bucket Sort, step by step
x -b -prev_max -best 0
Each column is one bucket. The top row is bucket_min, the middle row is bucket_max, and the bottom row is the gap measured when the walk reaches that bucket.
putthe bucket that receives the value xgapthe bucket whose minimum is measured against prev_maxprev_maxthe bucket whose maximum becomes prev_maxemptya bucket with no values, skipped in the walk
Start: lo = 1, hi = 25, n = 6, so width = max(1, (25 - 1) // 5) = 4. That makes 7 buckets.
Gotcha 1 of 4
- The trap
Gotcha 1: Computing
widthwithout the floor of 1Mistakewidth = (hi - lo) // (n - 1)Fixwidth = max(1, (hi - lo) // (n - 1))What happens: With repeated values,
hi - locan be smaller thann - 1. Then(hi - lo) // (n - 1)is 0. The next division,(hi - lo) // width, divides by zero.max(1, ...)keeps every bucket at least one value wide.Example: nums = [1, 1, 1, 4, 2]Mistake: raises ZeroDivisionErrorFix: returns 2
Deep Dive
The procedure: compute the bucket from the value
A bucket is a place that holds every value inside one range. Bucket Sort puts each value into its bucket with one formula, b = (x - lo) // width. The formula uses only x, lo and width. It never compares x with another value.
The buckets are laid out in order. Bucket 0 holds the smallest values and the last bucket holds the largest. So after the placing loop, the values are already ordered between buckets. Only the order inside one bucket is unknown.
The key can be any bounded number. In maximum_gap the key is the value itself, cut into ranges of width numbers. In Top K Frequent Elements (LC 347) and Sort Characters By Frequency (LC 451) the key is a count. A count is never more than the input length, so buckets[count] can hold every item with that count.
For example, take nums = [1, 1, 1, 2, 2, 3] and k = 2. The counts are 3 for the value 1, 2 for the value 2 and 1 for the value 3. buckets[3] holds [1], buckets[2] holds [2] and buckets[1] holds [3]. Reading buckets from the last index down gives 1, then 2, so the answer is [1, 2].
Maximum Gap, traced on six numbers
Take nums = [1, 2, 10, 4, 25, 7]. Here n = 6, lo = 1 and hi = 25. So width = max(1, 24 // 5) = 4, and count = 24 // 4 + 1 = 7 buckets.
The placing loop gives these buckets. Bucket 0 holds 1, 2 and 4, so its minimum is 1 and its maximum is 4. Bucket 1 holds 7. Bucket 2 holds 10. Buckets 3, 4 and 5 stay empty. Bucket 6 holds 25.
The walk starts with prev_max = lo = 1. Bucket 0 gives a gap of 1 - 1 = 0. Bucket 1 gives 7 - 4 = 3, and bucket 2 gives 10 - 7 = 3. The board marks buckets 3, 4 and 5 as skipped.
Bucket 6 gives 25 - 10 = 15. The last non-empty bucket before it is bucket 2, not bucket 5. This is the trap on the board: bucket_max[5] is -inf, so the formula with bucket_max[b - 1] would give inf. The variable prev_max is 10 and gives the right gap.
The answer is 15. On the LeetCode example [3, 6, 9, 1] the same code returns 3.
Why the answer is never inside one bucket
There are n values, so there are n - 1 gaps between sorted neighbours. The gaps add up to hi - lo. The largest gap is at least the average, (hi - lo) / (n - 1). A gap is a whole number, so the largest gap is at least (hi - lo) // (n - 1), which is width, whenever hi > lo. This is the pigeonhole principle.
A bucket covers width whole numbers, so two values in one bucket differ by at most width - 1. That is smaller than width. So no pair inside one bucket can be the largest gap. The largest gap always starts at the maximum of one non-empty bucket and ends at the minimum of a later non-empty bucket. The other values in a bucket never matter, so each bucket keeps only bucket_min and bucket_max.
The first trap is width = (hi - lo) // (n - 1) with no floor of 1. For nums = [1, 1, 1, 4, 2], hi - lo = 3 and n - 1 = 4, so the result is 0 and the next division raises a ZeroDivisionError. With max(1, ...), width = 1 and the answer is 2. The second trap is a width rounded up with + 1. For nums = [6, 18] the width becomes 13, both values share bucket 0, and the code returns 0 instead of 12.
Cost, and when not to use buckets
The placing loop does O(n) work and the walk visits every bucket once. The number of buckets is (hi - lo) // width + 1, which is less than 2n. So the time is O(n) and the space is O(n). Sorting first costs O(n log n).
Buckets need the range to be known before the data is placed. If the values arrive one at a time and the answer is needed after each arrival, as in Kth Largest Element in a Stream (LC 703), there is no range to lay out. Use a heap there.
If every value is a small whole number, one slot per exact value is enough and no range is needed. That is the next card, Counting Sort.
60-Second Pitch
"Sorting costs O(n log n), so I use buckets. There are n values and n - 1 gaps that add up to hi - lo, so the largest gap is at least (hi - lo) // (n - 1). I make each bucket that wide, with a floor of 1. Each value goes to the bucket (x - lo) // width, and each bucket keeps only its minimum and maximum. Two values in one bucket are closer than the width, so the answer is never inside a bucket. I walk the buckets in order, skip the empty ones, and measure each non-empty bucket's minimum against prev_max. The time is O(n) and the space is O(n). The two traps are the floor of 1 on width, and using prev_max instead of the previous bucket's maximum."
Common Questions
- Why is it enough to keep only the minimum and maximum of each bucket?
Two values in one bucket differ by at most
width - 1, and the largest gap is at leastwidth. So the largest gap is never between two values of one bucket. It always runs from the maximum of one bucket to the minimum of a later bucket.- What is the pigeonhole argument here?
There are
n - 1gaps between sorted neighbours, and they add up tohi - lo. The largest of them is at least the average. If every bucket is narrower than the average gap, two neighbours cannot be in the same bucket and span the largest gap.- What happens when all values are equal?
Then
hi - lois 0, so(hi - lo) // (n - 1)is 0. The floor giveswidth = 1andcount = 1. All values fall in bucket 0, and the walk measures a gap of 0.- How does Top K Frequent Elements (LC 347) use buckets?
Count the values with a dictionary. A count is at most
len(nums), so make a listbucketswithlen(nums) + 1entries and put each value inbuckets[count]. Read the list from the last index down untilkvalues are collected. The time isO(n).- When would you use a heap or a sort instead?
Use a heap when values arrive one at a time and the range is not known, as in Kth Largest Element in a Stream (LC 703). Use a sort when
nis small and the code must be short. Buckets win when the range is known and the limit needsO(n).
SWE Reasoning
Spot it
- The values lie in a known, bounded range, and a sort at O(n log n) is too slow for the limit.
- The question asks for the largest gap between neighbours in sorted order, as in Maximum Gap (LC 164), but the array may not be sorted.
- The key is a count that is never more than
n, and the items must come out ordered by it, as in Sort Characters By Frequency (LC 451) or Top K Frequent Elements (LC 347).
The invariant, step by step
loandhiare the smallest and largest value.width = max(1, (hi - lo) // (n - 1))is a number that is at most the smallest possible largest gap.- Bucket
bholds exactly the values in[lo + b * width, lo + (b + 1) * width). - Each value goes to bucket
b = (x - lo) // width. The bucket keeps only its smallest and largest value. - The walk visits the buckets in order. It skips empty buckets and keeps
prev_max, the largest value of the last non-empty bucket. - The gap
bucket_min[b] - prev_maxis measured only across buckets, and the largest of these is the answer.
Traps
- No floor of 1 on
width. With repeated values,(hi - lo) // (n - 1)is 0 and the next division raises aZeroDivisionError. - No check for
n < 2. With one value,n - 1is 0 and the division raises aZeroDivisionError. The answer is 0. - A width rounded up with
+ 1. Two values can then share a bucket, and the gap between them is never measured.[6, 18]returns 0 instead of 12. - Using
bucket_max[b - 1]instead ofprev_max. An empty previous bucket has the maximum-inf, and the gap becomesinf.
Practice
Builds on Hash Map, which you haven’t finished yet.
Bucket Sort (Top K Frequent, Maximum Gap)
You get an integer array nums. Picture its values sorted from smallest to largest: each pair of values that end up side by side has a difference, and you must return the largest of those differences. If nums holds fewer than two values, there is no such pair, so return 0.
Sorting only defines the answer; it is not a way you may use to find it. Your algorithm has to run in time linear in the length of nums, and it may use extra space that is also linear in it.
1, 3, 6, 9. The differences between neighbours are 2, 3 and 3, so the largest is 3; both 3 → 6 and 6 → 9 reach it.Bucket Sort
min to max), and you need their order, the top k, or the largest gap in linear time, without an O(n log n) sort.- Values arrive one at a time and the answer is needed after every arrival, as in Kth Largest Element in a Stream (LC 703): there is no fixed range to lay the buckets over before the data starts. See Top-K Heap
- Every value that matters is a whole number no larger than the input length, as in H-Index (LC 274). There, any citation count above
ncan be treated asn. One slot for each exact value is enough. No bucket needs to cover a range, and nothing inside a slot needs to be compared. See Counting Sort
| 1 | def maximum_gap(nums: list[int]) -> int: |
| 2 | """Maximum Gap: pigeonhole buckets, compare neighbours across buckets only. |
| 3 | Time: O(N) | Space: O(N) |
| 4 | """ |
| 5 | # 1. Handle Too Few Values: |
| 6 | # With fewer than two values there is no neighbour at all, so no gap can be measured. |
| 7 | n = len(nums) |
| 8 | if n < 2: |
| 9 | return 0 |
| 10 | Â |
| 11 | # 2. Size The Buckets From The Pigeonhole Bound: |
| 12 | # The largest gap is at least the average gap (hi - lo) / (n - 1); max(1, ...) keeps every |
| 13 | # bucket at least one value wide so the later division never hits zero. |
| 14 | lo, hi = min(nums), max(nums) |
| 15 | width = max(1, (hi - lo) // (n - 1)) |
| 16 | count = (hi - lo) // width + 1 # Enough buckets to hold hi as well |
| 17 | bucket_min = [float("inf")] * count # Smallest value that fell in each bucket |
| 18 | bucket_max = [float("-inf")] * count # Largest value that fell in each bucket |
| 19 | Â |
| 20 | # 3. Drop Each Value Into Its Bucket, Keeping Only The Extremes: |
| 21 | # No two values are compared here; because width is at most the smallest possible gap, the |
| 22 | # answer can never sit inside one bucket, so only its smallest and largest value matter. |
| 23 | for x in nums: |
| 24 | b = (x - lo) // width |
| 25 | bucket_min[b] = min(bucket_min[b], x) |
| 26 | bucket_max[b] = max(bucket_max[b], x) |
| 27 | Â |
| 28 | # 4. Walk The Buckets In Order And Measure Cross-Bucket Gaps: |
| 29 | # Skipping empty buckets and carrying prev_max forward means each gap is measured from the |
| 30 | # last non-empty bucket's maximum to the next non-empty bucket's minimum. |
| 31 | best = 0 |
| 32 | prev_max = lo |
| 33 | for b in range(count): |
| 34 | if bucket_min[b] == float("inf"): |
| 35 | continue |
| 36 | best = max(best, bucket_min[b] - prev_max) |
| 37 | prev_max = bucket_max[b] |
| 38 | Â |
| 39 | return best |
b holds exactly the values in [lo + b * width, lo + (b + 1) * width), and width is at most the smallest possible maximum gap.Bucket Sort drops each value straight into the bucket that covers its range, b = (x - lo) // width, so placing a value needs no comparison with other values and the buckets come out in order. The key can be any bounded number: bucket by value range to find gaps, or by count to order items by frequency. For the largest gap between neighbours, keep only bucket_min[b] and bucket_max[b], walk the buckets once, and measure bucket_min[b] - prev_max, where prev_max is the largest value of the last non-empty bucket. That is enough because of the pigeonhole principle: n values from lo to hi leave n - 1 gaps that add up to hi - lo, so the largest gap is at least their average, and being a whole number it is at least width whenever hi > lo. Two values in the same bucket differ by at most width - 1, so the maximum gap is never inside one bucket: it always runs from one bucket's maximum to a later bucket's minimum, and the values in between never matter. The trap is the floor of 1: when hi - lo < n - 1, for example with repeated values, (hi - lo) // (n - 1) is 0 and (x - lo) // width divides by zero, so write width = max(1, (hi - lo) // (n - 1)).
See it run
Bucket Sort, step by step
x -b -prev_max -best 0
Each column is one bucket. The top row is bucket_min, the middle row is bucket_max, and the bottom row is the gap measured when the walk reaches that bucket.
putthe bucket that receives the value xgapthe bucket whose minimum is measured against prev_maxprev_maxthe bucket whose maximum becomes prev_maxemptya bucket with no values, skipped in the walk
Start: lo = 1, hi = 25, n = 6, so width = max(1, (25 - 1) // 5) = 4. That makes 7 buckets.
Gotcha 1 of 4
- The trap
Gotcha 1: Computing
widthwithout the floor of 1Mistakewidth = (hi - lo) // (n - 1)Fixwidth = max(1, (hi - lo) // (n - 1))What happens: With repeated values,
hi - locan be smaller thann - 1. Then(hi - lo) // (n - 1)is 0. The next division,(hi - lo) // width, divides by zero.max(1, ...)keeps every bucket at least one value wide.Example: nums = [1, 1, 1, 4, 2]Mistake: raises ZeroDivisionErrorFix: returns 2
Deep Dive
The procedure: compute the bucket from the value
A bucket is a place that holds every value inside one range. Bucket Sort puts each value into its bucket with one formula, b = (x - lo) // width. The formula uses only x, lo and width. It never compares x with another value.
The buckets are laid out in order. Bucket 0 holds the smallest values and the last bucket holds the largest. So after the placing loop, the values are already ordered between buckets. Only the order inside one bucket is unknown.
The key can be any bounded number. In maximum_gap the key is the value itself, cut into ranges of width numbers. In Top K Frequent Elements (LC 347) and Sort Characters By Frequency (LC 451) the key is a count. A count is never more than the input length, so buckets[count] can hold every item with that count.
For example, take nums = [1, 1, 1, 2, 2, 3] and k = 2. The counts are 3 for the value 1, 2 for the value 2 and 1 for the value 3. buckets[3] holds [1], buckets[2] holds [2] and buckets[1] holds [3]. Reading buckets from the last index down gives 1, then 2, so the answer is [1, 2].
Maximum Gap, traced on six numbers
Take nums = [1, 2, 10, 4, 25, 7]. Here n = 6, lo = 1 and hi = 25. So width = max(1, 24 // 5) = 4, and count = 24 // 4 + 1 = 7 buckets.
The placing loop gives these buckets. Bucket 0 holds 1, 2 and 4, so its minimum is 1 and its maximum is 4. Bucket 1 holds 7. Bucket 2 holds 10. Buckets 3, 4 and 5 stay empty. Bucket 6 holds 25.
The walk starts with prev_max = lo = 1. Bucket 0 gives a gap of 1 - 1 = 0. Bucket 1 gives 7 - 4 = 3, and bucket 2 gives 10 - 7 = 3. The board marks buckets 3, 4 and 5 as skipped.
Bucket 6 gives 25 - 10 = 15. The last non-empty bucket before it is bucket 2, not bucket 5. This is the trap on the board: bucket_max[5] is -inf, so the formula with bucket_max[b - 1] would give inf. The variable prev_max is 10 and gives the right gap.
The answer is 15. On the LeetCode example [3, 6, 9, 1] the same code returns 3.
Why the answer is never inside one bucket
There are n values, so there are n - 1 gaps between sorted neighbours. The gaps add up to hi - lo. The largest gap is at least the average, (hi - lo) / (n - 1). A gap is a whole number, so the largest gap is at least (hi - lo) // (n - 1), which is width, whenever hi > lo. This is the pigeonhole principle.
A bucket covers width whole numbers, so two values in one bucket differ by at most width - 1. That is smaller than width. So no pair inside one bucket can be the largest gap. The largest gap always starts at the maximum of one non-empty bucket and ends at the minimum of a later non-empty bucket. The other values in a bucket never matter, so each bucket keeps only bucket_min and bucket_max.
The first trap is width = (hi - lo) // (n - 1) with no floor of 1. For nums = [1, 1, 1, 4, 2], hi - lo = 3 and n - 1 = 4, so the result is 0 and the next division raises a ZeroDivisionError. With max(1, ...), width = 1 and the answer is 2. The second trap is a width rounded up with + 1. For nums = [6, 18] the width becomes 13, both values share bucket 0, and the code returns 0 instead of 12.
Cost, and when not to use buckets
The placing loop does O(n) work and the walk visits every bucket once. The number of buckets is (hi - lo) // width + 1, which is less than 2n. So the time is O(n) and the space is O(n). Sorting first costs O(n log n).
Buckets need the range to be known before the data is placed. If the values arrive one at a time and the answer is needed after each arrival, as in Kth Largest Element in a Stream (LC 703), there is no range to lay out. Use a heap there.
If every value is a small whole number, one slot per exact value is enough and no range is needed. That is the next card, Counting Sort.
60-Second Pitch
"Sorting costs O(n log n), so I use buckets. There are n values and n - 1 gaps that add up to hi - lo, so the largest gap is at least (hi - lo) // (n - 1). I make each bucket that wide, with a floor of 1. Each value goes to the bucket (x - lo) // width, and each bucket keeps only its minimum and maximum. Two values in one bucket are closer than the width, so the answer is never inside a bucket. I walk the buckets in order, skip the empty ones, and measure each non-empty bucket's minimum against prev_max. The time is O(n) and the space is O(n). The two traps are the floor of 1 on width, and using prev_max instead of the previous bucket's maximum."
Common Questions
- Why is it enough to keep only the minimum and maximum of each bucket?
Two values in one bucket differ by at most
width - 1, and the largest gap is at leastwidth. So the largest gap is never between two values of one bucket. It always runs from the maximum of one bucket to the minimum of a later bucket.- What is the pigeonhole argument here?
There are
n - 1gaps between sorted neighbours, and they add up tohi - lo. The largest of them is at least the average. If every bucket is narrower than the average gap, two neighbours cannot be in the same bucket and span the largest gap.- What happens when all values are equal?
Then
hi - lois 0, so(hi - lo) // (n - 1)is 0. The floor giveswidth = 1andcount = 1. All values fall in bucket 0, and the walk measures a gap of 0.- How does Top K Frequent Elements (LC 347) use buckets?
Count the values with a dictionary. A count is at most
len(nums), so make a listbucketswithlen(nums) + 1entries and put each value inbuckets[count]. Read the list from the last index down untilkvalues are collected. The time isO(n).- When would you use a heap or a sort instead?
Use a heap when values arrive one at a time and the range is not known, as in Kth Largest Element in a Stream (LC 703). Use a sort when
nis small and the code must be short. Buckets win when the range is known and the limit needsO(n).
SWE Reasoning
Spot it
- The values lie in a known, bounded range, and a sort at O(n log n) is too slow for the limit.
- The question asks for the largest gap between neighbours in sorted order, as in Maximum Gap (LC 164), but the array may not be sorted.
- The key is a count that is never more than
n, and the items must come out ordered by it, as in Sort Characters By Frequency (LC 451) or Top K Frequent Elements (LC 347).
The invariant, step by step
loandhiare the smallest and largest value.width = max(1, (hi - lo) // (n - 1))is a number that is at most the smallest possible largest gap.- Bucket
bholds exactly the values in[lo + b * width, lo + (b + 1) * width). - Each value goes to bucket
b = (x - lo) // width. The bucket keeps only its smallest and largest value. - The walk visits the buckets in order. It skips empty buckets and keeps
prev_max, the largest value of the last non-empty bucket. - The gap
bucket_min[b] - prev_maxis measured only across buckets, and the largest of these is the answer.
Traps
- No floor of 1 on
width. With repeated values,(hi - lo) // (n - 1)is 0 and the next division raises aZeroDivisionError. - No check for
n < 2. With one value,n - 1is 0 and the division raises aZeroDivisionError. The answer is 0. - A width rounded up with
+ 1. Two values can then share a bucket, and the gap between them is never measured.[6, 18]returns 0 instead of 12. - Using
bucket_max[b - 1]instead ofprev_max. An empty previous bucket has the maximum-inf, and the gap becomesinf.