Problem overview
The method longestConsecutive receives an unsorted array of integers, nums, and returns the length of the longest run of consecutive values: numbers that follow each other with a gap of exactly 1, such as 1, 2, 3, 4. The numbers don't have to be next to each other in the array.
In the first example, [100, 4, 200, 1, 3, 2], the values 1, 2, 3 and 4 form a run of length 4, while 100 and 200 are alone. The answer is 4.
In the second example, [0, 2, 0, 1, 2, 3, 1], the run is 0, 1, 2, 3, and the answer is again 4. Duplicates don't make the run longer: each value counts once, even though 0, 1 and 2 appear twice.
The statement sets a budget of O(n log n) time and O(1) extra space. Faster solutions exist, but they need extra memory.
Approach: sort, then measure each block
In the original order, finding what comes after a value means searching the whole array. After sorting in ascending order, every run of consecutive values is a contiguous block of the array, and the blocks follow one another. The only complication is duplicates, which sit inside a block: [0, 0, 1, 1, 2, 2, 3] is one run of length 4 in seven elements.
So the plan is to scan the sorted array once. At the start of each block, remember the first value and keep extending the block while the next element is either a duplicate of the last value or exactly one more. A larger gap ends the block, and the next block starts there. The length of a block is its last value minus its first value, plus 1.
Take the second example. The sorted array is [0, 0, 1, 1, 2, 2, 3]. The block starts at 0, and currVal is 0. The second 0 is a duplicate, so it is skipped. Then 1 raises currVal to 1, the second 1 is skipped, 2 raises it to 2, the second 2 is skipped, and 3 raises it to 3. The block ends at the end of the array, and its length is 3 - 0 + 1 = 4.
Step by step
- Start
resultat 1. A single element is already a run of length 1. It is aletvariable because it grows when a longer run is found. - Sort
numswith a comparator returningfirst - second, so values go in ascending order. Without it, the numbers would be compared as strings and 10 would come before 9. - Use an index
ithat marks the start of a block, and loop whilei < nums.length. The loop doesn't add 1 toi; the last line of its body jumps to where the next block starts. - Set
currValtonums[i]. It holds the last value of the current run, which at the start is just the first one. - Let
jscan forward fromi + 1. - Extend while the block continues. The inner loop runs while
jis inside the array andnums[j] <= currVal + 1. Because the array is sorted, such an element is either a duplicate ofcurrValor the next value. Anything larger means a gap. Thejcheck comes first, sonums[j]is never read past the end. - Grow the run only on the next value. If
nums[j] === currVal + 1, incrementcurrVal. A duplicate fails this check and leaves it alone. - Advance
jon every pass, whether the element was a duplicate or a new value. - Record the length with
Math.max(result, currVal - nums[i] + 1). - Jump to the next block with
i = j: everything beforejbelonged to the block just measured. - Return
resultafter the outer loop.
The solution in TypeScript
class Solution {
longestConsecutive(nums: number[]): number {
let result = 1;
nums.sort((first, second) => first - second);
let i = 0;
while (i < nums.length) {
let currVal = nums[i];
let j = i + 1;
while (j < nums.length && nums[j] <= currVal + 1) {
if (nums[j] === currVal + 1) {
currVal++;
}
j++;
}
result = Math.max(result, currVal - nums[i] + 1);
i = j;
}
return result;
}
}
Details worth knowing:
- Short-circuit
&&. Whenj < nums.lengthis false, the right side isn't evaluated. Reading past the end of an array in TypeScript doesn't throw; it givesundefined, andundefined <= 5is simply false. The check is still worth keeping, because it states the intent. - Strict equality.
===compares two numbers without any conversion, which is what you want here. Math.maxtakes any number of arguments and returns the largest one.- Sorting changes the caller's array. That keeps the extra space at
O(1). To keep the original, sort a copy with[...nums]. - No integer overflow worry.
numberis a double, and values near the limits of 32-bit integers are still exact. Beyond 2^53, integers start losing precision, which isn't a concern for this lesson's inputs.
Complexity
Time: O(n log n). The sort costs O(n log n). In the scan, j only moves forward and the outer loop continues from j, so each element is visited once by the inner loop. That makes the scan O(n), and the sort dominates.
Space: O(1). The code uses a handful of variables, and the sort works in place.
Edge cases and common mistakes
- Duplicates.
[5, 5, 5]returns 1: the duplicates fail the=== currVal + 1check, so the run stays at one value. - A gap.
[1, 2, 4, 5, 6]returns 3, from the block 4, 5, 6. - Negative numbers.
[-2, -1, 0]returns 3. The length formula uses a difference, so signs don't matter. - An empty array. This code returns 1, because
resultstarts at 1 and the loops never run. The lesson's inputs have at least one element; if empty input were allowed, the answer should be 0 and the method would need a check first. - Counting duplicates twice. Incrementing
currValfor every element in range would turn[0, 0, 1]into a run of 3 instead of 2. - Forgetting the comparator. With default string order,
[9, 10, 11]sorts as[10, 11, 9]and the run is found as 2 instead of 3.
Testing
[100, 4, 200, 1, 3, 2]gives4.[0, 2, 0, 1, 2, 3, 1]gives4.[5, 5, 5]gives1.[1, 2, 4, 5, 6]gives3.[-2, -1, 0]gives3.