数据结构:并查集解决方案的详解

并查集解决方案的详解

在说并查集之前,我们需要先来了解使用并查集的背景知识:等价类
我们先来看看等价关系的概念:假定有一个具有n个元素的集合U=1,2,3…,n和一个具有r个关系的集合R=(i1,j1)(i2,j2)(i3,j3)…(ir,jr),当满足:
1.自反: 对于所有的a属于U,有(a,a)属于R;
2.对称: 对于(a,b)属于R,当且仅当(b,a)属于R;
3.传递: 若(a,b)属于R且(b,c)属于R,则有(a,c)属于R。
我们说R是一个等价关系。而在一个等价关系中,若(a,b)属于R,则元素a,b等价。而所谓等价类是指相互等价的最大集合,而这就意味着不存在类以外的元素与类内部的元素等价。(一个元素只属于一个等价类)也就相当于把集合U划分为若干个不相交的等价类。
但是,等价类有两个不同的分类,分为离线等价类在线等价类离线等价类自不必说,每个元素只属于一个等价类,但是在线等价类问题中,初始时,每个元素都属于一个独立的等价类,那我们衍生出了一些操作:合并,查找,这就是我们的并查集问题的由来。

解决方案一:数组解决

数组是最简单的解决方法,我们创建一个数组,使得数组标号的等价类就是它本身,例如a[i]就属于等价类i,合并时就将其元素改成另一元素的值,查找时就输出数组内的元素。
代码也很简单:

int *equivClass;//等价类数组
int n;//元素个数

void initialize(int numberOfElement)//初始化
{
    n = numberOfElement;
    equivClass = new int[n+1];
    for(int i = 1;i<=n;i++)
        equivClass[i] = i;
}
void unite(int classA,int classB)//合并
{
    for(int i = 1;i<=n;i++)
        if(equivClass[i] == classB)
            equivClass[i] = classA;
}
int find(int theElement)//查找
{
    return equivClass[theElement];
}

int main()
{
    initialize(10);
    unite(1,4);
    unite(2,7);
    unite(1,2);
    for(int i = 1;i<=10;i++)
        cout<<i<<"的等价类:"<<find(i)<<endl;
}

程序运行的结果就在这里了,将1,2,4,7合并。
在这里插入图片描述
这种解决方法简单易懂,但是我们从复杂度上来讲,我们初始化数组复杂度为Q(n),合并的复杂度也为Q(n),find函数的复杂度为Q(1),但是综合来讲,复杂度未免有些大,那我们引入第二种解决方案。

解决方案二:模拟指针解决

如果将等价类作为一个链表,那么合并操作的复杂度就可大大降低为常数级,在一个等价类中,可以沿着链表的指针找的所有的元素,无需去检查所有的值。但是,如果用链表来解决,我们的find函数的复杂度就会提高,我们需要遍历链表。由此,我们想到了将数组的优势与链表的优势结合起来,通过使用整型指针来解决(也称之为模拟指针)。
那么首先我们来构造模拟指针的结构(struct):
首先,我们的结构中需要等价类的标识符,等价类的大小,以及指向下一个等价类元素的指针
代码如下:

struct equivNode
{
    int equivClass;//元素类标识符
    int size;//类的元素个数
    int next;//下一个元素指针
};

这样一来,我们就得到了比较简略的结构。
那么我们要实现数组与链表的结合,我们只需要构造节点类型的数组即可,我们就可以按索引来查找等价类。

equivNode *node;//节点的数组
int n;//元素个数

那么,基本的工作完成,接下来该考虑解决并查集的问题了。
当我们合并等价类是,我们为了尽量降低时间复杂度,我们采用将小的等价类合并到大的等价类中,并将小等价类插入到大等价类的头结点后(为了降低程序的时间复杂度,优化代码)。
接下来我们看看完整的代码:

struct equivNode
{
    int equivClass;//元素类标识符
    int size;//类的元素个数
    int next;//下一个元素指针
};

equivNode *node;//节点的数组
int n;//元素个数

void initialize(int numberOfElements)//初始化
{
    n = numberOfElements;
    node = new equivNode[n+1];

    for(int i = 1;i<=n;i++)
    {
        node[i].equivClass = e;
        node[i].next = 0;
        node[i].size = 1;
    }
}

void unite(int classA,int classB)//合并
{
    if(node[classA].size>node[classB].size)
        swap(classA,classB);

    int i;
    for(i = classA;node[i].next!=0;i=node[i].next)
        node[i].equivClass = classB;
    node[i].equivClass = classB;

    node[classB].size += node[classA].size;
    node[i].next = node[classB].next;
    node[classB].next = classA;
}

void find(int theElement)//查找
{
    return node[theElement].equivClass;
}

至此我们所采用的模拟指针的方案告一段落,但是我们依旧不满意,我们合并操作的复杂度依然没有降低,所以,前两种方案虽然简单,但是效率不高,那么我们给出第三种方案。

解决方案三:树结构解决

这是一块不同于以上解决方案的知识点,我们先来看一看一个集合的树形的描述:我们将一个节点代表一个元素,任何一个元素都可以代表根节点,剩余元素的任何子集都可以作为根元素的孩子,在剩余的元素可以作为根元素的孙子…
我们如果要实现并查集,我们的求解策略是将每一个等价类表示为一个数。在这里,我们要用到上边用到的虚拟指针,,而且我们用到的是数的链表描述。但这里,我们的实现方法与树有些不同,我们是从某个节点得到其所在的等价类,也就是说从子节点指向父节点。我们的每个节点需要一个父域,域内的数字我们采用整型数。
我们来看看代码:

int *parent;
void initialize(int numberOfElements)
{
    parent = new int[numberOfElements+1];
    for(int i = 1;i<=numberOfElements;i++)//初始化
        parent[i] = i;
}

void unite(int rootA,int rootB)
{
    parent[rootB] = rootA;
}

int find(int theElement)
{
    while (parent[theElement] != 0)
        theElement = parent[theElement];
    return theElement;
}

在这种数据结构中,我们可以看到,合并函数unite()的复杂度降到了Q(1),查找函数find的复杂度为Q(h),h为树的高度,均为常数级的复杂度,效率相比前两种大大提高。

学艺不精,可能未完,待续…

原创文章 20 获赞 144 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_43714332/article/details/102837200
今日推荐