哈希表&一致性哈希&超有爱的并查集

1. 哈希函数,哈希表与布隆过滤器

  链接 : https://blog.csdn.net/weixin_35479108/article/details/88830850 中,我总结了有关于信息指纹,哈希表,布隆过滤器,位数组的内容。
  补充一点内容,有关哈希函数,如果急需要1000个哈希函数,但是要求哈希函数相互独立,应该怎么实现?这个可以用1个哈希函数改出来,假设一个哈希函数 h h h映射之后对应的是一个16字节的输出,这16个字节肯定是相互独立的,这时可以用前8个字节作为 h 1 h_1 h1,后8个字节作为 h 2 h_2 h2,剩下的 h 3 = h 1 + h 2 , h 4 = h 1 + 2 ∗ h 2 ⋯ h_3 = h_1+h_2,h_4=h_1+2*h_2\quad \cdots h3=h1+h2,h4=h1+2h2。视频里介绍16个字节每一位都是相互独立的,最后生成多少个哈希函数都是独立。(我其实没有太想明白,了解的可以教我一下~)

2. 设计RandomPool结构

【题目】 设计一种结构,在该结构中有如下三个功能:
   insert(key):将某个key加入到该结构,做到不重复加入
   delete(key):将原本在结构中的某个key移除。
   getRandom(): 等概率随机返回结构中的任何一个key。
【要求】 Insert、delete和getRandom方法的时间复杂度都是 O(1)
  哈希表的性质是可以根据哈希函数基本均匀的映射到输出域中,但不是严格等概率的,如果要严格等概率返回哈希表中的值,肯定要做一些处理,改造哈希函数的话是很麻烦的,所以可以考虑利用哈希表的性质来实现。
  首先,准备两张哈希表,以及一个变量size,记录哈希表中数据的多少,其中一张哈希表key存储字符串,value存储size,另一张哈希表key存储size,value存储字符串,假设存储的字符是英文字母,则效果如下图:

在这里插入图片描述

  这时insert数据很简单,直接同时更新两个map并且size++就可以了。
  如果要取数据为什么就是等概率了呢,取数据的时候,我们可以根据size的值等概率的生成0~size-1的数字,根据生成的这个数字,我们在map2中取数据一定是等概率的。
  但是delete数据的时候,根据插入数据时size大小生成的index就会产生漏洞,size也不代表map的大小了。此时如果getRandom()会发生根据size等概率生成的数据取值发现没有数据的情况,getRandom就不是O(1)了。那么应该如何进行操作呢?最简单的方法就是当产生漏洞的时候,同时调整两张map成为没有漏洞的情况不就可以了。也就是只要产生漏洞,就使用最后一条记录填充漏洞,index是当前漏洞的index,同时删除最后一条记录就OK了。

#include <iostream>
#include <hash_map>
#include <string>
#include <random>
#include <time.h>

using namespace __gnu_cxx;
using namespace std;

struct hash_string{
        size_t operator()(const string& str) const
        {
                return __stl_hash_string(str.c_str());
        }
};

struct compare_str
{
    bool operator() (const string &str1,const string &str2) const
    {
        return str1 == str2;
    }
};


class RandomPool{
public:
    hash_map<string,int,hash_string,compare_str> map1;
    hash_map<int,string> map2;
    int length;

    RandomPool()
    {
        length = 0;
    }

    void initMap()
    {
        char ch [] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        string str;
        for(int i=0;i < 26;i++)
        {
            str = ch[i];
            map1[str] = i;
            map2[i] = str;
            length++;
        }
    }

    void insert(string str)
    {
        if(!map1[str])
        {
            map1[str] = length;
            map2[length] = str;
            length++;
        }
    }

    string getRandom()
    {
        default_random_engine e;
        e.seed(100);//100为随机种子
        uniform_real_distribution<double> u(0, 1); //随机数分布对象
        double random = u(e);
        int rand = int(random*(length+1));///产生0-ize-1的随机数
        string str = map2[rand];
        return str;
    }

    void delete1(string str)
    {
        if(!map1[str])
        {
            return;
        }
        else
        {
            int index = map1[str];
            map1.erase(str);
            map2.erase(index);
            string str1 = map2[length];
            map1[str1] = index;
            map2[index] = str1;
            length--;
        }
    }

    template<class T>
    void printMap(T &map1)
    {
        auto it = map1.begin();
        while(it != map1.end())
        {
            cout<< "key = "<< it->first <<"         value = "<< it->second <<endl;
            ++it;
        }
    }
};

int main()
{
    cout << "Hello world!" << endl;
    /**
    为什么创建的两个map传入的参数不一样,请参考
    http://www.cnblogs.com/waytofall/archive/2012/06/04/2534386.html
    **/
    RandomPool rp;
    rp.initMap();
    cout<<rp.length<<endl;
    cout<<rp.getRandom();
    rp.delete1("E");
    cout<<rp.length<<endl;
    cout<<rp.getRandom();
    if(rp.map1["key"])
        cout<<"hello"<<endl;
    return 0;
}

3. 一致性哈希

  链接:https://www.cnblogs.com/zhzhang/p/3740489.html 是哈希表的原理与实现,其中一致性哈希讲解的很清楚,也介绍了一些场景应用。
  一致性哈希设计到服务器的设计部署问题,假设现在有一个问题,有一个文件又100T,里面存储着字符串,这时肯定想着不能直接一台服务器干啊,那不得累死牛啊。假设我们有1000台服务器,这时将每一行的字符串读出,进行哈希映射,得到哈希code,hashcode%1000,得到结果就对应每一台服务器,因为对每个字符串的映射一定得到一样的哈希code,所以一样的字符串一定会分配到一样的机器上,这样并行处理就会快很多。
  但是这时有一些情况:

  1. 如果突然一台服务器坏掉了,那么分配给该机器的所有对象都需要重新分配,为了给每台机器分配均匀的对象就需要所有的对象重新分配
  2. 如果数据增加了很多,需要增加一些服务器进行处理,也需要所有对象重新分配

  1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;

  再来考虑第三个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。有什么方法可以改变这个状况呢,这就是 consistent hashing 一致性 hash 算法…

hash 算法和单调性

  Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:

  单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

  容易看到,上面的简单 hash 算法 hash(object)%N 难以满足单调性要求。

consistent hashing 算法的原理

  consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 服务器时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。

  下面就来按照 5 个步骤简单讲讲 consistent hashing 算法的基本原理。

1. 环形hash 空间

  考虑通常的 hash 算法都是将 value 映射到一个 32 位的 key 值,也即是 0 − 2 32 − 1 0-2^{32}-1 02321 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2 32 − 1 2^{32}-1 2321)相接的圆环,如下图所示的那样。

在这里插入图片描述

2. 把对象映射到hash 空间

  接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布如下图所示。

在这里插入图片描述
4 个对象的 key 值分布

1	1   hash(object1) = key1;
2	2   … …
3	3   hash(object4) = key4;

3. 把 服务器映射到hash 空间
  Consistent hashing 的基本思想就是将对象和 服务器都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。假设当前有 A,B 和 C 共 3 台 服务器,那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。

在这里插入图片描述
服务器和对象的 key 值分布

1	1   hash(cache A) = key A;
2	2   … …
3	3   hash(cache C) = key C;

  说到这里,顺便提一下 服务器的 hash 计算,一般的方法可以使用 服务器机器的 IP 地址或者机器名作为 hash 输入。

4. 把对象映射到cache

  现在 服务器和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,接下来要考虑的就是如何将对象映射到 服务器上面了。

  在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 服务器,那么就将该对象存储在这个 服务器上,因为对象和服务器的 hash 值是固定的,因此这个服务器必然是唯一和确定的。这样不就找到了对象和 服务器的映射方法了吗?!

  依然继续上面的例子(上图),那么根据上面的方法:

  • 对象 object1 将被存储到 服务器A 上;
  • object2和 object3 对应到 服务器C ;
  • object4 对应到 服务器B。

5. 考察服务器的变动

  前面讲过,通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 服务器有所变动时, 服务器会失效,进而对后台服务器造成巨大的冲击,现在就来分析分析 consistent hashing 算法。

  考虑假设 服务器B 挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 服务器B 逆时针遍历直到下一个 服务器( 服务器C )之间的对象,也即是本来映射到 服务器B 上的那些对象。

  因此这里仅需要变动对象 object4 ,将其重新映射到 服务器C 上即可:

在这里插入图片描述
服务器 B 被移除后的 服务器映射

  再考虑添加一台新的 服务器 D 的情况,假设在这个环形 hash 空间中, 服务器 D 被映射在对象 object2 和 object3 之间。这时受影响的将仅是那些沿 服务器 D 逆时针遍历直到下一个 服务器 ( 服务器 B )之间的对象(它们是也本来映射到 服务器 C 上对象的一部分),将这些对象重新映射到 服务器 D 上即可。

  因此这里仅需要变动对象 object2 ,将其重新映射到 服务器 D 上:

在这里插入图片描述
添加 服务器 D 后的映射关系

虚拟节点

  考量 Hash 算法的另一个指标是平衡性 (Balance) ,定义如下:

  平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

  hash 算法并不是保证绝对的平衡,如果 服务器较少的话,对象并不能被均匀的映射到 cache 上,比如在上面的例子中,仅部署 服务器A 和 服务器C 的情况下,在 4 个对象中, cache服务器A 仅存储了 object1 ,而 服务器C 则存储了 object2 、 object3 和 object4 ;分布是很不均衡的。

  为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:

  “虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一个实际节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。

  仍以仅部署 服务器A 和 服务器C 的情况为例,在前面 中我们已经看到, 服务器分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, 服务器A1, 服务器A2 代表了 服务器A ; 服务器C1, 服务器C2 代表了 服务器C ;假设一种比较理想的情况,参见下图 。

在这里插入图片描述
引入“虚拟节点”后的映射关系

  此时,对象到“虚拟节点”的映射关系为:

1	1   objec1->cache A2
2	2   objec2->cache A1
3	3   objec3->cache C1
4	4   objec4->cache C2 ;

  因此对象 object1 和 object2 都被映射到了 服务器A 上,而 object3 和 object4 映射到了 服务器C 上;平衡性有了很大提高。引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 服务器时的映射关系如图 7 所示。

在这里插入图片描述查询对象所在 服务器

  “虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 服务器A 的 IP 地址为 202.168.14.241 。

  引入“虚拟节点”前,计算 服务器A 的 hash 值:Hash(“202.168.14.241”);

  引入“虚拟节点”后,计算“虚拟节”点 服务器A1 和 服务器A2 的 hash 值:

1	1   Hash("202.168.14.241#1");  // cache A1
2	2   Hash("202.168.14.241#2");  // cache A2

4. 并查集结构

  超有爱的并查集

问题引入畅通工程

问题描述
  某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
input:
  测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
  注意:两个城市之间可以有多条道路相通,也就是说
  3 3
  1 2
  1 2
  2 1
  这种输入也是合法的
  当N为0时,输入结束,该用例不被处理。
output:
  对每个测试用例,在1行里输出最少还需要建设的道路数目。
  题目要做的就是,首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。
  如果是1个连通分支,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通分支,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都是连起来的了;如果是3个连通分支,则只要再修两条路……
  以下面这组数据输入数据来说明
  4 2
  1 3
  4 3

在这里插入图片描述

  第一行告诉你,一共有4个点,2条路。下面两行告诉你,1、3之间有条路,4、3之间有条路。那么整幅图就被分成了1-3-4和2两部分。只要再加一条路,把2和其他任意一个点连起来,畅通工程就实现了,那么这个这组数据的输出结果就是1。好了,现在编程实现这个功能吧,城镇有几百个,路有不知道多少条,而且可能有回路。 这可如何是好?
  我以前也不会呀,自从用了并查集之后,嗨,效果还真好!我们全家都用它!
  并查集的实现引用的文章中用数组实现的,就不再引用了,这里按照左老师的算法视频用哈希表实现。

并查集原理

   简单一些,并查集有两个功能(速度非常快)

  1. 判断两个元素是否属于同一个集合
  2. 如果两个元素属于同一个集合就进行合并,注意,这里的合并是指的两个元素所在的集合合并成一个大的集合
    注意:并查集在使用的时候必须将所有数据都输入,不能在线处理数据

   为了解释并查集的原理,我将举一个更有爱的例子。

   话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的帮派,通过两两之间的朋友关系串联起来。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?

   我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物。这样,每个圈子就可以这样命名“中国同胞队”美国同胞队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。

   但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。

在这里插入图片描述

   下面开始并查集的实现,C++使用hash_map的实现我只用了题目中确实出现的整数类型,因为C++中使用非基本类型数据的hash_map,需要实现哈希函数和等于函数,参考RandomPool.在fatherMap中记录了每个大侠的上级是谁。大侠们从1或者0开始编号(依据题意而定),fatherMap[15] = 3 就表示15号大侠的上级是3号大侠。如果一个的上级就是他自己,那说明他自己就是掌门人了,查找到此为止,也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛同学只只知道自己的上级是杨左使。张无忌是谁?不认识!要想知道自己的掌门是谁,只能一级级查上去。
   findHead这个函数就是找掌门用的,意义 再清楚不过了(路径压缩算法提高效率先不论,后面再说)。这里使用递归来实现找节点的根节点也就是代表节点,即所说的掌门人,也可以像原文中一样,遍历得到,如下面第二小段代码。

 //找到某一个节点的头节点
    int findHead(int node)
    {
        int father = fatherMap[node];
        if(father != node)
        {
            father = findHead(father);
        }
        //在从node向前遍历的过程中,每一个被经过的节点的头节点都设置为node的头结点
        //提高效率
        fatherMap[node] = father;
        return father;
    }
 int findHead2(int node)
    {
        int son = node;
        int tmp;
        int father = fatherMap[node];
        //寻找节点node的根节点
        while(father != node)
        {
            father = fatherMap[father];
        }
        //路径压缩,等同于fatherMap[node] = father;
        while(son != father)
        {
            tmp = fatherMap[son];
            fatherMap[son] = father;
            son = tmp;
        }
        return father;
    }

  再看Union函数,就是在两个点之间连一条线,这样一来,原先它们所在的两个板块的所有点就都可以互通了。这在图上很好办,画条线就行了。但我们现在是用并查集来描述武林中的状况的,用hash_map该如何实现呢? 还是举江湖的例子,假设现在武林中的形势如图所示。虚竹帅锅与周芷若MM是我非常喜欢的两个人物,他们的终极boss分别是玄慈方丈和灭绝师太,那明显就是两个阵营了。我不希望他们互相打架,就对他俩说:“你们两位拉拉勾,做好朋友吧。”他们看在我的面子上,同意了。这一同意可非同小可,整个少林和峨眉派的人就不能打架了。这么重大的变化,可如何实现呀,要改动多少地方?其实非常简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先的所有人员的终极boss都是师太,那还打个球啊!反正我们关心的只是连通性,门派内部的结构不要紧的。”玄慈一听肯定火大了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”于是,两人相约一战,杀的是天昏地暗,风云为之变色啊,但是啊,这场战争终究会有胜负,胜者为王。弱者就被吞并了。反正谁加入谁效果是一样的,门派就由两个变成一个了。这段函数的意思明白了吧?
  原文中借助数组来实现,实现的过程中如果两个大侠不是一个门派的,不管门派大小,直接连了一条线(pre[x] = y; ),自此灭绝就是玄慈的老大了,并不是真的打了一架找到更厉害的掌门。在hash_map的实现中借助一个辅助sizeMap记录每个门派的人数,人数较少的门派合并到人数较多的门派之中,如下面第二小段代码所示,两段代码的实质是一样的。

void join(int root1, int root2) //虚竹和周芷若做朋友
{
	int x, y;
	x = unionsearch(root1);//我老大是玄慈
	y = unionsearch(root2);//我老大是灭绝
	if(x != y) 
		pre[x] = y; //打一仗,谁赢就当对方老大
}

  void Union(int a,int b)
    {
        if(a == NULL || b == NULL)
            return ;
        int aHead = findHead(a);
        int bHead = findHead(b);
        if(aHead != bHead)
        {
            int aSetSize = sizeMap[a];
            int bSetSize = sizeMap[b];
            if(aSetSize <= bSetSize)
            {
                fatherMap[aHead] = bHead;
                sizeMap[bHead,aSetSize+bSetSize];
            }else
            {
                fatherMap[bHead] = aHead;
                sizeMap[aHead,aSetSize+bSetSize];
            }

        }
    }

  再来看看路径压缩算法。建立门派的过程是用Union函数两个人两个人地连接起来的,谁当谁的手下完全随机。最后的树状结构会变成什么样,我也无法预知,一字长蛇阵也有可能。这样查找的效率就会比较低下。最理想的情况就是所有人的直接上级都是掌门,一共就两级结构,只要找一次就找到掌门了。哪怕不能完全做到,也最好尽量接近。这样就产生了路径压缩算法。

   设想这样一个场景:两个互不相识的大侠碰面了,想知道能不能干一场。 于是赶紧打电话问自己的上级:“你是不是掌门?” 上级说:“我不是呀,我的上级是谁谁谁,你问问他看看。” 一路问下去,原来两人的最终boss都是东厂曹公公。 “哎呀呀,原来是自己人,有礼有礼,在下三营六组白面葫芦娃!” “幸会幸会,在下九营十八组仙子狗尾巴花!” 两人高高兴兴地手拉手喝酒去了。 “等等等等,两位大侠请留步,还有事情没完成呢!”我叫住他俩。 “哦,对了,还要做路径压缩。”两人醒悟。 白面葫芦娃打电话给他的上级六组长:“组长啊,我查过了,其实偶们的掌门是曹公公。不如偶们一起结拜在曹公公手下吧,省得级别太低,以后查找掌门麻烦。” “唔,有道理。” 白面葫芦娃接着打电话给刚才拜访过的三营长……仙子狗尾巴花也做了同样的事情。 这样,查询中所有涉及到的人物都聚集在曹公公的直接领导下。每次查询都做了优化处理,所以整个门派树的层数都会维持在比较低的水平上。路径压缩的代码,看得懂很好,看不懂可以自己模拟一下,很简单的一个递归而已。总之它所实现的功能就是这么个意思。

   前面findHead和findHead2分别用递归和非递归的方法实现了这个优化过程,这个优化过程再说明白一点就是,在查找的过程中,如果你要找白面葫芦娃的老大,你是不是要经过六组长,三营长再到曹公公,最后这几个人的老大都是曹公公,既然如此下次要找六组长的老大是不是还要遍历一次?这很浪费时间,因为多叉树的结构并不影响最后集合的判定,因此每一次findHead的过程中,向上经过的节点的老大我们都设置为曹公公,也就是下面右所示,递归只需要一句代码,非递归需要向上一个个设置,仅此。

在这里插入图片描述

   最终畅通工程问题的整个实现及结果如下所示

#include<iostream>
#include<hash_map>
#include<vector>
#include <sstream>
#include<string>

using namespace std;
using namespace __gnu_cxx;

class unionFind{
public:
    hash_map<int,int> fatherMap;
    hash_map<int,int> sizeMap;

    //将元素首先放到map当中,每一个元素就是自己代表节点
    void makeSet(vector<int> &arr)
    {

        for(int i=0;i<arr.size();i++)
        {
            fatherMap[arr[i]] = arr[i];
            sizeMap[arr[i]] = 1;
        }
    }

    //找到某一个节点的头节点
    int findHead(int node)
    {
        int father = fatherMap[node];
        if(father != node)
        {
            father = findHead(father);
        }
        //在从node向前遍历的过程中,每一个被经过的节点的头节点都设置为node的头结点
        //提高效率
        fatherMap[node] = father;
        return father;
    }

    bool isSameSet(int a,int b)
    {
        return findHead(a) == findHead(b);
    }
    void Union(int a,int b)
    {
        if(a == NULL || b == NULL)
            return ;
        int aHead = findHead(a);
        int bHead = findHead(b);
        if(aHead != bHead)
        {
            int aSetSize = sizeMap[a];
            int bSetSize = sizeMap[b];
            if(aSetSize <= bSetSize)
            {
                fatherMap[aHead] = bHead;
                sizeMap[bHead,aSetSize+bSetSize];
            }else
            {
                fatherMap[bHead] = aHead;
                sizeMap[aHead,aSetSize+bSetSize];
            }

        }
    }
};

int main()
{
    cout<<"Hello World!"<<endl;
    unionFind un;
    string str;
    int nodes;
    int roads;
    getline(cin, str);
    istringstream baseInfo(str);
    baseInfo >> nodes >> roads;
    cout << "nodes = " << nodes <<" , roads = " << roads << endl;
    vector<int> input;
    for(int i = 1 ; i <= nodes ; i++)
    {
        input.push_back(i);
    }
    un.makeSet(input);

    getline(cin,str);
    while(str != "0")
    {
        int a;
        int b;
        istringstream getLine(str);
        getLine >> a >> b;
        bool isSame = un.isSameSet(a,b);
        if(!isSame)
        {
            un.Union(a,b);
            nodes--;
        }
        getline(cin,str);
    }
    cout << "The total sets is " << nodes << endl;
    cout << nodes-1 << " roads need to be built" << endl;

    return 0;
}

在这里插入图片描述

5. 并查集结构应用【岛问题】

  一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右 四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个 矩阵中有多少个岛?
举例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
  这个矩阵中有三个岛。
  这个题目如果要实现,如果有思路反应一下应该是DFS或者BFS,也就是从头遍历整个矩阵,遇到1就进入感染函数,这个感染函数是干什么的呢?就是一个递归函数,将与之相连的1都感染成一个特定的值,eg:2,然后岛屿的数量加1;【这个感染函数进入条件为:if(i<0 || i>= rows || j>=columns || j<0 || arr[i][j] != 1),如果不满足就返回!】继续遍历矩阵,如果遇到1一定是一个新的岛屿,岛屿数目加1,然后重新感染。具体解法如下:【但是,还没有完哦~这个跟并查集有什么关系呢?代码的后面还有一点点内容 ~】

#include <iostream>
#include <vector>

using namespace std;

void infect(vector<vector<int>> &arr,int i,int j,int rows,int columns)
{
    if(i<0 || i>= rows || j>=columns || j<0 || arr[i][j] != 1)
        return;
    arr[i][j] = 2;
    infect(arr,i+1,j,rows,columns);
    infect(arr,i-1,j,rows,columns);
    infect(arr,i,j+1,rows,columns);
    infect(arr,i,j-1,rows,columns);
}

int countIsLand(vector<vector<int>> &arr)
{
    if(arr.size() == 0 || arr[0].size()==0)
        return 0;
    int rows = arr.size();
    int columns = arr[0].size();
    int res = 0;
    for(int i=0;i<rows;i++)
    {
        for(int j=0;j<columns;j++)
        {
            if(arr[i][j] == 1)
            {
                res++;
                infect(arr,i,j,rows,columns);
            }
        }
    }
    return res;
}

int main()
{
    cout << "Hello world!" << endl;
    vector<vector<int>> arr = {  { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
				        { 0, 1, 1, 1, 0, 1, 1, 1, 0 },
				        { 0, 1, 1, 1, 0, 0, 0, 1, 0 },
				        { 0, 1, 1, 0, 0, 0, 0, 0, 0 },
				        { 0, 0, 0, 0, 0, 1, 1, 0, 0 },
				        { 0, 0, 0, 0, 1, 1, 1, 0, 0 },
				        { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, };
    cout<<countIsLand(arr)<<endl;
    vector<vector<int>> arr1 = {{0, 0, 1, 0, 1, 0 },
                                {1, 1, 1, 0, 1, 0 },
                                {1, 0, 0, 1, 0, 0},
                                {0, 0, 0, 0, 0, 0}};
    cout<<countIsLand(arr1)<<endl;
    return 0;
}

  以上,发现这个问题的实现很简单,但是如果是一个很大的集合呢?如何做到矩阵分块并行运算呢?矩阵分割进行分别这样的统计处理是很简单的,但是关键是如何将分割后的矩阵合并到一起,应该使用什么的样合并逻辑。夸张一点,如图所示,如果从中间分块最后的统计结果会是三块岛屿,而不是1个。加粗样式
  当然我们只是统计边缘的信息将两个小矩阵合并起来,如果直接对比边缘元素,可能会出错,如在这里插入图片描述
红色的表示岛屿A,黑色的表示B,黄色的表示C,总共的岛屿个数应该是1,分块合并统计的时候会从lands = 3开始减,如遇到红黑交界,lands-1,黄黑交界lands-1,红黑交界又会lands-1,这时最后结果变成了0 。。。这种操作会面临这些风险,当然还可能会有更复杂的问题。这时如果用到并查集的判断两个元素是否是一个集合然后合并就很简单了,我们可以在获得每个小集合的时候设定这个集合最初元素为集合的代表节点,如图中标记的ABC,扫描边界时,如果边界都是1且两个集合代表元素不一样则合并,合并过后该集合的代表元素就是同一个了,下一次就不会再合并而出现lands=0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值