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}")
總結
這道題展示了原地演算法的靈活性:
- 順序可變的優勢:提供多種解法可能
- 雙指針的變化:標準法、優化法、雙向法
- 效能考量:根據實際情況選擇最佳解法
- 原地操作精髓:用 O(1) 空間完成任務
記住核心:把所有不等於 val 的元素移到陣列前面!
練習建議
- 實現所有三種解法,體會差異
- 分析不同情況下哪種解法更優
- 嘗試處理多個值的刪除
- 練習相關題目(如 LeetCode 283: Move Zeroes)
- 思考如何擴展到鏈表等其他資料結構