Problem overview
The function longestSubarray receives a slice nums of integers 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: [1, 2, 1, 2] has two distinct values although it has four elements.
In the example, nums = []int{1, 2, 1, 2, 3} and k = 2. The first four elements contain only the values 1 and 2, so a window of length 4 is allowed. Adding the 3 would make three distinct values, and the window [2, 3] at the end is shorter, so the answer is 4.
The statement allows O(n²) time and asks for O(n) extra space. The approach below needs only O(n) time.
Approach: grow the window, shrink it when it has too many kinds
The simple solution is to fix each start and extend to the right, tracking the distinct values in a set, until a value number k + 1 shows up. That scans up to n elements for each of n starts, which is the O(n²) budget.
The faster idea changes who moves. Instead of fixing the start, fix the end of the window and let the start follow:
- Visit each index
lastin turn and addnums[last]to the window. - If the window now has more than
kdistinct values, remove elements from the front (indexfirst) until it has at mostkagain. - The window is now the longest valid one ending at
last, so compare its length with the best so far.
This works because removing elements from the front can only reduce the number of distinct values, and a valid window stays valid when it loses elements. The start never needs to move backwards, since a window that was too wide for an earlier end is also too wide for a later one.
The window needs more than a sum. To know when a value really leaves, count the occurrences of each value in a map frequency. Adding a value increments its count. Removing a value decrements it, and only when the count reaches zero is the key deleted. Then len(frequency) is exactly the number of distinct values in the window.
On the example with k = 2:
- After indices 0 to 3,
frequencyholds two keys, 1 and 2, each with count 2, and the window length is 4. - Index 4 adds the value 3, which makes three keys. Removing
nums[0]leaves counts 1:1, 2:2, 3:1, still three keys. Removingnums[1]still leaves three, because the value 2 occurs again. Removingnums[2]deletes the key 1, leaving two keys, andfirstis now 3. The window is[2, 3]with length 2. - The best length seen is 4.
Step by step
- Create the map
frequencywithmake. A map that is only declared isnil, and writing to it panics. - Declare
answerandfirstas 0: the best length so far and the start of the window. - Loop over
lastfrom 0 tolen(nums) - 1, the end of the window. - Insert
nums[last]with the helperinsert. - Shrink while there are too many kinds. Go has no
while, so use aforwith only the conditionlen(frequency) > k. - Remove
nums[first]with the helperremove, then movefirstforward. Each index leaves the window at most once, so this inner loop is cheap in total. - Update
answerwith the lengthlast - first + 1, using the built-inmax. - Return
answerafter the loop. - Write
remove. It takes the map and a value and returns nothing. It decrements the value's count, and when the count hits 0, deletes the key. - Write
insert. It increments the value's count. A missing key reads as 0, so the first++creates the entry with count 1.
The solution in Go
func longestSubarray(nums []int, k int) int {
frequency := make(map[int]int)
answer, first := 0, 0
for last := 0; last < len(nums); last++ {
insert(frequency, nums[last])
for len(frequency) > k {
remove(frequency, nums[first])
first++
}
answer = max(answer, last-first+1)
}
return answer
}
func remove(frequency map[int]int, value int) {
frequency[value]--
if frequency[value] == 0 {
delete(frequency, value)
}
}
func insert(frequency map[int]int, value int) {
frequency[value]++
}
The Go details worth knowing:
- Maps are reference types.
insertandremovereceive the map by value, but that value is a handle to the same underlying table, so changes are visible to the caller without a pointer. - Missing keys read as zero.
frequency[value]++on an absent key reads 0, adds 1 and stores 1. That is whyinsertneeds noif. - Delete is required. A key left at count 0 still counts in
len(frequency), so the window would look as if it held values it no longer contains.deleteon a missing key is harmless. - Helpers can come after their callers. Go lets a function use functions declared anywhere in the same package, so
removeandinsertcan sit belowlongestSubarray. - Built-in
max. It has been part of the language since Go 1.21, so no helper is needed for two integers. - Map iteration order is random, but this solution never iterates the map. It only uses
len, so the order doesn't matter.
Complexity
Time: O(n). last visits each index once. first only moves forward and never passes last, so across the whole run the inner loop removes each element at most once. Map operations take constant time on average, so the total is linear.
Space: O(min(k, n)). After shrinking, the map holds at most k keys, and during an insert it may briefly hold k + 1. It can never hold more keys than there are elements, so the memory stays within the O(n) the statement allows.
Edge cases and common mistakes
- Empty slice. The loop never runs and the function returns 0.
kis 0. Every insert makes the map too big, so the window shrinks to empty each time, and the answer is 0. Fornums = []int{1, 2, 1, 2, 3}andk = 0the function returns 0.kat least the number of distinct values. The window never shrinks and the result islen(nums): for the example withk = 3ork = 10it returns 5.- All values equal.
nums = []int{4, 4, 4, 4}andk = 1returns 4, because the map has a single key whose count grows. - Forgetting to delete a zero count. The map then keeps a key for every value ever seen,
len(frequency)never drops, and the shrinking loop runsfirstpast the end of the slice. For the example withk = 2the program stops with an index out of range panic. - Deleting the key on every decrement. A value that occurs several times in the window would be forgotten after its first removal. For
nums = []int{1, 1, 1, 2, 3, 2}andk = 2that version returns 5 instead of 4, because it lets the window hold three kinds. - Using an
ifinstead of aforto shrink. One removal may not be enough: in the example, three removals are needed at index 4. - Reading
kas the window size.klimits the number of different values, not the number of elements.
Testing
nums = []int{1, 2, 1, 2, 3},k = 2gives 4.nums = []int{1, 2, 1, 2, 3},k = 1gives 1.nums = []int{1, 2, 3, 4},k = 2gives 2.nums = []int{4, 4, 4, 4},k = 1gives 4.nums = []int{},k = 2gives 0.