JavaScript數(shù)組去重的6種實現(xiàn)方式(從 O(n2) 到 O(n))
前言
今天在課程中,老師帶我們用 6 種不同的方式 解決了同一道題——數(shù)組去重。從最基礎(chǔ)的雙重循環(huán),到利用數(shù)組 API,再到 ES6 的 Set,每種方法都有其獨特的思路和適用場景。
更重要的是,通過這道題,我真正理解了時間復(fù)雜度和空間復(fù)雜度的概念,以及"空間換時間"的算法思想。
一、編碼規(guī)范:寫好函數(shù)的第一步
在開始之前,先聊聊代碼規(guī)范。老師在課上反復(fù)強(qiáng)調(diào):
1.1 注釋是代碼的一部分
/**
* @func 數(shù)組去重
* @param {Array} arr 數(shù)組
* @return {Array} 去重后的數(shù)組
* @author hzs
* @date 2026-05-25
*/
function unique(arr) {
// ...
}
代碼的開發(fā)者和使用者可能不是同一個人,你可能忘記當(dāng)時為什么這么寫。注釋會提高代碼的可讀性,是代碼的一部分。
1.2 函數(shù)設(shè)計三原則
| 原則 | 說明 |
|---|---|
| 一個函數(shù)一個功能 | 單一職責(zé),便于維護(hù)和測試 |
| 封裝復(fù)雜功能 | 調(diào)用者不需要了解內(nèi)部實現(xiàn) |
| 健壯性——校驗參數(shù) | 對輸入進(jìn)行類型檢查,避免異常 |
1.3 參數(shù)校驗?zāi)0?/h3>
function unique(arr) {
// Array.isArray() 是數(shù)組的靜態(tài)方法,無需實例化即可調(diào)用
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// ...具體邏輯
}
以下 6 種實現(xiàn)都會包含這個參數(shù)校驗,后續(xù)代碼中不再重復(fù)說明。
function unique(arr) {
// Array.isArray() 是數(shù)組的靜態(tài)方法,無需實例化即可調(diào)用
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// ...具體邏輯
}
以下 6 種實現(xiàn)都會包含這個參數(shù)校驗,后續(xù)代碼中不再重復(fù)說明。
二、方法一:雙重循環(huán)(暴力法)
思路
維護(hù)一個結(jié)果數(shù)組 res,遍歷原數(shù)組,對每個元素檢查是否已存在于 res 中。
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; // 標(biāo)記是否重復(fù)
for (let j = 0; j < res.length; j++) {
// === 恒等:值相等且類型相等
// 1 === '1' → false(弱類型語言中的嚴(yán)格比較)
if (arr[i] === res[j]) {
flag = false;
break;
}
}
if (flag) {
res.push(arr[i]);
}
}
return res;
}
console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]
復(fù)雜度分析
時間復(fù)雜度:O(n2)
├── 外層循環(huán) n 次
└── 內(nèi)層循環(huán)最多 n 次
總計:n × n = n2
空間復(fù)雜度:O(n)
└── 結(jié)果數(shù)組 res 最多存儲 n 個元素
優(yōu)點:思路最直觀,適合初學(xué)者理解。缺點:性能差,數(shù)據(jù)量大時明顯卡頓。
三、方法二:indexOf 優(yōu)化
思路
利用 Array.prototype.indexOf() 方法替代內(nèi)層循環(huán),判斷元素是否已存在于結(jié)果數(shù)組中。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
const res = [];
for (let i = 0; i < arr.length; i++) {
// indexOf 返回元素第一次出現(xiàn)的索引
// 如果返回 -1,說明 res 中不存在該元素
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i]);
}
}
return res;
}
關(guān)鍵 API
arr.indexOf(item) // 返回 item 在 arr 中第一次出現(xiàn)的索引 // 找不到返回 -1 [1, 2, 3, 2].indexOf(2) // 1 [1, 2, 3].indexOf(4) // -1
復(fù)雜度分析
時間復(fù)雜度:O(n2)
├── 外層循環(huán) n 次
└── indexOf 內(nèi)部也是一次遍歷 O(n)
總計:仍然是 n2
空間復(fù)雜度:O(n)
本質(zhì)上和方法一相同,只是用 indexOf 替代了手寫內(nèi)層循環(huán),代碼更簡潔,但時間復(fù)雜度沒有改善。
四、方法三:filter + indexOf
思路
利用 Array.prototype.filter() 方法,配合 indexOf 進(jìn)行過濾。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return arr.filter(function(item, index) {
// 只保留第一次出現(xiàn)的元素
// indexOf 返回第一個索引,如果等于當(dāng)前 index,說明是第一次出現(xiàn)
return index === arr.indexOf(item);
});
}
console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]
關(guān)鍵 API
arr.filter(function(item, index) {
// 返回 true → 保留該元素
// 返回 false → 過濾掉該元素
return true | false;
});
工作原理
原數(shù)組:[1, 2, 3, 4, 5, 5, 6] 索引: 0 1 2 3 4 5 6 filter 遍歷過程: ┌──────┬───────┬────────────────┬────────┐ │ item │ index │ indexOf(item) │ 保留? │ ├──────┼───────┼────────────────┼────────┤ │ 1 │ 0 │ 0 │ ? │ │ 2 │ 1 │ 1 │ ? │ │ 3 │ 2 │ 2 │ ? │ │ 4 │ 3 │ 3 │ ? │ │ 5 │ 4 │ 4 │ ? │ │ 5 │ 5 │ 4 │ ? │ ← 第二個 5 被過濾 │ 6 │ 6 │ 6 │ ? │ └──────┴───────┴────────────────┴────────┘
復(fù)雜度分析
時間復(fù)雜度:O(n2)
├── filter 遍歷 n 次
└── 每次 indexOf 遍歷 O(n)
總計:仍然是 n2
空間復(fù)雜度:O(n)
函數(shù)式編程風(fēng)格,代碼最簡潔優(yōu)雅,但性能上仍然是 O(n²)。
五、方法四:排序后相鄰比較
思路
先對數(shù)組排序,然后只需比較相鄰元素是否相同。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
// O(n2) → O(nlogn)
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;
}
為什么更快?
排序前:[3, 1, 4, 1, 5, 9, 2, 6, 5]
排序后:[1, 1, 2, 3, 4, 5, 5, 6, 9]
↑ ↑
相鄰比較即可,無需兩兩比較
復(fù)雜度分析
時間復(fù)雜度:O(nlogn)
├── sort() 排序:O(nlogn)
└── 遍歷比較:O(n)
總計:O(nlogn) + O(n) = O(nlogn) ← 顯著提升!
空間復(fù)雜度:O(n)
性能提升明顯,從 O(n²) 降到 O(nlogn)。但注意:sort() 默認(rèn)按字符串排序,對數(shù)字?jǐn)?shù)組需要傳入比較函數(shù) arr.sort((a, b) => a - b)。
六、方法五:對象字面量 / HashMap(空間換時間)
思路
利用 JavaScript 對象字面量作為 HashMap,以數(shù)組元素為 key,實現(xiàn) O(1) 的查找。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
let res = [],
obj = {}; // 對象字面量充當(dāng) HashMap
for (let i = 0; i < arr.length; i++) {
// obj[variable] — 變量作為 key(動態(tài)屬性訪問)
// obj.name — 常量作為 key(點號訪問)
if (!obj[arr[i]]) {
res.push(arr[i]);
obj[arr[i]] = 1; // 標(biāo)記為已存在
} else {
obj[arr[i]]++; // 記錄出現(xiàn)次數(shù)
}
}
return res;
}
核心原理
對象字面量充當(dāng) HashMap
遍歷 [1, 2, 3, 2, 4, 3]
Step 1: obj = {}, res = [1] obj[1] = 1
Step 2: obj = {1:1}, res = [1,2] obj[2] = 1
Step 3: obj = {1:1,2:1}, res = [1,2,3] obj[3] = 1
Step 4: obj[2] 已存在!跳過
Step 5: obj = {1:1,2:1,3:1}, res = [1,2,3,4] obj[4] = 1
Step 6: obj[3] 已存在!跳過
結(jié)果:[1, 2, 3, 4]
復(fù)雜度分析
時間復(fù)雜度:O(n)
├── 只需遍歷一次數(shù)組
└── 對象屬性查找是 O(1)
總計:O(n) × O(1) = O(n) ← 最優(yōu)!
空間復(fù)雜度:O(n)
├── 結(jié)果數(shù)組 O(n)
└── HashMap 對象 O(n)
總計:O(n) ← 用空間換時間
經(jīng)典的空間換時間策略。JavaScript 早期沒有 HashMap,對象字面量就是最好的替代方案。注意:如果數(shù)組元素是對象,需要用 JSON.stringify() 轉(zhuǎn)換為字符串作為 key。
七、方法六:ES6 Set(終極方案)
思路
利用 ES6 新增的 Set 數(shù)據(jù)結(jié)構(gòu)——天生不重復(fù)的集合。
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return [...new Set(arr)]; // Set 轉(zhuǎn)換為數(shù)組
}
一行代碼搞定
const unique = arr => [...new Set(arr)];
Set 是什么?
Set 的特性 ├── 不重復(fù)的數(shù)據(jù)容器 ├── 內(nèi)部使用 HashMap 實現(xiàn) ├── 查找/插入的時間復(fù)雜度 O(1) └── ES6 新增的數(shù)據(jù)結(jié)構(gòu)
復(fù)雜度分析
時間復(fù)雜度:O(n)
├── new Set(arr):遍歷數(shù)組構(gòu)建 Set,O(n)
└── ...展開運算符:遍歷 Set 轉(zhuǎn)數(shù)組,O(n)
總計:O(n)
空間復(fù)雜度:O(n)
└── Set 容器存儲 n 個元素
生產(chǎn)環(huán)境推薦方案:代碼最簡潔、性能最優(yōu)、語義最清晰。
八、六種方法全面對比
8.1 復(fù)雜度對比
| 方法 | 時間復(fù)雜度 | 空間復(fù)雜度 | 核心思路 |
|---|---|---|---|
| ① 雙重循環(huán) | O(n²) | O(n) | 暴力枚舉 |
| ② indexOf | O(n²) | O(n) | API 替代內(nèi)層循環(huán) |
| ③ filter+indexOf | O(n²) | O(n) | 函數(shù)式風(fēng)格 |
| ④ 排序+相鄰比較 | O(nlogn) | O(n) | 先排序降低比較次數(shù) |
| ⑤ 對象字面量/HashMap | O(n) | O(n) | 空間換時間 |
| ⑥ ES6 Set | O(n) | O(n) | 利用 Set 天生去重 |
8.2 復(fù)雜度直觀感受
執(zhí)行時間對比(假設(shè) n = 10000) O(n2) :100,000,000 次操作 ?? O(nlogn) : 132,877 次操作 ?? O(n) : 10,000 次操作 ?? 差距巨大!算法選擇直接影響程序性能
8.3 適用場景
| 場景 | 推薦方法 | 原因 |
|---|---|---|
| 生產(chǎn)環(huán)境 | ⑥ Set | 簡潔、高效、現(xiàn)代 |
| 面試手寫 | ⑤ HashMap | 展示算法思維 |
| 學(xué)習(xí)理解 | ①②③④ | 理解基本原理 |
| 大數(shù)據(jù)量 | ④⑤⑥ | 避免 O(n²) |
| 兼容舊瀏覽器 | ④⑤ | 不依賴 ES6 |
九、涉及的核心數(shù)組 API
速查表
| API | 類型 | 作用 | 示例 |
|---|---|---|---|
Array.isArray() | 靜態(tài)方法 | 判斷是否是數(shù)組 | Array.isArray([1]) → true |
arr.indexOf(item) | 實例方法 | 返回首次出現(xiàn)的索引 | [1,2,3].indexOf(2) → 1 |
arr.filter(fn) | 實例方法 | 過濾數(shù)組,返回新數(shù)組 | arr.filter(x => x > 0) |
arr.sort() | 實例方法 | 排序(原地修改) | arr.sort((a,b) => a-b) |
十、知識圖譜
?? 數(shù)組去重知識圖譜
編碼規(guī)范
├── JSDoc 注釋規(guī)范
├── 一個函數(shù)一個功能
├── 參數(shù)校驗(健壯性)
└── === 嚴(yán)格相等
六種實現(xiàn)方式
├── O(n2) 暴力法
│ ├── 雙重循環(huán)
│ ├── indexOf
│ └── filter + indexOf
│
├── O(nlogn) 排序法
│ └── sort + 相鄰比較
│
└── O(n) 哈希法
├── 對象字面量 / HashMap
└── ES6 Set
核心概念
├── 時間復(fù)雜度(執(zhí)行效率)
├── 空間復(fù)雜度(內(nèi)存占用)
├── 空間換時間(算法權(quán)衡)
└── HashMap 原理(O(1) 查找)
結(jié)語
一道簡單的數(shù)組去重題,從 O(n²) 到 O(n),從暴力循環(huán)到 Set 一行代碼,背后是算法思維的進(jìn)化。
面試中,面試官考的不僅是你能不能寫出答案,更是你能否分析不同方案的時間復(fù)雜度和空間復(fù)雜度,能否根據(jù)實際場景選擇最優(yōu)方案。
記住這六個方法,理解背后的原理,你就能在面試中游刃有余。
以上就是JavaScript數(shù)組去重的6種實現(xiàn)方式(從 O(n²) 到 O(n))的詳細(xì)內(nèi)容,更多關(guān)于JavaScript數(shù)組去重實現(xiàn)方式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript使用Promise對象實現(xiàn)異步編程
這篇文章主要介紹了javascript使用Promise對象實現(xiàn)異步編程的相關(guān)資料,需要的朋友可以參考下2016-03-03
微信小程序局部刷新觸發(fā)整頁刷新效果的實現(xiàn)代碼
這篇文章主要介紹了微信小程序局部刷新觸發(fā)整頁刷新效果的實現(xiàn)代碼,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-11-11
JavaScript 實現(xiàn)鼠標(biāo)拖動元素實例代碼
這篇文章主要介紹了JavaScript 實現(xiàn)鼠標(biāo)拖動元素實例代碼,需要的朋友可以參考下2014-02-02
微信小程序 多行文本顯示...+顯示更多按鈕和收起更多按鈕功能
這篇文章主要介紹了微信小程序多行文本顯示...+顯示更多按鈕和收起更多按鈕,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-09-09

