Problem overview
The method longestSubarray receives an array of integers, nums, and a number K. It returns the length of the longest contiguous subarray that contains at most K different values. Repeats of the same value count once, so [1, 2, 1, 2] has two distinct values even though it has four elements.
In the example, nums = [1, 2, 1, 2, 3] and K = 2. The first four elements, [1, 2, 1, 2], hold only the values 1 and 2, so the answer is 4. Adding the 3 would make three distinct values, which is too many.
Approach: a window that grows and shrinks
The slow way is to check every subarray and count its distinct values. That repeats a lot of work, because neighbouring subarrays share almost all of their elements.
A sliding window avoids this. Keep a window nums[first...last] and move its right end last across the array one index at a time. Each new element may push the number of distinct values above K. When that happens, move first to the right, dropping elements from the left, until the window is valid again. After that, the window ending at last is the longest valid one that ends there, so its length is a candidate for the answer.
The window needs a way to tell how many distinct values it holds. A dictionary from value to count does it: freq[v] is the number of times v occurs in the window, and freq.count is the number of keys, which is the number of distinct values. For this to work, a value whose count drops to 0 must be removed from the dictionary, not left there with a zero.
Step by step
- Create the counts.
freqis an empty[Int: Int]. It is avarbecause the helpers change it. - Create the answer and the left edge. Both
answerandfirststart at 0, declared together on onevarline. - Loop over the right edge.
for last in 0..<nums.counttries every index as the end of a subarray. - Add the new element. Call
insert(&freq, nums[last]). The window now coversfirst...last. - Shrink while the window is invalid. A
whileloop runs as long asfreq.count > K. - Drop the leftmost element. Inside the loop, call
remove(&freq, nums[first])and then dofirst += 1. Swift has no++operator. - Record the length. Once the loop ends, the window is valid, and its length is
last - first + 1. Keep the larger of that andanswerwithmax. - Return
answerafter the loop. - Write
remove. It takes the dictionary as aninout [Int: Int]and a value, both with the_label so the calls don't name the arguments. It subtracts one from the value's count, and if the count reaches 0 it deletes the key. - Write
insert. It has the same parameters and adds one to the value's count, starting from 0 when the key is missing.
The solution in Swift
class Solution {
func insert(_ freq: inout [Int: Int], _ val: Int) {
freq[val, default: 0] += 1
}
func remove(_ freq: inout [Int: Int], _ val: Int) {
freq[val]! -= 1
if freq[val] == 0 {
freq.removeValue(forKey: val)
}
}
func longestSubarray(_ nums: [Int], _ K: Int) -> Int {
var freq = [Int: Int]()
var answer = 0, first = 0
for last in 0..<nums.count {
insert(&freq, nums[last])
while freq.count > K {
remove(&freq, nums[first])
first += 1
}
answer = max(answer, last - first + 1)
}
return answer
}
}
The Swift details worth a closer look:
inoutand&. A dictionary is a value type, so passing it normally gives the function a copy. Marking the parameterinoutand writing&freqat the call lets the helper change the caller's dictionary.default: 0. Readingfreq[val]gives an optional, which isnilfor a missing key. The formfreq[val, default: 0] += 1reads 0 for a missing key and stores the new count, soinsertneeds noif.- The
!inremove.freq[val]! -= 1force-unwraps the optional. It is safe here because only values currently in the window are removed. On a missing key it would crash the program. - Comparing an optional with
0.freq[val] == 0compares anInt?with a literal, which Swift allows. It is false for a missing key and true only when the count is 0. - Ignored return value.
removeValue(forKey:)returns the old value, but Swift doesn't warn about ignoring it because the method is marked as discardable. - Why delete the zero. If a count of 0 stayed in the dictionary,
freq.countwould include a value that is no longer in the window, and the window would shrink too far.
Complexity
Time: O(n). The index last visits each position once. The index first only moves right and never passes last, so across the whole run the inner loop does at most n removals. Each dictionary operation takes constant time on average.
Space: O(n) in the worst case. The dictionary holds at most K + 1 keys, because the window shrinks as soon as it exceeds K, and it can never hold more keys than the array has elements.
Edge cases and common mistakes
K = 0. No value is allowed, so the window is emptied after every step and the method returns 0. For[1, 2, 3]withK = 0it returns 0.- Empty array. The loop never runs, so the method returns 0.
- Large
K. WhenKis at least the number of distinct values, the window never shrinks and the answer isnums.count. For[1, 2, 3, 4]withK = 10the result is 4. - One value repeated.
[4, 4, 4]withK = 1returns 3. - Leaving zero counts behind. Decrementing without deleting the key at 0 makes
freq.counttoo large. - Forgetting
&. Callinginsert(freq, nums[last])is a compile error, because aninoutargument must be passed with&, andfreqmust be avar. - Off-by-one length. Both ends are inside the window, so the length is
last - first + 1, notlast - first.
Testing
nums = [1, 2, 1, 2, 3],K = 2gives4.nums = [1, 2, 3],K = 0gives0.nums = [],K = 3gives0.nums = [4, 4, 4],K = 1gives3.nums = [1, 2, 3, 4],K = 10gives4.
