并查集-树形数据结构

字面意义的直观解释:

并查集简单的说有两种功能:合并和查找,所操作的对象是一个元素或集合。

合并:元素与元素之间,元素与集合之间,集合与集合之间,都可以实现合并,换句话说建立起一个关系,非常简单的双向关系。

查找:我们可以判断这个元素属于哪个集合,或是自己独立于集合之外。

通过以上解释,我们可以直接了解其两个最重要的功能,可以发现并查集的本质是将数据储存下来,在进行查询操作,因此并查集是一种数据结构,而且是树形的数据结构,在理解时要注意以树的概念去理解。在实际代码实现时,我们仅仅用了一维数组去模拟这个树的结构,所以代码整体比较简洁,注重理解。


原理及代码实现:

原理:假设1和2有关系,我们可以让1和2建立起一个集合,而在思想上以树的形式反映:


我们再让2和3建立起关系:  并查集-树形数据结构_第1张图片

我们思考一下为什么这样做:在一棵树中,根节点必然只有一个,此时我让这个根节点作为这个集合的代表,在上图中,只要根是1,那么就属于这个集合。

然后假设有集合A和集合B,A和B中都有若干元素,此时我让AB合并,此时是否需要将A中所有元素分别操作,让其属于B,或者对B所有元素操作让其属于A,完全不需要,我们已有现成的逻辑,每个集合只有一个根,所以我们只需将B的根节点指向A的根节点,此时,包括B连带B中所有元素均指向了A,此时AB合并完毕,且A为新的根节点,如下图:

并查集-树形数据结构_第2张图片合并之后:并查集-树形数据结构_第3张图片

此处的箭头表示指向根节点,只是方便理解,实际关系仍然是双向的。我们可以发现,每次操作总是针对于当前集合的根节点进行操作,此时必然可以保证其余的子关系不乱,而到底是A合并到B,还是B合并到A,没有特殊要求都一样,因为我们最终目的是两者合并。


代码实现:我们申请一维数组,下标表示当前元素,其对应的值a[i]代表父亲节点,定义就这样简单。

在一开始,所有元素均没有关系,那么自己就是自己的根节点:并查集-树形数据结构_第4张图片

假设1和2有关:,可以看出我让1做了根节点,每次合并,一定让根节点合并,所以查找为先,我们观察数组可以发现查找很简单,每次代入下标,判断是否与下标内的值相等,即为根,否则代入其值,即父亲节点,继续判断,可以直接递归实现,定义数组为f[i]:

int fd(int x)
{
    if(f[x]!=x)    //不等时不为根,继续查找
    {
        return fd(f[x]);
    }
    return f[x];  //此时相等,查找完毕,返回根节点
}

合并自然是对两个对象操作,分别找到其根,然后合并,就完成了。

int Union(int a,int b)  
{
    int p1=fd(a);   //找到a和b的根
    int p2=fd(b);
    if(p1!=p2)  //如果根相同,则不需要合并
    {
        f[p2]=p1;  //此时也可写f[p1]=p2 合并顺序无所谓,重点是合二为一
        return 1;
    }
    return 0;  //合并失败即已经是一个集合,返回0
}

总结:最终数组内的值,只要和下标相等的就是根节点,所以也可以很容易的判断出有几个集合或几颗树。

优化:路径压缩

并查集-树形数据结构_第5张图片此处,我想让4找到根,需要4->3->2->1共转移3次,同时这个递归过程会原路返回,此时能不能利用一下?

当返回时,我们将根直接赋给当前节点,就大大缩短了路径长度:


此时,如果再找4,只需1次,注意这个优化是对于接下来的查询方便的。代码也只需要原基础上稍微修改:

int fd(int x)
{
    if(f[x]!=x)
    {
        f[x]=fd(f[x]); //返回时赋值给f[x]即可完成路径压缩
    }
    return f[x];   //注意一定返回f[x],如果返回x下标,x只是当前的子节点,那么路径压缩无效
}

你可能感兴趣的:(数据结构)