具代表性的面試主題

程式設計面試:如何實作均勻的原地陣列洗牌?

程式題中等
Offer.cc 編輯團隊發佈 更新

題幹

請實作支援 reset 與 shuffle 的陣列類別,要求每個排列等可能、shuffle 平均 O(n) 且不複製額外陣列,並解釋為何每個位置的隨機範圍必須逐步縮小。

題目與使用情境

實作 reset() 回傳初始順序,shuffle() 回傳均勻隨機排列。題目考察隨機演算法、原地交換、隨機數邊界與可測試性;抽樣播放、隨機抽籤和測試夾具都可重用同一模式。

面試官考察什麼

  • 是否選擇 Fisher–Yates,而非反覆交換兩個隨機位置。
  • 是否讓第 i 次交換只從尚未固定的區間取索引。
  • 是否區分閉區間與半開區間隨機數,避免越界或偏差。
  • 是否保存不可變的初始快照,讓 reset() 不受洗牌影響。
  • 是否給出 O(n) 時間、O(1) 額外空間和均勻性的理由。
  • 是否說明偽隨機種子、空陣列、重複元素與統計測試邊界。

作答前的釐清問題

  • shuffle() 要回傳新陣列,還是可修改工作陣列後回傳?
  • reset() 是否必須回傳獨立副本,避免呼叫方修改內部狀態?
  • 隨機來源是可注入的偽隨機產生器,還是系統安全隨機源?
  • 元素是否允許重複?相同值但不同位置是否仍視為不同排列?
  • 是否需要並行呼叫安全、可重現種子或加密級不可預測性?
  • 輸入規模是否要求串流處理或只能使用常數額外空間?

30 秒回答框架

“我保存一份初始快照和一份工作陣列。洗牌時從尾端向前走,在 [0, i] 取均勻索引 j,交換 a[i]a[j]。每輪固定一個位置,剩餘前綴繼續處理,所以時間 O(n)、額外空間 O(1)。reset() 回傳初始快照副本;隨機源可注入,方便重現與統計測試。”

分步驟深入解答

步驟 1:隔離狀態。 建構子複製輸入成 originalworkingreset() 重新複製 original,避免外部參照改變基準。

步驟 2:定義隨機邊界。in - 1 遞減到 1,用半開 API randomInt(i + 1) 取得 0..i;若 API 是閉區間,明確傳入 0i

步驟 3:原地交換。 交換 working[i]working[j],位置 i 從此固定;不建立與輸入同規模的暫存陣列。

步驟 4:說明均勻性。 第一個位置有 n 種等機率選擇,第二個有 n-1 種,依序相乘得到 n! 個排列且機率相同。隨機源必須在每個候選索引上等機率。

步驟 5:處理重複元素。 演算法對元素位置均勻;若值重複,多個位置排列可能顯示成相同值序列,不能把可見序列數量當作位置排列數量。

步驟 6:實作 reset。 回傳 original 副本並將 working 恢復為副本;若直接暴露內部陣列,後續 shuffle 會產生別名污染。

步驟 7:驗證和複雜度。 用固定種子檢查重現,用小陣列列舉排列頻數檢查近似均勻,測試空陣列和單元素。每次洗牌 O(n) 時間、O(1) 額外空間;保存快照本身占 O(n) 狀態。

高品質示範回答

“我維護 originalworking 兩個陣列。shufflei = n-1..1 呼叫均勻的 j ∈ [0,i],交換兩項;reset 回傳 original 副本並重建 working。反覆交換兩個任意隨機位置會讓前面位置被重複改寫,排列機率不均。Fisher–Yates 每輪只從尚未固定的前綴選擇,因此所有位置排列等可能,執行時間 O(n),除狀態快照外不需要額外陣列。隨機源可注入,便於種子重現、統計測試和安全場景替換。”

常見錯誤

  • 每輪都在 [0,n-1] 取隨機位置 → 已固定位置會再次被改寫 → 使用 [0,i]
  • 生成 floor(random * i) 漏掉索引 i使用 i + 1 作為半開上界。
  • 交換兩個任意隨機位置 n 次 → 不保證均勻排列 → 採用逐位固定的 Fisher–Yates。
  • reset 回傳同一內部參照 → 呼叫方會污染初始快照 → 回傳副本並隔離狀態。
  • 把偽隨機當成安全隨機 → 結果可能可預測 → 依威脅模型注入 CSPRNG。

追問及應對

追問 1:為什麼從後往前也可以從前往後?

前向版本在位置 i[i,n-1] 選取並固定 i,證明完全對稱;關鍵是每輪只從未固定區間取樣。

追問 2:如何證明均勻?

位置 n-1n 個等機率選擇,位置 n-2n-1 個,以此類推;任一完整選擇路徑機率為 1/n!

追問 3:如何測試隨機性?

對小陣列執行大量次,統計每個位置排列的頻數並設定允許誤差;同時用固定種子驗證可重現,避免把一次樣本當成證明。

追問 4:什麼時候不能用普通偽隨機?

抽籤、權杖或洗牌結果會影響安全與權益時,應使用系統 CSPRNG;一般模擬、遊戲和測試可使用可播種 PRNG。

追問 5:輸入是鏈結串列怎麼辦?

先轉成陣列會占 O(n) 空間;若必須原地,可交換鏈結節點但隨機存取代價高,需重新討論限制。

追問 6:並行呼叫如何處理?

給每個實例獨立狀態並加鎖,或回傳不可變快照;不要讓一個執行緒在另一個執行緒 reset 時看到半次交換。

追問 7:為什麼 Java 官方 shuffle 也採用這種思路?

Oracle 文件明確說明從後向前走,並把隨機元素交換到目前位置;在公平隨機源下所有排列等可能。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具