【5】js集合的实现-JavaScript学习数据结构

326 阅读6分钟

集合数据结构的概念

集合(Set,或简称集),是数学中的概念,是指具有某个特定性质的事物的总体,里面的每个事物称为集合的一个元素。如所有的自然数就是一个集合,在数学中的表示为:N = {0, 1, 2, 3, 4, 5, 6, …}。不仅仅是数字,集合的元素可以是任何事物,可以是人,可以是物,也可以是字母或数字等。如下图所示,一些多边形凑在一起也组成了一个集合。创建一个集合其实就是把事物进行归类,一类东西就是一个集合。

一个包括一些多边形的集合

集合具有的特性包括:

  • 无序性:集合里的东西是没有属性的,这也是其作为一种数据结构与列表的不同。如动物(集合)包括:狗、猫、鱼……,这里各种动物的罗列是没有先后关系的。
  • 互异性:同一个集合里的元素是不相同的,即每个只出现一次。如动物(集合)包括:狗、狗、猫。鱼……,这里说了两遍狗,是不行的。
  • 确定性:对于一个集合,某个元素那么在这个集合里,要么不在这个集合里,在不在的结果是确定的。如“大象”在不在动物集合里?“杯子”在不在动物集合里。

数据结构中的集和数学中的集是一样的,在中,数据项是无序的,也不允许存在相同数据项。集支持添加、删除和查找项目。一些语言内建对集的支持,而在其它语言中,可以利用散列表实现集,JavaScript中即可以用对象来实现。

对象实现集合

JavaScript中有原生就有Set类,但是这也是语言给我们封装好的,虽然可以直接拿来用,但是作为学习,有必要自己来实现Set,而不是仅仅会使用而已。上面我们已经对集合的概念有了认识,要实现集合,那首先要确定集合应该具有哪些操作。

  • add(element):向集合中添加一个新元素
  • delete(element):删除一个元素
  • clear():清空整个集合
  • has(element):集合中有没有某个元素,返回Boolean
  • size():集合中有多少元素,返回数量,同数组的length
  • values:将集合返回成一个数组,数组包含结合所有的元素
class Set {
    constructor() {
        this.items = {};
    }

    has(element) {
        //return element in this.items;
        // return this.items.hasOwnProperty(element);
        return Object.prototype.hasOwnProperty.call(this.items, element)
    }

    add(element) {
        if (!this.has(element)) {
            this.items[element] = element;
            return true;
        }
        return false;
    }

    delete(element) {
        if (!this.has(element)) {
            return false;
        } else {
            delete this.items[element];
            return true;
        }
    }

    clear() {
        this.items = {}
    }

    size() {
        let counter = 0;
        for (let key in this.items) {
            //这里还要验证集合(对象)中自己有该属性,而不是原型上的属性
            if (this.items.hasOwnProperty(key)) {
                counter++;
            }
        }
        return counter;
    }

    values() {
        // return Object.values(this.items)
        let result = [];
        for (let key in this.items) {
            if (this.items.hasOwnProperty(key)) {
                result.push(key)
            }
        }
        return result;
    }
}

在JavaScript中,利用对象来自己实现集合非常容易,因为JavaScript中已经内置了大量好用的对象的方法。

  • obj.hasOwnProperty(property) / property in obj:对象中是否有某个元素(属性),返回Boolean
  • Object.values(obj)Object.keys(obj):对象变成数组
  • for in:遍历的是对象的属性
for (x in object) {
  statement
}

//里面的x表示属性

对象的方法有很多,之前的文章对数组的常用方法已经有了一个梳理,并且自己去实现这些方法。但对于对象的很多方法,有时间再认真捋一遍,以及对象和数组之间的关系,虽然数组也是对象,但在使用中(API boy),两者还是有些区别。

集合的运算

数学中的集合运算

集合源于数学中的概念,这里数据结构集合的元素也是和数学是一模一样的。包括并集、交集、差集、子集。

集合的运算

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

集合运算JavaScript的实现

//并集:创建一个新集合,把要运算的两个集合都add到新集合里去
union(otherSet) {
        const unionSet = new Set();
        this.values().forEach(x => unionSet.add(x));
        otherSet.values().forEach(x => unionSet.add(x));
        return unionSet;
}

//交集:创建一个新集合,两重循环遍历(短的在外侧,为性能优化),将重复的add到新集合中
intersection(otherSet) {
        const intersectionSet = new Set();
        const values = this.values();
        const otherValues = otherSet.values();
        let bigSet = values;
        let smallSet = otherValues;

        if (values.length < otherValues.length) {
            bigSet = otherValues;
            smallSet = values;
        }

        smallSet.forEach(x => {
            if (bigSet.includes(x)) {
                intersectionSet.add(x)
            }
        })
        return intersectionSet;
}

//差集:创建一个新集合,把在集合1中而不在集合2中的元素add到新集合中
difference(otherSet) {
        const differenceSet = new Set();
        let values = this.values();

        values.forEach(x => {
            if (!otherSet.has(x)) {
                differenceSet.add(x)
            }
        })
        return differenceSet;
}

//子集:返回的是Boolean
isChildOf(otherSet) {

        //这里的if判断不要也可以,加上是为了性能优化,更快的判断为错
        if(this.size > otherSet.size) {
            return false;
        }
        const values = this.values();
        return values.every(x => otherSet.has(x))
}

实现结合的这些运算方法并不难,并且有很多种方法都可以,但逻辑都是一样的。在JavaScript中,实现这些逻辑,尤其是数组的遍历操作上,上面的实现过程中反复用到了数组的迭代方法,如forEach。这些方法是函数式编程的基础。至于函数式编程与OOP编程,这是一个庞大的话题,没有大量的积累是无法去体会的,这里暂且有函数式编程这个概念即可。同时,不管是OOP还是functional Programming,只是不同的编程风格,我始终觉得代码之后的想法和逻辑才是最重要的,不同编程风格只是不容工具而已。

JavaScript原生Set类

ECMAScript 2015 新增了 Set 类作为 JavaScript API 的一部分。Set类可以把数组转换成集合,利用new Set()实例化即可即可轻松创建一个集合。

//实例化传入的参数为数组
const arr = [1, 2, 3, 4, 4];
const set = new Set(arr)

//set 为集合 {1, 2, 3, 4},会自动去除重复的4;

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用,包括NaN、undefined都可以。判断里面的元素是否重复,Set判断用的是===

const set = new Set([1, 2, 3, 4, "4"]);
//{1, 2, 3, 4, "4"}  数字4和字符4是不同的元素,不是重复的。

之前我们自己实现的Set 类实现了并集、交集、差集、子集等数学运算,然而 ES2015 原生的 Set 并没有 这些功能。但是可以通过扩展运算符来快速进行集合的数学运算。具体过程为:

  • 将集转化为数组
  • 执行并集、交集、差集,子集运算
  • 将结果转化为集合(并集、交集、差集返回一个新集合,子集运算返回Boolean)

扩展运算符可以将集合转化成数组。

let set = new Set(1, 2, 3, 4, 5);

console.log([...set]); //[1, 2, 3, 4, 5]

假如现在有两个集合setA和setB,现在利用原生的Set和扩展运算符来对二者进行集合运算。

let setA = new Set([1, 2, 3])
let setB = new Set([1, 2, 3, 4, 5])

//并集
let unionSet = new Set([...setA, ...setB])

//交集
let intersectionSet = new Set([...setA].filter(x => setB.has(x)))

//差集
let differenceSet = new Set([...setA].filter(x => !setB.has(x)))

//子集(返回Boolean)
let AisChildOfB = [...setA].every(x => setB.has(x))

参考资料