LeetCode 解題思路:26. Remove Duplicates From Sorted Array(刪除有序陣列中的重複項)
掌握雙指針技巧與原地演算法的精髓
目錄
題目描述
給定一個按照非遞減順序排列的整數陣列 nums
,請你原地刪除重複出現的元素,使每個元素只出現一次。元素的相對順序應該保持一致。然後返回 nums
中唯一元素的個數。
假設 nums
的唯一元素個數為 k
,為了通過測試,你需要做以下事情:
- 更改陣列
nums
,使nums
的前k
個元素包含原來的唯一元素,且順序與原陣列相同 nums
的其餘元素以及nums
的大小都不重要
什麼是非遞減順序?
在深入解題之前,先理解一個重要概念:
非遞減順序是指陣列中的每個元素都不小於前一個元素。簡單來說,就是「排序好的陣列,但允許有重複值」。
# ✅ 非遞減順序的例子
[1, 2, 3, 4, 5] # 嚴格遞增
[1, 1, 2, 2, 3] # 有重複值(這題的重點!)
[5, 5, 5, 5, 5] # 全部相同
[1, 2, 2, 3, 3, 3] # 部分重複
# ❌ 不是非遞減順序
[5, 4, 3, 2, 1] # 遞減
[1, 3, 2, 4] # 有下降
視覺化理解
非遞減順序(像爬樓梯,可以有平地):
┌─────┐
┌─┤ │
┌─┤ │ │
─┤ │ │ │
└─┴─┴─────┘
1 2 2 3 3
關鍵:因為是非遞減順序,相同的元素必定相鄰!
範例
範例 1:
輸入:nums = [1,1,2]
輸出:2, nums = [1,2,_]
解釋:函數應該返回 k = 2,並且 nums 的前兩個元素為 1, 2
不需要考慮陣列中超出新長度後面的元素(用 _ 表示)
範例 2:
輸入:nums = [0,0,1,1,1,2,2,3,3,4]
輸出:5, nums = [0,1,2,3,4,_,_,_,_,_]
解釋:函數應該返回 k = 5,並且 nums 的前五個元素為 0, 1, 2, 3, 4
限制條件
1 <= nums.length <= 3 * 10⁴
-100 <= nums[i] <= 100
nums
按非遞減順序排列
核心概念
1. 什麼是原地演算法(In-place)?
原地演算法是指:
- 不能創建新陣列來存儲結果
- 只能使用 O(1) 額外空間(幾個變數)
- 直接修改原陣列
# ❌ 錯誤示範:不是原地演算法
def removeDuplicates_wrong(nums):
unique_nums = [] # 創建新陣列!
for num in nums:
if num not in unique_nums:
unique_nums.append(num)
return len(unique_nums)
# ✅ 正確示範:原地修改
def removeDuplicates_correct(nums):
# 只用幾個變數,直接修改 nums
i = 0
for j in range(1, len(nums)):
if nums[j] != nums[i]:
i += 1
nums[i] = nums[j]
return i + 1
2. 為什麼這題適合雙指針?
關鍵洞察:因為陣列是非遞減順序,相同的元素必定相鄰!
這讓我們可以使用雙指針:
- 慢指針:維護不重複元素的部分
- 快指針:探索新元素
解法一:雙指針法(最優解)
思路
使用快慢指針:
- 慢指針
i
:指向下一個不重複元素要放置的位置 - 快指針
j
:遍歷陣列,尋找不重複的元素
程式碼實現
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if not nums:
return 0
# 慢指針,指向最後一個不重複元素的位置
i = 0
# 快指針,從第二個元素開始遍歷
for j in range(1, len(nums)):
# 發現不同的元素
if nums[j] != nums[i]:
i += 1
nums[i] = nums[j]
# i 是索引,長度要加 1
return i + 1
執行過程視覺化
讓我們詳細追蹤 nums = [0,0,1,1,1,2,2,3,3,4]
:
初始狀態:
nums = [0,0,1,1,1,2,2,3,3,4]
↑ ↑
i j
說明:i 指向最後一個唯一元素,j 探索新元素
步驟 1:nums[j]=0 == nums[i]=0,重複,j 前進
nums = [0,0,1,1,1,2,2,3,3,4]
↑ ↑
i j
步驟 2:nums[j]=1 != nums[i]=0,發現新元素!
i 前進到位置 1,複製 nums[j] 到 nums[i]
nums = [0,1,1,1,1,2,2,3,3,4]
↑ ↑
i j
步驟 3-4:nums[j]=1 == nums[i]=1,重複,j 繼續前進
nums = [0,1,1,1,1,2,2,3,3,4]
↑ ↑
i j
步驟 5:nums[j]=2 != nums[i]=1,發現新元素!
nums = [0,1,2,1,1,2,2,3,3,4]
↑ ↑
i j
...繼續執行...
最終結果:
nums = [0,1,2,3,4,2,2,3,3,4]
↑
i
前 5 個元素是唯一的:[0,1,2,3,4]
返回 i + 1 = 5
為什麼這樣有效?
- 利用非遞減特性:相同元素必相鄰,不會錯過
- 慢指針維護結果:前 i+1 個位置始終保存著不重複元素
- 快指針探索:找到不同的元素就加入結果區
解法二:更直觀的理解方式
思路
把慢指針理解為「唯一元素的計數器」。
程式碼實現
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if not nums:
return 0
# k 表示目前有多少個唯一元素
k = 1 # 第一個元素肯定是唯一的
for i in range(1, len(nums)):
# 如果當前元素與前一個不同,就是新的唯一元素
if nums[i] != nums[i - 1]:
nums[k] = nums[i] # 放到第 k 個位置
k += 1 # 唯一元素數量加 1
return k
這個寫法的好處
- 語義清晰:
k
直接表示唯一元素個數 - 容易理解:比較相鄰元素更直觀
常見錯誤與陷阱
錯誤 1:違反原地要求
# ❌ 錯誤!創建了新陣列
def removeDuplicates(self, nums: List[int]) -> int:
unique = []
for num in nums:
if not unique or num != unique[-1]:
unique.append(num)
# 即使複製回去也不對,因為過程中用了 O(n) 空間
nums[:] = unique
return len(unique)
錯誤 2:忘記非遞減順序的特性
# ❌ 效率低下!沒有利用排序特性
def removeDuplicates(self, nums: List[int]) -> int:
i = 0
for j in range(len(nums)):
# 檢查 nums[j] 是否在前面出現過
if nums[j] not in nums[:i]: # O(n) 操作!
nums[i] = nums[j]
i += 1
return i
錯誤 3:返回錯誤的值
# ❌ 常見錯誤
def removeDuplicates(self, nums: List[int]) -> int:
# ... 處理邏輯 ...
return i # 錯誤!i 是最後一個元素的索引,應該返回 i + 1
進階思考
1. 如果陣列未排序怎麼辦?
未排序的情況下,相同元素不一定相鄰,問題變得複雜:
# 方法 1:先排序(改變了相對順序)
def removeDuplicates_unsorted_v1(nums):
nums.sort() # O(n log n)
# 然後用原本的方法
# 方法 2:使用額外空間(不是原地)
def removeDuplicates_unsorted_v2(nums):
seen = set()
k = 0
for num in nums:
if num not in seen:
seen.add(num)
nums[k] = num
k += 1
return k
2. 如果允許每個元素最多出現 k 次?
這是 LeetCode 80 題的泛化版本:
def removeDuplicates(self, nums: List[int], k: int) -> int:
if len(nums) <= k:
return len(nums)
# i 指向下一個要填入的位置
i = k
for j in range(k, len(nums)):
# 與第 i-k 個位置比較
if nums[j] != nums[i - k]:
nums[i] = nums[j]
i += 1
return i
3. 統計刪除了多少元素?
def removeDuplicatesWithCount(self, nums: List[int]) -> tuple:
original_length = len(nums)
unique_length = self.removeDuplicates(nums)
removed_count = original_length - unique_length
print(f"原始長度:{original_length}")
print(f"唯一元素:{unique_length}")
print(f"刪除數量:{removed_count}")
return unique_length, removed_count
複雜度分析
時間複雜度:O(n)
- 快指針遍歷整個陣列一次
- 每個元素最多被移動一次
空間複雜度:O(1)
- 只使用了兩個指針變數
- 真正的原地演算法
相關題目推薦
這些題目都運用了類似的雙指針技巧:
- LeetCode 27: Remove Element - 移除特定元素
- LeetCode 80: Remove Duplicates II - 允許重複兩次
- LeetCode 283: Move Zeroes - 移動零到末尾
- LeetCode 88: Merge Sorted Array - 合併有序陣列
實戰技巧總結
1. 雙指針模板
def twoPointers(nums):
# 慢指針:維護結果
slow = 0
# 快指針:探索元素
for fast in range(len(nums)):
if 滿足某條件:
nums[slow] = nums[fast]
slow += 1
return slow
2. 調試技巧
def removeDuplicates(self, nums: List[int]) -> int:
if not nums:
return 0
i = 0
print(f"初始: nums = {nums}")
for j in range(1, len(nums)):
if nums[j] != nums[i]:
i += 1
nums[i] = nums[j]
print(f"Step {j}: 發現新元素 {nums[j]}")
print(f" nums = {nums[:i+1]} | {nums[i+1:]}")
return i + 1
3. 完整測試案例
def test_removeDuplicates():
test_cases = [
# [輸入, 預期輸出, 預期陣列前綴]
([], 0, []),
([1], 1, [1]),
([1,1,1,1,1], 1, [1]),
([1,2,3,4,5], 5, [1,2,3,4,5]),
([1,1,2,2,3,3], 3, [1,2,3]),
([-1,0,0,0,3,3], 3, [-1,0,3]),
([1,1,2], 2, [1,2])
]
solution = Solution()
for nums, expected_k, expected_prefix in test_cases:
nums_copy = nums.copy()
k = solution.removeDuplicates(nums_copy)
assert k == expected_k
assert nums_copy[:k] == expected_prefix
print(f"✅ 通過:{nums} -> {k}")
總結
這道題完美展示了:
- 非遞減順序的重要性:讓問題簡化,相同元素必相鄰
- 雙指針的優雅:一個維護結果,一個探索新元素
- 原地演算法的精髓:空間效率極高
- 通用的解題模式:可應用於多種陣列操作問題
記住核心思想:利用排序特性,用雙指針原地去重!
練習建議
- 先理解「非遞減順序」的含義
- 在紙上畫出指針移動過程
- 寫程式時加入列印語句觀察執行
- 嘗試各種邊界測試案例
- 挑戰相關題目,鞏固雙指針技巧