LeetCode 解題思路:27. Remove Element(移除元素)

理解原地演算法的靈活應用與多種解題策略

題目描述

給定一個整數陣列 nums 和一個整數 val,請你原地移除所有數值等於 val 的元素。元素的順序可以改變。然後返回 nums 中與 val 不相等的元素數量。

假設 nums 中不等於 val 的元素數量為 k,要通過測試,你需要執行以下操作:

  • 更改陣列 nums,使 nums 的前 k 個元素包含不等於 val 的元素
  • nums 中剩餘元素及陣列大小都不重要

範例

範例 1:

輸入:nums = [3,2,2,3], val = 3
輸出:2, nums = [2,2,_,_]
解釋:函數應返回 k = 2,nums 的前兩個元素均為 2
不需要考慮陣列中超出新長度後面的元素

範例 2:

輸入:nums = [0,1,2,2,3,0,4,2], val = 2
輸出:5, nums = [0,1,4,0,3,_,_,_]
解釋:函數應返回 k = 5,nums 的前五個元素為 0, 1, 4, 0, 3
注意這五個元素可為任意順序

限制條件

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

核心概念理解

1. 關於 “_” 符號的說明

首先要澄清:_ 只是示意符號,表示「我們不在乎這裡是什麼」。

# 原始陣列
nums = [3,2,2,3], val = 3

# 執行後的實際狀況
nums = [2,2,2,3]  # 後面仍然有數字!
        ↑───↑
      只看這部分

# 題目用 _ 表示
nums = [2,2,_,_]  # 只是說後面不重要

2. 與第 26 題的差異

特性第 26 題(刪除重複)第 27 題(移除特定值)
陣列狀態已排序未排序
刪除什麼重複的元素等於 val 的元素
保持順序必須保持可以改變
解法彈性較低較高

關鍵差異:這題允許改變順序,給了我們更多解法選擇!

解法一:雙指針法(標準解法)

思路

使用快慢指針,類似第 26 題的方法:

  • 慢指針 i:指向下一個要填入的位置
  • 快指針 j:遍歷整個陣列

程式碼實現

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        # 慢指針,指向下一個要填入的位置
        i = 0
        
        # 快指針,遍歷整個陣列
        for j in range(len(nums)):
            # 如果當前元素不等於 val,保留它
            if nums[j] != val:
                nums[i] = nums[j]
                i += 1
        
        return i

執行過程視覺化

範例:nums = [3,2,2,3], val = 3

初始狀態:
nums = [3,2,2,3]
        ↑
       i,j

步驟 1:nums[0] = 3 = val,跳過
nums = [3,2,2,3]
        ↑ ↑
        i j

步驟 2:nums[1] = 2 ≠ val,複製到位置 i
nums = [2,2,2,3]
        ↑   ↑
        i   j
        i 移動到 1

步驟 3:nums[2] = 2 ≠ val,複製到位置 i
nums = [2,2,2,3]
          ↑ ↑
          i j
          i 移動到 2

步驟 4:nums[3] = 3 = val,跳過
nums = [2,2,2,3]
          ↑   ↑
          i   j

結果:i = 2,表示有 2 個元素不等於 val

複雜度分析

  • 時間複雜度:O(n),遍歷一次陣列
  • 空間複雜度:O(1),只用兩個指針

解法二:雙指針優化版(當 val 很少時)

思路

如果要刪除的元素很少,我們可以減少不必要的賦值操作。

程式碼實現

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        i = 0
        n = len(nums)
        
        while i < n:
            if nums[i] == val:
                # 將當前元素與最後一個元素交換
                nums[i] = nums[n - 1]
                # 縮小考慮範圍
                n -= 1
                # 注意:不要 i += 1,因為交換來的元素也需要檢查
            else:
                i += 1
        
        return n

執行過程視覺化

範例:nums = [3,2,2,3], val = 3

初始:i = 0, n = 4
[3,2,2,3]
 ↑     ↑
 i   n-1

步驟 1:nums[0] = 3 = val,與最後交換
[3,2,2,3] → [3,2,2,3](與自己交換)
 ↑     ↑      ↑   ↑
 i   n-1      i  n-1
n 變成 3

步驟 2:檢查新的 nums[0] = 3 = val,與最後交換
[3,2,2] → [2,2,2]
 ↑   ↑     ↑   
 i n-1     i   
n 變成 2

步驟 3:nums[0] = 2 ≠ val,i 前進
[2,2]
   ↑
   i

步驟 4:nums[1] = 2 ≠ val,i 前進
結束,返回 n = 2

優點

  • 當要刪除的元素很少時,賦值操作更少
  • 適合 val 出現頻率低的情況

解法三:雙向指針法

思路

使用頭尾兩個指針,將不等於 val 的元素往前放。

程式碼實現

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        left = 0
        right = len(nums) - 1
        
        while left <= right:
            if nums[left] == val:
                # 用右邊的元素覆蓋
                nums[left] = nums[right]
                right -= 1
            else:
                left += 1
        
        return left

解法比較

解法優點缺點適用場景
標準雙指針保持相對順序可能有多餘賦值需要保持部分順序時
優化雙指針賦值次數少完全打亂順序val 出現次數少時
雙向指針程式碼簡潔打亂順序一般情況

常見錯誤與陷阱

錯誤 1:創建新陣列

# ❌ 錯誤!不是原地演算法
def removeElement(self, nums: List[int], val: int) -> int:
    result = []
    for num in nums:
        if num != val:
            result.append(num)
    
    nums[:] = result  # 即使複製回去也不對
    return len(result)

錯誤 2:使用內建函數不當

# ❌ 錯誤!remove() 每次只刪除一個,且時間複雜度是 O(n)
def removeElement(self, nums: List[int], val: int) -> int:
    while val in nums:
        nums.remove(val)  # 總時間複雜度變成 O(n²)
    return len(nums)

錯誤 3:優化版忘記重新檢查

# ❌ 錯誤示範
def removeElement(self, nums: List[int], val: int) -> int:
    i = 0
    n = len(nums)
    
    while i < n:
        if nums[i] == val:
            nums[i] = nums[n - 1]
            n -= 1
            i += 1  # 錯誤!交換來的元素可能也是 val
        else:
            i += 1
    
    return n

進階思考

1. 如果要保持相對順序?

如果題目要求保持非 val 元素的相對順序,只能用標準雙指針法:

def removeElement_preserve_order(nums, val):
    write_idx = 0
    
    for read_idx in range(len(nums)):
        if nums[read_idx] != val:
            nums[write_idx] = nums[read_idx]
            write_idx += 1
    
    return write_idx

2. 如果要統計刪除了多少?

def removeElementWithCount(nums, val):
    original_len = len(nums)
    k = removeElement(nums, val)
    removed_count = original_len - k
    
    print(f"刪除了 {removed_count}{val}")
    return k, removed_count

3. 如果要原地標記而非刪除?

def markElement(nums, val, mark=-1):
    """將所有 val 標記為 mark"""
    count = 0
    for i in range(len(nums)):
        if nums[i] == val:
            nums[i] = mark
            count += 1
    return len(nums) - count

效能優化技巧

1. 根據 val 出現頻率選擇演算法

def removeElement_adaptive(nums, val):
    # 快速統計 val 的數量
    val_count = nums.count(val)
    
    # 如果 val 很少,使用交換法
    if val_count < len(nums) // 4:
        return removeElement_swap(nums, val)
    else:
        return removeElement_standard(nums, val)

2. 批次處理

def removeMultipleValues(nums, vals):
    """移除多個值"""
    vals_set = set(vals)
    write_idx = 0
    
    for num in nums:
        if num not in vals_set:
            nums[write_idx] = num
            write_idx += 1
    
    return write_idx

測試案例

def test_removeElement():
    test_cases = [
        # (nums, val, expected_k, possible_results)
        ([3,2,2,3], 3, 2, [[2,2]]),
        ([0,1,2,2,3,0,4,2], 2, 5, [[0,1,3,0,4], [0,1,4,0,3]]),
        ([], 1, 0, [[]]),
        ([1], 1, 0, [[]]),
        ([1], 2, 1, [[1]]),
        ([1,1,1,1], 1, 0, [[]]),
        ([1,2,3,4,5], 3, 4, [[1,2,4,5]]),
    ]
    
    solution = Solution()
    for nums, val, expected_k, possible_results in test_cases:
        nums_copy = nums.copy()
        k = solution.removeElement(nums_copy, val)
        
        assert k == expected_k
        # 檢查前 k 個元素都不等於 val
        assert all(nums_copy[i] != val for i in range(k))
        
        print(f"✅ 通過:{nums} 移除 {val} → k={k}")

總結

這道題展示了原地演算法的靈活性:

  1. 順序可變的優勢:提供多種解法可能
  2. 雙指針的變化:標準法、優化法、雙向法
  3. 效能考量:根據實際情況選擇最佳解法
  4. 原地操作精髓:用 O(1) 空間完成任務

記住核心:把所有不等於 val 的元素移到陣列前面!

練習建議

  1. 實現所有三種解法,體會差異
  2. 分析不同情況下哪種解法更優
  3. 嘗試處理多個值的刪除
  4. 練習相關題目(如 LeetCode 283: Move Zeroes)
  5. 思考如何擴展到鏈表等其他資料結構
0%