引言/概述
数组去重是前端开发中常见的基础问题,也是面试中高频出现的手写代码题目。 本学习笔记系统地探讨了多种数组去重的实现方式,从基础的双重循环到现代的 Set 数据结构,覆盖了不同时间复杂度和空间复杂度的解决方案。 通过理论与实践相结合的方式,深入理解算法优化的核心思想。
核心概念与理论要点
1. 数组去重的定义与应用场景
数组去重是指从包含重复元素的数组中提取出唯一元素的过程。在实际业务中,数组去重常用于:
- 数据清洗与预处理
- 避免重复操作(如避免重复请求、重复渲染)
- 统计分析中的去重计数
2. 关键数组 API
| API | 功能描述 | 返回值 |
|---|---|---|
Array.isArray(arr) |
检查参数是否为数组 | Boolean |
arr.indexOf(item) |
返回元素首次出现的下标 | Number(-1表示不存在) |
arr.lastIndexOf(item) |
返回元素最后出现的下标 | Number(-1表示不存在) |
arr.filter(callback) |
过滤数组元素 | 新数组(包含返回true的元素) |
arr.sort() |
数组排序(默认字典序) | 原数组(会修改原数组) |
arr.reverse() |
反转数组 | 原数组 |
3. 复杂度分析理论
算法的性能通常用时间复杂度和空间复杂度来衡量:
- O(n²):双重循环结构,外层遍历每个元素,内层检查是否重复
- O(n log n):先排序(O(n log n))再去重(O(n)),总体由排序主导
- O(n):空间换时间策略,利用哈希表/对象实现 O(1) 查找
代码实现与分析
方法一:双重循环法(O(n²))
js
function unique(arr) {
if (!Array.isArray(arr)) {
console.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;
}
实现思路:维护一个结果数组,对每个新元素遍历结果数组检查是否重复。
复杂度:时间 O(n²),空间 O(n)(最坏情况)
方法二:indexOf 方法(O(n²))
js
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 方法替代内层循环,代码更简洁,但本质仍是 O(n²)。
方法三:filter + indexOf(O(n²))
js
function unique(arr) {
if (!Array.isArray(arr)) {
console.error('输入必须是数组');
return [];
}
return arr.filter(function(item, index) {
return arr.indexOf(item) === index;
});
}
实现思路 :利用 filter 的过滤特性,只保留首次出现的元素(indexOf 返回的索引等于当前索引)。
方法四:排序后去重(O(n log n))
js
function unique(arr) {
if (!Array.isArray(arr)) {
console.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;
}
实现思路:先排序使重复元素相邻,再遍历比较相邻元素。
注意 :sort() 默认按字典序排序,对数字排序需额外处理。
方法五:哈希表法(O(n))
js
function unique(arr) {
if (!Array.isArray(arr)) {
console.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;
} else {
obj[arr[i]]++;
}
}
return res;
}
实现思路:利用对象字面量作为哈希表,将数组元素作为 key,实现 O(1) 时间复杂度的存在性检查。
复杂度:时间 O(n),空间 O(n)
方法六:Set 数据结构(O(n))
js
function unique(arr) {
if (!Array.isArray(arr)) {
console.error('输入必须是数组');
return [];
}
return [...new Set(arr)];
}
实现思路 :ES6 新增的 Set 数据结构天然不允许重复值,配合扩展运算符 ... 转换回数组。
复杂度:时间 O(n),空间 O(n)
关键发现与心得
1. 时间复杂度的递进关系
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 双重循环 | O(n²) | O(n) | 基础易懂,性能较差 |
| indexOf | O(n²) | O(n) | 代码简洁,本质仍是双重循环 |
| filter+indexOf | O(n²) | O(n) | 函数式编程风格 |
| sort+去重 | O(n log n) | O(n) | 排序后比较,性能提升 |
| 哈希表 | O(n) | O(n) | 空间换时间,最优时间复杂度 |
| Set | O(n) | O(n) | ES6 语法糖,简洁高效 |
2. 参数校验的重要性
所有实现都包含了 Array.isArray() 检查,体现了代码健壮性原则:
- 防止非数组输入导致运行时错误
- 提供友好的错误提示
- 返回空数组作为安全默认值
3. 空间换时间的思想
哈希表法和 Set 方法通过额外的空间开销(O(n))换取了时间复杂度的优化(O(n)),这是算法设计中的经典权衡策略。
4. ES6 特性的优势
Set 数据结构的引入极大简化了去重逻辑,一行代码即可完成,体现了语言进化带来的开发效率提升。
总结与展望
学习总结
通过系统学习数组去重的多种实现方式,我们掌握了:
- 基础算法思想:双重循环、排序比较、哈希查找
- 复杂度分析方法:时间复杂度与空间复杂度的权衡
- 代码健壮性原则:参数校验、错误处理
- 语言特性应用:ES6 Set、扩展运算符、高阶函数
实践建议
在实际开发中选择去重方法时,可参考以下原则:
- 小规模数据 :任意方法均可,推荐使用
Set保持代码简洁 - 大规模数据:优先选择 O(n) 复杂度的哈希表法或 Set 方法
- 需要保持原顺序:避免使用排序法,选择哈希表或 Set
- 兼容性要求:ES5 环境使用哈希表法,ES6+ 环境使用 Set
未来扩展
数组去重问题可以进一步扩展:
- 对象数组去重:根据对象属性进行去重
- 复杂数据结构:处理嵌套数组、函数等特殊类型
- 性能优化:针对特定数据分布优化算法