第五章 集合
作者: 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 = 0;
for(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》