如何答一道驚豔面試官的數組去重問題?

爲何寫這篇文章?

  1. 數組去重應該是面試必考問題之一。
  2. 雖然它是一道並不複雜的問題,可是也能看出面試者的廣度和深度,還有考慮問題的全面性。
  3. 實際開發中咱們應該選擇哪一種方式數組去重,本文告訴你。
  4. 你覺得的不必定你覺得,面試官不僅是讓你去重一個數組,他想知道的有點多,包括你的思想。

做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 github.com/koala-codin…javascript

當面試官問到時怎麼回答?

首先:我知道多少種去重方式

雙層 for 循環

function distinct(arr) {
    for (let i=0, len=arr.length; i<len; i++) {
        for (let j=i+1; j<len; j++) {
            if (arr[i] == arr[j]) {
                arr.splice(j, 1);
                // splice 會改變數組長度,因此要將數組長度 len 和下標 j 減一
                len--;
                j--;
            }
        }
    }
    return arr;
}
複製代碼

思想: 雙重 for 循環是比較笨拙的方法,它實現的原理很簡單:先定義一個包含原始數組第一個元素的數組,而後遍歷原始數組,將原始數組中的每一個元素與新數組中的每一個元素進行比對,若是不重複則添加到新數組中,最後返回新數組;由於它的時間複雜度是O(n^2),若是數組長度很大,效率會很低html

Array.filter() 加 indexOf

function distinct(a, b) {
    let arr = a.concat(b);
    return arr.filter((item, index)=> {
        return arr.indexOf(item) === index
    })
}
複製代碼

思想: 利用indexOf檢測元素在數組中第一次出現的位置是否和元素如今的位置相等,若是不等則說明該元素是重複元素java

Array.sort() 加一行遍歷冒泡(相鄰元素去重)

function distinct(array) {
    var res = [];
    var sortedArray = array.concat().sort();
    var seen;
    for (var i = 0, len = sortedArray.length; i < len; i++) {
        // 若是是第一個元素或者相鄰的元素不相同
        if (!i || seen !== sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i];
    }
    return res;
}
複製代碼

思想: 調用了數組的排序方法 sort(),V8引擎 的 sort() 方法在數組長度小於等於10的狀況下,會使用插入排序,大於10的狀況下會使用快速排序(sort函數在我以前高階函數那篇文章有詳細講解【JS必知必會】高階函數詳解與實戰)。而後根據排序後的結果進行遍歷及相鄰元素比對(其實就是一行冒泡排序比較),若是相等則跳過該元素,直到遍歷結束。git

ES6 中的 Set 去重

function distinct(array) {
   return Array.from(new Set(array));
}
複製代碼

甚至能夠再簡化下:程序員

function unique(array) {
    return [...new Set(array)];
}
複製代碼

還能夠再簡化下:github

let unique = (a) => [...new Set(a)]
複製代碼

思想: ES6 提供了新的數據結構 Set,Set 結構的一個特性就是成員值都是惟一的,沒有重複的值。(同時請你們注意這個簡化過程)面試

Object 鍵值對

function distinct(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
複製代碼

這種方法是利用一個空的 Object 對象,咱們把數組的值存成 Object 的 key 值,好比 Object[value1] = true,在判斷另外一個值的時候,若是 Object[value2]存在的話,就說明該值是重複的,可是最後請注意這裏obj[typeof item + item] = true沒有直接使用obj[item],是由於 由於 123 和 '123' 是不一樣的,直接使用前面的方法會判斷爲同一個值,由於對象的鍵值只能是字符串,因此咱們可使用 typeof item + item 拼成字符串做爲 key 值來避免這個問題。算法

而後:詢問面試官具體場景

(若是你考慮的這些和你問的,面試官不覺得然,可能本身都沒想,隨便讓你數組去重,可能這個面試官也...)數據庫

性能考慮(是想要最快的速度查到數據嗎?)

爲了測試這些解法的性能,我寫了一個測試模版,用來計算數組去重的耗時。 模版代碼以下:c#

// distinct.js

let arr1 = Array.from(new Array(100000), (x, index)=>{
    return index
})

let arr2 = Array.from(new Array(50000), (x, index)=>{
    return index+index
})

let start = new Date().getTime()
console.log('開始數組去重')

let arr = a.concat(b);

function distinct(arr) {
    // 數組去重
}

console.log('去重後的長度', distinct(arr).length)

let end = new Date().getTime()
console.log('耗時', end - start)
複製代碼

上面的多種數組去後,計算耗費時間

雙重 for 循環 > Array.filter()加 indexOf > Array.sort() 加一行遍歷冒泡 > Object 鍵值對去重複 > ES6中的Set去重

注意:這裏只是本人測試的結果,具體狀況可能與場景不一樣,好比排序過的數組直接去重,直接使用冒泡相鄰比較性能可能更好。你們也能夠本身嘗試一下,有問題歡迎一塊兒討論指出。

兼容性與場景考慮(數組中是否包含對象,NaN等?)

咱們要考慮這個數組中是否有null、undefined、NaN、對象若是兩者都出現,上面的全部數組去重方法並非都是適用哦,下面詳細說一下。

先說一下 == 和 === 區別

=== 嚴格相等,會比較兩個值的類型和值 == 抽象相等,比較時,會先進行類型轉換,而後再比較值 想更詳細瞭解轉換過程的能夠看這篇文章js 中 == 和 === 的區別

說一下我說的幾個類型的相等問題

let str1 = '123';
let str2 = new String('123');

console.log(str1 == str2); // true
console.log(str1 === str2); // false

console.log(null == null); // true
console.log(null === null); // true

console.log(undefined == undefined); // true
console.log(undefined === undefined); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(/a/ == /a/); // false
console.log(/a/ === /a/); // false

console.log({} == {}); // false
console.log({} === {}); // false
複製代碼

幾種去重函數針對帶有特殊類型的對比

indexOf 與 Set 的一點說明

上面代碼中console.log(NaN === NaN); // false, indexOf 底層使用的是 === 進行判斷,因此使用 indexOf 查找不到 NaN 元素

// demo1
var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1
複製代碼

Set能夠去重NaN類型, Set內部認爲儘管 NaN === NaN 爲 false,可是這兩個元素是重複的。

// demo2
function distinct(array) {
   return Array.from(new Set(array));
}
console.log(unique([NaN, NaN])) // [NaN]
複製代碼

具體去重比較

將這樣一個數組按照上面的方法去重後的比較:

var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN];
複製代碼
方法 結果 說明
雙層 for 循環 [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 對象和 NaN 不去重
Array.sort()加一行遍歷冒泡 [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] 對象和 NaN 不去重 數字 1 也不去重
Array.filter()加 indexOf [1, "1", null, undefined, String, String, /a/, /a/] 對象不去重 NaN 會被忽略掉
Object 鍵值對去重 [1, "1", null, undefined, String, /a/, NaN] 所有去重
ES6中的Set去重 [1, "1", null, undefined, String, String, /a/, /a/, NaN] 對象不去重 NaN 去重

內存考慮(去重複過程當中,是想要空間複雜度最低嗎?)

雖說對於 V8 引擎,內存考慮已經顯得不那麼重要了,並且真的數據量很大的時候,通常去重在後臺處理了。儘管如此,咱們也不能放過任何一個能夠證實本身優秀的,仍是考慮一下,嘿嘿。

以上的全部數組去重方式,應該 Object 對象去重複的方式是時間複雜度是最低的,除了一次遍歷時間複雜度爲O(n) 後,查找到重複數據的時間複雜度是O(1),相似散列表,你們也可使用 ES6 中的 Map 嘗試實現一下。

可是對象去重複的空間複雜度是最高的,由於開闢了一個對象,其餘的幾種方式都沒有開闢新的空間,從外表看來,更深刻的源碼有待探究,這裏只是要說明你們在回答的時候也能夠考慮到時間複雜度還有空間複雜度

另外補充一個誤區,有的小夥伴會認爲 Array.filter()indexOf 這種方式時間複雜度爲 O(n) ,其實不是這樣,我以爲也是O(n^2)。由於 indexOf 函數,源碼其實它也是進行 for 循環遍歷的。具體實現以下

String.prototype.indexOf = function(s) {
    for (var i = 0; i < this.length - s.length; i++) {
        if (this.charAt(i) === s.charAt(0) &&
            this.substring(i, s.length) === s) {
            return i;
        }
    }
    return -1;
};
複製代碼

補充說明第三方庫lodash

lodash 如何實現去重

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

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

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

總結

面試時回答面試官的問題,除了你能把代碼編出來運行出正確的結果,正確還包含對問題的獨到看法,還須要考慮下面的問題:

  • 優化
  • 代碼規範
  • 容錯性 其實若是是很是難的問題,對你的競爭對手來講,也是難的,關鍵在於你所表達出的解決問題的思路,甚至經過表達解題思路的方向,以及你考慮問題的全面性,其實不只僅這一道簡單面試題,算法題是如此。我是koala,今天這篇先寫這麼多,一塊兒加油。

參考文章

MDN中一些函數講解

深刻分析數組去重

JavaScript專題之數組去重

排序算法學習總結

關注我

  • 歡迎加我微信【coder_qi】,拉你進技術羣,長期交流學習...
  • 歡迎關注「程序員成長指北」,一個用心幫助你成長的公衆號...

Node系列原創文章

深刻理解Node.js 中的進程與線程

想學Node.js,stream先有必要搞清楚

require時,exports和module.exports的區別你真的懂嗎

源碼解讀一文完全搞懂Events模塊

Node.js 高級進階之 fs 文件模塊學習

相關文章
相關標籤/搜索