JavaScript 数组去重的 6 种实现方式:从 O(n²) 到 O(n) 的进化之路
数组去重是前端面试中的高频题目。本文通过 6 种不同的实现方式,带你从暴力双重循环一路进化到 ES6 的 Set 一行代码,同时深入理解时间复杂度与空间复杂度的权衡。
前言
今天在课程中,老师带我们用 6 种不同的方式 解决了同一道题------数组去重。从最基础的双重循环,到利用数组 API,再到 ES6 的 Set,每种方法都有其独特的思路和适用场景。
更重要的是,通过这道题,我真正理解了时间复杂度 和空间复杂度的概念,以及"空间换时间"的算法思想。
一、编码规范:写好函数的第一步
在开始之前,先聊聊代码规范。老师在课上反复强调:
1.1 注释是代码的一部分
javascript
/**
* @func 数组去重
* @param {Array} arr 数组
* @return {Array} 去重后的数组
* @author hzs
* @date 2026-05-25
*/
function unique(arr) {
// ...
}
代码的开发者和使用者可能不是同一个人,你可能忘记当时为什么这么写。注释会提高代码的可读性,是代码的一部分。
1.2 函数设计三原则
| 原则 | 说明 |
|---|---|
| 一个函数一个功能 | 单一职责,便于维护和测试 |
| 封装复杂功能 | 调用者不需要了解内部实现 |
| 健壮性------校验参数 | 对输入进行类型检查,避免异常 |
1.3 参数校验模板
javascript
function unique(arr) {
// Array.isArray() 是数组的静态方法,无需实例化即可调用
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// ...具体逻辑
}
💡 以下 6 种实现都会包含这个参数校验,后续代码中不再重复说明。
二、方法一:双重循环(暴力法)
思路
维护一个结果数组 res,遍历原数组,对每个元素检查是否已存在于 res 中。
javascript
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++) {
// === 恒等:值相等且类型相等
// 1 === '1' → false(弱类型语言中的严格比较)
if (arr[i] === res[j]) {
flag = false;
break;
}
}
if (flag) {
res.push(arr[i]);
}
}
return res;
}
console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]
复杂度分析
scss
时间复杂度:O(n²)
├── 外层循环 n 次
└── 内层循环最多 n 次
总计:n × n = n²
空间复杂度:O(n)
└── 结果数组 res 最多存储 n 个元素
📌 优点 :思路最直观,适合初学者理解。缺点:性能差,数据量大时明显卡顿。
三、方法二:indexOf 优化
思路
利用 Array.prototype.indexOf() 方法替代内层循环,判断元素是否已存在于结果数组中。
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
const res = [];
for (let i = 0; i < arr.length; i++) {
// indexOf 返回元素第一次出现的索引
// 如果返回 -1,说明 res 中不存在该元素
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i]);
}
}
return res;
}
关键 API
javascript
arr.indexOf(item)
// 返回 item 在 arr 中第一次出现的索引
// 找不到返回 -1
[1, 2, 3, 2].indexOf(2) // 1
[1, 2, 3].indexOf(4) // -1
复杂度分析
scss
时间复杂度:O(n²)
├── 外层循环 n 次
└── indexOf 内部也是一次遍历 O(n)
总计:仍然是 n²
空间复杂度:O(n)
📌 本质上和方法一相同 ,只是用
indexOf替代了手写内层循环,代码更简洁,但时间复杂度没有改善。
四、方法三:filter + indexOf
思路
利用 Array.prototype.filter() 方法,配合 indexOf 进行过滤。
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return arr.filter(function(item, index) {
// 只保留第一次出现的元素
// indexOf 返回第一个索引,如果等于当前 index,说明是第一次出现
return index === arr.indexOf(item);
});
}
console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]
关键 API
javascript
arr.filter(function(item, index) {
// 返回 true → 保留该元素
// 返回 false → 过滤掉该元素
return true | false;
});
工作原理
scss
原数组:[1, 2, 3, 4, 5, 5, 6]
索引: 0 1 2 3 4 5 6
filter 遍历过程:
┌──────┬───────┬────────────────┬────────┐
│ item │ index │ indexOf(item) │ 保留? │
├──────┼───────┼────────────────┼────────┤
│ 1 │ 0 │ 0 │ ✅ │
│ 2 │ 1 │ 1 │ ✅ │
│ 3 │ 2 │ 2 │ ✅ │
│ 4 │ 3 │ 3 │ ✅ │
│ 5 │ 4 │ 4 │ ✅ │
│ 5 │ 5 │ 4 │ ❌ │ ← 第二个 5 被过滤
│ 6 │ 6 │ 6 │ ✅ │
└──────┴───────┴────────────────┴────────┘
复杂度分析
scss
时间复杂度:O(n²)
├── filter 遍历 n 次
└── 每次 indexOf 遍历 O(n)
总计:仍然是 n²
空间复杂度:O(n)
📌 函数式编程风格,代码最简洁优雅,但性能上仍然是 O(n²)。
五、方法四:排序后相邻比较
思路
先对数组排序,然后只需比较相邻元素是否相同。
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// O(n²) → O(nlogn)
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;
}
为什么更快?
css
排序前:[3, 1, 4, 1, 5, 9, 2, 6, 5]
排序后:[1, 1, 2, 3, 4, 5, 5, 6, 9]
↑ ↑
相邻比较即可,无需两两比较
复杂度分析
scss
时间复杂度:O(nlogn)
├── sort() 排序:O(nlogn)
└── 遍历比较:O(n)
总计:O(nlogn) + O(n) = O(nlogn) ← 显著提升!
空间复杂度:O(n)
📌 性能提升明显 ,从 O(n²) 降到 O(nlogn)。但注意:
sort()默认按字符串排序,对数字数组需要传入比较函数arr.sort((a, b) => a - b)。
六、方法五:对象字面量 / HashMap(空间换时间)
思路
利用 JavaScript 对象字面量作为 HashMap,以数组元素为 key,实现 O(1) 的查找。
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
let res = [],
obj = {}; // 对象字面量充当 HashMap
for (let i = 0; i < arr.length; i++) {
// obj[variable] --- 变量作为 key(动态属性访问)
// obj.name --- 常量作为 key(点号访问)
if (!obj[arr[i]]) {
res.push(arr[i]);
obj[arr[i]] = 1; // 标记为已存在
} else {
obj[arr[i]]++; // 记录出现次数
}
}
return res;
}
核心原理
css
对象字面量充当 HashMap
遍历 [1, 2, 3, 2, 4, 3]
Step 1: obj = {}, res = [1] obj[1] = 1
Step 2: obj = {1:1}, res = [1,2] obj[2] = 1
Step 3: obj = {1:1,2:1}, res = [1,2,3] obj[3] = 1
Step 4: obj[2] 已存在!跳过
Step 5: obj = {1:1,2:1,3:1}, res = [1,2,3,4] obj[4] = 1
Step 6: obj[3] 已存在!跳过
结果:[1, 2, 3, 4]
复杂度分析
scss
时间复杂度:O(n)
├── 只需遍历一次数组
└── 对象属性查找是 O(1)
总计:O(n) × O(1) = O(n) ← 最优!
空间复杂度:O(n)
├── 结果数组 O(n)
└── HashMap 对象 O(n)
总计:O(n) ← 用空间换时间
📌 经典的空间换时间策略 。JavaScript 早期没有 HashMap,对象字面量就是最好的替代方案。注意:如果数组元素是对象,需要用
JSON.stringify()转换为字符串作为 key。
七、方法六:ES6 Set(终极方案)
思路
利用 ES6 新增的 Set 数据结构------天生不重复的集合。
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return [...new Set(arr)]; // Set 转换为数组
}
一行代码搞定
javascript
const unique = arr => [...new Set(arr)];
Set 是什么?
scss
Set 的特性
├── 不重复的数据容器
├── 内部使用 HashMap 实现
├── 查找/插入的时间复杂度 O(1)
└── ES6 新增的数据结构
复杂度分析
scss
时间复杂度:O(n)
├── new Set(arr):遍历数组构建 Set,O(n)
└── ...展开运算符:遍历 Set 转数组,O(n)
总计:O(n)
空间复杂度:O(n)
└── Set 容器存储 n 个元素
🎯 生产环境推荐方案:代码最简洁、性能最优、语义最清晰。
八、六种方法全面对比
8.1 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 核心思路 |
|---|---|---|---|
| ① 双重循环 | O(n²) | O(n) | 暴力枚举 |
| ② indexOf | O(n²) | O(n) | API 替代内层循环 |
| ③ filter+indexOf | O(n²) | O(n) | 函数式风格 |
| ④ 排序+相邻比较 | O(nlogn) | O(n) | 先排序降低比较次数 |
| ⑤ 对象字面量/HashMap | O(n) | O(n) | 空间换时间 |
| ⑥ ES6 Set | O(n) | O(n) | 利用 Set 天生去重 |
8.2 复杂度直观感受
scss
执行时间对比(假设 n = 10000)
O(n²) :100,000,000 次操作 😱
O(nlogn) : 132,877 次操作 😊
O(n) : 10,000 次操作 🚀
差距巨大!算法选择直接影响程序性能
8.3 适用场景
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 生产环境 | ⑥ Set | 简洁、高效、现代 |
| 面试手写 | ⑤ HashMap | 展示算法思维 |
| 学习理解 | ①②③④ | 理解基本原理 |
| 大数据量 | ④⑤⑥ | 避免 O(n²) |
| 兼容旧浏览器 | ④⑤ | 不依赖 ES6 |
九、涉及的核心数组 API
速查表
| API | 类型 | 作用 | 示例 |
|---|---|---|---|
Array.isArray() |
静态方法 | 判断是否是数组 | Array.isArray([1]) → true |
arr.indexOf(item) |
实例方法 | 返回首次出现的索引 | [1,2,3].indexOf(2) → 1 |
arr.filter(fn) |
实例方法 | 过滤数组,返回新数组 | arr.filter(x => x > 0) |
arr.sort() |
实例方法 | 排序(原地修改) | arr.sort((a,b) => a-b) |
十、知识图谱
scss
📚 数组去重知识图谱
编码规范
├── JSDoc 注释规范
├── 一个函数一个功能
├── 参数校验(健壮性)
└── === 严格相等
六种实现方式
├── O(n²) 暴力法
│ ├── 双重循环
│ ├── indexOf
│ └── filter + indexOf
│
├── O(nlogn) 排序法
│ └── sort + 相邻比较
│
└── O(n) 哈希法
├── 对象字面量 / HashMap
└── ES6 Set
核心概念
├── 时间复杂度(执行效率)
├── 空间复杂度(内存占用)
├── 空间换时间(算法权衡)
└── HashMap 原理(O(1) 查找)
结语
一道简单的数组去重题,从 O(n²) 到 O(n),从暴力循环到 Set 一行代码,背后是算法思维的进化。
面试中,面试官考的不仅是你能不能写出答案,更是你能否分析不同方案的时间复杂度和空间复杂度 ,能否根据实际场景选择最优方案。
记住这六个方法,理解背后的原理,你就能在面试中游刃有余。
希望这篇文章对你有帮助!如果有任何问题,欢迎在评论区交流。
📌 参考资源
📌 文章标签
JavaScript算法数组去重前端面试时间复杂度学习笔记
如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持一下~你的鼓励是我持续输出的动力! 💪