给定一个升序数组,原地删除重复元素,使每个值只出现一次,返回去重之后的长度 k;数组前 k 个位置放去重后的元素,后面的内容不重要。核心是双指针:一个指针 i 扫描整个数组,一个指针 k 记录去重部分的末尾。本文实测原地写入的双指针与"遇到重复就 del 掉"的做法的真实差距,给出五种语言的实现,并把双指针的移动做成分步演示。
完整题目见官方页面(链接在文末)。评测只检查返回的 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 个元素(题目只检查这一段)
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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 的返回值一致。绿色是已经去好重的部分,琥珀色是刚写入的位置。