Problem overview
The method longestConsecutive receives an unsorted array of integers and returns the length of the longest run of values that follow each other by exactly 1: such as 1, 2, 3, 4. The values don't need to be next to each other in the array, and a repeated value counts once.
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, from 0, 1, 2, 3. The repeated 0, 1 and 2 add nothing.
Approach: sort, then scan the runs
After sorting, every run of consecutive values is a block of neighbours in the array, and the blocks follow each other. Within a block, each element is either equal to the previous one (a duplicate) or exactly one larger. When an element is two or more above the last value, there is a gap, and a new block starts.
So one pass is enough. Start a block at index i and extend it with a second index j. Keep currVal, the last value of the run so far. If nums[j] equals currVal, it is a duplicate: skip it. If it equals currVal + 1, the run grows: increase currVal. If it is larger still, the run has ended. The run's length is currVal - nums[i] + 1, and the next block starts at j.
Sorted, the second example is [0, 0, 1, 1, 2, 2, 3]. The block starts at the first 0. The scan skips the second 0, then grows through 1, skips the second 1, grows to 2, skips the second 2 and grows to 3. The length is 3 - 0 + 1 = 4.
Now the first example, sorted: [1, 2, 3, 4, 100, 200]. The first block starts at 1. The scan grows through 2, 3 and 4, then meets 100, which is above currVal + 1 = 5, so the block ends with length 4 and i jumps to the index of 100. That block is just 100, with length 1, and the last block is just 200. The maximum is 4.
Step by step
- Start with
result = 1. A non-empty array always has a run of at least one value. - Make a mutable copy. A parameter is a constant, so
var nums = numsshadows it with a variable. - Sort the copy with
sort(). - Open a
whileloop with the indexiat 0. It marks the first element of the block being measured. The loop doesn't add 1 toi; its last line movesito the start of the next block. - Set
currValtonums[i]: the run so far contains just that value. - Set
jtoi + 1, the next element to look at. - Inner loop: run while
j < nums.countandnums[j] <= currVal + 1. Checkingjfirst meansnums[j]is never read past the end. - Grow the run with
if nums[j] == currVal + 1 { currVal += 1 }. A duplicate fails the test and changes nothing. - Move on with
j += 1on every pass. - Record the length with
max(result, currVal - nums[i] + 1). - Jump to the next block with
i = j, then returnresultafter the outer loop.
The solution in Swift
class Solution {
func longestConsecutive(_ nums: [Int]) -> Int {
var result = 1
var nums = nums
nums.sort()
var i = 0
while i < nums.count {
var currVal = nums[i]
var j = i + 1
while j < nums.count && nums[j] <= currVal + 1 {
if nums[j] == currVal + 1 {
currVal += 1
}
j += 1
}
result = max(result, currVal - nums[i] + 1)
i = j
}
return result
}
}
The Swift details worth knowing:
- Short-circuit
&&. Ifj < nums.countis false, Swift doesn't evaluate the right side. That is why the bounds check has to come first. - No C-style
for. Becauseijumps by variable amounts, awhileloop is the natural fit. Afor i in 0..<nums.countloop can't have its counter changed inside it. - Shadowing.
var nums = numsis legal in a function body and gives you a mutable local copy. The caller's array is unchanged. - Global
max.max(a, b)returns the larger of two values. - Empty array. The function returns 1 for an empty array, since
resultstarts at 1 and the loop never runs. The lesson doesn't use that case, but it is the one place where 1 is not a true run length.
Complexity
Time: O(n log n). Sorting dominates. The two while loops together advance j through the array once, since i always jumps to where j stopped, so the scan is O(n).
Space: O(n). The sorted copy of the array holds n values. Apart from it, the code uses a few integers.
Edge cases and common mistakes
- Duplicates.
[1, 2, 2, 3]has the run 1, 2, 3, so the answer is 3, not 4. The inner testnums[j] == currVal + 1keeps duplicates from counting. - Negative values.
[-2, -1, 5]gives 2. The subtractioncurrVal - nums[i] + 1works the same for negative numbers. - A gap in the middle.
[1, 2, 4, 5, 6]gives 3, from 4, 5, 6. - Using the block's size. Measuring a block as
j - icounts duplicates. The length must come from the values,currVal - nums[i] + 1. - Stopping on a duplicate. If the loop condition were
nums[j] == currVal + 1, a duplicate would end the run too early. - Forgetting
i = j. The outer loop would never move forward and would repeat forever.
Testing
[100, 4, 200, 1, 3, 2]gives 4.[0, 2, 0, 1, 2, 3, 1]gives 4.[-2, -1, 5]gives 2.[1, 2, 4, 5, 6]gives 3.
