JavaScript 数组去重全家桶:从 O(n^2) 到 O(n) 的 6 种解法
JavaScript 数组去重六种讲解
一、 传统双重循环法:硬核逐个对比
这是最底层的去重逻辑,不依赖任何现代 API。通过两层 for 循环,拿老数组的每一项去跟新数组比对。
算法代码:
function unique(arr){
// 1. 参数校验:利用 Array 的静态方法判断是否为合法数组
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 初始化结果数组,默认放入第一个元素
let res = [arr[0]];
// 3. 外层循环:遍历原数组(从第2个元素开始)
for(int i = 1; i < arr.length; i++)
{
let flag = true; // 标志位:默认认为不重复
// 4. 内层循环:与新数组 res 里的元素逐个作全等对比
for(int j = 0; j < res.length; j++)
{
// 🌟 避坑:必须用 ===(值和类型皆相等),防止 1 == '1' 误判
if(arr[i] === res[j])
{
flag = false; // 抓到重复
break; // 立即跳出内层循环,节省算力
}
}
// 5. 若未发生重复,塞入新数组
if(flag){
res.push(arr[i]);
}
}
return res;
}
核心原理解析
-
静态方法校验:
Array.isArray(arr)是挂载在构造函数对象上的静态方法,无需new实例化即可直接调用,用于精准拦截非数组类型的错误输入。 -
全等运算符(===): JavaScript 是弱类型语言。使用 === 能保证数值和数据类型双重匹配,完美避免了数字 1 和字符串 ‘1’ 因为隐式类型转换而被判定为重复的 Bug。
-
flag与break优化: 一旦在内层循环中匹配到相同的元素,将flag置为false并立即执行break。这阻止了程序继续对剩余格子进行无意义的扫描,直接斩断不必要的算力消耗。
二、 现代 API 法:利用 indexOf 精简重构
利用 JavaScript 数组内置的 indexOf 方法,可以直接代替繁琐的内层循环。代码更少,阅读起来更直观。
算法代码
function unique(arr){
// 1. 参数校验
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 初始化空结果数组
const res = [];
// 3. 单层循环:遍历原数组的每一个元素
for(let i = 0; i < arr.length; i++)
{
// 🌟 核心:用 indexOf 检查当前元素是否已存在于新数组 res 中
// 如果返回 -1,说明 res 里面没有这个元素
if(res.indexOf(arr[i]) === -1){
res.push(arr[i]); // 放入新数组
}
}
return res;
}
核心原理解析内置
- API 代替内层循环:
res.indexOf(arr[i])的作用是在新数组res中从左往右查找指定的元素。它完美取代了第一种方法中手写的内层for循环和flag标志位。 - 特殊暗号 -1:
indexOf()如果找到了元素,会返回对应的索引下标;如果找了一圈完全没有找到,则会铁律般地返回-1。因此=== -1就是判断元素“不存在”的最直接手段。 - 性能本质未变: 虽然代码少了一层嵌套循环,但
indexOf在 JS 底层依然是通过遍历实现的。所以该算法的实际时间复杂度依然是 O ( n 2 ) O(n^2) O(n2)。
额外补充:利用 includes 查重法
function unique(arr){
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
const res = [];
// 🌟 核心:用 forEach 代替 for 循环,用 includes 代替 indexOf
arr.forEach(item => {
// 如果新数组 res 里面“不包含”当前项
if (!res.includes(item)) {
res.push(item); // 塞入新数组
}
});
return res;
}
唯一的区别(语义更现代化):includes() 是 ES6 后面才新增的 API。相比于老旧的 indexOf 还要去判断是否等于 -1,includes() 直接返回 true(包含)或 false(不包含),代码读起来更像是在读一句英文,语义化更完美。
三、 高阶现代法:利用 filter + indexOf 终极单行流
通过组合使用 filter(过滤)和 indexOf(查下标)这两个高阶 API,可以省去手动创建新数组和写 for 循环的步骤,直接一行代码搞定去重。
算法代码
function unique(arr){
// 1. 参数校验
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 🌟 核心:利用 filter 过滤出符合条件的元素
return arr.filter(function(item, index){
// 判断当前元素的实际第一次出现位置,是否等于它现在的遍历下标
return arr.indexOf(item) === index;
});
}
核心原理解析
-
filter 的筛选机制: filter 会遍历原数组,为每个元素执行一次回调函数。如果回调函数返回 true,该元素就会被保留入新数组;返回 false 则被过滤掉。
-
下标对齐去重核心: * index 代表当前元素在遍历时的真实位置。
-
arr.indexOf(item) 永远只会返回该元素在整个数组中第一次出现的下标。
-
结论: 如果一个元素重复出现了(比如在下标 3 的位置再次出现),indexOf 算出来的结果依然是它第一次出现的位置(比如 1)。此时 1 === 3 为 false,该重复项就会被 filter 直接剔除。
-
四、 排序相邻对比法:巧用排序化繁为简
先将原数组进行排序,使重复的元素在空间上相邻。随后只需遍历一次数组,通过对比相邻元素即可完成去重。
算法代码
function unique(arr){
// 1. 参数校验
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 🌟 核心:先将数组排序,让相同的元素紧紧挨在一起
arr = arr.sort();
// 3. 初始化结果数组,放入排序后的第一个元素
let res = [arr[0]];
// 4. 单层循环:只需对比当前元素和前一个元素
for(let i = 1; i < arr.length; i++)
{
// 如果当前元素不等于前一个元素,说明是一个新的唯一值
if(arr[i] !== arr[i - 1]){
res.push(arr[i]); // 塞入新数组
}
}
return res;
}
核心原理解析
- 空间换思维: 通过
arr.sort()排序后,重复的元素(如多个 5)会被强制排列在一起。这就免去了像前几种方法一样去整个新数组里“翻箱倒柜”找重复项的麻烦。 - 相邻对比逻辑: 循环内使用
arr[i] !== arr[i - 1]判定。只要当前项和它的前一项对不上,就证明它是一个全新出现的安全元素。 - 性能跃升: 该算法只用了一层
for 循环(复杂度为 O ( n ) O(n) O(n)),但因为前面调用了sort(),而 JS 的底层排序算法(如快排/V8的Timsort)平均时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)。因此,该去重算法的整体时间复杂度成功从 O ( n 2 ) O(n^2) O(n2) 降到了 O ( n log n ) O(n \log n) O(nlogn),在处理大数据量时效率明显高于前几种方法。
五、 对象键值对法:空间换时间的终极性能
利用对象属性名(Key)的唯一性来当做哈希表。遍历数组时,把元素直接存成对象的属性,如果下次还能读到这个属性,说明重复了。
算法代码
function unique(arr){
// 1. 参数校验
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 🌟 核心:利用对象字面量做“哈希表”
let obj = {};
let res = [];
for(let i = 0; i < arr.length; i++)
{
// 3. 动态读取当前项:把数组的值 arr[i] 作为对象的变量 Key
if(!obj[arr[i]]){
// 如果对象里没有这个 Key,说明是第一次见
res.push(arr[i]); // 塞入结果数组
obj[arr[i]] = true; // 并在对象里打个标记,下次别放过它
}
}
return res;
}
核心原理解析
- 哈希表思想: 早期 JavaScript 为了快速存取数据,常把对象字面量
let obj = {}当作 HashMap 使用。对象的属性名(Key)在底层是唯一的,天然具备排他性。 - 变量作 Key 的语法: 代码中使用
obj[arr[i]](中括号语法),是因为 arr[i] 是一个动态变化的变量。如果用点语法(如 obj.arr[i]),电脑会去死板地查找名为 “arr” 的常量属性,直接引发错误。 - O ( n ) O(n) O(n) 降维打击: 无论你在对象里存了多少个
Key,电脑去查obj[arr[i]]是否存在的时间复杂度都是惊人的 O ( 1 ) O(1) O(1)(一步到位,不需要遍历)。所以整个算法只需一层for 循环,整体时间复杂度降到了 O ( n ) O(n) O(n)。
六、 现代 ES6 法:利用 Set 容器极简去重
Set 是 ES6 新增的一种数据结构。它类似于数组,但其成员的值都是唯一样本,没有重复的值。利用它我们可以实现真正的“一行代码去重”。
算法代码
function unique(arr){
// 1. 参数校验
if(!Array.isArray(arr)){
console.log('type error');
return [];
}
// 2. 🌟 核心:new Set(arr) 把数组丢进不重复容器去重
// 再用 [... ] 展开运算符,把 Set 容器打散,重新组装回真正的数组
return [...new Set(arr)];
}
核心原理解析
- 不重复的数据容器:
Set底层实现了类似HashMap的排他机制。当你执行new Set(arr)时,它会自动扫描数组,并在 O ( 1 ) O(1) O(1) 的单次查询时间复杂度内踢出所有重复值,只留下一套干净的“非重复成员容器”。 - 解构语法还原: 直接返回
new Set(arr)得到的是一个类似 {1, 2, 3} 的 Set 对象,无法使用数组的 API。所以必须通过 […Set](展开运算符) 或者Array.from(new Set(arr))把它重新伪装并释放回普通的 […] 数组格式。 - 双料王者: 这个方法在代码简洁度和执行性能上都做到了极致。它省去了所有手动循环,且整体时间复杂度与对象键值对法一样,达到了完美的 O ( n ) O(n) O(n),是现代前端开发的标准答案。
性能总结
| 序号 | 算法大类 | 具体方法 | 时间复杂度 | 空间复杂度 | 优缺点一句话死记 |
|---|---|---|---|---|---|
| 1 | 传统循环 | ① 双重循环对比 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 性能最差,不依赖现代 API,全靠底层硬编码实现。 |
| 2 | 现代 API | ② indexOf / includes 查重 |
O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 换汤不换药。includes 语义化更好,但底层依然在遍历。 |
| 3 | 高阶组合 | ③ filter + indexOf |
O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 终极单行流,代码最精简,但大数集下依然较慢。 |
| 4 | 巧用排序 | ④ sort 相邻对比 |
O ( n log n ) O(n \log n) O(nlogn) | O ( n ) O(n) O(n) | 性能大幅跃升,但会破坏原数组的初始元素顺序。 |
| 5 | 哈希思想 | ⑤ 对象键值对 (HashMap) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | 速度极快,利用 O ( 1 ) O(1) O(1) 查找,老版本 JS 的性能王者。 |
| 6 | 现代顶级 | ⑥ 现代 ES6 Set |
O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | 现代开发标准答案。兼顾极致简炼与顶级性能表现。 |
本期关于数组去重的六种解法分享到此结束,我们下期再见👋
更多推荐

所有评论(0)