马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
JavaScript 数组去重的 20 种实现方式,用差异思绪办理标题
数组去重是最常见的算法。看似简单,但差异实现方式的性能差异大概高达几百倍。本文整理 JavaScript 数组去重的 20 种写法,按 5 个战略分类,充实利用JavaScript的弱范例和动态性,资助你明白语言特性,同时把握多种办理标题的的思绪。AI期间,你须要知道代码背后的原理,如许才气更好地引导AI编程。
为什么性能差异这么大?
最简单的写法,新建一个数组,把不在结果里的添加进去。- function unique(arr) {
- const result = []
- for (const item of arr) {
- // 遍历原数组,逐个取出元素判断是否存在新数组,若新数组中不存在则添加
- if (!result.includes(item)) {
- result.push(item)
- }
- }
- return result
- }
复制代码 标题在于每次 includes 都要全量扫一遍 result,复杂度是 O(n²)。
优化思绪:换一种判重方式
- Set / Map O(1) 查询:new Set(arr)
- 排序 O(n log n):雷同元素相邻后扫一遍
- filter + 闭包:在函数式管道里携带"已见"状态
- JSON 序列化:处理处罚对象、嵌套数组等不可哈希元素
- 递归:换种表达方式,本质还是上面的思绪
本文源码所在:https://github.com/microwind/algorithms
保举方案
需求代码性能保序一行最简[...new Set(arr)]O(n)✓函数式风格arr.filter((x,i,a) => a.indexOf(x)===i)O(n²)✓函数式 + Setarr.filter(x => !seen.has(x) && seen.add(x))O(n)✓要排序[...new Set(arr)].sort((a,b)=>a-b)O(n log n)排序对象数组JSON.stringify 作为 Set 的键O(n×m)✓第1类:根本循环(方法1-6)
战略原理:不消任何内置数组方法,纯靠下标、嵌套循环、indexOf 这种"原始"本领完成去重。每一步判重都是 O(n),团体 O(n²)。
实用场景:教学、口试手撕。生产代码不发起利用。
%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%graph LR A([原数组]) --> B[取下一个元素] B --> C{遍历结果数组
是否已存在?} C -->|否| D[push 追加] C -->|是| E[跳过] D --> F([继承]) E --> F F --> B classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px classDef step fill:#3A86FF,color:#fff,stroke:#2b63c4,stroke-width:2px classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px class A,F start class B,D,E step class C check- // 方法1:双循环索引比较——i 与左侧每个 j 比对
- function unique1(arr) {
- const result = []
- for (let i = 0, l = arr.length; i < l; i++) {
- for (let j = 0; j <= i; j++) {
- if (arr[i] === arr[j]) {
- // i === j 表示前面没有相同值,当前项是首次出现,追加到新数组中
- if (i === j) result.push(arr[i])
- // 只要遇到相同值,要么刚添加了,要么前面已添加,可跳出循环
- break
- }
- }
- }
- return result
- }
- // 方法2:新建数组 + includes 检查
- function unique2(arr) {
- const result = []
- for (const item of arr) {
- // 不存在新数组就添加,利用includes判断
- if (!result.includes(item)) result.push(item)
- }
- return result
- }
- // 方法3:从后往前原地 splice
- function unique3(arr) {
- let l = arr.length
- while (l-- > 0) {
- for (let i = 0; i < l; i++) {
- // 将当前项逐个与前面项比较,若遇到重复就删除当前项,原数组操作
- if (arr[l] === arr[i]) {
- arr.splice(l, 1)
- break
- }
- }
- }
- return arr
- }
- // 方法4:从前往后原地 splice(删后面相同项)
- function unique4(arr) {
- let l = arr.length
- for (let i = 0; i < l; i++) {
- for (let j = i + 1; j < l; j++) {
- // 将当前项逐个与后面项比较,若遇到重复就删除重复项,原数组操作
- if (arr[i] === arr[j]) {
- arr.splice(j, 1)
- // 因为自前向后遍历,删除重复项后,需要将下标和总长度各自减1位
- j--; l--
- }
- }
- }
- return arr
- }
- // 方法5:forEach + indexOf
- // indexOf 返回首次出现下标,等于当前下标即首次
- function unique5(arr) {
- const result = []
- arr.forEach((item, i) => {
- // 与unique1思路一致,利用indexOf实现查找
- if (arr.indexOf(item) === i) result.push(item)
- })
- return result
- }
- // 方法6:双重 while 倒序 splice
- // 与 unique3 同类:自尾向前,当前尾元素若在前段出现过则删掉该尾元素
- function unique6(arr) {
- let l = arr.length
- while (l-- > 0) {
- let i = l
- while (i-- > 0) {
- // 与左侧某项相等说明重复,删掉当前下标 l 的元素后跳出内层
- if (arr[l] === arr[i]) {
- arr.splice(l, 1)
- break
- }
- }
- }
- return arr
- }
复制代码 第2类:内置数组方法(方法7-11)
战略原理:JavaScript 数组自带 filter、reduce、forEach 等高阶方法,可以把"判重 + 网络"写成函数式风格。注意indexOf / includes 还是 O(n),须要用 Set 闭包才气压到 O(n)。
实用场景:当代 JS 工程的常态写法。可读性高,链式组合方便。
%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%graph LR A([原数组]) --> B[filter / reduce 管道] B --> C{选择战略} C -->|indexOf 判重| D["O(n²) 但轻便"] C -->|Set 闭包判重| E["O(n) 保举"] C -->|对象键| F["O(n) 但有范例陷阱"] D --> G([新数组]) E --> G F --> G classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px classDef step fill:#0F3460,color:#fff,stroke:#0a2647,stroke-width:2px classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px class A,G start class B,D,E,F step class C check- // 方法7:filter + indexOf 一行经典
- // 写法最短,但每次 indexOf 都是 O(n)
- function unique7(arr) {
- // indexOf 得首次下标,与当前 i 相同才保留,否则是后面出现的重复项
- return arr.filter((item, i) => arr.indexOf(item) === i)
- }
- // 方法8:filter + Set 闭包——推荐写法
- // Set.add 返回 Set 自身,结合短路 && 返回布尔值
- function unique8(arr) {
- const seen = new Set()
- // 未见过才执行 add;add 恒为真值,整体表达式作 filter 谓词
- return arr.filter(item => !seen.has(item) && seen.add(item))
- }
- // 方法9:reduce 累加(用数组)
- // 函数式风格,但 includes 仍是 O(n²)
- function unique9(arr) {
- return arr.reduce((acc, item) => {
- // 与 unique2 同思路,只是用 reduce 折叠出结果数组
- if (!acc.includes(item)) acc.push(item)
- return acc
- }, [])
- }
- // 方法10:reduce + Set 闭包——O(n) 函数式
- function unique10(arr) {
- const seen = new Set()
- return arr.reduce((acc, item) => {
- // Set 判重 O(1),首次出现才同步记入 acc
- if (!seen.has(item)) {
- seen.add(item)
- acc.push(item)
- }
- return acc
- }, [])
- }
- // 方法11:Object + typeof 键
- // 用 typeof + value 拼成字符串作为对象键,避免 1 与 '1' 冲突
- function unique11(arr) {
- const obj = {}
- return arr.filter(item => {
- const key = typeof item + item
- // hasOwnProperty 防原型链上同名属性;赋值表达式求值为 true,表示首次保留
- return Object.prototype.hasOwnProperty.call(obj, key)
- ? false
- : (obj[key] = true)
- })
- }
复制代码 第3类:聚集容器(方法12-14)
战略原理:ES6 引入的 Set 与 Map 用 SameValueZero 算法判等,键唯一且 O(1),是 JS 里最天然的去重工具。Object 字面量固然也能当哈希用,但有"键自动字符串化""数字键被引擎重排"等坑。
实用场景:一样平常项目首选 Set;须要保存 value 选 Map;只在小数据或特别兼容场景才用 Object。
%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%graph LR A([原数组]) --> B{选择容器} B -->|Set| C[键唯一
保持插入序次] B -->|Map| D[键唯一
值可携带额外信息] B -->|Object| E[键自动字符串化
有重排陷阱] C --> F([转回数组]) D --> F E --> F classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px classDef step fill:#8338EC,color:#fff,stroke:#5e27a8,stroke-width:2px classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px class A,F start class C,D,E step class B check- // 方法12:new Set 转数组——一行经典
- // Set 用 SameValueZero 比较,NaN 也能正确去重
- function unique12(arr) {
- // 展开成数组,迭代顺序与元素首次插入 Set 的顺序一致(保序)
- return [...new Set(arr)]
- }
- // 方法13:Map.set + keys
- // 适合"按键去重,值携带其他信息"的场景
- function unique13(arr) {
- const map = new Map()
- // 键重复时覆盖值,keys 迭代顺序仍为各键首次插入顺序
- arr.forEach(item => map.set(item, item))
- return [...map.keys()]
- }
- // 方法14:Object 字面量哈希
- // 注意:1 与 '1' 会被合并;数字键会被引擎按升序重排
- // [1, 'a', 2, 'b', -1] 会变成 [1, 2, 'a', 'b', -1]
- function unique14(arr) {
- const obj = {}
- // 属性名会转成字符串,对象等引用类型键易撞成 '[object Object]'
- for (const item of arr) obj[item] = item
- return Object.values(obj)
- }
复制代码警惕 Object 的两个坑:① 数字字符串键会被 V8/SpiderMonkey 重排到前面(升序);② 引用范例(对象、数组)会酿成 [object Object] 之类的字符串,全部归并成一个键。生产代码不要用 Object 当 Set。
第4类:排序后去重(方法15-17)
战略原理:先 sort 让雷同元素相邻,再扫一遍删除相邻雷同项。复杂度由排序决定,O(n log n)。长处是不须要额外的哈希布局,"相邻判等"是最自制的判重方式;缺点是会粉碎原序次。
实用场景:输出本就须要排序、不在意原序次。
%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%graph LR A([原数组]) --> B[sort
雷同元素相邻] B --> C{相邻是否雷同?} C -->|是| D[splice/skip] C -->|否| E[保存] D --> F([结果]) E --> F classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px classDef step fill:#FF6B6B,color:#fff,stroke:#cc4444,stroke-width:2px classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px class A,F start class B,D,E step class C check- // 方法15:sort + splice 升序去重
- // 注意 sort 不传比较函数会按字符串排序,数字数组要传 (a, b) => a - b
- function unique15(arr) {
- arr.sort((a, b) => a - b)
- let l = arr.length
- while (l-- > 1) {
- // 排序后相等必相邻,删后一重复项;从后往前 splice 不影响已扫下标
- if (arr[l] === arr[l - 1]) arr.splice(l, 1)
- }
- return arr
- }
- // 方法16:sort + filter 相邻判重
- function unique16(arr) {
- arr.sort((a, b) => a - b)
- // 首元素必留;之后仅当与前一项不同才保留,即每段相同值只留第一个
- return arr.filter((item, i) => i === 0 || item !== arr[i - 1])
- }
- // 方法17:经典双指针(LeetCode 26)
- // 排序后原地双指针,O(1) 额外空间
- function unique17(arr) {
- if (arr.length === 0) return arr
- arr.sort((a, b) => a - b)
- let slow = 0
- for (let fast = 1; fast < arr.length; fast++) {
- // fast 遇到新值则扩展「唯一前缀」,写入 slow+1 位置
- if (arr[fast] !== arr[slow]) arr[++slow] = arr[fast]
- }
- // 前缀长度为 slow+1,截掉尾部重复占位
- return arr.slice(0, slow + 1)
- }
复制代码 第5类:递归与特别(方法18-20)
战略原理:递归用自调用替换循环,是函数式头脑的表现,重要用于教学。JSON.stringify 把对象映射为字符串,是 JS 里处理处罚"不可哈希元素"(对象数组、嵌套数组)的常见招数。
实用场景:递归——教学;JSON——对象数组按团体布局去重。
%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 25, 'padding': 8}}}%%graph LR A([数组 length=n]) --> B{length |是| C([返回]) B -->|否| D[查抄末端元素
是否在前面出现] D --> E{重复?} E -->|是| F[扬弃末端] E -->|否| G[保存末端] F --> H[递归 length-1] G --> H H --> A classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a,stroke-width:2px classDef step fill:#118AB2,color:#fff,stroke:#0b5f7a,stroke-width:2px classDef check fill:#FFB703,color:#000,stroke:#cc8c00,stroke-width:2px class A,C start class D,F,G,H step class B,E check- // 方法18:递归原地删除
- // 先看当前「末尾」是否在前缀中出现过,重复则 splice 掉末尾
- function unique18(arr, length) {
- if (length <= 1) return arr
- const last = length - 1
- for (let i = last - 1; i >= 0; i--) {
- // 与前段某项相同则末尾是重复,删掉后不再比较
- if (arr[last] === arr[i]) {
- arr.splice(last, 1)
- break
- }
- }
- // 本层已处理原末尾,子问题规模减一(与是否 splice 无关)
- return unique18(arr, length - 1)
- }
- // 方法19:递归拼接返回(不修改原数组)
- // 每层只决定「当前末尾项」是否并入结果,前缀由递归算好
- function unique19(arr, length) {
- if (length <= 1) return arr.slice(0, length)
- const last = length - 1
- const lastItem = arr[last]
- let isRepeat = false
- for (let i = last - 1; i >= 0; i--) {
- // 末尾值在前段出现过则本层不追加
- if (lastItem === arr[i]) {
- isRepeat = true
- break
- }
- }
- const head = unique19(arr, length - 1)
- // 非重复才把当前末尾接到前缀结果后面
- return isRepeat ? head : head.concat(lastItem)
- }
- // 方法20:JSON 字符串判重——处理对象数组
- // 把对象序列化成字符串作为 Set 的键,能去重 {id:1} 这类结构
- function unique20(arr) {
- const seen = new Set()
- const result = []
- for (const item of arr) {
- // 结构一致则键一致(字段顺序不同会得到不同键)
- const key = JSON.stringify(item)
- if (!seen.has(key)) {
- seen.add(key)
- result.push(item)
- }
- }
- return result
- }
- // 用法示例:
- // unique20([{id: 1}, {id: 2}, {id: 1}])
- // => [{id: 1}, {id: 2}]
复制代码JSON 的两个限定:① 字段序次差异的对象会被以为差异({a:1,b:2} ≠ {b:2,a:1});② undefined、函数、循环引用会丢失或抛错。
选择指南
%%{init: {'flowchart': {'nodeSpacing': 25, 'rankSpacing': 15, 'padding': 5}}}%%graph TD Start(["数组去重"]) --> Need{"是否须要保序?"} Need -->|不须要| Fast["看数据特性"] Need -->|须要| Ordered["保存原序次"] Fast --> Q1{"数据形态"} Q1 -->|趁便要排序| Sort["sort + Set"] Q1 -->|纯根本范例| Set1["[...new Set(arr)]"] Ordered --> Q2{"偏重点"} Q2 -->|代码轻便| Set2["[...new Set(arr)]
一行办理"] Q2 -->|函数式| FilterSet["filter + Set 闭包"] Q2 -->|按字段去重| MapByKey["Map + keyFn"] Q2 -->|对象数组| JSON["JSON.stringify + Set"] classDef start fill:#2E8B57,color:#fff,stroke:#1e5c3a classDef decision fill:#FE8B57,color:#fff,stroke:#141b2d classDef fast fill:#3A86FF,color:#fff,stroke:#2b63c4 classDef ordered fill:#8338EC,color:#fff,stroke:#5e27a8 classDef method fill:#0f3460,color:#fff,stroke:#0a2647 class Start start class Need,Q1,Q2 decision class Fast fast class Ordered ordered class Sort,Set1,Set2,FilterSet,MapByKey,JSON method种别时间复杂度是否保序重要场景根本循环O(n²)是教学、口试手撕内置数组方法O(n) ~ O(n²)是函数式风格聚集容器O(n)看具体类一样平常项目首选排序后去重O(n log n)否趁便要排序递归 / JSON视实现看实现教学 / 对象数组现实项目里怎么选
绝大多数情况一行就够:- // 保序、O(n)、写法最短,工程首选
- const result = [...new Set(arr)]
- // 或函数式风格,O(n)
- const seen = new Set()
- const result = arr.filter(x => !seen.has(x) && seen.add(x))
复制代码 按业务字段去重:- const result = [...new Map(arr.map(x => [x.id, x])).values()]
复制代码 对象数组去重:- const seen = new Set()
- const result = arr.filter(x => {
- const key = JSON.stringify(x)
- return !seen.has(key) && seen.add(key)
- })
复制代码 须要排序:- const result = [...new Set(arr)].sort((a, b) => a - b)
复制代码 带业务逻辑的去重
现实工作里常常碰到如许的情况:碰到重复时不能简单扬弃,要按某个规则做处理处罚。好比:
- 按 id 去重,但要保存分数最高的那条记录
- 去重的同时累加重复次数
- 数值在某个区间内才参加去重
这类需求 Set 直接搞不定,须要把"判重"和"处理处罚"两步拆开来写。JS 里通常用 Map + 归并函数:- /**
- * 带业务规则的去重。
- *
- * @param {Array} data 原数据
- * @param {Function} keyFn 从元素提取去重键
- * @param {Function} onDup 遇到重复时如何合并 (旧值, 新值) -> 新代表值
- */
- function uniqueBy(data, keyFn, onDup) {
- // Map 保证遍历顺序与首次出现顺序一致
- const chosen = new Map()
- for (const item of data) {
- const key = keyFn(item)
- if (!chosen.has(key)) {
- chosen.set(key, item)
- } else if (onDup) {
- chosen.set(key, onDup(chosen.get(key), item))
- }
- }
- return [...chosen.values()]
- }
复制代码 例 1:按 id 去重,保存分数最高的:- const students = [
- { id: 1, name: '张三', score: 90 },
- { id: 1, name: '张三', score: 95 }, // 同 id,分数更高
- { id: 2, name: '李四', score: 85 },
- ]
- const result = uniqueBy(
- students,
- x => x.id,
- (old, news) => news.score > old.score ? news : old,
- )
- // [{id:1, score:95, ...}, {id:2, score:85, ...}]
复制代码 例 2:去重同时统计频次:- const counts = new Map()
- for (const item of data) {
- counts.set(item, (counts.get(item) || 0) + 1)
- }
- // counts.keys() 是保序的去重结果
- // [...counts.entries()] 是 [[元素, 次数], ...]
复制代码 例 3:区间过滤——只对 [0, 100] 区间内的值去重,区间外原样保存:- const seen = new Set()
- const result = []
- for (const x of data) {
- if (x >= 0 && x <= 100) {
- if (seen.has(x)) continue
- seen.add(x)
- }
- result.push(x)
- }
复制代码 写法 2:按多字段组合去重- // 用 Map 按 id 去重
- const result = [...new Map(arr.map(x => [x.id, x])).values()]
复制代码 写法 3:按团体布局去重(用 JSON)- const seen = new Set()
- const result = arr.filter(x => {
- const key = JSON.stringify(x)
- return !seen.has(key) && seen.add(key)
- })// 注意:字段序次差异的对象会被以为差异
复制代码 总结
工程应用选择:
- 默认用 [...new Set(arr)]:保序、一行、O(n)
- 函数式风格用 arr.filter(x => !seen.has(x) && seen.add(x))
- 按字段去重用 [...new Map(arr.map(x => [x.key, x])).values()]
- 对象团体去重用 JSON.stringify 作为键
- 趁便排序用 [...new Set(arr)].sort((a, b) => a - b)
- 业务规则干预用 Map + 归并函数
焦点思绪:
- 同一个标题可以从多个角度切入
- 选对数据布局每每比写更智慧的代码更紧张
- O(n²) 与 O(n) 在数据变大时是几百倍的现实差距
- 不要过分优化——能用 new Set 就别绕弯
- 碰到新标题先写最直观的版本,再按瓶颈渐渐优化
更多算法
差异语言算法实现:https://github.com/microwind/algorithms
AI编程知识库:https://microwind.github.io
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |