【翻译】集合 | 掘金技术征文-双节特别篇

258 阅读7分钟

第五章 集合

书籍出处: 《Learning JavaScript Data Structures and Algorithm》

作者: Loiane Groner

目前,我们已经学习了数组、栈、队列和链表等一些列数据结构。在本章,我们将学习集合结构。

集合结构由一些唯一且未排序的成员组成。这一结构的逻辑其实和数学里的有限集是一脉相承的。

在我们深入集合的实现方法时,我们先看看有限集的数学概念吧。在数学中,一个集合就是一个唯一的对象。

比如说,我们又一个自然数等集合。这个集合是由大于等于零的自然数组成的:N = {0,1,2,3,4,5,6…}。这个对象是被花括号包起来的。

当然,集合中还有空集的概念。一个没有成员的集合,被称为空集。比如说,一个24到29之间的质数的集合,就为空。

你也把集合理解为一个没有成员的数组。

在数学中,集合还有求交集、求并集、求差等操作。这些操作都会在本章被介绍到。

生成一个集合

这是set类的骨架:

function Set(){
	var items = {};
}

一个重要的细节是,我们是使用对象,而非数组来表示集合的成员。当然,我们也是可以用数组来实现set结构的。但是,在JavaScript中,是不允许一个对象的不同值对应一个相对的key的,而这也很好的符号了集合中唯一性的特性。

接下来,我们将要声明集合结构的常用方法:

  • add(value): 在集合中加入一个新成员
  • remove(value): 从集合中删除一个成员
  • has(value): 如果参数在集合内,会返回true;反之则返回false
  • clear( ): 清空集合
  • size( ): 返回集合成员的个数。这个方法和数组的length很像
  • values( ): 将集合的成员以数组的形式返回。

has方法

第一个要实现的是has方法。我们第一个实现它,是因为他将被add 和 remove方法使用。以下为实现的代码:

this.has = function(value){
	return value in items;
};

既然我们用了对象来存储集合的值,那么我们可以使用JavaScript的 in 操作符来验证一个值是否在items对象内。

但是,以下还有一个更好的验证方法:

this.has = function(value){
	return items.hasOwnProperty(value);
}

所有的JavaScript对象都可以使用hasOwnProperty方法。这个方法会返回一个布尔值,来告诉大家该对象是否有这个属性。

add方法

接下来要实现的是add方法:

this.add = function(value){
	if (!this.has(value){
		items[value] = value; //{1}
		return = true;
    }
    return false;
};

首先,函数会检验这个给定值是否在这个集合中***(行 {1})***,如果不存在则将之加入到集合中。反之,函数会返回false;

Note

我们使用value本身来作为新加入属性的索引,以帮助我们寻找相关属性。

remove方法 和 clear方法

接下来,我们要实现的是remove方法:

this.remove = function(value){
	if(this.has(value)){
		delete items[value]; // {2}
		return true;
	}
	return false;
}

在remove方法中,首先要验证给定值在集合中是否存在。如果存在,我们将会从集合中删除这个值***(行 {2})***,并返回true;反之,会返回false。

因为我们是用对象来存储集合成员,我们可以使用deleter操作符来删除相关属性***(行 {2})***。

要使用集合结构,我们可以使用以下的代码:

var set = new Set( );
set.add(1);
set.add(2);

Note

如果使用console.log来检验查看上述代码,我们会得到:

Object { 1: 1, 2: 2}

如我们所见,这是一个有两个属性的对象。在这个对象中,键和值是一样的。

如果要把链表清空,我们可以使用clear方法:

this.clear = function(){
	items = {}; // {3}
}

我们只要将items设置为空值即可***(行 {3})***。我们也可遍历整个集合,将每个成员逐个删除,但这样还是太复杂了。

size方法

下一个要实现的是会返回集合成员个数的size方法。有三种办法可以实现size方法。

第一个办法是使用一个length变量,在集合结构调用add方法或者remove方法后对length进行加减操作,就如我们在链表中做的那样。

第二个办法是使用JavaScript中对象的内置方法:

this.size = function( ){
	return Object.keys(items).length; // {4}
}

JavaScript对象的内置keys方法会返回一个包含所有属性的数组。这样,我们就可以使用数组的length属性了***(行 {3})***。

第三个方法是遍历集合的每一个成员,再逐个进行计数。这个方法在任何一个电脑浏览器都可以实现,其效果和之前的代码是一样的:

this.size = function(){
	var count = 0for(prop in items){ // {5}
		if(items.hasOwnProperty(prop)){ // {6}
			++count; // {7}
		}
	}
	return count
}

所以,我们我们首先要遍历集合的所有属性***(行 {5}),并检验每个属性是否是一个真实的属性(所以我们不会遍历第二次——行 {6})。如果检验为真,会对count加一(行 {7})***,并在推出循环后返回count变量。

Note

我们不能仅仅使用 for-in语句来遍历items的所有属性,再用count进行计数。我们还需要使用has方法(去验证items对象是否有这个属性)因为items的原型还继承了其他的属性(继承自Object基类的属性)。

values方法

我们可以使用对象的keys方法来获得包含集合所有属性的数组,而这样就得可以实现values方法了:

this.values = function( ){
	return Object.keys(items);
}

这段代码只能在版本较新的浏览器中运行。

如果我们想让代码能够在任何浏览器中运行,我们可以使用以下的代码:

this.valuesLegacy = function(){
	var keys = [];
	for (var key in items){ // {7}
		keys.push(key); //{8}
	};
	return keys;
}

首先,我们要遍历集合的所有成员***(行 {7}),再将之加入到数组中(行 {8})***, 最后再返回这个数组。

使用集合结构

我们已经构建完了集合结构,让我们看看如何使用集合结构吧:

var set = new Set( );

set.add(1);
console.log(set.values()); // 输出为[“1”]
console.log(set.has(1)); // 输出为 true
console.log(set.size()); // 输出为 1

set.add(2);
console.log(set.values()); // 输出为[“1”, ”2”]
console.log(set.has(2)); // 输出为 true
console.log(set.size()); // 输出为 1

set.remove(1);
console.log(set.values());// 输出为[“2”]

set.remove(2);
console.log(set.values());// 输出为[ ]

现在,我们的集合结构和ES6的集合非常像了。

集合的操作

我们可以实现集合的以下操作:

  • 并:求两个集合的并集
  • 交:求两个集合的交集
  • 差:求两个集合的差
  • 子集:确认一个集合是否为另一个集合的子集

求并集

并集在数学上的定义是集合A与集合B的联合,数学符号上表示为x 集合的数学定义为:

下图诠释了这个概念:

现在我们一起来实现求并集的操作:

this.union = function( otherSet ){
	var unionSet = new Set( ); // {1}
	
	var values = this.values(); // {2}
	for (var i = 0; i<values.length; i++){
		unionSet.add(values[i]);
	}

	values = otherSet.values(); // {3}
	for (var i = 0; i<values.length; i++){
		unionSet.add(values[i]);
	}
	return unionSet;
};

首先,我们要建立一个新的集合来作为两个集合的并集***(行 {1})。接下来我们要通过遍历的方法(行 {2}),得到第一个集合所有成员的值,并将之加入到并集中。之后,我们只要对参数里的集合做同样的事情就可以了(行 {3})***。最后。我们在函数内返回并集。

让我们来测试一下这些代码:

var setA = new Set( );
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set( );

setB.add(3);
setB.add(4);
setB.add(5);
setB.add(6);

var unionAB = set.union(setB);
console.log(unionAB.values( ));

代码的输出结果为[“1”, “2”, “3”, “4”, “5”, “6”]

求交集

交集在数学上的定义是集合A与集合B的交差部分,集合的数学定义为

这意味着x(集合成员)既存在与集合A,也存在于集合B。下图诠释了求交集的逻辑:

现在,进行代码实现:

this.intersection = function(otherSet){
	var intersectionSet = new Set(); // {1}

	var values = this.values();
	for (var i =0; i<values.length; i++ ){ // {2}
		if (otherSet.has(values[i])){ // {3}
			intersectionSet.add(values[i]); // {4}
		}
	}
	return intersectionSet;
}

让我们测试一下:

var setA = new Set( );
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set( );
setB.add(2);
setB.add(3);
setB.add(4);

var intersectionAB = setA.intersection(setB);
console.log(intersectionAB.values());

对于intersction方法,我们要找到即在实例集合中,又在参数集合中的成员。所以,我们要新建一个名为intersectionSet的集合来接受符合这个条件的成员***(行 {1})。之后,我们要遍历实例集合中的所有成员(行 {2}),并验证每一个成员是否存在于参数集合中(行 {3})。为此,我们可以使用之前讲过的has方法来验证其是否存在于另一个集合中。之后,如果这个成员存在于参数集合中,我们把这个成员添加至intersectionAB中(行 {4})***,并返回intersectionAB。

代码的输出结果为[“2”, “3”],因为2和3存在于集合A和集合B中。

求差集

差集在数学上的概念是,集合A与集合B的不同,用数学符号标记为 A – B,其数学定义为:

这意味着,差集中的成员是存在于集合A,但不存在于集合B。下图形象的展示了差集的概念:

现在,让我们用代码来实现difference方法:

this.difference = function(otherSet){
	var differenceSet = new Set(); // {1}
	var values = this.values();

	for (var i= 0; i< values.length; i++){  // {2}
		if( !otherSet.has(values[i])){	   // {3}
			diffenernceSet.add(values[i]); // {4}
		}
	}
	return differenceSet;
};

intersection方法里的成员是同时存在于两个集合的成员。而difference方法的成员是存在于集合A但不存在于集合B的成员。所以,这两种方法的区别仅仅在于行 {3}

让我们来测试一下上面的代码:

var setA = new Set();
setA.add(1);
setA.add(2);
setA.add(3);

var setB = new Set();
setB.add(2);
setB.add(3);
setB.add(4);

var differenceAB = setA.differnece(setB);
console.log(differneceAB.values( ));

代码的输出结果为['1'],因为只有1是只存在于集合A的。

求子集

我们要讲到的最后一个操作是求子集。子集在数学上的意思是集合A是集合B的子集,其数学定义为:

这意味着,集合A的每一个成员也要存在与集合B。下图展示了集合A为集合B的子集:

现在,让我们来实现subset方法:

this.subset = function(otherSet){

	if (this.size( ) > otherSet.size( )){	// {1}
		return false;
    } else {
        var values= this.values( );
        for (var i = 0; i<values.length; i++){	// {2}
            if (!otherSet.has(values[i])){ // {3}
            return false; // {4}
            }
        }
        return true;  // {5}
    }
};

我们首先要验证的是,实例集合的大小是否比参数集合的大。如果实例的比参数集合大,那么实例集合就不是参数集合大子集***(行 {3})***。子集的成员个数应当小于或等于父集的个数。

接下来,我们要遍历集合的所有成员***(行 {2}),并验证该成员是否存在于参数集合中(行 {3})。如果实例集合有成员不存在于参数集合,那么它们就不是子集关系,函数会返回false(行 {4})***。如果所有成员都在参数集合中,行 {4}将不会被执行,函数并在最后返回true(行 {5})

让我们检测一下之前到代码:

var setA = new Set();
setA.add(1);
setA.add(2);

var setB = new Set();
setB.add(1);
setB.add(2);
setB.add(3);


var setC = new Set();
setC.add(2);
setC.add(3);
setC.add(4);

console.log(setA.subset(setB));
console.log(setA.subset(setC));

我们有三个集合:集合A是集合B的子集,但是,集合A并不是集合C的子集,所以console.log(setA.subset(setC))的结构为false。

小结

在这一章,我们已经学会了如何构建一个与ES6的集合功能类似的集合。我们也讲了集合的一些操作(虽然他们在编程实践中并不常用),汝求交集、求并集、求差集、求子集。

在下一章我们将会讲到非序列结构的哈希和字典。

注:本文翻译自Loiane Groner的《Learning JavaScript Data Structures and Algorithm》

🏆 掘金技术征文|双节特别篇