談談含複雜資料型別的陣列去重問題
阿新 • • 發佈:2019-01-31
陣列去重是一個老生常談的問題。平常的處理可能只是對僅包含簡單資料型別的陣列進行操作,今天我們對複合資料型別做一個討論。
_.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)。 不知道有沒有比較高效的演算法來實現。