Facebook Pixel

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] and k = 2, the entire array is good because each element appears at most 2 times, so the answer would be 5.
  • If nums = [1, 1, 1, 2] and k = 2, the longest good subarray would be [1, 1, 2] with length 3, 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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) and i (right boundary) of the sliding window

Algorithm Steps:

  1. Initialize variables:

    • cnt: A default dictionary to store element frequencies in the current window
    • ans: To track the maximum length found so far, initialized to 0
    • j: Left pointer of the window, initialized to 0
  2. Expand the window (right pointer movement):

    • Iterate through the array using enumerate to get both index i and element x
    • For each element x, increment its count: cnt[x] += 1
  3. Contract the window when needed (left pointer movement):

    • After adding element x, check if its frequency exceeds k: 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
    • Continue shrinking until cnt[x] <= k (the window becomes valid again)
  4. 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)
  5. Return the result:

    • After processing all elements, return ans

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 to i 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 Evaluator

Example 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)
  • 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More