并查集

再简单的数据结构也有实现的必要


并查集

并查集(Union-Find)还是一个很值得我们学习的数据结构,它代码简单,但带来的性能改善却是非常明显的,而且是一个动态的数据结构。

并查并查,顾名思义,它能进行合并和查询,主要能够查询两个元素在不在一个集合之中或者将两个集合合并成一个集合。

Find(p,q):判断p和q在是否在一个集合中
Union(p,q):将p和q所在的集合合并成一个

应用

可以根据下面这幅图来想象
p和q是否连通
如果节点是数学集合中的元素,边表示他们同处一个集合,那么我们就可以知道任意两个元素再不在一个集合。
如果节点是计算机,边表示他们之间的连接,那么我们可以知道任意两个计算机是否连通。
如果节点是人,边表示他们互相认识,那么我们可以知道任意两个人是否处于同一个社交网中。
如果节点时候小岛,边表示两个岛之间有桥相连,那么我们可以知道任意两个小岛是否能通过陆路相连。
当然应用原不止于此


实现

我下面主要介绍两种实现,一种Quick-Find可以保证快速的查找,一种Quick-Union可以保证快速的合并。
首先,我们要把每一个节点都编号(从0到n-1),无论Quick-Find还是Quick-Union都是用线性数组来表示树形结构,只因他们选取的父节点的不同而不同,Quick-Find中,一个集合中所有的元素只有一个父节点,而Quick-Union却未必。

  • Quick-Find
    这里写图片描述
    如果两个节点相连,我们就在相应的位置储存相同的数字。

首先把数组从小到大初始化为0—n-1,再把所有边两端的节点都相互Union一下。
Find(p,q):判断p和q在id数组中的值是否相等
Union(p,q):遍历整个数组,让所有id等于p的元素都改成p的id
这里写图片描述

class UF
{
public:
    UF()
    {
        for(int i=0;i<MAX;i++)
            id[i] = i;
    }
    bool Find(int p,int q)
    {
        return id[p] == id[q];
    }
    void Union(int p,int q)
    {
        int pid = id[p];
        int qid = id[q];   //避免频繁读取p和q的id

        for(int i=0;i<MAX;i++)
        {
            if(id[i]==pid)
                id[i] = qid;
        }
    }
private:
    int id[MAX];
};

对于这个版本来说,查询是可以在O(1)的时间内完成,但是每次合并都需要遍历整个数组,这样效率过低,需要O(n)的时间,如果我们需要做n次合并甚至可能达到O(n^2).这样的效率是我们所不能接受的,所以仍需改进。

  • Quick-Union
    这里写图片描述
    id数组中储存父节点的位置

首先把数组从小到大初始化为0—n-1,再把所有边两端的节点都相互Union一下。
root(q):不断地把访问父节点。。。
find(p,q):找到p和q的根节点,根据他们是否相同给出判断
Union(p,q):将p的根节点指向q

class UF
{
public:
    UF()
    {
        for(int i=0;i<MAX;i++)
            id[i] = i;
    }
    int Root(int p)
    {
        while(id[p]!=p)
            p = id[p];
        return p;
    }
    bool Find(int p,int q)
    {
        return Root(p) == Root(q);
    }
    void Union(int p,int q)
    {
        int proot = Root(p);
        int qroot = Root(q);

        id[proot] = qroot;
    }
private:
    int id[MAX];
};

最原始的Quick-Union,root函数的最坏情况要遍历所有的节点才能够找到根节点,正是一点,导致find和union的效率都是O(N),这是我们不能接受的。


  • 改进1:加权合并
    我们只用一点很小的改变,就可以极大地改变树的平衡性。
    用一个辅助的size数组,在每棵树的根节点记录树的大小,每次Union时,我们只把小的树连接到大的树上,这么做可以保证生成树的深度最多为lgN,可用归纳法证明。
    P(n) =”加权合并得到的N个节点的树,其深度至多为lgN”
    当n = 1时,lgN=0显然成立
    则当n=k时,不妨设最后一次合并,将一颗大小为i的树连接到大小为j的树上,得到一颗大小为k的树,i+j = k && i<=j
    由归纳假设知,i树的深度小于lgi,j树的深度小于lgj,
    所以 深度=1+lgi = lg(2*i) = lg (i+i) <=lg(i+j)=lgk.
    所以归纳假设成立。

  • 改进2:压缩路径
    把寻找root过程中,每一个碰到的点都指向根节点,方便下次查找。

class UF
{
    public:
    UF()
    {
        for(int i=0;i<MAX;i++)
        {
            id[i] = i;
            sz[i] = 1;  
        }
    }
    int Root(int n)
    {
        int r = n;
        while(id[r]!=r)
        r = id[r];
        id[n] = r;     //压缩路径
        return r;
    }   
    bool Find(int p,int q)
    {
        return Root(p) == Root(q);
    }
    void Union(int p,int q)
    {
        int x =Find(p);
        int y =Find(q);

        if(sz[x]>=sz[y])
        {
            sz[x]+=sz[y];
            id[y] = x;
        }
        else 
        {
            sz[y]+=sz[x];
            id[x] = y;
        }
    }
    private:
        int id[MAX];
        int sz[MAX];
};

这里写图片描述
以上内容参考了红宝书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值