[路飞]_并查集

176 阅读3分钟

「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战

并查集解决的是连通性的问题,我们在解决问题时找到连接关系是重点。另外并查集是一种高度抽象的数据结构,针对不同的场景,并查集内部的实现可能不一样。

并查集本身针对不同的侧重点也有不同的实现版本。有快速查找、快速合并、合并优化、完整版(包括了路径压缩和合并优化)等等。

快速查找

快速查找中 find 的速度很快。合并的时候要循环遍历,效率比较慢。

class QuickFind {
    constructor(n) {
        //这里数组的初始化个数是 n + 1,因为有些场景
        this.color = new Array(n + 1).fill(0).map((item, index) => index)
    }

    find(x) {
        return this.color(x)
    }

    merge(a, b) {
        const ca = this.color[a]
        const cb = this.color[b]
        //染色,将颜色 b 染成颜色 a
        for (let i = 0; i < this.color.length; i++) {
            if (this.color[i] === cb) this.color[i] === ca
        }
    }
}

快速合并

快速合并中,直接把 b 集合合并到 a 集合,合并的效率很高。但是每次查找都要一级一级往上查,效率不高。

class QuickUnion {
    constructor(n) {
        //初始化父节点数组,每个节点的父节点默认为自己
        this.pa = new Array(n + 1).fill(0).map((item, index) => index)
    }

    find(x) {
        if (this.pa[x] === x) return x
        //递归找到父节点
        return this.find(this.pa[x])
    }

    merge(a, b) {
        //找到a的根节点
        const ra = this.find(a)
        //找到b的根节点
        const rb = this.find(b)
        
        //将b集合合并到a集合
        this.[ra] = rb
    }
}

合并优化

把节点数少的集合合并到节点数大的集合当中。因为合并后再做路径优化时,数量少的集合路径优化的次数更少。

class WeightedQuickUnion {
    constructor(n) {
        //初始化父节点数组,每个节点的父节点默认为自己
        this.pa = new Array(n + 1).fill(0).map((item, index) => index)
        
        //初始化每个集合的节点数
        this.size = new Array(n + 1).fill(1)
    }

    find(x) {
        if (this.pa[x] === x) return x
        //递归找到父节点
        return this.find(this.pa[x])
    }

    merge(a, b) {
        //找到a的根节点
        const ra = this.find(a)
        //找到b的根节点
        const rb = this.find(b)

        //如果a和b在一个集合中则不需要合并
        if (ra === rb) return

        //把节点总数小的集合合并到节点总数多的集合里
        //更新节点总数多的集合为 a和b之和
        if (this.size[ra] < this.size[rb]) {
            this.pa[ra] = rb
            this.size[rb] += this.size[ra]
        } else {
            this.pa[rb] = ra
            this.size[ra] += this.size[rb]
        }
    }
}

算法竞赛版

合并优化对性能影响不是太大,而合时的代码比较繁琐,容易出错,所以在竞赛时可以不做优化。

class UnionSet {
    constructor(n) {
        //初始化父节点数组,每个节点的父节点默认为自己
        this.pa = new Array(n + 1).fill(0).map((item, index) => index)
    }

    get(x) {
        //查找x的父节点,并且完成路径优化
        //优化后,x的父节点指向所在集合的根节点
        return this.pa[x] = this.pa[x] === x ? x : this.get(this.pa[x])
    }

    merge(a, b) {
        //将b集合合并到a集合
        this.pa[this.get(a)] = this.get(b)
    }
}

完整版

完成版包括了路径压缩和合并优化,性能是做好的。

class UnionSet {
    constructor(n) {
        //初始化父节点数组,每个节点的父节点默认为自己
        this.pa = new Array(n + 1).fill(0).map((item, index) => index)

        //初始化每个集合的节点数
        this.size = new Array(n + 1).fill(1)
    }

    get(x) {
        //查找x的父节点,并且完成路径优化
        //优化后,x的父节点指向所在集合的根节点
        return this.pa[x] = this.pa[x] === x ? x : this.get(this.pa[x])
    }

    merge(a, b) {
        //找到a的根节点
        const ra = this.get(a)
        //找到b的根节点
        const rb = this.get(b)

        //如果a和b在一个集合中则不需要合并
        if (ra === rb) return

        //把节点总数小的集合合并到节点总数多的集合里
        //更新节点总数多的集合为 a和b之和
        if (this.size[ra] < this.size[rb]) {
            this.pa[ra] = rb
            this.size[rb] += this.size[ra]
        } else {
            this.pa[rb] = ra
            this.size[ra] += this.size[rb]
        }
    }
}

如有错误欢迎指出,欢迎一起讨论!