陣列去重的各種方式對比

幽涯發表於2018-06-17

陣列去重,是一個老生常談的問題了,在各廠的面試中也會有所提及,接下來就來細數一下各種陣列去重的方式吧;

對於以下各種方式都統一命名為 unique,公用程式碼如下:

// 生成一個包含100000個[0,50000)隨機數的陣列
var arr = [];
for(var i = 0; i < 100000; i++) {
    arr.push(Math.floor(Math.random() * 50000));
}
Array.prototype.unique = function() { // 演算法 };
console.log(arr.unique());  // 一個已經去重的陣列
複製程式碼

1、雙重遍歷

雙重遍歷的實現主要是通過兩次遍歷的對比,生成一個新的,不含重複資料的陣列;

其實現方式有如下兩種:

/*
 * 第一種實現方式:
 * 對陣列的每個元素在推入新陣列之前與新陣列中每個元素比較,如果沒有相同值則推入
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [], isRepeat;
    for(var i = 0, length = this.length; i < length; i++) {
        isRepeat = false;
        for(var j = 0, newLength = newArray.length; j < newLength; j++) {
            if(this[i] === newArray[j]) {
                isRepeat = true;
                break;
            }
        }
        if(!isRepeat) newArray.push(this[i]);
    }
    return newArray;
};
/*
 * 第二種實現方式
 * 將陣列的每個元素與其後面所有元素做遍歷對比,若有相同的值,則不推入新陣列,
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [], isRepeat;
    for(var i = 0, length = this.length; i < length; i++) {
        isRepeat = false;
        for(var j = i + 1; j < length; j++) {
            if(this[i] === this[j]) {
                isRepeat = true;
                break;
            }
        }
        if(!isRepeat) newArray.push(this[i]);
    }
    return newArray;
};

// 實測耗時
// 方式一:2372 ms
// 方式二:4025 ms
複製程式碼

2、Array.prototype.indexOf()

通過 indexOf 方法查詢值在陣列中的索引,並通過對索引的判斷來實現去重;

主要實現方式有下面兩種:

/**
 * 第一種實現方式
 * 結合陣列的 filter 方法,將相同值的第一個合併到一個新的陣列中返回
 * indexOf 檢測到的索引為出現當前值的第一個位置
 * 若 indexOf 檢測到的索引和當前值索引不相等則說明前面有相同的值
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    return this.filter(function(item, index, array) {
        return array.indexOf(item) === index;
    });
};
/**
 * 第二種實現方式
 * 對陣列進行遍歷,並將每個元素在新陣列中匹配
 * 若新陣列中無該元素,則插入
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [];
    this.forEach(function(item) {
        if(newArray.indexOf(item) === -1) newArray.push(item);
    });
    return newArray;
};

// 實測耗時
// 方式一:3972 ms
// 方式二:2650 ms
複製程式碼

3、Array.prototype.sort()

sort 方法可對陣列進行排序,此時相同的值就會被排到一起,然後通過相鄰元素比較就可知是否有相同值了;

舉個栗子:

/**
 * 第一種實現方式
 * 先將陣列通過 sort 排序
 * 再遍歷陣列,將每個元素與其前面一個元素比較
 * 若值不同則表示該元素第一次出現,則插入到新陣列
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [];
    this.sort();
    for(var i = 0, length = this.length; i < length; i++) {
        if(this[i] !== this[i - 1]) newArray.push(this[i]);
    }
    return newArray;
};
/**
 * 第二種實現方式
 * 先將陣列通過 sort 排序
 * 再遍歷陣列,將每個元素與插入到新陣列中的最後一個元素比較
 * 若值不同則表示該元素第一次出現,則插入到新陣列
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [];
    this.sort();
    for(var i = 0, length = this.length; i < length; i++) {
        if(this[i] !== newArray[newArray.length - 1]) newArray.push(this[i]);
    }
    return newArray;
};

// 實測耗時
// 方式一:105 ms
// 方式二:112 ms
複製程式碼

由於方式二在每次比較的時候需要重新計算一次 newArray.length 故會稍微比方式一慢一點;

3、Array.prototype.includes(searchElm, fromIndex)

該方法判斷陣列中是否存在指定元素

引數:

  • searchElm:需要查詢的元素
  • fromIndex:開始查詢索引位置(若未負值,則從 array.length - fromIndex 位置開始查詢

返回值:

  • Boolean:陣列中是否存在該元素
/**
 * 實現方式
 * 遍歷陣列,通過 includes 判斷每個值在新陣列中是否存在
 * 若不存在,則將值插入到新陣列中
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArray = [];
    this.forEach(function(item) {
        if(!newArray.includes(item)) newArray.push(item);
    });
    return newArray;
};

// 實測耗時:2597 ms
複製程式碼

4、Array.prototype.reduce()

/**
 * 實現方式
 * 先將陣列進行排序
 * 然後利用 reduce 迭代和累加的特性,將符合要求的元素累加到新陣列並返回
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    return this.sort().reduce(function(newArr, curr) {
        if(newArr[newArr.length - 1] !== curr) newArr.push(curr);
        return newArr;
    }, []);
};

// 實測耗時:127 ms
複製程式碼

5、物件的鍵值對

利用物件的 key 不能重複的特性來去重; 之前看到有人使用物件的鍵值對去重的時候,直接將陣列的每個值設定為物件的 keyvalue 都為1,每出現一個相同的值時就 value++,這樣既可以去重,又可以知道對應的值出現的次數,例如:

var array = ['a', 'b', 'c', 'a', 'a', 'c'];
var newArr = [], obj = {};
array.forEach(function(item) {
    if(obj[item]) {
        obj[item]++;
    } else {
        obj[item] = 1;
        newArr.push(item);
    }
});
console.log(newArr); // ["a", "b", "c"]
console.log(obj);    // {a: 3, b: 1, c: 2}
複製程式碼

咋一看好像很完美,可是仔細一想,會發現有以下幾點原因:

  • 若陣列的值中出現了隱式型別轉換成字串後相等的值,則無法區分,例如 '1' 和 1;
  • 若陣列中的值有物件,寫成 key 之後都是 [object Object],無法區分;

解決方案:

若元素值的型別為 object,則通過 JSON.stringify 方法進行轉換;

Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var newArr = [], obj = {};
    this.forEach(function(item) {
        if(!obj[typeof item + JSON.stringify(item)]) {
            obj[typeof item + JSON.stringify(item)] = 1;
            newArr.push(item);
        }
    });
    return newArr;
};

// 實測耗時:142 ms
複製程式碼

6、Set

Set 物件的特性就是其中的值是唯一的,可利用該特性很便捷的處理陣列去重的問題;

/**
 * 實現方式一
 * 將陣列轉換成 Set 物件
 * 通過 Array.from 方法將 Set 物件轉換為陣列
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    var set = new Set(this);
    return Array.from(set);
};

// 實測耗時:45 ms

/**
 * 實現方式二
 * 將陣列轉換成 Set 物件
 * 通過 Array.from 方法將 Set 物件轉換為陣列
 */
Array.prototype.unique = function() {
    if(!Array.isArray(this)) throw new Error('Type Error: The target should be an Array!');
    return [...new Set(this)];
};

// 實測耗時:65 ms
複製程式碼

由以上耗時結果可以看出:

  • filter, forEach 等方法都會對全陣列進行遍歷;
  • indexOf, for+break, includes 等方法會對陣列遍歷,在滿足條件的地方跳出遍歷

相關文章