数据结构 -- 集合

465 阅读5分钟

这是我参与11月更文挑战的第13天, 活动详情查看:2021最后一次更文挑战

介绍

集合是由一组无序且唯一(即不能重复)的项组成的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。

在数学中,集合是一组不同的对象(的集)。比如,一个由大于或等于0的整数组成的自然数集合:N = {1,2,3,4,5,6...}。集合中的对象列表用 "{}" (大括号)包围。

空集。不包含任何元素的集合。用 "{}" 表示。

可以把集合想象成一个既没有重复元素,也没有顺序概念的数组。

创建一个集合

这里以es6中 Set 类的实现为基础,以下是我们的 Set 类的骨架:

function Set () {
    var items = {}; // 这里使用的是对象(也可以用数组),js对象不允许一个键指向两个不同的属性,保证集合里的元素都是唯一的
}

集合可用的方法

  1. add(value):向集合添加一个新的项。
  2. remove(value): 从集合移除一个值。
  3. has(value): 如果值在集合中,返回true,否则返回false。
  4. clear(): 移除集合中的所有项。
  5. size(): 返回集合所包含元素的数量。与数组的length属性类似。
  6. values(): 返回一个包含集合中所有值的数组。
has(value) 方法

首先实现 has(value) 方法,因为它会被 add、remove 等其他方法调用。

this.has = function(value){
    return value in items; // 既然我们使用对象来存储集合的值,就可以用js的in操作验证
}

或者还有个好方式:

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

所有的 js 对象都有 hasOwnProperty 方法。这个方法返回一个表明对象是否具有特定属性的布尔值。

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

对于给定的 value ,可以检查它是否存在于集合中。如果不存在,就把 value 添加到集合中 行{1},返回true,表示添加了这个值。如果集合中已经有了这个值,返回 false ,表示没有添加它。

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

在remove方法中,我们会验证给定的value是否存在于集合中。如果存在,就从集合中移 除value(行{2}),返回true,表示值被移除;否则返回false。 既然用对象来存储集合的items对象,就可以简单地使用delete操作符从items对象中移除 属性(行{2})。

clear 方法
this.clear = function(){
    items = {}; // 重置,重新赋值给它
}
size 方法

第一种方法就是使用一个 length 变量,每当使用 add 或 remove 方法时控制它。

// 方法二:
// JavaScript的Object类有一个keys方法,它返回一个包含给定对象所有属性的数组。可以使用这个数组的length属性返回items对象的属性个数
this.size = function () {
    return Object.keys[items].length;
}
// 方法三:
this.sizeLegacy = function () {
    var count = 0;
    for(var prop in items){ // 遍历items对象的所有属性
        if(items.hasOwnProperty(prop)){ // 检查它们是否是对象自身属性,避免重复计数
            ++ count;
        }
        return count;
    }
}

这里需要注意的是:方法三中,不能简单地使用for-in语句遍历items对象的属性,递增count变量的值。 还需要使用 has 方法(以验证items对象具有该属性),因为对象的原型包含了额外的属性(属性既有继承自JavaScript的Object类的,也有属于对象自身,未用于数据结构的)。

values 方法
this.values = function () {
    return Object.keys(items); // 提取items对象所有属性,以数组形式返回
}
以上代码只能在现代浏览器中运行。既然在本书中我们使用的测试浏览器是ChromeFirefox,代码就能工作。如果想让代码在任何浏览器中都能执行,可以用下面的等价

// 遍历items对象的所有属性(行{7}),把它们添加一个数组中(行{8}),并返回这个数组。
this.valuesLegacy = function(){
    var keys = [];
    for(var key in items){
        keys.push(key);
    }
    return keys;
}
使用 Set 类
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()); // 2

set.remove(1);
console.log(set.values()); // ['2']

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

集合操作

  1. 并集: 对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
  2. 交集: 对于给定的两个集合,返回一个包含两个集合中共有元素的新集合。
  3. 差集: 对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在第二个集合的元素的新集合。
  4. 子集: 验证一个给定集合是否是另一集合的子集。
并集

数学中定义: A∪B = { x | x ∈ A∨x ∈ B } ,意思就是x存在于A中,或x存在于B中,如下图: image.png 用代码表示:

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})。接下来,获取第一个集合(当前的Set类实例)所有的值(values),遍历并全部添加到代表并集的集合中(行{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);
var unionAB = setA.union(setB);
console.log(unionAB.values()); // ['1','2','3','4','5','6']
交集

数学中定义: A∩B = { x | x ∈ A∧x ∈ B } , 意思是x(元素)存在于A中,且x存在于B中。下图展示了交集操作: image.png 代码表示:

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;
}

intersection方法需要找到当前Set实例中,所有也存在于给定Set实例中的元素。首先创 建一个新的Set实例,这样就能用它返回共有的元素(行{1})。接下来,遍历当前Set实例所有 的值(行{2}),验证它们是否也存在于otherSet实例(行{3})。可以用这一章前面实现的has 方法来验证元素是否存在于Set实例中。然后,如果这个值也存在于另一个Set实例中,就将其 添加到创建的intersectionSet变量中(行{4}),最后返回它。

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()); // ['2,'3']
差集

数学中定义: A - B = { x | x ∈ A ∧ x ∉ B } , 意思是 x (元素)存在于A中,且 x 不存在于B中。如图: image.png 代码实现:

this.difference = function (otherSet) {
    var diffrenceSet = new Set(); // {1}
    var values = this.values();
    for(var i=0;i<values.length;i++){ // {2}
        if(!otherSet.has(values[i])){ // {3}
            differenceSet.add(values[i]); // {4}
        }
    }
    return differenceSet;
}

intersection方法会得到所有同时存在于两个集合中的值。而difference方法会得到所 有存在于集合A但不存在于B的值。因此这两个方法在实现上唯一的区别就是行{3}。只获取不存 在于otherSet实例中的值,而不是也存在于其中的值。行{1}、{2}和{4}是完全相同的。

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.difference(setB);
console.log(differenceAB.values()); // ['1']
子集

数学概念:集合A是集合B的子集(或者集合B包含了集合A),表示为 A⊆B,定义:∀x { x ∈ A → x ∈ B } ,意思是集合A中的每一个x(元素),也需要存在于B中。如下图: image.png 代码实现:

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;
    }
}

首先需要验证的是当前Set实例的大小。如果当前实例中的元素比otherSet实例更多,它就不是一个子集(行{1})。子集的元素个数需要小于或等于要比较的集合。 接下来要遍历集合中的所有元素(行{2}),验证这些元素也存在于otherSet中(行{3})。 如果有任何元素不存在于otherSet中,就意味着它不是一个子集,返回false(行{4})。如果 所有元素都存在于otherSet中,行{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)); // true
console.log(setA.subset(setC)); // false

综上,家人们,我们一起实现了一个完备的 Set 类。