LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

数组去重,从双重循环到一行 Set,我经历了什么?

zhenglin
2026年6月4日 10:21 本文热度 79

数组去重,从双重循环到一行 Set,我经历了什么?

没错,Set 确实是最简单的方案,但如果你只知道这一种,那可能错过了一整个成长过程。今天我把数组去重的 六种姿势 从头撸到尾,顺便聊聊注释怎么写、API 怎么用、时间/空间复杂度怎么算


一、题目长这样

javascript

输入:[1, 2, 3, 2, 5]

输出:[1, 2, 3, 5]

很基础吧?但不同的写法背后,体现的是不同的思考深度。


二、先聊聊注释 —— 你真的会写吗?

很多人觉得代码写出来就能跑,注释是多余的。但现实是:

  • 代码的开发者和使用者不是同一个人,你写的函数三个月后别人(甚至你自己)可能完全忘记它是干嘛的。


  • 注释是代码的一部分,它不占用运行时间,却能在维护时救你一命。


  • 好的注释能提高代码的可读性,尤其是参数、返回值、作者、日期这些元信息。


来看一个规范的函数注释长什么样:

javascript


/**

 * 数组去重(双重循环版)

 * @param {Array} arr 

 * @returns {Array}

 */

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++) {

            // 使用 === 恒等比较,值相等且类型相等

            if (arr[i] === res[j]) {

                flag = false;

                break;

            }

        }

        if (flag) {

            res.push(arr[i]);

        }

    }

    return res;

}

这不仅仅是形式主义。当你用 IDE 或者文档生成工具时,这些注释可以直接变成 API 文档。而且,一个函数只做一件事(单一职责),复杂功能要封装入口要做参数校验 —— 注释能让这些设计意图更清晰。

后面所有的去重函数,我都会加上参数校验和注释,养成好习惯。



三、方法一:双重循环 —— 最朴素的理解

javascript

/**

 * 数组去重(双重循环版)

 * @param {Array} arr 

 * @returns {Array}

 */

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++) {

            // 使用 === 恒等比较,值相等且类型相等

            if (arr[i] === res[j]) {

                flag = false;

                break;

            }

        }

        if (flag) {

            res.push(arr[i]);

        }

    }

    return res;

}

思路:拿原数组的每一项,去已经存了结果的新数组里逐个比对,没出现过就加进去。
时间复杂度:O(n²) —— 两重循环,数据量大了会明显变慢。
空间复杂度:O(n) —— 需要一个新数组存放结果。

这里用到了 Array.isArray —— 这是一个静态方法,不需要实例化数组就能调用。养成好习惯:所有对外暴露的函数,都要做参数类型校验



四、方法二:indexOf —— 用 API 偷个懒

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 表示不存在

        if (res.indexOf(arr[i]) === -1) {

            res.push(arr[i]);

        }

    }

    return res;

}

思路:不再自己写内层循环,而是用数组的 indexOf 方法判断当前元素在 res 中是否存在。
复杂度:仍然是 O(n²) —— 因为 indexOf 内部也是一次遍历。但代码量减少了一点。
注意indexOf 使用严格相等比较,所以 NaN 的问题依然存在(NaN !== NaNindexOf(NaN) 永远返回 -1,导致 NaN 被重复保留)。



五、方法三:filter —— 声明式的优雅

javascript

function unique(arr) {

    if (!Array.isArray(arr)) {

        console.log('type error');

        return [];

    }

    // filter 过滤:返回新数组,元素是那些满足条件的原数组项

    return arr.filter(function(item, index) {

        // 当前元素在原数组中第一次出现的位置 === 当前索引

        // 满足这个条件的就是首次出现,保留

        return arr.indexOf(item) === index;

    });

}

思路filter 本身会遍历数组,对每个元素执行回调,回调返回 true 就保留,false 就过滤掉。这里利用 arr.indexOf(item) 返回第一次出现的索引,如果和当前索引相等,说明是第一次遇到,应该保留。
复杂度:还是 O(n²) —— filter 遍历 O(n),内部 indexOf 又遍历 O(n)。
优点:代码简洁,可读性高,适合中小数组。
缺点:本质上没解决性能问题。

这里多说一句:filter 是数组的一个常用实例方法,掌握它可以写出更函数式的代码。



六、方法四:排序后相邻比较 —— 换个思路降复杂度

javascript

function unique(arr) {

    if (!Array.isArray(arr)) {

        console.log('type 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;

}

 

思路:先对整个数组排序(默认升序),重复元素就会挨在一起。然后遍历一次,只保留那些和前一元素不同的项。
时间复杂度:排序 O(n log n) + 遍历 O(n) = O(n log n) ,比 O(n²) 快很多。
空间复杂度:O(n)(新数组),如果允许原地修改可以更低。
重要代价会改变原数组的顺序。排序后元素的位置和原来不一样了。如果你的业务要求保持原顺序(比如用户列表按时间排序),这个方法就不能用。
另一个坑:默认排序是把元素转成字符串,所以 [1, 5, 10] 会变成 [1, 10, 5]。稳妥起见,数字数组要传比较函数:arr.sort((a,b) => a - b)



七、方法五:对象哈希 —— 空间换时间

javascript

// O(n) 遍历一次

// 空间换时间 

function unique(arr) {

    if (!Array.isArray(arr)) {

        console.log('type error');

        return [];

    }

    let res = [];

    let obj = {};   // 用对象模拟 HashMap

    for (let i = 0; i < arr.length; i++) {

        // 用当前元素的值作为 key,检查是否已存在

        if (!obj[arr[i]]) {

            res.push(arr[i]);

            obj[arr[i]] = 1;

        } else {

            obj[arr[i]]++;

        }

    }

    return res;

}



思路:利用对象的属性存取是 O(1) 的特性,记录每个值是否出现过。一次遍历,每个元素 O(1) 判断,总体 O(n)。
复杂度:O(n),是目前几种方法里最快的。
代价:多了一个对象 obj,占用了额外内存 —— 这就是典型的空间换时间
严重缺点:对象的 key 只能是字符串(或 Symbol)。数字 1 和字符串 '1' 会被当作同一个 key,导致类型不同的值被错误去重。另外 nullundefinedNaN 也会被转成字符串,产生意外结果。

这个缺点在 ES6 的 Map 中得到完美解决,Map 的 key 可以是任意类型,并且不会隐式转换。



八、方法六:Set —— 真正的降维打击

javascript

// Set 是 ES6 新增的数据结构,内部实现类似 HashMap,O(1) 查重

// Set 中的元素不重复

function unique(arr) {

    if (!Array.isArray(arr)) {

        console.log('type error');

        return [];

    }

    // 将数组转为 Set,再展开回数组

    return [...new Set(arr)];

}

思路Set 是一种不重复的值的集合。把一个数组传给 new Set(arr),自动就完成了去重。再用扩展运算符 ... 把它变回数组。
时间复杂度:O(n) —— Set 内部基于哈希结构,插入和查找都是 O(1)。
空间复杂度:O(n)。
额外优点Set 使用 SameValueZero 比较算法,可以正确区分 NaNNaNNaN 被视为相等,只会保留一个)。
局限性:对于对象数组,Set 去重是基于引用,而不是基于对象内容。如果你有两个内容相同但引用不同的对象,Set 会认为它们不同而保留两个。这时候需要自己写比较逻辑,通常配合 Map 使用。



九、横向对比总结

方法时间复杂度空间复杂度是否保持原顺序特殊值处理(NaN等)代码量
双重循环O(n²)O(n)保持❌ NaN 会重复
indexOfO(n²)O(n)保持❌ NaN 会重复
filter + indexOfO(n²)O(n)保持❌ NaN 会重复
排序后相邻比较O(n log n)O(n)改变⚠️ 取决于排序
对象哈希(对象字面量)O(n)O(n)保持❌ 类型会被转字符串
SetO(n)O(n)保持✅ 完美支持一行

时间复杂度补充说明

  • O(n²) 级别:双重循环、indexOffilter + indexOf


  • O(n log n) 级别:排序 + 相邻比较


  • O(n) 级别:对象哈希(空间换时间)、Set



十、实际工作中怎么选?

  1. 现代浏览器 / Node.js 环境无脑用 Set。一行代码,性能好,无副作用。


  2. 需要兼容 IE 或老环境:可以用 filter + indexOf,或者自己封装一个 Set 的 polyfill。


  3. 数组非常大(几十万以上)且顺序无所谓:排序法也是不错的选择,而且内存占用相对可控。


  4. 数组里包含对象,且希望按引用去重Set 天然支持,因为对象引用是唯一的。


  5. 数组里包含对象,希望按某个属性值去重:用 Map 配合 reducefilter


javascript

// 按 id 去重的常见写法

const uniqueByKey = (arr, key) => [...new Map(arr.map(item => [item[key], item])).values()];


写在最后

从双重循环到 Set,不只是代码行数的减少,更是对数据结构和算法理解的升级。每一行代码背后,都有时间和空间的权衡。我希望这篇文章能帮你彻底搞懂数组去重,也顺便把注释习惯、API 用法、复杂度分析这些基本功一起带上。


阅读原文


该文章在 2026/6/4 10:21:50 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号