Problem overview
Given an unsorted IntArray named nums, longestConsecutive returns the length of the longest run of values that increase by exactly 1, such as 1, 2, 3, 4. The values don't have to be next to each other in the input.
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 (0, 1, 2, 3): each value counts once, even though 0, 1 and 2 appear twice.
Approach
The brute-force idea is to start from each value and keep checking whether the next value exists somewhere in the array. Searching an unsorted array each time makes that O(n^2) or worse. A hash set can answer "does this value exist?" in constant time, but this lesson wants O(n log n) time with O(1) extra space, so it sorts instead.
After sorting, every consecutive run is a contiguous block of the array, and the blocks follow one another. Inside a block, each element is either the same as the previous one (a duplicate) or exactly one larger. A gap of 2 or more means the block has ended and the next one starts. So one left-to-right scan can measure every block, and we keep the longest.
Sorted, the second example is [0, 0, 1, 1, 2, 2, 3]. There is a single block from 0 to 3, so the answer is 3 - 0 + 1 = 4.
Tracing [100, 4, 200, 1, 3, 2] after sorting, [1, 2, 3, 4, 100, 200]: the first block starts at 1, currVal climbs to 4, and the result becomes 4. The scan then stops at 100 because it is more than 5, and each of 100 and 200 forms a block of length 1.
Step by step
- Create
resultas avarstarting at 1. Any non-empty array contains a run of length 1, and the value grows as longer runs are found. - Sort
numsin place withsort(). - Create the index
iat 0 and open awhileloop that runs whilei < nums.size. Hereiis the first element of the run being measured. The loop doesn't add 1 toiat the end; the last line of its body jumps it to the start of the next run. - Create
currValas avarequal tonums[i]. It is the last value of the run so far. - Create
jequal toi + 1, the next element to try. - Open an inner
whilewith the conditionj < nums.size && nums[j] <= currVal + 1. Because the array is sorted, an element that passes is either a duplicate ofcurrValor the next value. Anything bigger is a gap. The length check comes first sonums[j]is never read past the end. - Extend the run if it is the next value. If
nums[j] == currVal + 1, incrementcurrVal. A duplicate fails this test and leavescurrValunchanged, so it isn't counted twice. - Increment
jon every pass, whether the element was a duplicate or a new value. - After the inner loop, update
resultwithmaxOf(result, currVal - nums[i] + 1). A run fromatobcontainsb - a + 1values. - Set
i = j. Everything beforejbelongs to the run just measured, so the next run begins atj. - Return
result.
The solution in Kotlin
class Solution {
fun longestConsecutive(nums: IntArray): Int {
var result = 1
nums.sort()
var i = 0
while (i < nums.size) {
var currVal = nums[i]
var j = i + 1
while (j < nums.size && nums[j] <= currVal + 1) {
if (nums[j] == currVal + 1) {
currVal++
}
j++
}
result = maxOf(result, currVal - nums[i] + 1)
i = j
}
return result
}
}
Kotlin details worth knowing:
- Kotlin has no C-style
for (i = 0; ...; ...)loop, and hereijumps by variable amounts, so awhileloop is the natural fit. &&stops as soon as its left side is false. That's whyj < nums.sizeprotects the read ofnums[j].maxOf(a, b)is a built-in function that returns the larger of two values.currVal++works on avar. Writingval currValwould be a compile error.- An
Intis 32 bits. The expressioncurrVal + 1would wrap around ifcurrValwereInt.MAX_VALUE, which can only matter for inputs at the very limit of the type.
Complexity
- Time:
O(n log n)for the sort. The two nested loops look quadratic, butjonly moves forward andiis set tojafter each block, so every element is visited once:O(n). The sort dominates. - Space:
O(1)extra, since only a few variables are added and the sort works in place.
Edge cases and common mistakes
- Duplicates. They must neither end a run nor lengthen it. That's the reason for the two-part test:
<=in the loop condition and==before incrementing. - Using
==in the loop condition.nums[j] == currVal + 1would stop the scan at the first duplicate, so[1, 2, 2, 3]would give 2 instead of 3. - Measuring length as
j - i. That counts duplicates. For[1, 2, 2, 3]it gives 4, but the answer is 3. - Negative numbers. They work without changes:
[-2, -1, 0]gives 3. - All values equal.
[5, 5, 5]gives 1. - Reading past the end. Putting
nums[j] <= currVal + 1before the length check throws anArrayIndexOutOfBoundsExceptionwhenjreachesnums.size. - Empty array. The loop is skipped and the function returns 1, although the true answer is 0. If empty inputs are possible, return 0 first.
Testing
[100, 4, 200, 1, 3, 2]returns4.[0, 2, 0, 1, 2, 3, 1]returns4.[1, 2, 2, 3]returns3.[1, 3, 5]returns1.