谈谈含复杂数据类型的数组去重问题

数组去重是一个老生常谈的问题。平常的处理可能只是对仅包含简单数据类型的数组进行操作,今天我们对复合数据类型做一个讨论。

_.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

最坏情况下算法的时间复杂度的计算公式为: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)。
不知道有没有比较高效的算法来实现。

你可能感兴趣的:(算法)