这是一篇总结文章,涵盖你给出的六种数组去重方法,从原理、复杂度到适用场景逐一分析。
JavaScript 数组去重:从 O(n²) 到一行代码的六种写法
数组去重是前端面试和日常业务中极为高频的需求。看似简单的问题,却能考察对数据结构、算法复杂度和语言特性的理解深度。本文梳理六种去重方法,按时间复杂度的演进顺序展开。
1. 双重循环(暴力法)
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
let flag = true;
for (let j = 0; j < res.length; j++) {
if (arr[i] === res[j]) {
flag = false;
break;
}
}
if (flag) {
res.push(arr[i]);
}
}
return res;
}
原理: 外层遍历原数组,内层遍历已收集的结果数组 res,逐一比对当前元素是否已存在。
复杂度: 时间复杂度 O(n²),空间复杂度 O(n)。
最直观的「人肉」思路,不需要任何 API 知识,但双重循环在数据量大时性能堪忧。优点是兼容性最好,远古浏览器也能跑。
2. indexOf 法
function unique(arr) {
if (!Array.isArray(arr)) {
console.error('输入必须是数组');
return [];
}
let res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i]);
}
}
return res;
}
原理: 用 indexOf 替代内层循环,检查 res 中是否已包含当前元素。
复杂度: 时间复杂度仍是 O(n²)——indexOf 内部本质上是遍历查找;空间复杂度 O(n)。
代码比双重循环简洁不少,但性能上没有本质提升。indexOf 是 ES5 方法,IE9+ 支持。
3. filter + indexOf 法
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return arr.filter(function(item, index) {
return arr.indexOf(item) === index;
});
}
原理: filter 遍历数组,对每个元素取其第一次出现的位置 arr.indexOf(item),只有该位置与当前索引相等时才保留。巧妙之处:重复元素第一次出现时被保留,后续出现时 indexOf(item) !== index 被过滤掉。
复杂度: 时间复杂度 O(n²)(indexOf 在每次回调中都要遍历),空间复杂度 O(n)。
一行链式调用,最「函数式」的写法,代码极其简洁。但性能并不比前两种好,而且不适合对超大数组使用。
4. 排序 + 相邻比较法
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
arr = arr.sort();
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) {
res.push(arr[i]);
}
}
return res;
}
注意: 原代码中
if(arr[i] !== [i-1])有笔误,[i-1]是一个数组字面量而非arr[i-1],上面已修正。
原理: 先排序让相同元素相邻,再一次遍历,相邻元素不相等时才保留。
复杂度: 时间复杂度 O(n log n)(排序的代价),空间复杂度 O(n)。
在去重问题中,O(n log n) 是一个重要的分水岭——比 O(n²) 快了一个数量级。代价是 sort() 改变了原数组,且默认排序按字符串字典序,对数字数组可能出现 [1, 10, 2, 20] 的意外结果。如需数值排序应传入比较函数 arr.sort((a, b) => a - b)。
5. 对象字面量 / HashMap 法(空间换时间)
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
let res = [],
obj = {};
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
res.push(arr[i]);
obj[arr[i]] = 1;
}
}
return res;
}
原理: 用对象的 key 做哈希映射。遍历数组时,以当前元素为 key 去对象中查找——对象 key 查找是 O(1),总复杂度降到 O(n)。
复杂度: 时间复杂度 O(n),空间复杂度 O(n + m)(m 为不重复元素数)。
用额外的内存换取了线性的时间。有两个注意事项:
- 对象 key 只能是字符串或 Symbol,
obj[123]和obj['123']会被视为同一个 key,无法区分数字 123 和字符串'123'。 - 同样,
obj[{a:1}]会变成obj['[object Object]'],无法处理对象元素。
改进方案:用 ES6 的 Map 代替普通对象,Map 的 key 可以是任意类型,能精确区分。
6. Set 法(ES6,终极方案)
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return [...new Set(arr)];
}
原理: Set 是 ES6 新增的数据结构,其核心特性就是值唯一——重复值自动被忽略。内部实现基于类似 HashMap 的机制,插入和查找均为 O(1)。展开运算符 ... 将 Set 还原为数组。
复杂度: 时间复杂度 O(n),空间复杂度 O(n)。
一行代码解决战斗,简洁且性能优秀。唯一的「缺点」是 IE 不支持(但这已经不是缺点了)。对于绝大多数现代项目,这是首选的去重方案。
总结对比
| 方法 | 时间复杂度 | 空间复杂度 | 代码量 | 关键 API |
|---|---|---|---|---|
| 双重循环 | O(n²) | O(n) | 多 | 无 |
| indexOf | O(n²) | O(n) | 中 | indexOf |
| filter + indexOf | O(n²) | O(n) | 少 | filter + indexOf |
| 排序 + 相邻 | O(n log n) | O(n) | 中 | sort |
| HashMap | O(n) | O(n) | 中 | 对象字面量 |
| Set | O(n) | O(n) | 极少 | Set + 展开运算符 |
六种方法,本质上是三条学习曲线:
- 从「能写出来」到「写得简洁」——用 API 替代手动循环(1 → 2 → 3)
- 从「写得简洁」到「写得快」——引入算法思维,用排序降低复杂度(4)
- 从「写得快」到「用对工具」——理解数据结构的力量,用正确工具一行解决(5 → 6)
在日常开发中,直接使用 [...new Set(arr)] 即可。但面试场景往往要求手写兼容实现或考察对复杂度分析的意识,这时掌握 HashMap 法(方法 5)是最佳选择——既展示了对数据结构的理解,又给出了 O(n) 的优雅解。