LeetCode 164 - Maximum Gap
The problem asks us to compute the largest difference between two consecutive elements after sorting the array. At first glance, this may seem straightforward: sort the array, then scan through adjacent pairs to find the maximum difference.
Difficulty: 🟡 Medium
Topics: Array, Sorting, Bucket Sort, Radix Sort
Solution
Problem Understanding
The problem asks us to compute the largest difference between two consecutive elements after sorting the array.
At first glance, this may seem straightforward: sort the array, then scan through adjacent pairs to find the maximum difference. However, there is an important constraint: the algorithm must run in linear time, O(n), and use linear extra space, O(n). This immediately rules out standard comparison-based sorting algorithms such as quicksort, mergesort, or heapsort, because they require O(n log n) time.
The input is an integer array nums, where each element represents a non-negative integer. We are asked to return the maximum gap between any two neighboring values in the array's sorted order.
For example:
nums = [3,6,9,1]
If we sort it:
[1,3,6,9]
The consecutive differences are:
3 - 1 = 2
6 - 3 = 3
9 - 6 = 3
The maximum difference is 3.
The output is therefore a single integer representing this maximum adjacent difference.
The constraints tell us several important things:
1 <= nums.length <= 10^5, meaning the input can be very large, so inefficient solutions will time out.0 <= nums[i] <= 10^9, meaning values can be very large, so we cannot rely on small bounded ranges for counting sort.- We must achieve linear time, which strongly suggests using a non-comparison sorting strategy or exploiting a mathematical observation.
There are several important edge cases to consider upfront.
If the array contains fewer than two elements, there are no adjacent pairs, so the answer must be 0.
If all elements are identical, every difference is 0.
Very sparse ranges can create huge gaps. For example:
[1, 1000000]
The maximum gap is 999999.
Large duplicate groups can also cause mistakes if bucket boundaries are not handled carefully.
Approaches
Brute Force Approach
The simplest solution is to sort the array and then compute the maximum difference between adjacent elements.
The process is straightforward:
- Sort
nums. - Iterate through the sorted array.
- Compute differences between neighboring elements.
- Track the largest difference.
This works because after sorting, consecutive elements are exactly the pairs whose gap we care about.
However, sorting takes O(n log n) time, which violates the requirement for a linear-time solution.
For example:
nums = [3,6,9,1]
Sorted:
[1,3,6,9]
Adjacent gaps:
2, 3, 3
Maximum gap = 3.
Although correct, this approach is too slow for the problem's strict time complexity requirement.
Optimal Approach, Bucket Sort Using the Pigeonhole Principle
The key insight is that we do not actually need the fully sorted array.
We only need the largest adjacent gap.
Suppose:
min_num= minimum valuemax_num= maximum valuen= number of elements
If we divide the range into buckets, we can use an important observation:
The maximum adjacent gap cannot occur inside a bucket, it must occur between buckets.
Why?
If bucket size is chosen carefully, numbers inside the same bucket are guaranteed to be relatively close together. Therefore, any very large gap must appear between the maximum of one bucket and the minimum of another.
We use the Pigeonhole Principle.
The minimum possible maximum gap is:
(max_num - min_num) / (n - 1)
This tells us an appropriate bucket size.
Each bucket stores only:
- The minimum value inside it
- The maximum value inside it
We do not need to store every element.
After placing all numbers into buckets, we scan bucket boundaries:
current_bucket_min - previous_bucket_max
The largest such value is the answer.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(1) or O(n) | Sort array and scan adjacent differences |
| Optimal | O(n) | O(n) | Bucket sort using pigeonhole principle |
Algorithm Walkthrough
Step 1: Handle Small Inputs
If the array contains fewer than two elements, return 0.
No adjacent comparison is possible.
Step 2: Find Global Minimum and Maximum
We determine:
min_num = min(nums)
max_num = max(nums)
These values define the overall range.
If:
min_num == max_num
all elements are identical, so return 0.
Step 3: Compute Bucket Size
We calculate the minimum guaranteed maximum gap:
bucket_size = max(1, (max_num - min_num) // (n - 1))
We use max(1, ...) to avoid division issues.
This bucket size ensures that any maximum gap must occur between buckets.
Step 4: Determine Number of Buckets
We compute:
bucket_count =
(max_num - min_num) // bucket_size + 1
Each bucket covers a continuous range.
For example:
nums = [1,3,6,9]
range = 8
bucket_size = 2
bucket_count = 5
Buckets:
[1-2]
[3-4]
[5-6]
[7-8]
[9-10]
Step 5: Initialize Buckets
Each bucket stores:
- Minimum value
- Maximum value
Initially:
bucket_min = [inf] * bucket_count
bucket_max = [-inf] * bucket_count
We also track whether a bucket is used.
Step 6: Place Numbers Into Buckets
For each number:
bucket_index =
(num - min_num) // bucket_size
Update:
bucket_min[index]
bucket_max[index]
Only extremes matter.
Step 7: Scan Buckets for Maximum Gap
We skip empty buckets.
For every non-empty bucket:
gap =
current_bucket_min - previous_bucket_max
Update maximum gap.
Then move forward:
previous_bucket_max =
current_bucket_max
Step 8: Return Result
The maximum inter-bucket difference is the answer.
Python Solution
from typing import List
class Solution:
def maximumGap(self, nums: List[int]) -> int:
n = len(nums)
if n < 2:
return 0
min_num = min(nums)
max_num = max(nums)
if min_num == max_num:
return 0
bucket_size = max(
1,
(max_num - min_num) // (n - 1)
)
bucket_count = (
(max_num - min_num)
// bucket_size
) + 1
bucket_min = [float("inf")] * bucket_count
bucket_max = [float("-inf")] * bucket_count
bucket_used = [False] * bucket_count
for num in nums:
bucket_index = (
num - min_num
) // bucket_size
bucket_min[bucket_index] = min(
bucket_min[bucket_index],
num
)
bucket_max[bucket_index] = max(
bucket_max[bucket_index],
num
)
bucket_used[bucket_index] = True
max_gap = 0
previous_max = min_num
for i in range(bucket_count):
if not bucket_used[i]:
continue
max_gap = max(
max_gap,
bucket_min[i] - previous_max
)
previous_max = bucket_max[i]
return max_gap
The implementation begins by handling arrays with fewer than two elements. Since no adjacent comparison exists, the result is immediately 0.
Next, the global minimum and maximum values are found. These determine the numerical range we must cover with buckets. If all numbers are identical, every adjacent difference is zero.
The bucket size is then computed using the pigeonhole principle. This is the critical mathematical insight that guarantees the maximum gap appears between buckets rather than inside one.
Instead of storing every number, each bucket keeps only its minimum and maximum value. This greatly reduces memory usage while preserving all information needed to compute the answer.
Finally, the algorithm scans through the buckets. Empty buckets are skipped. For each non-empty bucket, we compute the difference between its minimum value and the previous bucket's maximum value. The largest such difference becomes the result.
Go Solution
func maximumGap(nums []int) int {
n := len(nums)
if n < 2 {
return 0
}
minNum := nums[0]
maxNum := nums[0]
for _, num := range nums {
if num < minNum {
minNum = num
}
if num > maxNum {
maxNum = num
}
}
if minNum == maxNum {
return 0
}
bucketSize := (maxNum - minNum) / (n - 1)
if bucketSize < 1 {
bucketSize = 1
}
bucketCount :=
(maxNum-minNum)/bucketSize + 1
const inf = int(^uint(0) >> 1)
bucketMin := make([]int, bucketCount)
bucketMax := make([]int, bucketCount)
bucketUsed := make([]bool, bucketCount)
for i := 0; i < bucketCount; i++ {
bucketMin[i] = inf
bucketMax[i] = -inf
}
for _, num := range nums {
index :=
(num - minNum) / bucketSize
if num < bucketMin[index] {
bucketMin[index] = num
}
if num > bucketMax[index] {
bucketMax[index] = num
}
bucketUsed[index] = true
}
maxGap := 0
previousMax := minNum
for i := 0; i < bucketCount; i++ {
if !bucketUsed[i] {
continue
}
gap :=
bucketMin[i] - previousMax
if gap > maxGap {
maxGap = gap
}
previousMax = bucketMax[i]
}
return maxGap
}
The Go implementation follows the same logic as Python, but there are a few language-specific differences.
Go does not have built-in infinity values for integers, so we simulate positive infinity using:
int(^uint(0) >> 1)
This represents the maximum integer value.
Instead of Python lists, Go uses slices initialized with make(). We also explicitly initialize bucket minimums and maximums because Go integers default to 0, which would break the logic for tracking extremes.
Go handles empty slices naturally, so no special nil handling is required beyond the length check.
Worked Examples
Example 1
Input:
nums = [3,6,9,1]
Step 1: Find Min and Max
| Variable | Value |
|---|---|
| min_num | 1 |
| max_num | 9 |
| n | 4 |
Step 2: Compute Bucket Size
bucket_size =
(9 - 1) // (4 - 1)
= 2
Step 3: Create Buckets
bucket_count =
(9 - 1) // 2 + 1
= 5
Buckets:
| Bucket Index | Range |
|---|---|
| 0 | [1,2] |
| 1 | [3,4] |
| 2 | [5,6] |
| 3 | [7,8] |
| 4 | [9,10] |
Step 4: Place Numbers
| Number | Bucket | Bucket Min | Bucket Max |
|---|---|---|---|
| 3 | 1 | 3 | 3 |
| 6 | 2 | 6 | 6 |
| 9 | 4 | 9 | 9 |
| 1 | 0 | 1 | 1 |
Step 5: Scan Buckets
| Bucket | Gap Calculation | Gap |
|---|---|---|
| 0 | 1 - 1 | 0 |
| 1 | 3 - 1 | 2 |
| 2 | 6 - 3 | 3 |
| 4 | 9 - 6 | 3 |
Maximum gap:
3
Example 2
Input:
nums = [10]
Since:
n < 2
We immediately return:
0
No adjacent pair exists.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | One pass for min/max, one pass for bucket filling, one pass for scanning |
| Space | O(n) | Buckets require linear auxiliary memory |
The algorithm achieves linear time because every element is processed a constant number of times. We never sort the array explicitly. Instead, bucket placement and bucket scanning each require only a single traversal.
The space complexity is linear because the number of buckets grows proportionally to the number of elements.
Edge Cases
Edge Case 1: Array With One Element
Consider:
[10]
There are no adjacent elements after sorting, so no gap exists. A naive implementation may accidentally attempt comparisons and cause index errors. Our solution handles this immediately with:
if n < 2:
return 0
Edge Case 2: All Elements Are Equal
Consider:
[5,5,5,5]
Sorted form:
[5,5,5,5]
All gaps are zero.
This case is important because:
max_num - min_num = 0
which could cause division-by-zero problems when computing bucket size. Our implementation avoids this by returning 0 immediately when:
min_num == max_num
Edge Case 3: Large Numerical Gaps
Consider:
[1, 1000000000]
The maximum gap is:
999999999
A poor bucket strategy might overflow or create incorrect ranges. Our implementation computes bucket size dynamically and correctly places both values into separate buckets, preserving the large difference.
Edge Case 4: Duplicate Values
Consider:
[1,1,1,10]
Duplicates should not confuse bucket boundaries.
Sorted form:
[1,1,1,10]
Gaps:
0, 0, 9
The algorithm correctly tracks only bucket minimums and maximums, ensuring duplicates do not distort the final answer.