☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
專案實戰 進階

面試白板題:動態規劃與排序

什麼是動態規劃?

💡 比喻:聰明的備忘錄 假設你在爬樓梯,每次可以爬 1 或 2 階。 如果有人問你「爬到第 10 階有幾種方法?」—— 你可以把「爬到第 1 階」「爬到第 2 階」的答案記下來, 後面的答案就是前面兩個相加,不用重複計算。 這就是動態規劃的核心:記住已經算過的結果


題目一:Climbing Stairs(爬樓梯)

暴力遞迴 O(2^n) — 超時!

// 暴力遞迴:每次選擇爬 1 或 2 階 // Brute force: exponential time
public int ClimbStairsBrute(int n) // 爬樓梯暴力解
{
    if (n <= 2) return n; // 1 階有 1 種,2 階有 2 種
    return ClimbStairsBrute(n - 1) + ClimbStairsBrute(n - 2); // 遞迴計算
    // ❌ 會重複計算非常多次,指數級時間複雜度 // 會超時!
}

記憶化遞迴 O(n)

// 記憶化遞迴:用字典記住算過的結果 // Memoization: cache computed results
public int ClimbStairsMemo(int n, Dictionary<int, int>? memo = null) // 記憶化版本
{
    memo ??= new Dictionary<int, int>(); // 初始化備忘錄
    if (n <= 2) return n; // 基本情況
    if (memo.ContainsKey(n)) return memo[n]; // 如果算過了直接回傳
    memo[n] = ClimbStairsMemo(n - 1, memo) + ClimbStairsMemo(n - 2, memo); // 計算並記住
    return memo[n]; // 回傳結果
}

DP 表格解法 O(n)

// DP 表格:從小問題往上計算 // Bottom-up DP table
public int ClimbStairsDP(int n) // 爬樓梯 DP 解法
{
    if (n <= 2) return n; // 基本情況
    int[] dp = new int[n + 1]; // 建立 DP 表格
    dp[1] = 1; // 1 階有 1 種方法
    dp[2] = 2; // 2 階有 2 種方法
    for (int i = 3; i <= n; i++) // 從第 3 階開始算
    {
        dp[i] = dp[i - 1] + dp[i - 2]; // 等於前兩階的方法數相加
    }
    return dp[n]; // 回傳第 n 階的方法數
}

空間優化 O(1)

// 空間優化:只需要前兩個數字 // Space optimized: only need two variables
public int ClimbStairsOptimal(int n) // 最佳化爬樓梯
{
    if (n <= 2) return n; // 基本情況
    int prev2 = 1; // 前兩階的方法數
    int prev1 = 2; // 前一階的方法數
    for (int i = 3; i <= n; i++) // 從第 3 階開始
    {
        int current = prev1 + prev2; // 當前階 = 前一階 + 前兩階
        prev2 = prev1; // 更新前兩階
        prev1 = current; // 更新前一階
    }
    return prev1; // 回傳結果
}

題目二:Coin Change(零錢問題)

題目說明

給定不同面額的硬幣和一個總金額,計算湊出總金額所需的最少硬幣數。

暴力遞迴

// 暴力遞迴:嘗試每種硬幣 // Brute force: try every coin
public int CoinChangeBrute(int[] coins, int amount) // 零錢問題暴力解
{
    if (amount == 0) return 0; // 金額為 0 不需要硬幣
    if (amount < 0) return -1; // 金額為負代表這條路不通
    int minCoins = int.MaxValue; // 初始化最小硬幣數
    foreach (int coin in coins) // 嘗試每種硬幣
    {
        int result = CoinChangeBrute(coins, amount - coin); // 遞迴計算剩餘金額
        if (result >= 0 && result + 1 < minCoins) // 如果這條路可行且更少
        {
            minCoins = result + 1; // 更新最小硬幣數
        }
    }
    return minCoins == int.MaxValue ? -1 : minCoins; // 回傳結果
}

DP 表格解法

// DP 解法:建表從金額 0 算到目標金額 // DP: build table from 0 to amount
public int CoinChangeDP(int[] coins, int amount) // 零錢問題 DP 解法
{
    int[] dp = new int[amount + 1]; // 建立 DP 表格
    Array.Fill(dp, amount + 1); // 初始化為不可能的大數
    dp[0] = 0; // 金額 0 需要 0 個硬幣
    for (int i = 1; i <= amount; i++) // 從金額 1 算到目標金額
    {
        foreach (int coin in coins) // 嘗試每種硬幣
        {
            if (coin <= i) // 如果這個硬幣面額不超過當前金額
            {
                dp[i] = Math.Min(dp[i], dp[i - coin] + 1); // 取較少的硬幣數
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount]; // 回傳結果
}

DP 推導過程圖解

硬幣 = [1, 3, 5],目標金額 = 7

dp[0] = 0  (0 元需要 0 個硬幣)
dp[1] = 1  (1 元 → 用 1 個「1 元」)
dp[2] = 2  (2 元 → 用 2 個「1 元」)
dp[3] = 1  (3 元 → 用 1 個「3 元」)
dp[4] = 2  (4 元 → 用 1+3)
dp[5] = 1  (5 元 → 用 1 個「5 元」)
dp[6] = 2  (6 元 → 用 1+5 或 3+3)
dp[7] = 3  (7 元 → 用 1+1+5 或 1+3+3)

答案:dp[7] = 3

題目三:Quick Sort vs Merge Sort 實作

Quick Sort(快速排序)

// 快速排序:選基準值,分成大小兩堆 // Quick Sort: pivot partitioning
public void QuickSort(int[] arr, int low, int high) // 快速排序主方法
{
    if (low < high) // 還有元素要排
    {
        int pivotIndex = Partition(arr, low, high); // 取得基準值的正確位置
        QuickSort(arr, low, pivotIndex - 1); // 遞迴排左半邊
        QuickSort(arr, pivotIndex + 1, high); // 遞迴排右半邊
    }
}

private int Partition(int[] arr, int low, int high) // 分割:把小的放左邊大的放右邊
{
    int pivot = arr[high]; // 選最後一個元素作為基準值
    int i = low - 1; // i 指向小於基準值區域的邊界
    for (int j = low; j < high; j++) // 走訪所有元素
    {
        if (arr[j] < pivot) // 如果當前元素小於基準值
        {
            i++; // 擴大小於區域
            (arr[i], arr[j]) = (arr[j], arr[i]); // 交換位置
        }
    }
    (arr[i + 1], arr[high]) = (arr[high], arr[i + 1]); // 基準值放到正確位置
    return i + 1; // 回傳基準值的索引
}

Merge Sort(合併排序)

// 合併排序:分成兩半再合併 // Merge Sort: divide and merge
public int[] MergeSort(int[] arr) // 合併排序主方法
{
    if (arr.Length <= 1) return arr; // 只有一個元素不用排
    int mid = arr.Length / 2; // 找到中間點
    int[] left = MergeSort(arr[..mid]); // 遞迴排左半邊
    int[] right = MergeSort(arr[mid..]); // 遞迴排右半邊
    return Merge(left, right); // 合併兩個已排序的陣列
}

private int[] Merge(int[] left, int[] right) // 合併兩個已排序陣列
{
    int[] result = new int[left.Length + right.Length]; // 建立結果陣列
    int i = 0, j = 0, k = 0; // 三個索引指標
    while (i < left.Length && j < right.Length) // 當兩邊都還有元素
    {
        if (left[i] <= right[j]) // 左邊的比較小
            result[k++] = left[i++]; // 放入左邊的元素
        else // 右邊的比較小
            result[k++] = right[j++]; // 放入右邊的元素
    }
    while (i < left.Length) result[k++] = left[i++]; // 放入左邊剩餘的元素
    while (j < right.Length) result[k++] = right[j++]; // 放入右邊剩餘的元素
    return result; // 回傳合併後的陣列
}

比較表

特性            Quick Sort          Merge Sort
──────────────────────────────────────────────
平均時間        O(n log n)          O(n log n)
最差時間        O(n²)               O(n log n)
空間複雜度      O(log n)            O(n)
穩定排序        ❌ 不穩定            ✅ 穩定
原地排序        ✅ 是                ❌ 否
適合場景        一般排序              需要穩定排序時

基本二分搜尋

// 基本二分搜尋:在已排序陣列中找目標值 // Basic binary search
public int BinarySearch(int[] nums, int target) // 二分搜尋
{
    int left = 0; // 左邊界
    int right = nums.Length - 1; // 右邊界
    while (left <= right) // 當搜尋範圍還有效
    {
        int mid = left + (right - left) / 2; // 計算中間位置(避免溢位)
        if (nums[mid] == target) return mid; // 找到了
        if (nums[mid] < target) left = mid + 1; // 目標在右半邊
        else right = mid - 1; // 目標在左半邊
    }
    return -1; // 找不到回傳 -1
}

變體一:找第一個出現的位置

// 變體:找目標值第一次出現的位置 // Variant: find first occurrence
public int FindFirst(int[] nums, int target) // 找第一個出現的位置
{
    int left = 0; // 左邊界
    int right = nums.Length - 1; // 右邊界
    int result = -1; // 預設找不到
    while (left <= right) // 當搜尋範圍還有效
    {
        int mid = left + (right - left) / 2; // 計算中間位置
        if (nums[mid] == target) // 找到目標值
        {
            result = mid; // 記錄位置
            right = mid - 1; // 繼續往左找更早出現的
        }
        else if (nums[mid] < target) left = mid + 1; // 目標在右半邊
        else right = mid - 1; // 目標在左半邊
    }
    return result; // 回傳第一次出現的位置
}

變體二:找插入位置

// 變體:找目標值應該插入的位置 // Variant: find insert position
public int SearchInsertPosition(int[] nums, int target) // 找插入位置
{
    int left = 0; // 左邊界
    int right = nums.Length - 1; // 右邊界
    while (left <= right) // 當搜尋範圍還有效
    {
        int mid = left + (right - left) / 2; // 計算中間位置
        if (nums[mid] == target) return mid; // 找到直接回傳
        if (nums[mid] < target) left = mid + 1; // 目標在右半邊
        else right = mid - 1; // 目標在左半邊
    }
    return left; // left 就是應該插入的位置
}

🤔 我這樣寫為什麼會錯?

錯誤一:DP 忘記初始化

// ❌ 錯誤:dp 陣列沒有初始化 // Wrong: dp not initialized
int[] dp = new int[amount + 1]; // 預設都是 0
dp[0] = 0; // 設定起點
// ❌ dp[1] 到 dp[amount] 都是 0,Math.Min 永遠選 0

// ✅ 正確:初始化為不可能的大數 // Correct: initialize to impossible value
int[] dp2 = new int[amount + 1]; // 建立 DP 表格
Array.Fill(dp2, amount + 1); // 填入大數代表「還沒算」
dp2[0] = 0; // 金額 0 需要 0 個硬幣

錯誤二:二分搜尋中間值溢位

// ❌ 錯誤:可能溢位 // Wrong: potential overflow
int mid = (left + right) / 2; // ❌ left + right 可能超過 int.MaxValue

// ✅ 正確:避免溢位的寫法 // Correct: overflow-safe
int mid2 = left + (right - left) / 2; // 永遠不會溢位

錯誤三:Quick Sort 基準值選擇不當

// ❌ 風險:總是選第一個或最後一個元素 // Risky: always pick first/last
int pivot = arr[low]; // 如果陣列已排序,會退化成 O(n²)

// ✅ 更好:隨機選擇或三數取中 // Better: random or median-of-three
int randomIndex = new Random().Next(low, high + 1); // 隨機選一個
(arr[randomIndex], arr[high]) = (arr[high], arr[randomIndex]); // 交換到最後
int pivot2 = arr[high]; // 再用最後一個當基準值

DP 解題框架

解 DP 題目的五步驟:

1. 定義狀態:dp[i] 代表什麼?
   → 爬樓梯:dp[i] = 爬到第 i 階的方法數
   → 零錢:dp[i] = 湊出金額 i 的最少硬幣數

2. 定義轉移方程:dp[i] 怎麼從前面的值算出來?
   → 爬樓梯:dp[i] = dp[i-1] + dp[i-2]
   → 零錢:dp[i] = min(dp[i], dp[i-coin] + 1)

3. 初始化:dp[0] 或 dp[1] 的值是什麼?
   → 爬樓梯:dp[1] = 1, dp[2] = 2
   → 零錢:dp[0] = 0

4. 計算順序:從小到大填表格

5. 回傳結果:dp[n] 就是答案

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊