Problem overview
The function longest_consecutive receives an unsorted vector of integers, nums, and returns the length of the longest run of consecutive values, such as 1, 2, 3, 4. The values don't have to be neighbours in the input, and repeated values count once.
For nums = [100, 4, 200, 1, 3, 2] the values 1, 2, 3 and 4 form a run of length 4, so the answer is 4. For [0, 2, 0, 1, 2, 3, 1] the answer is also 4: the run is 0, 1, 2, 3, and the repeated 0, 1 and 2 are counted once each. An empty vector gives 0.
The lesson asks for O(n log n) time and O(1) extra space.
Approach: sort, then measure each block
In an unsorted vector, finding the neighbours of a value means searching for it. After sorting, a run of consecutive values becomes a block of adjacent elements, and different runs follow one after another. The sorted form of the second example is:
[0, 0, 1, 1, 2, 2, 3]
One pass over the sorted vector can then measure each block. For a block that starts at index i, keep curr_val, the last value of the run so far, and scan forward with j. The next element nums[j] falls into one of three cases:
- It equals
curr_val: a duplicate. The run doesn't grow, but it doesn't end either. - It equals
curr_val + 1: the run grows by one value. - It is larger than
curr_val + 1: there is a gap, and the run is over.
Since the vector is sorted, nothing can be smaller than curr_val. When the run ends at j, its length is the distance between its first and last values plus one, curr_val - nums[i] + 1. Counting by values and not by elements is what makes duplicates harmless. The next run starts at j, so i jumps there. Every element is passed over once, so the scan is O(n) and the sort dominates.
The first example sorts to [1, 2, 3, 4, 100, 200]. The first block starts at 1 and grows to 4, giving length 4. At 100 there is a gap, so 100 alone is a run of 1, and 200 is another run of 1.
Step by step
- Sort
numsin place withsort_unstable(). The starting code already returns 0 for an empty vector and rebindsnumsas mutable. Order among equal integers doesn't matter, so the unstable sort is fine. - Start
resultat 1. It is a mutablei32. A single element is already a run of length 1. - Open the outer loop. The index
iis a mutableusizestarting at 0. The loop runs whilei < nums.len(), andimarks the first element of the run to measure. The loop doesn't add 1 toi: its last line jumps it to where the next run starts. - Remember the last value.
curr_valstarts asnums[i]. After sorting, a run is one block, so its last value is enough to decide whether the next element belongs. - Create the scan index.
jstarts ati + 1, the next element to try. - Open the inner loop. It runs while
j < nums.len()andnums[j] <= curr_val + 1. Such an element is a duplicate or the next value; anything larger is a gap. The bounds check goes first, sonums[j]is never read past the end. - Grow on the next value. If
nums[j] == curr_val + 1, add 1 tocurr_val. A duplicate leaves it unchanged. - Advance
jby one on every pass, for duplicates and new values alike. - Record the length. After the inner loop, set
resultto the larger of itself andcurr_val - nums[i] + 1. - Jump to the next run by setting
i = j. Everything beforejbelonged to the run just measured. - Return
resultas the last expression.
The solution in Rust
impl Solution {
fn longest_consecutive(nums: Vec<i32>) -> i32 {
if nums.is_empty() {
return 0;
}
let mut nums = nums;
nums.sort_unstable();
let mut result = 1i32;
let mut i = 0usize;
while i < nums.len() {
let mut curr_val = nums[i];
let mut j = i + 1;
while j < nums.len() && nums[j] <= curr_val + 1 {
if nums[j] == curr_val + 1 {
curr_val += 1;
}
j += 1;
}
result = result.max(curr_val - nums[i] + 1);
i = j;
}
result
}
}
The Rust details worth a closer look:
- Literal suffixes.
1i32and0usizefix the types ofresultandi. The first must match the return type, and the second must beusizeto index a vector. - Short-circuit
&&. Inj < nums.len() && nums[j] <= curr_val + 1, the right side runs only if the left side is true. That is what keepsnums[j]in bounds. whileand manual indices. Becauseijumps by a variable amount, aforrange doesn't fit, so the code useswhilewithi = j.maxis a method.result.max(...)returns the larger of twoi32values.- Overflow at the edge. The lessons build with optimizations, where
i32overflow wraps silently, and a debug build would panic instead. Withcurr_valequal toi32::MAX, the expressioncurr_val + 1wraps toi32::MIN. In an optimized build, the input[2147483646, 2147483647, 2147483647]still returns 2, because the wrapped bound only stops the scan one step early, and the leftover duplicate forms a run of its own.
Complexity
Time: O(n log n). The sort takes O(n log n). The two nested while loops look quadratic, but j only moves forward and i is set to j after each block, so every element is visited once in total: O(n).
Space. The scan uses a few integers. The sort is in place, and sort_unstable() needs no temporary buffer, so the extra space is O(1).
Edge cases and common mistakes
- Empty vector. The first guard returns 0. Without it, the starting value of 1 would be wrong.
- Duplicates. They must neither end a run nor lengthen it. For
[1, 2, 2, 3]the answer is 3, not 4. That is why the length comes fromcurr_val - nums[i] + 1and not from the number of elements scanned. - Counting elements instead of values. Using
j - ias the length gives the number of elements in the block, which includes duplicates. For[0, 2, 0, 1, 2, 3, 1]that would be 7. - Using
<instead of<=. The conditionnums[j] < curr_val + 1accepts only duplicates, so the run never grows. - Negative numbers. Runs such as -2, -1, 0 work like any others. For
[-2, -1, 0, 3, 4]the answer is 3. - A gap in the middle. For
[1, 2, 2, 3, 10, 11, 12, 13]the runs have lengths 3 and 4, and the answer is 4.
Testing
nums = [100, 4, 200, 1, 3, 2]gives 4.nums = [0, 2, 0, 1, 2, 3, 1]gives 4.nums = []gives 0.nums = [7]gives 1.nums = [5, 5, 5]gives 1.nums = [-2, -1, 0, 3, 4]gives 3.
