2958. Length of Longest Subarray With at Most K Frequency
Problem Description
You are given an integer array nums
and an integer k
.
The frequency of an element is defined as the number of times that element appears in an array.
An array is considered good if every element in the array appears at most k
times (i.e., the frequency of each element is less than or equal to k
).
Your task is to find the length of the longest good subarray in nums
.
A subarray is a contiguous sequence of elements from the array that must be non-empty.
For example:
- If
nums = [1, 2, 1, 2, 3]
andk = 2
, the entire array is good because each element appears at most 2 times, so the answer would be5
. - If
nums = [1, 1, 1, 2]
andk = 2
, the longest good subarray would be[1, 1, 2]
with length3
, since including all three 1's would violate the frequency constraint.
The solution uses a sliding window approach with two pointers. The right pointer i
expands the window by iterating through the array, while the left pointer j
contracts the window whenever the frequency constraint is violated. A hash map tracks the frequency of elements in the current window, and the maximum valid window size encountered is returned as the answer.
Intuition
The key insight is recognizing this as a sliding window problem. We need to find the longest contiguous subarray where no element appears more than k
times.
Think of it like maintaining a flexible window that can expand and contract. We want to make this window as large as possible while ensuring it remains "good" (no element frequency exceeds k
).
We can start with an empty window and gradually expand it by adding elements from the right. As we add each element, we track its frequency using a hash map. This allows us to know instantly if adding an element violates our constraint.
When we encounter a violation (some element's frequency becomes greater than k
), we need to fix it. The question is: how do we fix it efficiently? Instead of starting over, we can shrink the window from the left side. We keep removing elements from the left until the problematic element's frequency drops back to k
or below.
Why does this work? Because once we've found a valid window of a certain size, we don't need to check smaller windows anymore - we're looking for the maximum length. So we only shrink the window when absolutely necessary (when the constraint is violated), and we shrink it just enough to make it valid again.
This gives us a two-pointer approach: the right pointer (i
) continuously expands the window by moving forward through the array, while the left pointer (j
) only moves when needed to maintain validity. At each step, we update our answer with the current valid window size i - j + 1
.
The beauty of this approach is that each element is visited at most twice (once by each pointer), giving us an efficient linear time solution.
Learn more about Sliding Window patterns.
Solution Approach
We implement a sliding window solution using two pointers to find the longest good subarray.
Data Structures Used:
- A hash map (
defaultdict(int)
) to track the frequency of each element in the current window - Two pointers:
j
(left boundary) andi
(right boundary) of the sliding window
Algorithm Steps:
-
Initialize variables:
cnt
: A default dictionary to store element frequencies in the current windowans
: To track the maximum length found so far, initialized to 0j
: Left pointer of the window, initialized to 0
-
Expand the window (right pointer movement):
- Iterate through the array using
enumerate
to get both indexi
and elementx
- For each element
x
, increment its count:cnt[x] += 1
- Iterate through the array using
-
Contract the window when needed (left pointer movement):
- After adding element
x
, check if its frequency exceedsk
:while cnt[x] > k
- If violated, shrink the window from the left:
- Decrement the count of the element at position
j
:cnt[nums[j]] -= 1
- Move the left pointer right:
j += 1
- Decrement the count of the element at position
- Continue shrinking until
cnt[x] <= k
(the window becomes valid again)
- After adding element
-
Update the maximum length:
- After ensuring the window is valid, calculate current window size:
i - j + 1
- Update the answer:
ans = max(ans, i - j + 1)
- After ensuring the window is valid, calculate current window size:
-
Return the result:
- After processing all elements, return
ans
- After processing all elements, return
Why this works:
- The right pointer
i
continuously expands the window by including new elements - The left pointer
j
only moves when necessary to maintain the "good" property - At any point, the subarray from index
j
toi
is guaranteed to be good - We track the maximum valid window size throughout the process
Time Complexity: O(n)
where n
is the length of the array, as each element is visited at most twice (once by each pointer)
Space Complexity: O(m)
where m
is the number of unique elements in the array, for storing the frequency map
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [1, 2, 1, 2, 1, 3]
and k = 2
.
We'll track the sliding window with pointers j
(left) and i
(right), maintaining a frequency map cnt
for elements in the current window.
Initial state:
j = 0
,ans = 0
,cnt = {}
Step 1: i = 0, x = 1
- Add 1 to window:
cnt = {1: 1}
- Check:
cnt[1] = 1 ≤ 2
✓ (valid) - Update answer:
ans = max(0, 0-0+1) = 1
- Window:
[1]
Step 2: i = 1, x = 2
- Add 2 to window:
cnt = {1: 1, 2: 1}
- Check:
cnt[2] = 1 ≤ 2
✓ (valid) - Update answer:
ans = max(1, 1-0+1) = 2
- Window:
[1, 2]
Step 3: i = 2, x = 1
- Add 1 to window:
cnt = {1: 2, 2: 1}
- Check:
cnt[1] = 2 ≤ 2
✓ (valid) - Update answer:
ans = max(2, 2-0+1) = 3
- Window:
[1, 2, 1]
Step 4: i = 3, x = 2
- Add 2 to window:
cnt = {1: 2, 2: 2}
- Check:
cnt[2] = 2 ≤ 2
✓ (valid) - Update answer:
ans = max(3, 3-0+1) = 4
- Window:
[1, 2, 1, 2]
Step 5: i = 4, x = 1
- Add 1 to window:
cnt = {1: 3, 2: 2}
- Check:
cnt[1] = 3 > 2
✗ (violated!) - Shrink window from left:
- Remove
nums[0] = 1
:cnt = {1: 2, 2: 2}
,j = 1
- Check:
cnt[1] = 2 ≤ 2
✓ (now valid)
- Remove
- Update answer:
ans = max(4, 4-1+1) = 4
- Window:
[2, 1, 2, 1]
Step 6: i = 5, x = 3
- Add 3 to window:
cnt = {1: 2, 2: 2, 3: 1}
- Check:
cnt[3] = 1 ≤ 2
✓ (valid) - Update answer:
ans = max(4, 5-1+1) = 5
- Window:
[2, 1, 2, 1, 3]
Final Answer: 5
The longest good subarray is [2, 1, 2, 1, 3]
with length 5, where no element appears more than k = 2
times.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def maxSubarrayLength(self, nums: List[int], k: int) -> int:
6 # Dictionary to track frequency of each element in current window
7 frequency_map = defaultdict(int)
8
9 # Initialize result and left pointer of sliding window
10 max_length = 0
11 left = 0
12
13 # Iterate through array with right pointer
14 for right, current_num in enumerate(nums):
15 # Add current element to window by incrementing its frequency
16 frequency_map[current_num] += 1
17
18 # Shrink window from left while current element's frequency exceeds k
19 while frequency_map[current_num] > k:
20 # Remove leftmost element from window
21 frequency_map[nums[left]] -= 1
22 # Move left pointer forward
23 left += 1
24
25 # Update maximum length found so far
26 # Window size is (right - left + 1)
27 max_length = max(max_length, right - left + 1)
28
29 return max_length
30
1class Solution {
2 public int maxSubarrayLength(int[] nums, int k) {
3 // HashMap to store the frequency of each element in the current window
4 Map<Integer, Integer> frequencyMap = new HashMap<>();
5
6 // Variable to store the maximum length of valid subarray
7 int maxLength = 0;
8
9 // Sliding window approach: left pointer starts at 0
10 int left = 0;
11
12 // Iterate through the array with right pointer
13 for (int right = 0; right < nums.length; right++) {
14 // Add current element to the window and increment its frequency
15 frequencyMap.merge(nums[right], 1, Integer::sum);
16
17 // Shrink window from left while current element's frequency exceeds k
18 while (frequencyMap.get(nums[right]) > k) {
19 // Decrement frequency of element at left pointer
20 frequencyMap.merge(nums[left], -1, Integer::sum);
21 // Move left pointer to the right
22 left++;
23 }
24
25 // Update maximum length with current valid window size
26 maxLength = Math.max(maxLength, right - left + 1);
27 }
28
29 return maxLength;
30 }
31}
32
1class Solution {
2public:
3 int maxSubarrayLength(vector<int>& nums, int k) {
4 // Hash map to store the frequency count of each element in current window
5 unordered_map<int, int> frequencyMap;
6
7 // Variable to track the maximum length of valid subarray
8 int maxLength = 0;
9
10 // Sliding window approach with left pointer (left) and right pointer (right)
11 int left = 0;
12
13 for (int right = 0; right < nums.size(); ++right) {
14 // Add current element to the window and increment its frequency
15 frequencyMap[nums[right]]++;
16
17 // Shrink window from left while current element's frequency exceeds k
18 while (frequencyMap[nums[right]] > k) {
19 // Remove leftmost element from window and decrement its frequency
20 frequencyMap[nums[left]]--;
21 // Move left pointer to the right
22 left++;
23 }
24
25 // Update maximum length with current valid window size
26 maxLength = max(maxLength, right - left + 1);
27 }
28
29 return maxLength;
30 }
31};
32
1/**
2 * Finds the maximum length of a subarray where no element appears more than k times
3 * @param nums - The input array of numbers
4 * @param k - The maximum frequency allowed for any element in the subarray
5 * @returns The length of the longest valid subarray
6 */
7function maxSubarrayLength(nums: number[], k: number): number {
8 // Map to track the frequency of each element in the current window
9 const frequencyMap: Map<number, number> = new Map<number, number>();
10
11 // Variable to store the maximum subarray length found
12 let maxLength: number = 0;
13
14 // Use two pointers to maintain a sliding window
15 let leftPointer: number = 0;
16
17 for (let rightPointer: number = 0; rightPointer < nums.length; rightPointer++) {
18 // Add current element to the window and update its frequency
19 const currentElement: number = nums[rightPointer];
20 const currentFrequency: number = frequencyMap.get(currentElement) ?? 0;
21 frequencyMap.set(currentElement, currentFrequency + 1);
22
23 // Shrink the window from the left while the current element's frequency exceeds k
24 while (frequencyMap.get(currentElement)! > k) {
25 // Remove the leftmost element from the window
26 const leftElement: number = nums[leftPointer];
27 const leftFrequency: number = frequencyMap.get(leftElement)!;
28 frequencyMap.set(leftElement, leftFrequency - 1);
29
30 // Move the left pointer forward
31 leftPointer++;
32 }
33
34 // Update the maximum length with the current valid window size
35 const currentWindowSize: number = rightPointer - leftPointer + 1;
36 maxLength = Math.max(maxLength, currentWindowSize);
37 }
38
39 return maxLength;
40}
41
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses a sliding window approach with two pointers. The outer loop iterates through each element in nums
exactly once using enumerate()
, giving us n
iterations. The inner while
loop might seem to add complexity, but each element can only be removed from the window once (when j
pointer moves past it). Therefore, the j
pointer moves at most n
times throughout the entire execution. This means each element is visited at most twice - once when added to the window (by i
) and once when removed (by j
). The total operations are thus O(2n) = O(n)
.
Space Complexity: O(n)
The space is dominated by the cnt
dictionary (implemented as defaultdict(int)
), which stores the frequency count of elements in the current window. In the worst case, all elements in nums
are unique and appear in the window simultaneously, requiring the dictionary to store n
distinct key-value pairs. Additional variables (ans
, j
, i
, x
) use constant space O(1)
. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to shrink the window for the specific violating element
A common mistake is checking if ANY element in the window exceeds frequency k
, rather than specifically checking the element that was just added. This leads to incorrect window shrinking logic.
Incorrect approach:
# Wrong: Checking all elements in frequency map
for right, current_num in enumerate(nums):
frequency_map[current_num] += 1
# This checks ALL elements, not just the one we added
for num, freq in frequency_map.items():
while freq > k:
frequency_map[nums[left]] -= 1
left += 1
Correct approach:
# Right: Only check the element that was just added
for right, current_num in enumerate(nums):
frequency_map[current_num] += 1
# Only shrink if the CURRENT element violates the constraint
while frequency_map[current_num] > k:
frequency_map[nums[left]] -= 1
left += 1
2. Not handling the case when frequency becomes zero
While not critical for correctness, failing to remove elements with zero frequency from the hash map can lead to unnecessary memory usage in cases with many unique elements.
Better practice:
while frequency_map[current_num] > k: frequency_map[nums[left]] -= 1 # Clean up zero-frequency entries to save memory if frequency_map[nums[left]] == 0: del frequency_map[nums[left]] left += 1
3. Off-by-one error in calculating window size
A frequent mistake is calculating the window size as right - left
instead of right - left + 1
.
Incorrect:
max_length = max(max_length, right - left) # Wrong!
Correct:
max_length = max(max_length, right - left + 1) # Correct!
4. Using regular dictionary instead of defaultdict
Using a regular dictionary requires checking if a key exists before incrementing, which adds complexity and potential for errors.
Error-prone approach:
frequency_map = {}
for right, current_num in enumerate(nums):
# Need to check if key exists first
if current_num not in frequency_map:
frequency_map[current_num] = 0
frequency_map[current_num] += 1
Cleaner approach:
frequency_map = defaultdict(int)
for right, current_num in enumerate(nums):
# No need to check - defaultdict handles it
frequency_map[current_num] += 1
How does quick sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!