1. 程式人生 > >談談含複雜資料型別的陣列去重問題

談談含複雜資料型別的陣列去重問題

陣列去重是一個老生常談的問題。平常的處理可能只是對僅包含簡單資料型別的陣列進行操作,今天我們對複合資料型別做一個討論。

_.isEqual

要實現陣列去重,我們首先要有一個比較兩個資料是否相等的函式。Underscore.js給我們提供了一個很好的功能函式,名為:_.isEqual()。在對複合資料型別的比較上,它在實現上引入的兩個類似於堆疊(後進後出)的陣列(只調用了其push()和pop()方法實現堆疊),並採用遞迴的方式巢狀判斷。其原始碼如下:

// eq函式只在isEqual方法中呼叫, 用於比較兩個資料的值是否相等
  // 與 === 不同在於, eq更關注資料的值
  // 如果進行比較的是兩個複合資料型別, 不僅僅比較是否來自同一個引用, 且會進行深層比較(對兩個物件的結構和資料進行比較)
  var eq = function(a, b, aStack, bStack) {
    // 檢查兩個簡單資料型別的值是否相等
    // 對於複合資料型別, 如果它們來自同一個引用, 則認為其相等
    // 如果被比較的值其中包含0, 則檢查另一個值是否為-0, 因為 0 === -0 是成立的
    // 而 1 / 0 == 1 / -0 是不成立的(1 / 0值為Infinity, 1 / -0值為-Infinity, 而Infinity不等於-Infinity)
    if (a === b) return a !== 0 || 1 / a == 1 / b;
    // 將資料轉換為布林型別後如果值為false, 將判斷兩個值的資料型別是否相等(因為null與undefined在非嚴格比較下值是相等的)
    if (a == null || b == null) return a === b;
    // 如果進行比較的資料是一個Underscore封裝的物件(通過判斷比較物件是否是_函式的原型)
    // 則將物件解封后獲取本身的資料(通過_wrapped訪問), 然後再對本身的資料進行比較
    // 它們的關係類似與一個jQuery封裝的DOM物件, 和瀏覽器本身建立的DOM物件
	
    // 以上的比較基本解決所有簡單資料型別的值的比較問題,對於複合資料型別會先通過比較Object.prototype.toString()方法在比較物件上作用後的結果,若不相等直接返回false,否則做進一步判斷
    if (a instanceof _) a = a._wrapped;
    if (b instanceof _) b = b._wrapped;
	
    var className = toString.call(a);
	// 不相等說明資料型別不一致,直接返回false
    if (className != toString.call(b)) return false;
    switch (className) {
	  // 字串,數字,日期和布林值通過值來比較
      case '[object String]':
        // 原始值和它們對應的封裝物件呼叫toString()方法後的結果是一樣的,如下:
		// Object.prototype.toString.call(new String('5'))-->"[object String]"
		// Object.prototype.toString.call('5')-->"[object String]"
        return a == String(b);
      case '[object Number]':
		// 通過+a將a轉成一個Number, 如果a被轉換之前與轉換之後不相等, 則認為a是一個NaN型別
        // 因為NaN與NaN是不相等的, 因此當a值為NaN時, 無法簡單地使用a == b進行匹配, 而是用相同的方法檢查b是否為NaN(即 b != +b)
        // 當a值是一個非NaN的資料時, 則檢查a是否為0, 因為當b為-0時, 0 === -0是成立的(實際上它們在邏輯上屬於兩個不同的資料)
        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
		// 對日期型別沒有使用return或break, 因此會繼續執行到下一步(無論資料型別是否為Boolean型別, 因為下一步將對Boolean型別進行檢查)
      case '[object Date]':
      case '[object Boolean]':
        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
        // millisecond representations. Note that invalid dates with millisecond representations
        // of `NaN` are not equivalent.
		// 將日期或布林型別轉換為數字
        // 日期型別將轉換為數值型別的時間戳(無效的日期格式將被換轉為NaN,從而無效的日期格式被認為是不相等的)
		// 布林型別中, true被轉換為1, false被轉換為0
        // 比較兩個日期或布林型別被轉換為數字後是否相等
        return +a == +b;
      // RegExps are compared by their source patterns and flags.
	  // 正則表示式型別, 通過source訪問表示式的字串形式
      // 檢查兩個表示式的字串形式是否相等
      // 檢查兩個表示式的全域性屬性是否相同(包括g, i, m)
		// 如果完全相等, 則認為兩個資料相等
      case '[object RegExp]':
        return a.source == b.source &&
               a.global == b.global &&
               a.multiline == b.multiline &&
               a.ignoreCase == b.ignoreCase;
		break;
		// 這裡是我自己加的一個判斷,因為在Underscore原來的程式碼一下結果為true:
		// _.isEqual({a:1},{a:1});// true
		// 而對以下結果則判定為false:
		// _.isEqual({a:function(){}},{a:function(){}}); // false
		// 我覺得不甚合理(如果不對請指出),故完善了此處的判斷,讓上面的結果為true
		case '[object Function]':
			var repReg = /\r|\n|\t|\v|\s*/g;
			return a.toString().replace(repReg,'') === b.toString().replace(repReg,'');
    }
	// 當執行到此時, ab兩個資料應該為型別相同的物件或陣列型別
    if (typeof a != 'object' || typeof b != 'object') return false;
	// 假設迴圈結構是相等的。檢測迴圈結構相等的演算法被ES標準15.12.3部分所採納(這是在赤果果的炫耀嗎),將操作抽象為'JO'
    var length = aStack.length;
    while (length--) {
	  // 線性搜尋。效能是跟某一處的巢狀結構的數量成反比的。
	  // 其實此處就是對一個複合資料型別全部遞迴判斷完成後的判斷條件。而該複合資料型別也可能是另一個複合資料型別的一部分
      if (aStack[length] == a) return bStack[length] == b;
    }
	// 物件的建構函式如果不同則認為是不想等的,但是Object instanceof Object為true,Function instanceof Function為true,認為他們是相等的
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
                             _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
      return false;
    }
    // Add the first object to the stack of traversed objects.
	// 將對比物件加入到堆疊已進行遞迴遍歷
    aStack.push(a);
    bStack.push(b);
    var size = 0, result = true;
    // Recursively compare objects and arrays.
	// 遞迴的進行物件和陣列的複合資料型別
    if (className == '[object Array]') {
      // Compare array lengths to determine if a deep comparison is necessary.
	  // size記錄陣列的長度,長度不相等直接返回false
      size = a.length;
      result = size == b.length;
      if (result) {
        // Deep compare the contents, ignoring non-numeric properties.
		// 遞迴進行檢測,忽略非數值的屬性(非數值屬性不會進行到這)
        while (size--) {
          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
        }
      }
    } else {
      // Deep compare objects.
	  // 深層比較物件
      for (var key in a) {
        if (_.has(a, key)) {
          // Count the expected number of properties.
		  // 記錄屬性的個數
          size++;
          // Deep compare each member.
		  // 深層次遞迴遍歷比較結果
          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
        }
      }
      // Ensure that both objects contain the same number of properties.
	  // 如果對某一層的符合資料型別比較結果為true,因為如果b物件包含a的所有屬性且值相等而b包含a不包含的屬性,此時也會返回true。骨堆它們的屬性數量進行比較(包含原型鏈上的屬性)
      if (result) {
        for (key in b) {
          if (_.has(b, key) && !(size--)) break;
        }
        result = !size;
      }
    }
    // Remove the first object from the stack of traversed objects.
	// 根據遞迴往“堆疊”裡push內容的順序,將已經進行過比較的內容出棧。
    aStack.pop();
    bStack.pop();
    return result;
  };

  // Perform a deep comparison to check if two objects are equal.
  _.isEqual = function(a, b) {
    return eq(a, b, [], []);
  };

有一點要特別注意的是,Underscore對_.isEqual({a:1},{a:1});的結果為true,而對_.isEqual({a:function(){}},{a:function(){}});的結果為false,這一點讓我尤為不解。我對其原始碼進行了小的改動,讓後者的比較也為true。

陣列去重

有了上面比較兩個任意值是否想等的函式,我們就可以進行陣列去重的操作了。 我們採用最簡單的二重迴圈的方式來實現,程式碼如下:
function removeRepeat(ary){
		var len = ary.length;
		for(var i=0;i<len;i++){
			var ie = ary[i];
			for(var j=i+1;j<len;j++){
				var je = ary[j];
				if(_.isEqual(ie,je)){
					console.error(ie,j);
					ary.splice(j--,1);
					len = ary.length;
				}
			}
		}
		return ary;
	}
	console.log(removeRepeat([{a:1},{a:1},{f:function(){}},{f:function(){}},{n:function(){return 1;}},{n:function(){
		return 1;}}]));// [{a:1},{f:function(){}},{n:function(){return 1;}}]

最壞情況下演算法的時間複雜度的計算公式為:n*(n-1)+(n-1)*(n-2)+......2*1,結果為O(n^2)。 還有一種實現,是定義一個空陣列,然後從左到右遍歷一遍要去重的陣列,將值push到空陣列,遍歷到下一個值的時候判斷該值是否與數組裡的值有重複,有重複就不操作,無重複則push進陣列。這樣的時間複雜度為1+2+3+......+(n-1),結果為O(n*(n-1)/2),也是:O(n^2)。 不知道有沒有比較高效的演算法來實現。