跟underscore一起學陣列去重

EdwardXuan發表於2019-03-12

引子

陣列去重是一個老生常談的話題,在面試中也經常會被問道。對於去重,有兩種主流思想:

  1. 先排序,線性遍歷後去重,時間複雜度O(n*log2n);
  2. 使用雜湊,空間換時間,時間複雜度O(n);

上一篇文章,我分析了underscore的函式是如何組織的,我們能夠依照這種方法書寫自己的函式庫,這篇文章,來看看關於函式去重underscore是如何做的?

Underscore的去重

功能介紹

underscore的去重是指陣列(Arrays)中uniq函式,其API如下:

uniq _.uniq(array, [isSorted], [iteratee]) 別名: unique
說明:返回 array去重後的副本, 使用 === 做相等測試. 如果您確定 array 已經排序, 那麼給 isSorted 引數傳遞 true值, 此函式將執行的更快的演算法. 如果要處理物件元素, 傳參 iterator 來獲取要對比的屬性.

上述API主要想說明幾點:

  1. 返回陣列副本,不影響原陣列
  2. 相等的標準是a===b,表明不僅要值相等,型別也需要相等
  3. 如果陣列是排序的,去重運算效率更高
  4. uniq也可以比較物件,前提是需要指定比較的物件屬性

我們簡單使用以下_.uniq(array, [isSorted], [iteratee]),如下:

  console.log(_.uniq([1,4,2,2,3,3]));
  console.log(_.uniq([1,2,2,2,3,3],true));
  console.log(_.uniq([{
            name:1,
            gender:"male"
        },{
            name:2,
            gender:"female"
        },{
            name:2,
            gender:"male"
        },{
            name:4,
            gender:"male"
    }],true,"gender"));
複製程式碼

結果如下:

跟underscore一起學陣列去重

去重思想及實現

underscore去重的核心思想:

新建結果集陣列res,遍歷待去重陣列,將每個遍歷值在res陣列中遍歷檢查,將不存在當前res中的遍歷值壓入res中,最後輸出res陣列。

    function uniq(array){
        var res = [];
        array.forEach(function(element) {
            if(res.indexOf(element)<0){
                res.push(element);
            }
        }, this);
        return res;
    }
    console.log(uniq([1,4,2,2,3,3]));  //[1,4,2,3]
複製程式碼

其中如果陣列是排序的,去重運算效率更高,因為排序能夠將相同的數排列在一起,方便前後比較。

    function uniq(array, isSorted) {
        var res = [];
        var seen = null;

        array.forEach(function (element,index) {
            if (isSorted) { 
                //當陣列有序
                if(!index || seen !== element) res.push(element);
                seen = element;
            } else {
                if (res.indexOf(element) < 0) {
                    res.push(element);
                }
            }
        }, this);
        return res;
    }
    console.log(uniq([1,2,"2",3,3,3,5],true)); //(5) [1, 2, "2", 3, 5]
複製程式碼

對於物件的去重,我們知道{}==={}為false,所以使用===比較物件在實際場景中沒有意義。
在這裡我舉個實際場景的例子:

我要在小組中選擇一名男生(male)和一名女生(female),小組組員情況如下:

var array = [{
    name:"Tom",
    gender:"female"
},{
    name:"Lucy",
    gender:"female"
},{
    name:"Edward",
    gender:"male"
},{
    name:"Molly",
    gender:"female"
}] 
複製程式碼

我們修改上面的uniq

    function uniq(array, isSorted, iteratee) {
        var res = [];
        var seen = [];
        array.forEach(function (element, index) {
            if (iteratee) {
                //判斷iteratee是否存在,存在的話,取出真正要比較的屬性
                var computed = element[iteratee];
                if (seen.indexOf(computed) < 0) {
                    seen.push(computed);
                    res.push(element);
                }
            } else if (isSorted) {
                //當陣列有序
                if (!index || seen !== element) res.push(element);
                seen = element;
            } else {
                if (res.indexOf(element) < 0) {
                    res.push(element);
                }
            }
        }, this);
        return res;
    }
    console.log(uniq([{
            name:"Tom",
            gender:"female"
        },{
            name:"Lucy",
            gender:"female"
        },{
            name:"Edward",
            gender:"male"
        },{
            name:"Molly",
            gender:"female"
        }],true,"gender")); 
複製程式碼

結果如下:

跟underscore一起學陣列去重

underscore的uniq的實現,基本上使用的上述思想。在附錄中我附上了原始碼和一些註釋。

關於去重的思考

上述我分析了underscore的uniq函式實現,在這之前我也看過諸如《JavaScript去重的N種方法》...之類的文章,underscore中的uniq函式實現方法並不是最優解,至少從時間複雜度來講不是最優。
那麼為什麼underscore不用Set物件來解決去重問題,使用indexof查詢的時間複雜度是O(n),而hash查詢是O(1)
我個人認為Set是ES6中引的物件,underscore是為了考慮相容性問題
那為什麼不用obj作為Set的替代方案呢?
這裡我猜是underscore的設計者只想用自己內部實現的_.indexOf函式。此處是我的猜測,大家如果有想法,歡迎大家留言!
下面我附上ES6的實現(大家最熟悉的):

var a = [1,1,2,3,4,4];
var res = [...new Set(a)];
複製程式碼

再附上obj的實現:

    function uniq(array,iteratee){
        var res = [];
        var obj = {};
        array.forEach(function(element) {
            var computed = element;
            if(iteratee) computed = element[iteratee];
            if(!obj.hasOwnProperty(computed))
                obj[(typeof computed)+"_"+JSON.stringify(computed)] = element;
            }, this);
            for(var p in obj){
                res.push(obj[p]);
            }
            return res;
        }
    uniq([1,"1",2,3,4,4]);// (5) [1, "1", 2, 3, 4]
複製程式碼

附錄

underscore的uniq函式原始碼及註釋:

    _.uniq = _.unique = function(array, isSorted, iteratee, context) {
    if (array == null) return [];
    if (!_.isBoolean(isSorted)) {
      //如果沒有排序
      context = iteratee;
      iteratee = isSorted;
      isSorted = false;
    }
    /**
    ** 此處_.iteratee
    **  function (key){
    *      return function(obj){
    *        return obj[key];
    *      }
    **  }
    **  key就是這裡的iteratee(物件的屬性),這裡使用了閉包
    **/
    if (iteratee != null) iteratee = _.iteratee(iteratee, context); 
    var result = [];//返回去重後的陣列(副本)
    var seen = [];
    for (var i = 0, length = array.length; i < length; i++) {
      var value = array[i];//當前比較值
      if (isSorted) {
        //如果i=0時,或者seen(上一個值)不等於當前值,放入去重陣列中
        if (!i || seen !== value) result.push(value); 
        seen = value;//儲存當前值,用於下一次比較
      } else if (iteratee) {
        var computed = iteratee(value, i, array);
        if (_.indexOf(seen, computed) < 0) {
          seen.push(computed);
          result.push(value);
        }
      } else if (_.indexOf(result, value) < 0) {
        result.push(value);
      }
    }
    return result;
  };
複製程式碼

相關文章