一、 传统双重循环法:硬核逐个对比

这是最底层的去重逻辑,不依赖任何现代 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。

  • flagbreak 优化: 一旦在内层循环中匹配到相同的元素,将 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 标志位。
  • 特殊暗号 -1indexOf() 如果找到了元素,会返回对应的索引下标;如果找了一圈完全没有找到,则会铁律般地返回 -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) 现代开发标准答案。兼顾极致简炼与顶级性能表现。

本期关于数组去重的六种解法分享到此结束,我们下期再见👋

更多推荐