具代表性的面試主題

程式面試:用拓撲排序解外星字典

程式題困難
OfferCC 編輯團隊發佈 更新

題幹

給定一個依未知字母表字典順序排列的非空小寫 ASCII 單字陣列,請回傳一個包含所有不同字元且每個字元恰好出現一次的有效順序;若不存在能解釋輸入的字母表,則回傳空字串。請說明建圖、無效前綴、環、多解、正確性、複雜度與測試。

題目與適用情境

給定一個非空小寫 ASCII 字串陣列 words,陣列聲稱已依某個未知字母表排序。請回傳一個包含 輸入中所有不同字元、且每個字元恰好出現一次的順序,使整份單字清單保持有序。若不存在這種 順序,回傳空字串。存在多個可行字母表時,回傳任意一個即可。

本題假設單字數不超過 10,000,總字元數不超過 100,000;這些是面試練習限制,不代表特定平台 上限。["wrt", "wrf", "er", "ett", "rftt"] 的一個答案是 "wertf"。 ["abc", "ab"] 無解,因為較長單字排在自身前綴之前;["z", "x", "z"] 同時要求 z < x 和 x < z,也無解。

難點發生在拓撲排序之前。輸入只有已排序單字,圖的邊需要推導。解法必須只擷取字典順序真正 支援的限制,保留沒有邊的字元,並分別處理前綴矛盾與有向環。

面試官在評估什麼

第一個訊號是能否從相鄰單字的第一個不同字元推導邊。"wrt" 排在 "wrf" 前面,只能證明 t < f。第一個差異已決定這對單字的字典順序,後續位置無法再提供這對單字的字元先後關係。

第二個訊號是前綴規則。共同部分完全相同時,較短單字必須在前。"ab" 位於 "abc" 前面 合法且不新增邊;"abc" 位於 "ab" 前面與任何字母表都衝突。單獨執行拓撲排序無法發現 這個問題,因為這對單字不會產生不同字元。

第三個訊號是建圖是否完整。每個出現過的字元都要成為節點,包括單字中沒有任何邊的孤立字元。 同一條邊出現多次時,入度只能增加一次;為每個來源字元使用相鄰集合可保持相鄰關係與入度一致。

最後評估證明與驗證。Kahn 演算法只有處理完全部節點才能回傳完整順序;結果較短表示剩餘圖中 有環。某一步同時出現多個零入度字元,代表證據無法確定唯一字母表,但在基礎題契約中仍然有效。

回答前要先確認的問題

  • 輸入是否包含字母表中的所有字元? 本解法只排列單字中出現過的字元。沒有外部字母表定義,

就無法補充或放置未出現字元。

  • 任意有效順序都可以嗎? 基礎題允許任意答案。若要求依一般字元順序最小,需要最小堆,

複雜度也會改變。

  • 如何表示無解? 本題對無效前綴和環都回傳空字串。正式 API 可以回傳結構化原因與證據。
  • 字元的定義是什麼? 基礎輸入只包含小寫 ASCII。Unicode 碼點或字素叢集需要先約定斷詞規則。
  • 單字可以重複嗎? 可以。兩個相同的相鄰單字不增加限制,也不會讓輸入無效。
  • 必須存在唯一字母表嗎? 不需要。追問可透過每一步零入度候選數判斷唯一性。
  • 陣列可以為空嗎? 本版本至少包含一個非空單字。若允許空輸入,要先確認回傳空字母表還是

回報請求無效。

30 秒回答架構

「我會為每個不同字元建立節點。逐對比較相鄰單字,只掃描到第一個不同位置,並從前一個單字的 字元向後一個單字的字元連邊。若沒有不同字元且前一個單字較長,就出現較長單字排在自身前綴 之前的矛盾,直接回傳空字串。建圖時對邊去重並維護入度,再從所有零入度字元開始執行 Kahn 拓撲排序。處理完所有節點就得到滿足全部限制的順序;結果較短表示存在環。總時間為輸入字元數 加圖規模的線性複雜度,多種拓撲順序都可作為答案。」

分步驟深入解答

記 C 為所有單字的總字元數,U 為不同字元數,E 為不同先後邊數。先為每個已出現字元建立 相鄰集合 graph[ch] 和初始值為零的 indegree[ch]。這個步驟必須在比較單字前完成:一個字元 即使沒有任何限制也是合法節點,不能只從邊的端點收集字元。

只比較相鄰單字就足夠。若回傳的字母表能證明每一對相鄰單字有序,遞移性會保證整份清單有序; 同時也避免比較平方數量的單字對。對 first 與 second 掃描到較短長度:

  1. 第一次遇到 first[i] != second[i] 時,加入 first[i] -> second[i],並停止比較該詞對。
  2. 所有共同位置都相同且 first 較長時,回傳空字串。
  3. 所有共同位置都相同且 first 不較長時,不新增邊。

只有新邊插入相鄰集合時才增加目標字元入度。例如 "za" < "zb" 與 "ca" < "cb" 都 推出 a -> b。若將其計數兩次,移除 a 後 b 的入度仍大於零,合法輸入會被誤判成環。

Kahn 演算法把所有零入度字元放入佇列。它維護兩個不變量:每個未處理字元的入度等於其他未處理 字元指向它的邊數;佇列恰好包含沒有未處理前驅的字元。取出佇列前端字元是安全的,接著走訪其 出邊並減少鄰居入度;鄰居的最後一個前驅消失時進入佇列。

python
from collections import deque


def alien_order(words: list[str]) -> str:
    graph = {char: set() for word in words for char in word}
    indegree = {char: 0 for char in graph}

    for first, second in zip(words, words[1:]):
        limit = min(len(first), len(second))

        for index in range(limit):
            before = first[index]
            after = second[index]
            if before == after:
                continue

            if after not in graph[before]:
                graph[before].add(after)
                indegree[after] += 1
            break
        else:
            if len(first) > len(second):
                return ""

    ready = deque(
        char for char, degree in indegree.items() if degree == 0
    )
    order: list[str] = []

    while ready:
        char = ready.popleft()
        order.append(char)

        for neighbor in graph[char]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                ready.append(neighbor)

    return "".join(order) if len(order) == len(indegree) else ""

正確性分成兩層。建圖具有可靠性:每條邊都來自相鄰單字的第一個差異,因此任何可行字母表都 必須滿足它;前綴檢查排除了沒有不同字元卻依然無序的唯一相鄰情境。拓撲排序也具有可靠性:佇列 不變量保證輸出字元位於所有已推導前驅之後。於是每一對相鄰單字都有序,整份清單也有序。

若演算法輸出少於 U 個字元,剩餘節點的入度都大於零。從任意剩餘節點反覆沿入邊前進,在有限 圖中必然再次遇到某個節點,重複部分構成有向環,線性字母表無法滿足它。反過來,無環圖總有 零入度節點,所以 Kahn 演算法最終會處理全部節點並回傳有效順序。

初始化節點並掃描相鄰單字需要 O(C),Kahn 演算法各處理節點和不同邊一次,總時間為 O(C + U + E),額外空間為 O(U + E)。小寫 ASCII 下 U 最多為 26,但保留符號表達更方便 套用到其他字母表。

多解時,測試應驗證性質。非空結果必須恰好包含全部不同字元一次;建立字元排名後,依回傳順序 重新比較每對相鄰單字,並單獨檢查較長單字是否排在自身前綴前。測試要涵蓋單一單字、重複單字、 孤立字元、重複邊證據、合法前綴、無效前綴、環、長鏈,以及同時存在多個零入度節點的圖。

三色 DFS 也是正確替代方案:遇到灰色節點表示環,離開遞迴時記錄節點並反轉後序結果。Kahn 演算法可直接從候選集合觀察多解,也沒有遞迴深度問題,因此更適合本題契約。

高品質示範回答

「我先從已排序單字中擷取偏序。每個出現過的字元都建立節點,即使它沒有任何邊。逐對比較相鄰 單字並找到第一個不同字元:例如 wrt 與 wrf 推出 t -> f,後續位置無法再影響這對單字 的比較。若沒有不同字元,而前一個單字較長,例如 abc 位於 ab 前面,輸入已經矛盾。

相鄰表使用集合,同一先後關係只增加一次入度。接著執行 Kahn 演算法:所有零入度字元先進入 佇列,每次輸出一個字元、刪除它的出邊,並在鄰居入度降為零時加入佇列。佇列中的字元在未處理 圖中沒有前驅,所以每次選擇都不會違反已知限制。

若結果長度等於不同字元數,每條推導出的邊都得到滿足,加上前綴檢查即可證明每對相鄰單字有序, 進而證明整份清單有序。若長度更短,剩餘圖存在環,沒有任何字母表能解釋輸入。時間複雜度是 O(C + U + E),空間複雜度是 O(U + E)。我會測試無效前綴、兩邊構成的環、同一邊重複出現、 單一單字和多解;多解案例驗證順序性質,不固定比較某一個字串。」

常見錯誤

  • 把詞對中每個不同位置都連邊 → 第一個差異後的位置不再參與字典順序判斷 → **只加入第一個

差異對應的邊並立即停止。**

  • 只執行拓撲排序 → "abc" 位於 "ab" 前面時沒有邊可暴露錯誤 → **比較詞對時檢查較長

單字先於自身前綴的矛盾。**

  • 只為邊的端點建立節點 → 孤立字元從答案消失 → 為所有出現過的字元預先建立節點。
  • 重複邊重複增加入度 → 合法節點永遠無法降到零入度 → 使用相鄰集合,只在首次插入時增加。
  • 佇列為空就回傳部分結果 → 有環輸入被回報為成功 → 要求結果長度等於不同字元數。
  • 只接受一個固定字串 → 一個偏序可能有多種線性延伸 → 檢查字元集合、邊和單字相對順序。
  • 比較所有單字對 → 單字數量維度可能出現平方級工作 → 相鄰比較已足以建立清單有序性。
  • 把多解當成無效 → 多個字母表可同時解釋現有證據 → 基礎契約下回傳任意有效順序。

追問與回答

追問 1:如何判斷字母表是否唯一?

Kahn 演算法每次取出前檢查候選集合。只要某一步有兩個以上零入度字元,就至少存在兩種選擇順序, 證據無法確定唯一字母表。若每一步候選數都恰好為一且處理完全部節點,順序唯一;提前出現空集合 仍表示有環。

追問 2:如何回傳一般字元順序下最小的有效結果?

把普通佇列換成依宿主語言字元順序排列的最小堆。每次選擇目前可行字元中的最小者,可用交換論證 證明得到最小線性延伸。時間變成 O(C + E + U log U);這個排序只是外部打破平手的規則,不屬於 外星字母表本身。

追問 3:如何回傳可操作的無效原因?

前綴矛盾可回傳相鄰單字及其索引。Kahn 演算法停滯後,在剩餘圖上執行帶父指標的三色 DFS,遇到 灰色節點時還原一條環。結構化結果可區分 invalid_prefix、cycle 與 valid,不必讓空字串 承擔全部語意。

追問 4:可以串流處理單字嗎?

保留前一個單字;每讀到下一個單字就補充節點並推導這一對相鄰限制。圖和入度仍要保存到串流結束, 因為後續證據可能增加前驅或形成環。除非資料來源提供明確結束邊界,否則不能提前確認最終拓撲順序。

追問 5:如何列舉所有有效字母表?

對目前所有零入度字元回溯:選擇一個、暫時刪除其出邊、遞迴,再還原狀態。這樣只列舉合法順序, 但輸出數量可能接近 U!;實作前應確認字母表規模或最大輸出數量。

追問 6:Unicode 單字需要改什麼?

先定義比較單位。碼點不一定等於使用者看到的字元,地區排序還可能處理正規化形式或多碼點序列。 依題目約定把單字切成字母表符號,只在契約要求時正規化,再對這些 token 執行同一套圖演算法。 缺少該契約時,「字元順序」本身沒有完整定義。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具