具代表性的面試主題

程式設計面試:如何返回出現頻率最高的 K 個單字?

程式題中等
OfferCC 編輯團隊發佈 更新

題幹

給定單字陣列 words 和整數 k,返回出現頻率最高的 k 個單字;頻率相同的單字按字典序升序排列。請說明演算法、比較器、複雜度和邊界情況。

題幹與適用場景

給定單字陣列 words 和整數 k,返回出現頻率最高的 k 個單字;頻率相同的單字按字典序升序排列。

面試重點是如何維護大小為 k 的候選集合,以及堆頂應代表「最差候選」還是「最佳候選」。不要求綁定 Java;範例使用 Java 只為展示比較器。

面試官考察點

計數

候選人應先用雜湊表統計每個單字頻率,並說明唯一單字數 m 與陣列長度 n 的關係。

排序規則

頻率高者優先;頻率相同則字典序小者優先。最小堆應把較差的候選放在堆頂,方便超過 k 時淘汰。

複雜度

完整排序是 O(m log m);大小為 k 的堆是 O(n + m log k),適合 k 遠小於唯一單字數的情況。

正確性

要解釋堆比較器與最終輸出順序不同:堆用於淘汰,結果需要反轉或按最佳順序重新建立。

回答前需要釐清的問題

  • 單字是否只包含小寫英文,是否區分大小寫?
  • k 是否保證在 1 到唯一單字數之間?
  • 字典序按 ASCII、Unicode,還是業務 locale?
  • 是否需要串流處理,不能把所有詞放入記憶體?
  • 輸出是否必須穩定,是否允許任意順序?
  • 頻率是否可能超過 32 位整數範圍?

30 秒回答框架

「我先用雜湊表統計頻率。對每個唯一單字維護大小為 k 的最小堆,堆頂定義為較差的候選:頻率更低,或頻率相同但字典序更大。每次加入後若超過 k 就彈出堆頂。最後把堆中單字按『頻率降序、字典序升序』取出。計數 O(n),堆維護 O(m log k),空間 O(m)。

分步驟深入解答

第一步:統計頻率

遍歷陣列,把每個詞映射到出現次數。若輸入來自串流日誌,可先在外部聚合或使用近似計數,但本題預設記憶體可容納唯一詞表。

第二步:定義最差候選

候選 A 比 B 更差,當 A 頻率更低;若頻率相同,A 的字典序更大。比較器讓最差候選位於堆頂。

第三步:維護大小 k

遍歷頻率表,將每個詞加入堆;堆大小超過 k 時彈出一個。這樣堆中始終保留目前最有可能進入答案的 k 個詞。

第四步:產生輸出

從堆中彈出的順序是從最差到較好,不能直接作為答案。把元素放入陣列後反向讀取,或使用最終排序器按頻率降序和字典序升序排列。

第五步:證明正確性

每次超過 k 時刪除目前集合中最差元素;因此被刪除的詞不可能優於堆中保留的 k 個詞。歸納到遍歷結束,堆包含全域 Top K。

第六步:處理邊界

檢查 k=1、所有詞頻相同、只有一個唯一詞、重複詞很多和 k=m。比較器必須避免頻率相同的詞順序反轉。

高品質示範回答

java
class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        Map<String, Integer> count = new HashMap<>();
        for (String word : words) count.merge(word, 1, Integer::sum);

        PriorityQueue<String> heap = new PriorityQueue<>((a, b) -> {
            int byFrequency = Integer.compare(count.get(a), count.get(b));
            if (byFrequency != 0) return byFrequency;
            return b.compareTo(a); // 字典序較大的較差,放在堆頂
        });

        for (String word : count.keySet()) {
            heap.offer(word);
            if (heap.size() > k) heap.poll();
        }

        List<String> answer = new ArrayList<>();
        while (!heap.isEmpty()) answer.add(heap.poll());
        Collections.reverse(answer);
        return answer;
    }
}

計數需要 O(n),唯一詞數為 m 時每次堆操作 O(log k),總時間 O(n + m log k),空間 O(m)。

常見錯誤

  • 比較器把最佳候選放在堆頂 → 淘汰了正確答案 → 讓最差候選位於堆頂。
  • 頻率相同時字典序方向寫反 → 輸出順序錯誤 → 頻率相同按字典序升序保留。
  • 堆彈出結果直接返回 → 結果從差到好 → 反轉或用最終排序器重排。
  • 先全量排序卻聲稱 O(n log k) → 複雜度不成立 → 全量排序為 O(m log m)。
  • 只測試不同頻率 → 平局規則未驗證 → 測試所有詞頻相同和大量同頻詞。
  • 忽略 k=m → 不必要的淘汰或越界 → 允許堆增長到全部唯一詞。
  • 使用不穩定的 locale 比較 → 跨環境結果變化 → 明確題目規定的字典序規則。
  • 只說雜湊表不解釋空間 → 無法評估規模 → 給出 n、m、k 的複雜度。

追問及應對

追問一:k 接近唯一詞數時還用堆嗎?

當 k 接近 m,全量排序的常數和實作複雜度可能更合適;仍可使用堆,但要說明 O(m log k) 接近 O(m log m)。

追問二:輸入是無限串流怎麼辦?

頻率計數仍需狀態;可採用視窗、外部聚合或近似演算法。若要求精確 Top K,就必須保留足夠的計數狀態。

追問三:單字數量超過記憶體怎麼辦?

先分區雜湊到磁碟,分別統計後合併候選;也可用外部排序。不能直接把完整陣列塞進記憶體。

追問四:如何支援大小寫不敏感?

計數前按明確 locale 正規化,輸出保留原文還是正規化形式要先定義,並避免不同正規化形式重複計數。

追問五:如何測試比較器?

構造頻率相同、字典序相反、k=1、k=m 和重複資料的斷言;再與全量排序的基準實作做隨機對拍。

來源一:LeetCode 692

LeetCode 題目定義了頻率降序、同頻字典序升序和 O(n log k) 追問,明確了本題的輸出規則與複雜度目標。

來源二:NeetCode Top K

NeetCode 展示了頻率表與 Top K 維護的常見解法,並提醒比較器和堆/排序取捨是核心。

來源三:Oracle PriorityQueue

Oracle 文件說明 PriorityQueue 按自然順序或 Comparator 維護優先級,為自訂最小堆比較器和 poll 語義提供依據。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具