Problem overview
The method longestConsecutive receives an unsorted array of integers and returns the length of the longest run of consecutive values, such as 1, 2, 3, 4. The values do not have to be next to each other in the array, and each value counts once even if it appears several times.
For [100, 4, 200, 1, 3, 2] the longest run is 1, 2, 3, 4, so the answer is 4. For [0, 2, 0, 1, 2, 3, 1] the answer is also 4, because the run 0, 1, 2, 3 is counted once even though 0, 1 and 2 appear twice.
Approach: sort, then measure each run
In an unsorted array, finding what comes after a value means searching the whole array. After sorting, every run of consecutive numbers becomes one block of neighbouring elements, and the blocks follow each other. The sorted version of the second example is [0, 0, 1, 1, 2, 2, 3], and the whole array is a single block.
Inside a block, each element is one of two things: a duplicate of the run's current last value, or that value plus 1. An element larger than that leaves a gap, so it starts a new run. So the scan keeps currVal, the last value of the current run, and moves a second index forward while the next element is at most currVal + 1. The length of a run is currVal - start + 1, because a run from a to b contains b - a + 1 values. The sort costs O(n log n) and the scan visits each element once, so the total is O(n log n).
Step by step
- Start
resultat 1, as avar. A single element is already a run of length 1, and the value grows when a longer run is found. - Sort
numsin place withscala.util.Sorting.quickSort, so runs become contiguous blocks. - Open the outer
whileloop with an indexithat starts at 0. It marks the first element of the run about to be measured. The loop doesn't moveiby 1 each time; its last line jumps to where the next run starts. - Start
currValatnums(i). It holds the last value of the current run. - Start
jati + 1, the next element to try to add to the run. - Open the inner
whileloop: continue whilejis inside the array andnums(j) <= currVal + 1. The bounds check comes first, sonums(j)is never read past the end. - Extend the run on the next value. If
nums(j) == currVal + 1, add 1 tocurrVal. A duplicate fails this test and leavescurrValalone. - Advance
jon every pass, since duplicates and new values are both consumed. - Record the run's length. After the inner loop, set
resultto the larger of its old value andcurrVal - nums(i) + 1. - Jump
itoj. Everything beforejbelongs to the run just measured, andnums(j)is the first element after a gap. - Return
resultas the final expression.
The solution in Scala
class Solution {
def longestConsecutive(nums: Array[Int]): Int = {
var result = 1
scala.util.Sorting.quickSort(nums)
var i = 0
while (i < nums.length) {
var currVal = nums(i)
var j = i + 1
while (j < nums.length && nums(j) <= currVal + 1) {
if (nums(j) == currVal + 1) {
currVal += 1
}
j += 1
}
result = math.max(result, currVal - nums(i) + 1)
i = j
}
result
}
}
The Scala details worth knowing:
- Why
whileand notfor. Afor (i <- 0 until n)loop takes its values from a range and cannot be told to skip ahead. This loop changesiinside its body, so it needswhileand avar. &&short-circuits. Ifj < nums.lengthis false, the right-hand side isn't evaluated, so the out-of-range read never happens.math.maxis Scala's wrapper for the same operation asMath.maxfrom Java. Both work onIntvalues.- No
++. Counters grow withj += 1andcurrVal += 1. - The array is sorted in place.
quickSortchanges the caller's array.nums.sortedwould leave it untouched at the cost of a copy. - Last expression. The bare
resulton the final line is the method's return value.
Complexity
Time: O(n log n). Sorting dominates. The two while loops together move j over each element once: the inner loop advances j, and the outer loop sets i = j rather than rescanning. The scan is O(n).
Space: about O(1) extra, because the sort works in place and the scan keeps a few integers.
Edge cases and common mistakes
- Duplicates.
[1, 2, 2, 3]gives 3. A duplicate must neither break the run nor be counted twice, which is why theifchecks for exactlycurrVal + 1while the loop condition allows<= currVal + 1. - Gaps.
[1, 2, 4, 5, 6]gives 3: the run 4, 5, 6 starts after the gap. - Negative numbers.
[-2, -1, 0, 1]gives 4. Nothing special is needed. - One element.
[10]gives 1. - Empty array. The code returns 1, because
resultstarts at 1 and the loop never runs. An array with no elements has no run at all, so the code as written is only right for non-empty input. - Using
<instead of<=. The inner loop would accept only duplicates, because an element belowcurrVal + 1cannot be larger thancurrVal. The run could never grow, and every input would give 1. - Forgetting
i = j. The outer loop never advances and runs forever. - Measuring length as
j - i. That counts duplicates. For[0, 0, 1]it gives 3, while the run has 2 distinct values.
Testing
[100, 4, 200, 1, 3, 2]gives4.[0, 2, 0, 1, 2, 3, 1]gives4.[1, 2, 4, 5, 6]gives3.[-2, -1, 0, 1]gives4.[10]gives1.
