← 題庫 / Archive
2026-08-12 Daily Medium ArrayHash TableSliding Window

2958. Length of Longest Subarray With at Most K Frequency

題目 / Problem

中文: 給你一個整數陣列 nums 和一個整數 k。一個元素 x 的「頻率」是指它在陣列中出現的次數。如果一個陣列裡「每一種元素」出現的次數都「小於或等於」k,我們就稱它是「好的(good)」。請回傳 nums 中最長的「好子陣列」的長度。子陣列(subarray)是指陣列中連續且非空的一段元素。

English: You are given an integer array nums and an integer k. The frequency of an element x is how many times it appears. An array is good if every element in it appears at most k times. Return the length of the longest good subarray (a contiguous, non-empty slice) of nums.

Constraints / 限制條件: - 1 <= nums.length <= 10^5 - 1 <= nums[i] <= 10^9(元素值很大,最多 10 億 / values can be up to a billion) - 1 <= k <= nums.length

Worked example / 範例:

nums = [1,2,3,1,2,3,1,2], k = 2
Output: 6

最長的好子陣列是 [1,2,3,1,2,3]:其中 1、2、3 各出現 2 次,都不超過 k = 2。 The longest good subarray is [1,2,3,1,2,3], where 1, 2, and 3 each appear exactly twice — none exceeds k = 2.

名詞解釋 / Glossary

  • 子陣列 / Subarray: 陣列中一段連續的元素,例如 [2,3,1][1,2,3,1,2] 的子陣列,但 [1,3](跳過中間)不是。A contiguous stretch of the array — no skipping allowed.
  • 頻率 / Frequency: 某個值在一段區間內出現的次數。How many times a given value shows up inside a window.
  • 滑動視窗 / Sliding Window: 一種用「左指針 left 和右指針 right」框出一段連續區間的技巧。右指針不斷向右擴張,當區間違反條件時就移動左指針把左邊縮掉。A technique using two pointers to mark a contiguous window; grow it on the right, shrink it from the left when a rule is broken.
  • 雙指針 / Two Pointers: 用兩個索引(指針)同時掃描陣列,避免重複從頭計算。Two indices sweeping the array together so we never re-scan from scratch.
  • 雜湊表 / Hash Map: 一種能用「鍵 → 值」快速查詢的資料結構。這裡用它記錄「每個數字目前在視窗內出現幾次」,平均查詢/更新只需 O(1)。A key→value structure; here it stores each number's current count inside the window, with average O(1) lookup and update. C++ 用 unordered_map;C 因為沒有內建雜湊表,我們自己寫一個簡單的開放定址雜湊。
  • 開放定址 / Open Addressing: C 版自製雜湊表的一種衝突處理法:若某格已被占用,就往後線性探測下一格。A collision strategy for our hand-written C hash table: if a slot is taken, probe the next one.

思路

中文: 最直覺的暴力法是:枚舉所有可能的左端點 i 與右端點 j,對每個區間 [i, j] 數一遍每個元素的頻率,檢查是否全都 <= k。這樣有 O(n²) 個區間,每個還要花時間統計,總共大約 O(n³) 或用前綴技巧 O(n²),在 n = 10^5 時會超時。關鍵觀察是:如果區間 [i, j] 是好的,那麼它的任何子區間也是好的(把元素拿掉,頻率只會變小或不變)。反過來說,當我們固定右端點往右擴張時,一旦某個元素的頻率超過 k,我們只需要把左端點往右移,直到那個「超標」的元素頻率回到 k 以下即可,不需要整段重算。這正好是「滑動視窗」的模式:用一個雜湊表 count 記錄視窗內每個值的出現次數;右指針 right 每次讀入一個新元素 nums[right],把它的計數加一;如果這個新元素的計數變成 > k,就不斷把 nums[left] 的計數減一並右移 left,直到 count[nums[right]] <= k。因為我們只在「剛加入的元素」上可能超標(其他元素在加入前已保證合法),所以只需檢查它一個。每一步視窗都是合法的,長度 right - left + 1 就是候選答案,取最大值即可。左右指針各自最多走 n 步,所以是 O(n)。

English: The brute-force idea is to try every pair of endpoints i and j, count frequencies inside [i, j], and check they're all <= k. That's O(n²) windows and too slow for n = 10^5. The key insight: if a window is good, every sub-window of it is also good — removing elements can only lower frequencies. So as we extend the right end one step at a time, the only element that could newly violate the rule is the one we just added. When it does, we simply advance the left pointer, decrementing counts as we drop elements, until that element's count falls back to <= k. This is a textbook sliding window: keep a hash map count of how many times each value appears in the current window. On each step, move right forward and increment count[nums[right]]; if that count exceeds k, shrink from the left (decrement count[nums[left]], advance left) until it's legal again. We only ever need to re-check the just-added value, because everything else was already valid. Every window we hold is valid, so its length right - left + 1 is a candidate answer; track the maximum. Each pointer moves at most n times total, giving O(n).

逐步走查 / Walkthrough

輸入 / Input: nums = [1,2,3,1,2,3,1,2], k = 2. 初始 left = 0, ans = 0, count = {}.

right 加入 nums[right] / add count 加後 / after add 超標? >k 縮左動作 / shrink left 視窗 / window 長度 len ans
0 1 {1:1} 否 no 0 [1] 1 1
1 2 {1:1,2:1} 否 no 0 [1,2] 2 2
2 3 {1:1,2:1,3:1} 否 no 0 [1,2,3] 3 3
3 1 {1:2,2:1,3:1} 否 no (1→2, ≤2) 0 [1,2,3,1] 4 4
4 2 {1:2,2:2,3:1} 否 no 0 [1,2,3,1,2] 5 5
5 3 {1:2,2:2,3:2} 否 no 0 [1,2,3,1,2,3] 6 6
6 1 {1:3,2:2,3:2} 是 yes (1→3) 移除 nums[0]=1 → {1:2,...}, left=1 1 [2,3,1,2,3,1] 6 6
7 2 {1:2,2:3,3:2} 是 yes (2→3) 移除 nums[1]=2 → {2:2,...}, left=2 2 [3,1,2,3,1,2] 6 6

最終答案 / Final answer: ans = 6。注意在 right = 6 時,加入第三個 1 使 count[1] = 3 > 2,我們把最左邊的 1(索引 0)移出,count[1] 回到 2,視窗恢復合法。At right = 6, adding the third 1 pushed count[1] to 3, so we dropped the leftmost 1 (index 0) to restore validity.

Solution — C

/*
 * 演算法 / Algorithm: 滑動視窗 + 自製雜湊表 (Sliding window + hand-written hash map).
 * 用左右指針框出目前視窗,雜湊表記錄每個值在視窗內的出現次數。
 * Right pointer grows the window; when the just-added value's count exceeds k,
 * advance left until it's valid again. Answer is the largest window length seen.
 * 時間 O(n),空間 O(n)。Time O(n), space O(n).
 */

#include <stdlib.h>   // malloc, calloc, free (動態記憶體 / dynamic memory)

/* --- 一個極簡的「值 -> 次數」開放定址雜湊表 --- */
/* --- A minimal open-addressing hash map from value -> count --- */
typedef struct {
    long *keys;    // 存放鍵(也就是 nums 的值)/ stores keys (the nums values)
    int  *vals;    // 對應的計數 / the matching counts
    char *used;    // 該格是否已被占用 (1=用了, 0=空) / is this slot occupied
    int   cap;     // 桶子總數,取 2 的次方方便用位元遮罩取模 / bucket count, a power of two
} Map;

// 建立一個容量為 cap 的雜湊表 / build a hash map with capacity cap
static Map map_new(int cap) {
    Map m;
    m.cap  = cap;                        // 記住容量 / remember capacity
    m.keys = malloc(sizeof(long) * cap); // 配置鍵陣列 / allocate keys array
    m.vals = calloc(cap, sizeof(int));   // calloc 會把計數清成 0 / calloc zeroes the counts
    m.used = calloc(cap, sizeof(char));  // calloc 把 used 全設為 0=空 / all slots start empty
    return m;
}

// 用完後釋放記憶體,避免記憶體洩漏 / free memory afterwards to avoid leaks
static void map_free(Map *m) {
    free(m->keys);  // 釋放鍵陣列 / free keys
    free(m->vals);  // 釋放值陣列 / free vals
    free(m->used);  // 釋放占用旗標 / free used flags
}

// 找到 key 該待的桶子索引 (若不存在則回傳一個空桶) / find the slot for key (or an empty one)
static int map_slot(Map *m, long key) {
    // 用位元 AND 取代取模:因為 cap 是 2 的次方,key & (cap-1) 等同 key % cap
    // Bitwise AND as a fast modulo: since cap is a power of two, key & (cap-1) == key % cap
    int i = (int)(((unsigned long)key * 1000000007UL) & (m->cap - 1));
    // 線性探測:若桶子被別的鍵占用,就看下一格 / linear probing to next slot on collision
    while (m->used[i] && m->keys[i] != key) {
        i = (i + 1) & (m->cap - 1);      // 前進一格並回繞到頭 / step forward, wrap around
    }
    return i;                            // 回傳最終落腳的桶子 / return the resting slot
}

// 把 key 的計數加上 delta(可為 +1 或 -1),並回傳更新後的計數
// Add delta (+1 or -1) to key's count, return the new count
static int map_add(Map *m, long key, int delta) {
    int i = map_slot(m, key);            // 找到桶子 / locate the slot
    if (!m->used[i]) {                   // 若這是第一次遇到這個 key / first time we see this key
        m->used[i] = 1;                  // 標記占用 / mark occupied
        m->keys[i] = key;                // 寫入鍵 / store the key
        m->vals[i] = 0;                  // 計數從 0 開始 / count starts at 0
    }
    m->vals[i] += delta;                 // 更新計數 / update the count
    return m->vals[i];                   // 回傳新計數給呼叫者判斷 / return new count to caller
}

int maxSubarrayLength(int* nums, int numsSize, int k) {
    // 桶子數取大於 2*numsSize 的最小 2 次方,確保夠鬆、探測快
    // Pick a power-of-two capacity comfortably larger than the data to keep probing fast
    int cap = 1;
    while (cap < numsSize * 2) cap <<= 1; // cap 左移一位就是乘 2 / left shift doubles cap
    Map count = map_new(cap);            // 建立計數表 / build the count map

    int left = 0;                        // 視窗左端點 / window's left boundary
    int ans  = 0;                        // 目前最長好子陣列的長度 / best length so far

    // right 是視窗右端點,一步步向右擴張 / right pointer expands the window one step at a time
    for (int right = 0; right < numsSize; right++) {
        // 把新元素加入視窗,計數 +1,並拿到它的新頻率
        // Add the new element into the window (+1) and read back its new frequency
        int freq = map_add(&count, nums[right], +1);

        // 只有「剛加入的這個值」可能超標;若超過 k 就從左邊縮小視窗
        // Only the just-added value can break the rule; if it exceeds k, shrink from the left
        while (freq > k) {
            // 移除最左邊元素:計數 -1,然後 left 右移 / drop leftmost: count -1, then advance left
            map_add(&count, nums[left], -1);
            left++;                      // 左端點右移,視窗變小 / move left boundary rightwards
            // 重新讀取「剛加入值」目前的頻率,看看是否已合法
            // Re-check the just-added value's current frequency to see if it's now legal
            freq = map_add(&count, nums[right], 0); // delta=0 表示只查詢不改動 / delta 0 = query only
        }

        // 到這裡視窗一定合法;用它的長度更新答案 / window is valid now; update the answer
        int len = right - left + 1;      // 視窗長度 = 右 - 左 + 1 / window length
        if (len > ans) ans = len;        // 取較大者 / keep the maximum
    }

    map_free(&count);                    // 釋放雜湊表記憶體 / free the hash map
    return ans;                          // 回傳最長好子陣列的長度 / return the answer
}

Solution — C++

/*
 * 演算法 / Algorithm: 滑動視窗 + unordered_map 計數 (Sliding window + hash map counting).
 * 右指針擴張視窗,unordered_map 記錄每個值在視窗內的頻率;
 * Right pointer grows the window; the map tracks each value's frequency in the window.
 * 當剛加入的值頻率超過 k,就右移左指針縮小視窗,直到合法。
 * When the just-added value exceeds k, shrink from the left until valid.
 * 時間 O(n),空間 O(n)。Time O(n), space O(n).
 */

#include <vector>          // std::vector 動態陣列 / dynamic array
#include <unordered_map>   // std::unordered_map 雜湊表 / hash map
#include <algorithm>       // std::max 取最大值 / for std::max

class Solution {
public:
    int maxSubarrayLength(std::vector<int>& nums, int k) {
        // unordered_map<int,int>:鍵是元素值,值是它在視窗內的出現次數
        // key = element value, value = its count inside the current window
        std::unordered_map<int, int> count;

        int left = 0;   // 視窗左端點 / left boundary of the window
        int ans  = 0;   // 目前最長合法視窗長度 / best window length so far

        // 用範圍索引讓 right 從 0 掃到最後 / right pointer sweeps left to right
        for (int right = 0; right < (int)nums.size(); ++right) {
            // count[nums[right]]++ 會在鍵不存在時自動建立並初始化為 0,再加 1
            // Indexing a missing key auto-creates it at 0, then ++ makes it 1
            count[nums[right]]++;

            // 只有剛加入的值可能超標;超過 k 就從左邊縮小視窗
            // Only the newly added value can exceed k; if so, shrink from the left
            while (count[nums[right]] > k) {
                // 移除最左元素:對應計數 -1,然後 left 右移
                // Drop the leftmost element: decrement its count, then advance left
                count[nums[left]]--;
                ++left;   // 左端點右移,視窗縮小 / move left boundary rightwards
            }

            // 此刻視窗保證合法;用其長度更新答案 / window is valid; update the answer
            // std::max 回傳兩者中較大者 / std::max returns the larger of the two
            ans = std::max(ans, right - left + 1);
        }

        return ans;   // 回傳最長好子陣列的長度 / return the length of the longest good subarray
    }
};

複雜度 / Complexity

  • Time / 時間: O(n)nnums 的長度。右指針 right 從頭走到尾共 n 步;左指針 left 也只會向右移動,整個過程最多累計移動 n 步,兩者合計 O(n)。每次雜湊表操作平均 O(1)。The right pointer takes n steps; the left pointer only ever moves rightward, for at most n moves total across the whole run — not n per step. Each hash operation is O(1) on average, so the total is linear.
  • Space / 空間: O(n) — 最壞情況下(所有元素都不同)雜湊表要存下多達 n 個不同的鍵。In the worst case (all distinct values) the hash map holds up to n keys.

Pitfalls & Edge Cases

  • 只需檢查「剛加入的值」/ Only re-check the just-added value: 縮視窗的 while 條件是 count[nums[right]] > k,不是「檢查所有元素」。因為加入 nums[right] 之前整個視窗已合法,唯一可能超標的就是它。若誤寫成掃描全部元素,會退化成 O(n²)。The only value that can newly break the rule is the one we just added; scanning everything would make it O(n²).
  • 值可以很大(到 10^9)/ Values up to 10^9: 不能用「值當陣列索引」的計數法(陣列會爆記憶體),必須用雜湊表。C 版因此手寫雜湊。You cannot use the value directly as an array index — use a hash map.
  • C 版查詢用 delta = 0 / Querying with delta 0 in C: 縮完視窗後要重新讀取剛加入值的頻率,我們用 map_add(..., 0) 做「只查不改」,避免額外寫一個查詢函式,也不會意外改動計數。Re-reading the frequency with delta 0 queries without mutating.
  • 不要在縮視窗後忘記更新 freq / Refresh the loop variable: C 版 while 迴圈裡每次縮完都要重新取得 freq,否則條件永遠不變會變成無窮迴圈。C++ 版直接寫 count[nums[right]] > k,每次自動重新求值,沒有這個陷阱。Forgetting to refresh freq in C causes an infinite loop; C++ re-evaluates the map lookup each iteration automatically.
  • off-by-one 長度 / Window length: 長度是 right - left + 1(含兩端),漏了 + 1 會少算一格。The window length includes both ends, so the + 1 is required.
  • 答案至少為 1 / Answer is never 0 for valid input: 因為 k >= 1 且陣列非空,單一元素永遠是好子陣列,ans 最終必 >= 1。Since k >= 1 and the array is non-empty, a single element is always good, so the answer is at least 1.