7-数据结构-集合(顺便拓展一些算法:数组拉平、排列组合、斐波拉契数列)

756 阅读6分钟

集合的基本概念

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

它在计算机科学中的主要应用之一是数据库,而数据库是大多数应用程序的根基。集合被用于查询的设计和处理。当我们创建一条从关系型数据库(Oracle、Microsoft SQL Server、MySQL 等)中获取一个数据集合的查询语句时,使用的就是集合运算,并且数据库也会返回一个数据集合。

当我们创建一条SQL 查询命令时,可以指定是从表中获取全部数据还是获取其中的子集;也可以获取两张表共有的数据、只存在于一张表中的数据(不存在于另一张表中),或是存在于两张表内的数据(通过其他运算)。这些SQL 领域的运算叫作联接,而SQL 联接的基础就是集合运算。

当前ES6给我们提供了set结构, 天生就实现了集合的效果

集合的几个运算理念

并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合,如下图所示:

并集.png

交集:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合,如下图所示:

交集.png

差集(补集):对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合,如下图所示:

补集.png

子集:验证一个给定集合是否是另一集合的子集,如下图所示:

子集.png

集合的基本方法

add(element):向集合添加一个新元素。

delete(element):从集合移除一个元素。

has(element):如果元素在集合中,返回true,否则返回false。

clear():移除集合中的所有元素。

size():返回集合所包含元素的数量。它与数组的length 属性类似。

values():返回一个包含集合中所有值(元素)的数组。

集合结构的自我实现

class SET{
    constructor() {
        this.items = {} // 存储集合内的基本数据结构
    }
    
    has(ele){ // 判断是否存在元素,该元素必须是自有属性
        return Object.prototype.hasOwnProperty.call(this.items,ele) // 集合只存储元素
    }
    
    add(ele){ // 往集合中添加新值,不包括重复的
        // 往里面添加元素前应该先判断是否存在元素
        if(!this.has(ele)){ // 如果不存在这么一个值,则添加,否则不能添加
            this.items[ele] = ele // 直接用元素的内容做索引
            return true // 表示添加成功
        }
        return false // 表示添加失败
    }
    
    delete(ele){ // 删除一个值
        if(this.has(ele)){ // 当这个元素存在的时候才能删,否则不能删除
            delete this.items[ele]
            return true // 删除成功
        }
        return false // 删除失败
    }
    
    clear(){ // 清空集合
        this.items = {}
    }
    
    size(){
        return Object.keys(this.items).length // Object.keys方法返回一个数组,包含对象所有自有的可枚举的属性名
    }
    
    values(){ // 获得集合内所有的值
        return Object.values(this.items) // Object.values方法返回一个数组,包含给定对象所有可枚举的属性的值
    }
    
    union(otherSet){ // 并集,当前的SET实例与otherSet的实例合并起来
        let unionSet = new SET() // 用一个新的集合来保存两个集合的内容
        this.values().forEach(value=>unionSet.add(value)) // 将本地实例的集合的值添加到新创建的集合中
        otherSet.values().forEach(value=>unionSet.add(value)) // 将其他实例的集合的值添加到新创建的集合中
        return unionSet
    }
    
    intersection(otherSet){ // 交集
        // 必须在A里面又在B里面
        let intersectionSet = new SET() // 同样先新建一个集合来保存

        // 先把两个集合中的值各自取出来比较谁更少一点,可以用少的来比较,更快更能节省性能,因为必须在少的里面也有才算交集
        let values = this.values() // 取出当前实例的集合的值保存在变量values中
        let otherValues = otherSet.values() // 取出其他实例的集合的值保存在变量otherValues中

        // 创建两个变量,大的和小的
        let bigger,smaller
        // 不要牺牲可读性来实现代码的精简,比如下面的判断可以用三目,但会影响后期的可读性与可维护性,不容易让人理解
        if(values.length > otherValues.length){ // 当本地实例的集合的值多于其他实例的集合的值时,小的等于数组,大的等于set集合
            bigger = this // 大的等于集合,这里集合用this指代是因为后面的forEach遍历里面大的要使用集合的has方法来判断是否拥有相同值
            smaller = otherValues // 小的等于数组,因为下面的forEach遍历是用小的
        }else{
            bigger = otherSet // 大的等于集合,这里集合用otherSet指代是因为后面的forEach遍历里面大的要使用集合的has方法来判断是否拥有相同值
            smaller = values // 小的等于数组
        }

        // 然后用小的来遍历每一个值与大的作比较
        smaller.forEach(value=>{
            if(bigger.has(value)){ // 判断大的当中是否也有这个值
                intersectionSet.add(value) // 如果大的中也存在这个值,则添加进去
            }
        })
        return intersectionSet
    }
    
    difference(otherSet){ // 补集
        let differenceSet = new SET() // 创建一个新的补集

        // A的补集必须是B里面没有的
        this.values().forEach(value=>{
            if(!otherSet.has(value)){ // 只有当自己有但别人没有的时候
                differenceSet.add(value) // 才添加进去
            }
        })
    }
    
    isSubsetOf(otherSet){ // 判断A是否是B的子集
        if(this.size() > otherSet.size()){ // 如果A的长度大于B的长度,那不可能是子集
            return false
        }
        let isSubset = true // 先默认是
        let values = this.values()
        for(let i = 0; i < values.length; i++){ // 要判断A是不是B的子集,先遍历A
            if(!otherSet.has(values[i])){ // 如果B里面没有A里面的值,那么A肯定也不是B的子集,直接改为false并跳出循环
                isSubset = false
                break
            }
        }
        return isSubset
    }
}

额外拓展的算法

数组拉平(数组扁平化)不使用flat方法

将一个多维数组展开成一维数组(万老说他面试腾讯的时候,被问的唯一一个与js有关的问题就是数组拉平,其余都是问网络安全HTTP协议...)

let arr = [[1,2],[3,5,[2,6,[9,8]]],[1,5,[2,4,8]],[1,[2,[3,[4,[5,[6]]]]]]];
function wFlat(arr){
    return arr.reduce((pre,curr) => {
        return pre.concat(Array.isArray(curr)?wFlat(curr):curr);
        // 该三目运算符的意思是,如果接收的参数是数组则进行拉平,如果拉到最后已经没有数组了,都是数组中的项目元素了,就直接进行拼接即可
    },[])
}
wFlat(arr)

排列组合算法

假设有一个数组,let a = [['A','B'],['a','b'],['1','2']],求它的全排列组合方式有多少种?

例如:Aa1,Aa2,Ab1,Ab2,Ba1,Ba2...

一个思路是:

先处理前两个子数组,前两个得出结果,再以此结果与剩下的继续排列组合,类似于reduce方法

let a = [['A','B'],['a','b'],['1','2']]let a2 = [['Aa','Ab','Ba','Bb'],['1','2']]所得出的结果是相同的,即先排数组a中前两个子数组的所有可能结果,合并结果之后就变成了数组a2中的第一个子数组的情况了,然后再排数组a2剩下的两个数组即可。

let a = [['A','B'],['a','b'],['1','2']]
function compistionStr(arr1,arr2){ // 排列两个子数组
    let resultStrArr = [] // 先定义一个结果数组
    // 因为不知道两个数组中元素的具体个数,所以需要两个for循环
    for(let i = 0; i < arr1.length; i++){
        for(let j = 0; j < arr2.length; j++){
            resultStrArr.push(String(arr1[i])+String(arr2[j]));
        }
    }
    return resultStrArr;
}

function PAC(arr){
    // reduce方法,会将 上一次迭代的结果 作为 这一次的初始值 继续与新值进行迭代
    return arr.reduce((total,current) => {
        return compistionStr(total,current);
    })
}

PAC(a) // 即可得出结果

斐波拉契数列

斐波拉契数列:除了第一项以外,每一项都是前两项之和

0 1 1 2 3 5 8 13 ...

function fibonacci(length){
    let [pre, curr] = [0, 1];
    for (let index = 0; index < length; index++){
        console.log(pre);
        [pre, curr] = [curr, curr + pre] // 使用解构赋值,依旧是上一次的结果作为这一次的基础值
    }
}

fibonacci(10)