深刻分析數組去重

數組去重 是常見的面試考點,因此我就試着深刻學習一下。網上也有不少數組去重的文章,但我本身以爲分析地不夠深刻,其實其中不少的實現都是重複的,能夠歸爲一類,好比 雙重循環法 和 indexOf法 的本質都是雙重循環,故寫下此文,作進一步的總結,同時加深理解。git

1. 雙重循環

這種方法就很直接,很好理解。那就是建立一個新的空數組,每次咱們會從原數組中取出一個元素,拿它和新數組的元素進行一一比較。若是在新數組沒發現和取出元素相等的元素,就將其放入這個新數組中;若是發現有和取出元素相等的元素,不放入新數組中。當原數組中的數組全都取出來時,這個新數組就是去重後的全部數據了。github

這種數組去重的實現的時間複雜度是 O(n^2)。面試

const unique = arr => {
    let res = [];
    for (let i = 0, len = arr.length; i < len; i++) {
        let j = 0, len2 = res.length;
        for (; j < len2; j++) {
            if (arr[i] === res[j]) break;
        }
        if (j == len2) res.push(arr[i]);  // j == len2 表示沒有執行 break。
    }
    return res;
}
複製代碼

固然這裏的第一個循環能夠改成 forEach() 方法,第二個 for 循環能夠改成使用 includes() 或者 indexOf() 方法,時間複雜度沒什麼變化,不過代碼更簡潔。算法

const unique = arr => {
    let res = [];
    arr.forEach(item => {
        if (!res.includes(item)) res.push(item); 
    })
    return res;
} 
複製代碼

2. 查找元素第一次出現的位置

從後往前遍歷數組,檢測元素第一次出現的位置是否爲當前元素的位置。若是不是,說明有重複,移除當前元素;若是沒有,就不移除。數組

之因此從後往前遍歷,是由於咱們要搬移元素(其實就是 splice)。固然你也能夠選擇從前日後遍歷,不過這樣的話,若是遍歷時當前元素被移除了,那麼移除元素後的 arr[i] 對應的元素實際上是原來 arr[i+1],所以此時 i 不能自增,且結束的條件要改成 i == len-1,就很麻煩。bash

這種寫法不須要建立新的數組,空間複雜度爲 O(1)。數據結構

const unique = arr => {
    for (let i = arr.length - 1; i >= 0; i--) {
        for (let j = 0; j < i; j++) {
            if (arr[j] === arr[i]) arr.splice(i, 1);
        }
    }
    return arr;
}
複製代碼

這裏的代碼實現是儘可能減小時間複雜度的。說個題外話,其實上面這裏還能夠再優化一下,由於咱們這裏的元素搬移並非一次性搬移到最終的位置上的。優化思路是先標記要全部要刪除的元素索引,而後從前日後遍歷數組,每遇到第 m 個刪除索引,後面的元素就覆蓋掉它們往前第 m 位的數組元素,這裏就不實現了,也就隨便提一下。post

若是改成配合使用 filter()includes() 方法的話,咱們可讓代碼可讀性更好一些(性能會稍微降低,由於 incluedes 會遍歷整個數組),具體實現就不寫了。性能

3. 排序後去重

排序算法有不少種,咱們就用 js 自帶的排序算法吧。順帶一說,v8引擎 的 sort() 方法在數組長度小於等於10的狀況下,會使用插入排序,大於10的狀況下會使用快速排序。學習

排(guai)好(guai)序(zhanhao)後,檢查先後兩個相鄰元素,若是當前元素和前面的元素不相等,纔將當前元素放入新數組中。

const unique = arr => {
    if (arr.length < 2) return arr; 
    arr.sort();
    let r = [arr[0]];
    for (let i = 1, len = arr.length; i < len; i++) {
        if (a[i] !== a[i - 1]) r.push(a[i]);
    }
    return r;
}
複製代碼

這種去重侷限性很是大。它不適用於對象,由於對象不適合進行排序。sort() 的默認排序順序是根據字符串Unicode碼點進行排序,貌似會把對象轉爲字符串再進行排序,通常的對象都會轉爲 "[object Object]",沒法保證兩個引用同一個對象的變量能相鄰排列。

4. 使用散列表

散列表,在 JavaScript 中是經過對象來實現的。散列表的優勢是,通常狀況下讀取數據的時間複雜度是 O(1)。但 js 的對象的鍵只能爲字符串類型,不過能夠考慮使用 ES6 新增的 Map 數據結構,它容許使用任何類型的值做爲鍵。

下面的實現使用的是普通對象做爲散列表,有很大的侷限性,沒法對 js對象 進行去重(對象都會轉爲相似 [object Object] 的字符串)。另外,對於js對象來講,a['1'] 和 a[1] 是相等的,由於1會轉換爲'1',這樣就沒法分辨出 1 和 '1',從而錯誤地在去重過程當中丟棄其中的一個元素,因此我作了簡單地改良,鍵名使用的不是 arr[i] 而是 typeof(arr[i]) + arr[i]

const unique = arr => {
    let r = [];
    let map = {};
    for (let i = 0, len = arr.length; i < len; i++) {
        const item = arr[i];
        if (!map[typeof(item) + item]) {
            r.push(arr[i]);
        }
        map[typeof(item) + item] = true; 
    }
    return r;
} 
複製代碼

這種實現方式,時間複雜度能夠達到 O(n)。

若是考慮對象也能去重,能夠考慮使用 ES6 的 Map。

5. ES6 的 Set

ES6 提供了新的數據結構。Set 實例會認爲兩個 NaN 是相等的(儘管 NaN !== NaN),並認爲兩個對象是不等的(固然這裏兩個對象的意思,表示的是兩個指向不一樣內存空間的引用類型變量)。

並不太瞭解 Set 的源碼實現,就不分析性能了。

const unique = arr => {
    return Array.from(new Set(arr))
}
複製代碼

很是簡潔,若是你的運行環境支持 ES6,或者能夠編譯成 ES5,我很推薦使用這個去重方案。

考慮 NaN 的去重

若是要考慮 NaN 的去重,就須要稍微對代碼進行一些修改。

簡單來講就是,判斷 item 是否爲 NaN,而後檢查返回的數組中是否已有 NaN。若是有,放入數組;不然不放入。

const unique = arr => {
    let res = [];
    let hasNaN = false;
    arr.forEach(item => {
        if(!hasNaN && Number.isNaN(item)) {
            res.push(item);
            hasNaN = true
        }else if (!res.includes(item)) {
            res.push(item); 
        }
    })
    return res;
} 
複製代碼

lodash 如何實現去重

簡單說下 lodash 的 uniq 方法的源碼實現。

這個方法的行爲和使用 Set 進行去重的結果一致。

當數組長度大於等於 200 時,會建立 Set 並將 Set 轉換爲數組來進行去重(Set 不存在狀況的實現不作分析)。當數組長度小於 200 時,會使用相似前面提到的 雙重循環 的去重方案,另外還會作 NaN 的去重。

總結

通常來講,在開發中,要進行去重的數組並非很大,沒必要太考慮性能問題。因此在工程中,爲了避免把簡單的問題複雜化中,建議使用最簡潔的 ES6 的 Set 轉數組的方案來實現。固然具體問題具體分析,要根據場景選擇真正合適的去重方案。

另外,其實 「相等」 有不少種定義,ES6 中就有四種相等算法,這裏就很少說了,有興趣的話能夠看看這篇文章:JavaScript 中的相等性判斷。依舊是根據場景選擇合適的相等算法。

參考

相關文章
相關標籤/搜索