Problem overview
The method longestConsecutive receives an unsorted Array[Int] called nums and returns the length of the longest run of consecutive integers that can be formed from its values. The run doesn't have to appear in order in the array: it only has to be made of values that are all present.
For Array(100, 4, 200, 1, 3, 2) the values 1, 2, 3 and 4 are all present, so the answer is 4. The values 100 and 200 are isolated.
A second example is Array(0, 2, 0, 1, 2, 3, 1). The values 0, 1, 2 and 3 form a run of length 4, and the answer is 4, not 7. Each value counts once, even though 0, 1 and 2 appear twice.
The lesson asks for O(n) time and O(n) extra space.
Approach: start counting only at the beginning of a run
Sorting the array and scanning would work, but costs O(n log n). To stay linear, put all values in a hash set. Then a question like "is 5 in the array?" takes constant time on average.
The naive use of the set is to start from every value and walk upward while the next value exists. That is correct but can be slow: in 1, 2, 3, ..., n, starting from every number walks over the rest of the run each time, which is O(n²). The fix is to walk a run only from its first number. A value is the first of a run exactly when value - 1 is not in the set. For every other value, skip it: its run will be walked from the real start.
Every run is then walked once, from its start, and each number is visited a constant number of times overall. The walk from a start counts upward: for a length sequenceLength that begins at 1, the next number needed is value + sequenceLength. The walk ends at the first number that isn't in the set.
On Array(100, 4, 200, 1, 3, 2) the only values whose predecessor is missing are 100, 200 and 1. Starting from 100 gives length 1, from 200 gives 1, and from 1 the walk finds 2, 3 and 4 and then stops at 5, so the length is 4.
Step by step
- Declare
resultwithvar, starting at 0. This is also the right answer for an empty array. - Fill the set. Create
values, an emptyscala.collection.mutable.HashSet[Int](), and add each element ofnumsin aforloop. Adding a number that is already there does nothing, sovaluesholds every distinct number once. - Loop over the set, not the array:
for (value <- values). This skips duplicates automatically. - Find run starts. Test
!values.contains(value - 1). Only then isvaluethe first number of a run. - Start the length at 1 with
var sequenceLength = 1, because the run already containsvalue. - Extend the run.
while (values.contains(value + sequenceLength)), add 1 tosequenceLength. The loop stops at the first number that is missing. - Keep the best. After the
while, still inside theif, setresult = math.max(result, sequenceLength). - Return
resultbelow theforloop.
The solution in Scala
class Solution {
def longestConsecutive(nums: Array[Int]): Int = {
var result = 0
val values = scala.collection.mutable.HashSet[Int]()
for (value <- nums) {
values.add(value)
}
for (value <- values) {
if (!values.contains(value - 1)) {
var sequenceLength = 1
while (values.contains(value + sequenceLength)) {
sequenceLength += 1
}
result = math.max(result, sequenceLength)
}
}
return result
}
}
The Scala details worth a closer look:
- Two loops over different things. The first
foriterates the array to fill the set, and the second iterates the set itself. Iterating over a mutable set is fine as long as the loop doesn't change it, and this loop only readsvalues. - Same name, separate scopes. Both loops call their variable
value. Each name exists only inside its own loop, so there is no clash. varinside theif.sequenceLengthis created fresh for every run start, so each walk begins at 1.- Int arithmetic.
value - 1andvalue + sequenceLengthareIntoperations, andIntwraps silently. WithInt.MinValuein the array,value - 1wraps around toInt.MaxValue, so a set holding both extremes could be seen as a run. The lesson's inputs don't go there. - Return style. The explicit
return resultsits directly in the method body, not inside aforclosure, so it behaves normally. The bare expressionresultwould do the same.
Complexity
Time: O(n). Filling the set takes n constant-time insertions on average. In the second loop, every value costs one membership test, and the while loop runs only for run starts. Each distinct number is stepped over by exactly one walk, the one from the start of its run, so the walks together take at most n steps.
Space: O(n). The set holds each distinct value once.
Edge cases and common mistakes
- Empty array. Both loops do nothing and the result is 0.
- Duplicates.
Array(5, 5, 5)gives 1. The set keeps one 5. - Negative numbers.
Array(-2, -1, 0, 5)gives 3, since -2, -1, 0 is a run. - Skipping the start test. Without the
!values.contains(value - 1)check the answer is still correct, but a long run is re-walked from each of its numbers, which makes the timeO(n²). - Counting array length. Using the array's length instead of the distinct numbers counts duplicates. In the second example that would give 7 rather than 4.
- Starting the length at 0. A run contains its start, so the length begins at 1. Starting at 0 and checking
value + sequenceLengthwould be off by one. - Wrong next number. The next number to look for is
value + sequenceLength. Usingvalue + 1every time never moves forward.
Testing
Array(100, 4, 200, 1, 3, 2)gives4.Array(0, 2, 0, 1, 2, 3, 1)gives4.Array(-2, -1, 0, 5)gives3.Array(5, 5, 5)gives1.Array()gives0.
