高阶数据结构之-并查集

简介: 高阶数据结构之-并查集

并查集原理

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题,常常在使用中以森林来表示

适合于描述这类问题的抽象数据类型称为并查集(union-findset)


小例子-编号和人

如果我们想让编号和人构成关系, 即可以通过编号找到对应的人,也可以通过人找到对应的编号怎么做?

template<classT>  

classUnionFindSet

{

public:

   UnionFindSet(constT*a, size_tn)  

   {

       for (inti=0; i<n; i++)

       {

           _a.push_back(a[i]);

           _indexMap[a[i]] =i;

       }

   }

private:

   vector<T>_a;  //编号找人  下标对应的就是人名

   map<T, int>_indexMap;//人找编号  key:人名   value:编号

};

#include"UnionFind.h"

intmain()

{

   stringa[] = { "张三","李四","王五" };

   UnionFindSet<string>ufs(a, 3);

   return0;

}


并查集的实现

#include<iostream>

#include<vector>

classUnionFindSet

{

public:

   UnionFindSet(size_tn);

   size_tFindRoot(intx);

   voidUnion(intx1, intx2);

   boolInset(intx1,intx2);

   size_tSetCount();

private:

   std::vector<int>_set;

};


首先我们要知道如下规则:

  1. 数组的下标对应集合中元素的编号
  2. 数组中的值如果为负数,负号代表根,数字代表该集合中元素个数
  3. 数组中的值如果为非负数,代表该元素双亲在数组中的下标

例如:


基本结构

classUnionFindSet

{

private:

   std::vector<int>_set;

};

构造函数

最初每个元素各自构成一个集合, 初始状态就是-1,表示size个集合

UnionFindSet(intsize)

   :_set(size, -1)//size个元素都最初初始化为-1

   {}


查找根节点

返回根节点所在下标

//如果_set[x]是负数,那么x就是某个集合的根

//如果_set[x]不是负数,那么x的父亲节点就是_set[x]值对应的位置

size_tFindRoot(intx)

{

   while (_set[x] >=0)

   {

       x=_set[x];

   }

   //来到这,_set[x] <0, 此时x就是根节点

   returnx;

}


路径压缩技巧

路径压缩实际上是在数据量太大的时候,访问一些数据可能在位于叶子位置,导致访问的效率不高

本质上我们查找的代表节点的时候,可以对该路径上的节点进行路径压缩

注意:最好不要用递归的时候进行路径压缩,因为路径深可能导致栈溢出


做法:

1)先找到当前路径节点的代表节点

2)然后从当前位置_set[x]开始的节点,沿途的节点的父亲位置都改为代表节点的位置

  只需要根据下标关系往上迭代即可x节点的父亲位置就是_set[x]位置

注意:要提前保存x节点的父亲节点位置,因为更改当前位置的父亲节点, 会丢失以前的父亲节点

size_tFindRoot(intx)

{

   introot=x;

   while (_set[root] >=0)

   {

       root=_set[root];

   }

   //此时root就是x这条路径的头节点(代表节点)

   //把沿途的节点的父亲都改为root

   while (_set[x] >=0)

   {

       intparent=_set[x];//先保存x节点的父节点位置

       _set[x] =root;//将x节点的父亲位置改为root位置

       x=parent;//往上迭代

   }

   returnroot;

}


合并集合


这里选择合并到x2所在的集合合并到x1所在的集合

voidUnion(intx1, intx2)

{

   //找到x1和x2的代表节点(根节点)

   size_troot1=FindRoot(x1);

   size_troot2=FindRoot(x2);

   //两个节点不在一个集合才合并

   //把root2合并到root1所在集合

   if (root1!=root2)

   {

       _set[root1] +=_set[root2];//root2所在集合的元素累加到root1所在集合

       _set[root2] =root1; //root2的父亲变为root1

   }

}

如果我们希望是小集合合并到大集合呢?  即元素少的集合合并到元素多的集合

路径压缩技巧

两颗树合并的时候,节点少的数往节点多的树合并

目的:为了使节点层数增多的节点相对减少


做法:

1)判断哪颗树的节点更多, 让root1变为是较大的集合, root2往root1合并

如何判断呢? 因为root1和root2是根,_set[root]的值是负数,代表该集合的元素个数, _set[root]值越大,集合元素个数越少

//x1和x2合并 ->本质是代表节点(根节点)合并

voidUnion(intx1, intx2)

{

   //找到x1和x2的代表节点(根节点)

   size_troot1=FindRoot(x1);

   size_troot2=FindRoot(x2);

   //我们让root1的集合是大集合,小集合合并到大集合中

   //因为root1和root2是根,所以_set[root]的值为负数,代表集合的元素个数,值越小,元素越多

   if (_set[root1] >_set[root2]) //说明root2集合元素多

   {

       ::swap(root1, root2);

   }

   //两个节点不在一个集合才合并

   if (root1!=root2)

   {

       _set[root1] +=_set[root2];//root2所在集合的元素累加到root1所在集合

       _set[root2] =root1; //root2的父亲变为root1

   }

}


求集合个数

遍历_set,查看有多少元素是负数,就代表有多少个根节点, 即有多少个集合

size_tSetCount()

{

   size_tcount=0;

   for (autoe : _set)

   {

       if (e<0) count++;

   }

   returncount;

}


判断两个元素是否在同一个集合

只需要判断两个元素的代表节点的位置是否相同即可

boolInset(intx1,intx2)

{

   returnFindRoot(x1) ==FindRoot(x2);

}



相关文章
|
4月前
|
存储 算法 Serverless
【C/C++ 数据结构】深入探索数据结构中算法复杂度:从C++和数学的视角
【C/C++ 数据结构】深入探索数据结构中算法复杂度:从C++和数学的视角
63 0
|
1月前
|
算法
【初阶数据结构】复杂度算法题篇
该方法基于如下的事实:当我们将数组的元素向右移动 k 次后,尾部 kmodn 个元素会移动至数组头部,其余元素向后移动 kmodn 个位置。
|
3月前
|
存储 算法 C语言
数据结构和算法学习记录——初识 数据结构和算法&时间复杂度
数据结构和算法学习记录——初识 数据结构和算法&时间复杂度
24 4
|
2月前
|
存储 Linux 数据库
【数据结构】二叉搜索树——高阶数据结构的敲门砖
【数据结构】二叉搜索树——高阶数据结构的敲门砖
|
4月前
|
存储
【高阶数据结构】并查集 -- 详解
【高阶数据结构】并查集 -- 详解
|
4月前
|
存储 NoSQL 算法
【高阶数据结构】跳表 -- 详解
【高阶数据结构】跳表 -- 详解
|
存储 NoSQL 算法
【高阶数据结构】跳表
跳表的原理及其模拟实现
|
存储 人工智能 算法
【高阶数据结构】B树
B树、B+树和B*树的原理及其功能。
【数据结构和算法思想】递归思想
在程序中可以调用函数来完成任务,为了完成相同的任务可以调用同一个函数。如果在函数中调用函数本身,那么改函数就被称为递归函数。递归函数的调用是按层,不是次,有 N 层就同时调用(打开)了 N 个函数,不是 N 次。 无限递归(递而不归、死递归),栈溢出(函数的调用有时间和空间的开销,一个程序中同时调用的函数个数是有限的)。• 递归函数的调用有时间和空间的开销,而且递归的次数受到堆栈大小的限制。 • 循环没有函数调用和返回中的参数传递和返回值的额外开销,更快。 如何在递归和循环之间选择? 一般情况下,当循环方法比较容易实现时,应该避免使用递归。当很难简历一个循环方法时,递归可能是一个很好的选择