JavaScript實現(xiàn)數(shù)組去重的20種方式
引言
數(shù)組去重是最常見的算法??此坪唵危煌瑢崿F(xiàn)方式的性能差異可能高達幾百倍。本文整理 JavaScript 數(shù)組去重的 20 種寫法,按 5 個策略分類,充分利用JavaScript的弱類型和動態(tài)性,幫助你理解語言特性,同時掌握多種解決問題的的思路。AI時代,你需要知道代碼背后的原理,這樣才能更好地指導(dǎo)AI編程。
為什么性能差異這么大?
最簡單的寫法,新建一個數(shù)組,把不在結(jié)果里的添加進去。
function unique(arr) {
const result = []
for (const item of arr) {
// 遍歷原數(shù)組,逐個取出元素判斷是否存在新數(shù)組,若新數(shù)組中不存在則添加
if (!result.includes(item)) {
result.push(item)
}
}
return result
}
問題在于每次 includes 都要全量掃一遍 result,復(fù)雜度是 O(n²)。
優(yōu)化思路:換一種判重方式
- Set / Map O(1) 查詢:
new Set(arr) - 排序 O(n log n):相同元素相鄰后掃一遍
- filter + 閉包:在函數(shù)式管道里攜帶"已見"狀態(tài)
- JSON 序列化:處理對象、嵌套數(shù)組等不可哈希元素
- 遞歸:換種表達方式,本質(zhì)仍是上面的思路
推薦方案
| 需求 | 代碼 | 性能 | 保序 |
|---|---|---|---|
| 一行最簡 | [...new Set(arr)] | O(n) | ? |
| 函數(shù)式風(fēng)格 | arr.filter((x,i,a) => a.indexOf(x)===i) | O(n²) | ? |
| 函數(shù)式 + Set | arr.filter(x => !seen.has(x) && seen.add(x)) | O(n) | ? |
| 要排序 | [...new Set(arr)].sort((a,b)=>a-b) | O(n log n) | 排序 |
| 對象數(shù)組 | JSON.stringify 作為 Set 的鍵 | O(n×m) | ? |
第1類:基礎(chǔ)循環(huán)(方法1-6)
策略原理:不用任何內(nèi)置數(shù)組方法,純靠下標、嵌套循環(huán)、indexOf 這種"原始"手段完成去重。每一步判重都是 O(n),整體 O(n²)。
適用場景:教學(xué)、面試手撕。生產(chǎn)代碼不建議使用。
// 方法1:雙循環(huán)索引比較——i 與左側(cè)每個 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 表示前面沒有相同值,當前項是首次出現(xiàn),追加到新數(shù)組中
if (i === j) result.push(arr[i])
// 只要遇到相同值,要么剛添加了,要么前面已添加,可跳出循環(huán)
break
}
}
}
return result
}
// 方法2:新建數(shù)組 + includes 檢查
function unique2(arr) {
const result = []
for (const item of arr) {
// 不存在新數(shù)組就添加,利用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++) {
// 將當前項逐個與前面項比較,若遇到重復(fù)就刪除當前項,原數(shù)組操作
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++) {
// 將當前項逐個與后面項比較,若遇到重復(fù)就刪除重復(fù)項,原數(shù)組操作
if (arr[i] === arr[j]) {
arr.splice(j, 1)
// 因為自前向后遍歷,刪除重復(fù)項后,需要將下標和總長度各自減1位
j--; l--
}
}
}
return arr
}
// 方法5:forEach + indexOf
// indexOf 返回首次出現(xiàn)下標,等于當前下標即首次
function unique5(arr) {
const result = []
arr.forEach((item, i) => {
// 與unique1思路一致,利用indexOf實現(xiàn)查找
if (arr.indexOf(item) === i) result.push(item)
})
return result
}
// 方法6:雙重 while 倒序 splice
// 與 unique3 同類:自尾向前,當前尾元素若在前段出現(xiàn)過則刪掉該尾元素
function unique6(arr) {
let l = arr.length
while (l-- > 0) {
let i = l
while (i-- > 0) {
// 與左側(cè)某項相等說明重復(fù),刪掉當前下標 l 的元素后跳出內(nèi)層
if (arr[l] === arr[i]) {
arr.splice(l, 1)
break
}
}
}
return arr
}
第2類:內(nèi)置數(shù)組方法(方法7-11)
策略原理:JavaScript 數(shù)組自帶 filter、reduce、forEach 等高階方法,可以把"判重 + 收集"寫成函數(shù)式風(fēng)格。注意indexOf / includes 仍是 O(n),需要用 Set 閉包才能壓到 O(n)。
適用場景:現(xiàn)代 JS 工程的常態(tài)寫法。可讀性高,鏈式組合方便。
// 方法7:filter + indexOf 一行經(jīng)典
// 寫法最短,但每次 indexOf 都是 O(n)
function unique7(arr) {
// indexOf 得首次下標,與當前 i 相同才保留,否則是后面出現(xiàn)的重復(fù)項
return arr.filter((item, i) => arr.indexOf(item) === i)
}
// 方法8:filter + Set 閉包——推薦寫法
// Set.add 返回 Set 自身,結(jié)合短路 && 返回布爾值
function unique8(arr) {
const seen = new Set()
// 未見過才執(zhí)行 add;add 恒為真值,整體表達式作 filter 謂詞
return arr.filter(item => !seen.has(item) && seen.add(item))
}
// 方法9:reduce 累加(用數(shù)組)
// 函數(shù)式風(fēng)格,但 includes 仍是 O(n2)
function unique9(arr) {
return arr.reduce((acc, item) => {
// 與 unique2 同思路,只是用 reduce 折疊出結(jié)果數(shù)組
if (!acc.includes(item)) acc.push(item)
return acc
}, [])
}
// 方法10:reduce + Set 閉包——O(n) 函數(shù)式
function unique10(arr) {
const seen = new Set()
return arr.reduce((acc, item) => {
// Set 判重 O(1),首次出現(xiàn)才同步記入 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 字面量雖然也能當哈希用,但有"鍵自動字符串化""數(shù)字鍵被引擎重排"等坑。
適用場景:日常項目首選 Set;需要保留 value 選 Map;只在小數(shù)據(jù)或特殊兼容場景才用 Object。
// 方法12:new Set 轉(zhuǎn)數(shù)組——一行經(jīng)典
// Set 用 SameValueZero 比較,NaN 也能正確去重
function unique12(arr) {
// 展開成數(shù)組,迭代順序與元素首次插入 Set 的順序一致(保序)
return [...new Set(arr)]
}
// 方法13:Map.set + keys
// 適合"按鍵去重,值攜帶其他信息"的場景
function unique13(arr) {
const map = new Map()
// 鍵重復(fù)時覆蓋值,keys 迭代順序仍為各鍵首次插入順序
arr.forEach(item => map.set(item, item))
return [...map.keys()]
}
// 方法14:Object 字面量哈希
// 注意:1 與 '1' 會被合并;數(shù)字鍵會被引擎按升序重排
// [1, 'a', 2, 'b', -1] 會變成 [1, 2, 'a', 'b', -1]
function unique14(arr) {
const obj = {}
// 屬性名會轉(zhuǎn)成字符串,對象等引用類型鍵易撞成 '[object Object]'
for (const item of arr) obj[item] = item
return Object.values(obj)
}
小心 Object 的兩個坑:① 數(shù)字字符串鍵會被 V8/SpiderMonkey 重排到前面(升序);② 引用類型(對象、數(shù)組)會變成 [object Object] 之類的字符串,全部合并成一個鍵。生產(chǎn)代碼不要用 Object 當 Set。
第4類:排序后去重(方法15-17)
策略原理:先 sort 讓相同元素相鄰,再掃一遍刪除相鄰相同項。復(fù)雜度由排序決定,O(n log n)。優(yōu)點是不需要額外的哈希結(jié)構(gòu),"相鄰判等"是最便宜的判重方式;缺點是會破壞原順序。
適用場景:輸出本就需要排序、不在意原順序。
// 方法15:sort + splice 升序去重
// 注意 sort 不傳比較函數(shù)會按字符串排序,數(shù)字數(shù)組要傳 (a, b) => a - b
function unique15(arr) {
arr.sort((a, b) => a - b)
let l = arr.length
while (l-- > 1) {
// 排序后相等必相鄰,刪后一重復(fù)項;從后往前 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:經(jīng)典雙指針(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,截掉尾部重復(fù)占位
return arr.slice(0, slow + 1)
}
第5類:遞歸與特殊(方法18-20)
策略原理:遞歸用自調(diào)用替代循環(huán),是函數(shù)式思維的體現(xiàn),主要用于教學(xué)。JSON.stringify 把對象映射為字符串,是 JS 里處理"不可哈希元素"(對象數(shù)組、嵌套數(shù)組)的常見招數(shù)。
適用場景:遞歸——教學(xué);JSON——對象數(shù)組按整體結(jié)構(gòu)去重。
// 方法18:遞歸原地刪除
// 先看當前「末尾」是否在前綴中出現(xiàn)過,重復(fù)則 splice 掉末尾
function unique18(arr, length) {
if (length <= 1) return arr
const last = length - 1
for (let i = last - 1; i >= 0; i--) {
// 與前段某項相同則末尾是重復(fù),刪掉后不再比較
if (arr[last] === arr[i]) {
arr.splice(last, 1)
break
}
}
// 本層已處理原末尾,子問題規(guī)模減一(與是否 splice 無關(guān))
return unique18(arr, length - 1)
}
// 方法19:遞歸拼接返回(不修改原數(shù)組)
// 每層只決定「當前末尾項」是否并入結(jié)果,前綴由遞歸算好
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--) {
// 末尾值在前段出現(xiàn)過則本層不追加
if (lastItem === arr[i]) {
isRepeat = true
break
}
}
const head = unique19(arr, length - 1)
// 非重復(fù)才把當前末尾接到前綴結(jié)果后面
return isRepeat ? head : head.concat(lastItem)
}
// 方法20:JSON 字符串判重——處理對象數(shù)組
// 把對象序列化成字符串作為 Set 的鍵,能去重 {id:1} 這類結(jié)構(gòu)
function unique20(arr) {
const seen = new Set()
const result = []
for (const item of arr) {
// 結(jié)構(gòu)一致則鍵一致(字段順序不同會得到不同鍵)
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、函數(shù)、循環(huán)引用會丟失或拋錯。
選擇指南
| 類別 | 時間復(fù)雜度 | 是否保序 | 主要場景 |
|---|---|---|---|
| 基礎(chǔ)循環(huán) | O(n²) | 是 | 教學(xué)、面試手撕 |
| 內(nèi)置數(shù)組方法 | O(n) ~ O(n²) | 是 | 函數(shù)式風(fēng)格 |
| 集合容器 | O(n) | 看具體類 | 日常項目首選 |
| 排序后去重 | O(n log n) | 否 | 順便要排序 |
| 遞歸 / JSON | 視實現(xiàn) | 看實現(xiàn) | 教學(xué) / 對象數(shù)組 |
實際項目里怎么選
絕大多數(shù)情況一行就夠:
// 保序、O(n)、寫法最短,工程首選 const result = [...new Set(arr)] // 或函數(shù)式風(fēng)格,O(n) const seen = new Set() const result = arr.filter(x => !seen.has(x) && seen.add(x))
按業(yè)務(wù)字段去重:
const result = [...new Map(arr.map(x => [x.id, x])).values()]
對象數(shù)組去重:
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)
帶業(yè)務(wù)邏輯的去重
實際工作里經(jīng)常遇到這樣的情況:遇到重復(fù)時不能簡單丟棄,要按某個規(guī)則做處理。比如:
- 按
id去重,但要保留分數(shù)最高的那條記錄 - 去重的同時累加重復(fù)次數(shù)
- 數(shù)值在某個區(qū)間內(nèi)才參與去重
這類需求 Set 直接搞不定,需要把"判重"和"處理"兩步拆開來寫。JS 里通常用 Map + 合并函數(shù):
/**
* 帶業(yè)務(wù)規(guī)則的去重。
*
* @param {Array} data 原數(shù)據(jù)
* @param {Function} keyFn 從元素提取去重鍵
* @param {Function} onDup 遇到重復(fù)時如何合并 (舊值, 新值) -> 新代表值
*/
function uniqueBy(data, keyFn, onDup) {
// Map 保證遍歷順序與首次出現(xiàn)順序一致
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 去重,保留分數(shù)最高的:
const students = [
{ id: 1, name: '張三', score: 90 },
{ id: 1, name: '張三', score: 95 }, // 同 id,分數(shù)更高
{ 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:去重同時統(tǒng)計頻次:
const counts = new Map()
for (const item of data) {
counts.set(item, (counts.get(item) || 0) + 1)
}
// counts.keys() 是保序的去重結(jié)果
// [...counts.entries()] 是 [[元素, 次數(shù)], ...]
例 3:區(qū)間過濾——只對 [0, 100] 區(qū)間內(nèi)的值去重,區(qū)間外原樣保留:
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)
}
這三個例子是同一種思路:把判重與業(yè)務(wù)規(guī)則分開。判重用 Set/Map 保證 O(n),規(guī)則部分留給回調(diào)或顯式分支處理。
對象數(shù)組去重的幾種寫法
JS 里 === 比較對象比較的是引用,而不是內(nèi)容,所以 new Set([{id:1}, {id:1}]) 不會去重——兩個獨立的對象引用不相等。
實際項目里有三種常見寫法:
寫法 1:按字段去重(最常見)
// 用 Map 按 id 去重 const result = [...new Map(arr.map(x => [x.id, x])).values()]
寫法 2:按多字段組合去重
// 拼成復(fù)合鍵
const result = [...new Map(arr.map(x => [`${x.id}|${x.type}`, x])).values()]
寫法 3:按整體結(jié)構(gòu)去重(用 JSON)
const seen = new Set()
const result = arr.filter(x => {
const key = JSON.stringify(x)
return !seen.has(key) && seen.add(key)
})
// 注意:字段順序不同的對象會被認為不同
總結(jié)
工程應(yīng)用選擇:
- 默認用
[...new Set(arr)]:保序、一行、O(n) - 函數(shù)式風(fēng)格用
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) - 業(yè)務(wù)規(guī)則干預(yù)用 Map + 合并函數(shù)
核心思路:
- 同一個問題可以從多個角度切入
- 選對數(shù)據(jù)結(jié)構(gòu)往往比寫更聰明的代碼更重要
- O(n²) 與 O(n) 在數(shù)據(jù)變大時是幾百倍的實際差距
- 不要過度優(yōu)化——能用
new Set就別繞彎 - 遇到新問題先寫最直觀的版本,再按瓶頸逐步優(yōu)化
以上就是JavaScript實現(xiàn)數(shù)組去重的20種方式的詳細內(nèi)容,更多關(guān)于JavaScript數(shù)組去重方式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript筆記之數(shù)據(jù)屬性和存儲器屬性
本文給大家介紹js數(shù)據(jù)屬性和存儲器屬性,及兩種屬性的區(qū)別,對js數(shù)據(jù)屬性存儲器屬性相關(guān)知識感興趣的朋友一起學(xué)習(xí)2016-03-03
Javascript控制div屬性動態(tài)變化實例分析
這篇文章主要介紹了Javascript控制div屬性動態(tài)變化,以實例形式較為詳細的分析了JavaScript響應(yīng)鼠標事件動態(tài)操作頁面元素屬性的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10
淺析Virtual DOM的概念與其在現(xiàn)代前端框架中的實踐
這篇文章將深入探討Virtual DOM(虛擬DOM)的概念,分析其對前端開發(fā)的革新影響,并以此展示前端技術(shù)的深度和魅力,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12
在Js頁面通過POST傳遞參數(shù)跳轉(zhuǎn)到新頁面詳解
這篇文章主要給大家介紹了關(guān)于在Js頁面通過POST傳遞參數(shù)跳轉(zhuǎn)到新頁面的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08

