面试官冷笑:就这?我淡定:双重循环、indexOf、filter、排序、HashMap、Set,你想听哪个?他沉默了。
数组去重:从暴力到优雅——六种方法彻底拿下面试官
**大厂面试系列 · JavaScript **
· 时间复杂度全覆盖 ·面试高频
目录
1. 题目与背景
数组去重是大厂手写题的经典考点,表面上考的是 API 熟悉度,实质上考察的是算法思维和复杂度意识。面试官真正想听到的不只是"用 Set",而是你能否按复杂度梯度逐步优化,并解释每步优化背后的原理。
题目: 实现一个
unique(arr)函数,对数组[1, 2, 3, 2, 5]去重,返回[1, 2, 3, 5]。请尽可能多地给出不同实现方式。
相关标签: 数组 API 时间复杂度 空间复杂度 ES6 健壮性校验
2. 六种解法逐一拆解
方法一:双重循环(暴力法)
最直观的思路:遍历原数组,每个元素与已收集的结果数组比较,没出现过就加进去。
function unique(arr) {
if (!Array.isArray(arr)) 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;
}
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 两重循环嵌套 |
| 空间复杂度 | O(n) | 结果数组 |
| 适用场景 | 面试讲原理 | 展示基础思维 |
注意点:使用 ===(严格相等) 而非 ==,避免 1 == "1" 这类弱类型陷阱,是写出健壮代码的基本意识。
方法二:indexOf 遍历
利用数组的 indexOf 方法替代内层 for 循环,代码更简洁,逻辑相同,复杂度不变。
function unique(arr) {
if (!Array.isArray(arr)) 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;
}
本质: indexOf 内部仍然是线性扫描,所以外层 O(n) × 内层 O(n) = O(n²),与方法一相同,只是写法更优雅。
方法三:filter + indexOf(函数式写法)
利用 filter 的高阶函数风格,一行表达式完成去重。核心判断:如果某元素第一次出现的下标等于当前下标,说明它还没被记录过。
function unique(arr) {
if (!Array.isArray(arr)) return [];
return arr.filter(function(item, index) {
// 首次出现位置 === 当前位置,则保留
return arr.indexOf(item) === index;
});
}
filter 回调返回 true 则保留元素,返回 false 则丢弃。该写法简洁,但 indexOf 依然是 O(n) 内部扫描,整体时间复杂度仍为 O(n²)。
方法四:先排序再比较相邻
先调用 sort() 将数组排好序,重复元素必然相邻,只需一次线性遍历比较相邻项即可。
function unique(arr) {
if (!Array.isArray(arr)) return [];
arr = arr.sort(); // O(nlogn)
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) { // 相邻不同则保留
res.push(arr[i]);
}
}
return res;
}
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 排序主导 |
| 缺点 | 改变顺序 | 原数组顺序丢失 |
优化明显:从 O(n²) 降到 O(n log n)。代价是排序会改变元素原始顺序,面试时要主动说出这个 trade-off。
方法五:对象 HashMap(空间换时间)
用 JS 对象(Object 字面量)模拟 HashMap,以元素值作为 key,利用对象属性读写的 O(1) 平均时间,将整体复杂度降到线性。
// 空间换时间:额外 O(n) 空间,换取 O(n) 时间
function unique(arr) {
if (!Array.isArray(arr)) return [];
let res = [],
obj = {}; // 对象字面量充当 HashMap
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) { // key 不存在 → 新元素
res.push(arr[i]);
obj[arr[i]] = 1;
} else {
obj[arr[i]]++; // 统计出现次数(可选)
}
}
return res;
}
注意坑点: 对象的 key 会被自动转为字符串,
obj[1]和obj["1"]指向同一个 key,导致数字1和字符串"1"会被误判为重复。面试中需主动指出。
方法六:ES6 Set(最优解)
Set 是 ES6 引入的内置数据结构,天然不存储重复值,底层基于 HashMap,增删查均为 O(1),配合扩展运算符一行搞定。
function unique(arr) {
if (!Array.isArray(arr)) return [];
return [...new Set(arr)];
// 等价写法:Array.from(new Set(arr))
}
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | HashMap O(1) 查找 |
| 代码量 | 1 行 | 极致简洁 |
| 类型区分 | ✓ 正确 | 1 和 “1” 不重复 |
Set 正确区分 1 与 "1",解决了方法五的类型陷阱,是生产代码的首选。
3. 复杂度对比总表
| 方法 | 时间复杂度 | 空间复杂度 | 保留顺序 | 类型安全 | 推荐度 |
|---|---|---|---|---|---|
| 双重循环 | O(n²) | O(n) | ✓ | ✓ | ★☆☆☆☆ |
| indexOf 遍历 | O(n²) | O(n) | ✓ | ✓ | ★★☆☆☆ |
| filter + indexOf | O(n²) | O(1) | ✓ | ✓ | ★★☆☆☆ |
| 排序 + 相邻比较 | O(n log n) | O(n) | ✗ | ✓ | ★★★☆☆ |
| 对象 HashMap | O(n) | O(n) | ✓ | ✗ | ★★★☆☆ |
| ES6 Set | O(n) | O(n) | ✓ | ✓ | ★★★★★ |
4. 面试答题策略
大厂面试考这道题,不是让你背答案,而是看你能不能主动引出优化对话。建议按以下节奏作答:
第一步: 先给出暴力双重循环,说明思路直观、复杂度 O(n²)。
第二步: 主动说"能优化吗?",引出 sort + 相邻比较,降到 O(n log n),但指出顺序丢失的问题。
第三步: 提出 HashMap 思路,说明"空间换时间",降到 O(n),但指出 key 类型问题。
第四步: 最后给出 ES6 Set 作为最优解,总结它完美解决了类型安全和时间复杂度问题。
这个递进式的回答方式会让面试官看到你的算法演进思维,而不只是背题。
5. 代码健壮性要点
每道大厂手写题都暗藏对代码健壮性的考察,以下是数组去重必须注意的细节:
// 1. 入参类型校验
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// 2. 严格相等替代宽松相等
arr[i] === res[j] // ✓ 类型+值都相等
arr[i] == res[j] // ✗ 1 == "1" 会是 true
// 3. 使用 obj[variable] 而非 obj.variable
// .name 是访问常量键,obj[arr[i]] 才是动态键访问
obj[arr[i]] = 1; // ✓
obj.arr[i] = 1; // ✗
注释也是代码质量的体现。工程师写的代码不只给机器运行,也给团队其他人阅读。一个函数一个功能,加上清晰的参数说明,体现出良好的工程素养。
整理自学习笔记 · 2026-05-27
JavaScript 算法 大厂面试 ES6
更多推荐




所有评论(0)