bigywater.tech
算法与数据结构 · LeetCode 26

LeetCode 26. 删除有序数组中的重复项

给定一个升序数组,原地删除重复元素,使每个值只出现一次,返回去重之后的长度 k;数组前 k 个位置放去重后的元素,后面的内容不重要。核心是双指针:一个指针 i 扫描整个数组,一个指针 k 记录去重部分的末尾。本文实测原地写入的双指针与"遇到重复就 del 掉"的做法的真实差距,给出五种语言的实现,并把双指针的移动做成分步演示。

10,000
组随机对拍,与暴力解的结果全部一致
11 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。评测只检查返回的 k 和前 k 个元素,本文的用例输出也是 [k, 前 k 个元素]。基准解是 sorted(set(nums)),与原地双指针是两种独立的写法。一共 10 组用例,覆盖:官方示例、单个元素、空数组、没有重复、全部相同、含负数:

真实实测 · 本机执行输出
输入 [[1,1,2]]
输出 [2,[1,2]]
输入 [[0,0,1,1,1,2,2,3,3,4]]
输出 [5,[0,1,2,3,4]]
输入 [[1]]
输出 [1,[1]]
输入 [[]]
输出 [0,[]]
输入 [[1,2,3]]
输出 [3,[1,2,3]]
输入 [[5,5,5,5]]
输出 [1,[5]]

双指针:五种语言的实现

k 表示 nums[0..k) 是已经去好重的部分。从第二个元素开始扫描:如果当前值与去重部分的最后一个值 nums[k-1] 不同,就是一个新值,写到 nums[k] 并让 k 加一。因为数组有序,重复的值一定连在一起,只需要和去重部分的最后一个比较。Python、Java、Go、JavaScript、C++ 都是真实编译运行的;Java 和 C++ 的 int 数组不能真的"缩短",所以按题目的方式返回 k,由外部取前 k 个元素。

Python · 解法代码def solve(nums): if not nums: return [0, []] k = 1 # nums[0:k] 是已经去好重的部分 for i in range(1, len(nums)): if nums[i] != nums[k - 1]: # 遇到一个新的值:放到去重部分的末尾 nums[k] = nums[i] k += 1 return [k, nums[:k]] # 返回个数,并列出前 k 个元素(题目只检查这一段)
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,1,2]]  →  [2,[1,2]]
[[0,0,1,1,1,2,2,3,3,4]]  →  [5,[0,1,2,3,4]]
[[1]]  →  [1,[1]]
[[]]  →  [0,[]]
[[1,2,3]]  →  [3,[1,2,3]]
[[5,5,5,5]]  →  [1,[5]]
Java · 解法代码class Solution { public int removeDuplicates(int[] nums) { if (nums.length == 0) return 0; int k = 1; // nums[0..k) 是已经去好重的部分 for (int i = 1; i < nums.length; i++) { if (nums[i] != nums[k - 1]) { // 遇到一个新的值:放到去重部分的末尾 nums[k] = nums[i]; k++; } } return k; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,1,2]]  →  [2,[1,2]]
[[0,0,1,1,1,2,2,3,3,4]]  →  [5,[0,1,2,3,4]]
[[1]]  →  [1,[1]]
[[]]  →  [0,[]]
[[1,2,3]]  →  [3,[1,2,3]]
[[5,5,5,5]]  →  [1,[5]]
Go · 解法代码func removeDuplicates(nums []int) int { if len(nums) == 0 { return 0 } k := 1 // nums[0:k] 是已经去好重的部分 for i := 1; i < len(nums); i++ { if nums[i] != nums[k-1] { // 遇到一个新的值:放到去重部分的末尾 nums[k] = nums[i] k++ } } return k }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,1,2]]  →  [2,[1,2]]
[[0,0,1,1,1,2,2,3,3,4]]  →  [5,[0,1,2,3,4]]
[[1]]  →  [1,[1]]
[[]]  →  [0,[]]
[[1,2,3]]  →  [3,[1,2,3]]
[[5,5,5,5]]  →  [1,[5]]
JavaScript · 解法代码function removeDuplicates(nums) { if (nums.length === 0) return 0; let k = 1; // nums[0..k) 是已经去好重的部分 for (let i = 1; i < nums.length; i++) { if (nums[i] !== nums[k - 1]) { // 遇到一个新的值:放到去重部分的末尾 nums[k] = nums[i]; k++; } } return k; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,1,2]]  →  [2,[1,2]]
[[0,0,1,1,1,2,2,3,3,4]]  →  [5,[0,1,2,3,4]]
[[1]]  →  [1,[1]]
[[]]  →  [0,[]]
[[1,2,3]]  →  [3,[1,2,3]]
[[5,5,5,5]]  →  [1,[5]]
C++ · 解法代码#include <vector> using namespace std; class Solution { public: int removeDuplicates(vector<int>& nums) { if (nums.empty()) return 0; int k = 1; // nums[0..k) 是已经去好重的部分 for (int i = 1; i < (int)nums.size(); i++) { if (nums[i] != nums[k - 1]) { // 遇到一个新的值:放到去重部分的末尾 nums[k] = nums[i]; k++; } } return k; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,1,2]]  →  [2,[1,2]]
[[0,0,1,1,1,2,2,3,3,4]]  →  [5,[0,1,2,3,4]]
[[1]]  →  [1,[1]]
[[]]  →  [0,[]]
[[1,2,3]]  →  [3,[1,2,3]]
[[5,5,5,5]]  →  [1,[5]]
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

每遇到重复就删除,代价是什么?

一个直觉的做法是:遇到重复元素就把它从数组里删掉。Python 的 del nums[i] 会把后面所有元素前移一位,每次删除是 O(n),有 n 个重复就是 O(n²)。下面数组里每个值出现 3 次,所以三分之二的元素都要被删除,每个规模取三次中最快的一次:

真实实测 · 本机执行输出
每个值出现 3 次的有序数组:逐个删除重复元素 vs 双指针(每个规模取三次中最快)
n=  3000: 逐个 del     1.083 ms    双指针    0.083 ms    结果一致=True
n= 12000: 逐个 del    15.959 ms    双指针    0.357 ms    结果一致=True
n= 48000: 逐个 del   266.043 ms    双指针    1.377 ms    结果一致=True

n 从 12000 到 48000(4 倍)时,逐个 del 的耗时增长约 16.7 倍,接近平方(16 倍);双指针增长约 3.9 倍,接近线性。n = 48000 时,del 用了 266.0ms,双指针 1.4ms,约 193 倍。这个实验只在 Python 列表上做;其他语言里"删除数组元素"的代价取决于所用的数据结构,本文没有测。

复杂度实测

数组长度分别为 10000、100000、1000000、3000000,每个值出现 3 次,每个规模取三次中最快的一次:

真实实测 · 本机执行输出
数组长度 n(每个值出现 3 次) | 暴力解 | 本文解法
     10000 |     0.145 ms | 0.289 ms
    100000 |     1.533 ms | 2.949 ms
   1000000 |    19.934 ms | 30.316 ms
   3000000 |    72.170 ms | 95.175 ms

本文解法耗时随规模线性增长(从 100 万到 300 万,3 倍规模、耗时增长约 3.1 倍)。表里的"暴力解"是 sorted(set(nums)),它是 C 实现,所以比 Python 里逐元素扫描的双指针更快,这里不能用它来说明双指针慢:两者做的事情不同,sorted(set()) 没有原地修改数组,也没有利用数组已有序这个前提。

随机对拍

Python 版本另外和 sorted(set(nums)) 做了随机对拍,同时比较返回的个数和前 k 个元素:

真实实测 · 本机执行输出
随机对拍 10000 组(长度 0 到 20、取值 -8 到 8 的有序数组(重复值很多);比较返回的个数和前 k 个元素),solve 与 brute 的结果全部一致。

另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。

分步演示:两个指针怎么走

下面的演示用 [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。绿色是已经去好重的部分,琥珀色是刚写入的位置。

删除有序数组中的重复项:[0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
已经去好重的部分 nums[0:k]正在看的 nums[i]刚写入的位置

    参考与说明

    • 官方题目页:LeetCode 26. 删除有序数组中的重复项。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:允许每个值最多出现 2 次的变体(见第 80 题);在其他语言里逐个删除元素的代价。
    京ICP备2026067502号-1京公网安备11010502063487号