优雅的数据结构–并查集

概念介绍


先来想一想「亲戚」这个词的定义:「指和本身有血亲和姻亲的人」。你和你女朋友家眷自己并不是是亲戚关系,一旦结婚后,两家人便成为了一家人,你的家人包括你在内和你女朋友及其家人自动成为了亲戚,这就是一个典型的并查集应用。并查集是一种树形的数据结构,用于处理一些不相交集合的合并及查询,上面例子中「结婚」其实就是并查集的合并操做

下面咱们来演示下并查集的常规操做,咱们默认建立6个元素,这6个元素咱们能够当作是互不相交的6个集合node



进行几回简单合并操做,咱们把元素0,2,4合并为集合set0,1,3合并为set1,5单独当作一个set2


查询操做


实现方法


初始化(make_set)

咱们能够把并查集当作是由不少颗树组成的森林,每棵树中相连的结点都表明属于同一集合,树中parent指向本身的根结点被视为该集合的表明。初始的时候,咱们用一个parent数组存储全部结点的父结点下标,因为默认状况下每一个集合互不相交,因此咱们令每一个结点的parent都指向本身,这样就生成了N棵以本身为根的树组成的森林。ios


parent数组的初始化结构以下图所示:
git


初始化并查集的代码:github

void make_set (){
    for (int i=0;i<N;i++){
        parent[i] = i;// 如上图i的parent指向本身
    }
}
复制代码
合并(Union)

Union(a,b)会将a所在的集合与b所在的集合相结合。在数据结构的实现上,只须要将b的根结点指向a的根结点,或a的根结点指向b的根结点便可,本文中默认使用前者。假设咱们如今要将0,2,4合并为一个集合,1,3 合并为一个集合,5单独视为一个集合,那么运算的过程的可能以下:

数组

合并0和2,需将2指向0。bash



合并2和4,需将4指向2的根结点0。


合并1和3,需将3指向1。


接着,若是想要继续Union(5,3),咱们能够先得到结点3所处树的根结点1,让1指向5便可。可是这样树的高度要比5指向1的树要高,随着并查集规模的增大,树会多出不少没必要要的高度,这将致使并查集的查询更耗时。
数据结构



为了让合并后树的总体高度相对更矮,在每次合并时,咱们让高度较矮的树并入高度较高的树,这种优化会在以后的代码中体现出来。

函数

最后,若是咱们想要Union(2,3),因为2,3各自所处的树高度相同,因此按默认方式将「3」的根结点「1」指向「2」的根结点「0」便可。
优化



在实现Union函数以前,咱们先增长一个rank[N]数组记录高度,默认的时候rank数组所有设置为0,rank中数值随着并查集的合并而改变。下面给出Union的代码:ui

void union_set(int a,int b) {
    if (a==b) return; // 相同
    int root_a = find_root(a);//找到a的根结点
    int root_b = find_root(b);//找到b的根结点
    if (root_a == root_b)  return; //根结点相同
    int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 选出较高的树
    int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 选出较低的树
    if( higher_root == lower_root ) {
        // 两颗树高度相等的状况
        parent[root_b] = root_a; //root_b.parent 指向 root_a (默认操做)
        ++rank[root_a];// 高度+1
    }else {
        parent[lower_root] = higher_root; // 较矮的树指向较高的树,不会改变总体高度
    }
}
复制代码
查询(Find)

查询某个元素所在的集合很是简单,因为parent数组记录了每个元素的父结点,咱们只须要递归回溯便可。


执行find_root(5)后沿着红线向上回溯找到0,执行find_root(2)后沿着红线向上回溯也找到了0,说明5和2同属一个集合,而执行find_root(7)后沿着红线回溯找到了6,故7和元素5,2不属于同一个集合。下面给出实现代码:

int find_root(int node) {
    if (parent[node] == node) return node;
    return find_root(parent[node]);
}
复制代码
路径压缩(Path Compression)

在查询某个元素的所在集合的时候,上面的find_root(int node)函数会返回元素所在的树的根结点------这个集合的表明,在这个过程当中,咱们能够将当前待查找的元素直接指向这个根结点,下降树的高度,从而使得查询速度获得提高。以上图为例子,执行find_root(3),find_root(5)后树形结构会变成以下结构:


代码实现上的改动很是小:

int find_root(int node) {
    if (parent[node] == node) return node;
    parent[node] = find_root(parent[node]); // 指向根结点
    return parent[node];
}
复制代码
完整的代码+前面的例子

并查集的代码和逻辑都很是精简,在我看来是很是优雅的数据结构。

#include<iostream>
#include <stdio.h>
#define N 10000+10

int parent[N];
int rank[N];
void make_set (){
    for (int i=0;i<N;i++){
        parent[i] = i;
        rank[i] = 0;
    }
}
int find_root(int node) {
    if (parent[node] == node) return node;
    parent[node] = find_root(parent[node]);
    return parent[node];
}
void union_set(int a,int b) {
    if (a==b) return; // 相同
    int root_a = find_root(a);//找到a的根结点
    int root_b = find_root(b);//找到b的根结点
    if (root_a == root_b)  return; //根结点相同
    int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 选出较高的树
    int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 选出较低的树
    if( higher_root == lower_root ) {
        // 两颗树高度相等的状况
        parent[root_b] = root_a; //root_b.parent 指向 root_a (默认操做)
        ++rank[root_a];// 高度+1
    }else {
        parent[lower_root] = higher_root; // 较矮的树指向较高的树,不会改变总体高度
    }
}
int main(){
    make_set();
    union_set(0, 2);
    union_set(2, 4);
    union_set(1, 3);
    union_set(5, 3);
    union_set(2, 3);
    find_root(3);
    find_root(5);
    for(int i=0;i<6;i++) {
        printf("%d ",parent[i]);// 输出全部指向
    }
}
复制代码

参考文章


Union-Find Algorithms
维基百科

推荐题目

PAT- 1118 Birds in Forest
PAT- 1118 Birds in Forest--代码


本篇已同步到我的博客:优雅的数据结构–并查集

相关文章
相关标签/搜索