Problem overview
The function longestSubarray receives an IntArray, nums, and an integer K. It must return the length of the longest contiguous subarray that contains at most K different values.
For nums = [1, 2, 1, 2, 3] and K = 2, the answer is 4. The subarray nums[0..3] is [1, 2, 1, 2], which holds only the values 1 and 2. Extending it to include the 3 would make three distinct values.
Approach: grow on the right, shrink on the left
The brute force checks every subarray and counts its distinct values, which is slow. A better observation is that validity is easy to keep up to date as a window moves. If a subarray has at most K distinct values, then every smaller subarray inside it does too. So you can slide a window along the array:
- Extend the right end by one element,
nums[last]. - If the window now has more than
Kdistinct values, drop elements from the left, one at a time, until it is valid again. - The window is now the longest valid subarray ending at
last, so compare its length with the best so far.
To know how many distinct values the window holds, and to know when a value has left it completely, you keep a map from each value to the number of times it occurs in the window. The number of keys is the number of distinct values. Both ends of the window only move forward, so each index enters once and leaves at most once, which makes the work linear.
The statement mentions O(n²) time, but this approach does better, at O(n).
Step by step
- Create the map,
val freq = HashMap<Int, Int>(), mapping each value in the window to its count.valonly stops the variable from being reassigned. The map's contents still change. - Declare
answerandfirstwithvar, both 0.answeris the best length so far andfirstis where the window starts. - Loop with
lastovernums.indices. It is the right end of the window. - Add the new element with the helper
insert(freq, nums[last]). - Shrink while invalid: a
whileloop that runs whilefreq.size > K. - Remove the leftmost element with
remove(freq, nums[first]), then dofirst++. - Update the answer with
maxOf(answer, last - first + 1). The windownums[first..last]is valid at this point. - Return
answer. - Write
remove. It takes the map and a value. It subtracts 1 from the value's count, and if the count reaches 0, it deletes the key. Without the deletion,freq.sizewould still count a value that has left the window. - Write
insert. It sets the value's count to its old count plus 1. A value that isn't in the map reads asnull, so the lookup is wrapped in parentheses with?: 0to start from 0.
The solution in Kotlin
class Solution {
fun insert(freq: HashMap<Int, Int>, value: Int) {
freq[value] = (freq[value] ?: 0) + 1
}
fun remove(freq: HashMap<Int, Int>, value: Int) {
freq[value] = freq[value]!! - 1
if (freq[value] == 0) {
freq.remove(value)
}
}
fun longestSubarray(nums: IntArray, K: Int): Int {
val freq = HashMap<Int, Int>()
var answer = 0
var first = 0
for (last in nums.indices) {
insert(freq, nums[last])
while (freq.size > K) {
remove(freq, nums[first])
first++
}
answer = maxOf(answer, last - first + 1)
}
return answer
}
}
The Kotlin details worth knowing:
- A missing key gives
null.freq[value]has the typeInt?. Ininsert, the Elvis operator?:supplies 0 when the key is absent. Without the parentheses,freq[value] ?: 0 + 1would parse asfreq[value] ?: (0 + 1), and an existing count would never be incremented. !!inremove. It asserts that the value is in the map. That holds becauseremoveis only called on values thatinsertadded. Callingremovefor an absent value would throw aNullPointerException.freq[value] == 0. Comparing anInt?with0is allowed in Kotlin:==callsequalsand handlesnull. It istrueonly for a present count of 0.- Index assignment.
freq[value] = xcalls the map'sset, which inserts or replaces. freq.remove(value)removes the key and its count. For aHashMap<Int, Int>it removes by key, not by position.HashMap<Int, Int>is Kotlin's name for Java'sHashMapwith boxedIntegerkeys and values.
Complexity
Time: O(n). last advances once per element, and first advances at most n times in total, since it never moves back. Each step performs a constant number of map operations, which take constant time on average.
Space: O(K), and at most O(n). The map holds at most K + 1 keys at any moment: right after an insertion it may briefly hold one extra, before the shrinking loop trims it. It can never hold more keys than the array has elements, so the O(n) bound from the statement holds too.
Edge cases and common mistakes
- Empty array. The loop doesn't run, and the answer is 0.
K = 0. Every inserted value makes the size 1, which is above 0, so the window shrinks until it is empty. The answer is 0.Kat least the number of distinct values. The window never shrinks, and the answer isnums.size. For[1, 2, 3, 4]withK = 10it is 4.- One repeated value.
[4, 4, 4]withK = 1gives 3: the count of 4 grows to 3, but the size stays at 1. - Not deleting zero counts. Leaving a key with count 0 in the map inflates
freq.size, so the window shrinks too much. - Checking validity only once. Removing one element doesn't always reduce the distinct count. If that value still occurs inside the window, the size doesn't change, which is why the shrink step is a
whileloop and not anif. - Measuring before shrinking. Compute the length only after the loop, when the window is valid.
Testing
nums = [1, 2, 1, 2, 3],K = 2gives4.nums = [1, 2, 3],K = 1gives1.nums = [4, 4, 4],K = 1gives3.nums = [1, 2, 3],K = 0gives0.nums = [],K = 2gives0.