小树合并成大树:并查集,C语言实现

素颜马尾好姑娘i 2023-03-01 06:04 21阅读 0赞

假设一个公司只有一个老板,现在有n个人,而且知道这些人之间的一些关系。我们想知道他们属于几个公司,以及判断任意指定的两个人是不是属于同一个公司的问题。
在初始状态中,我们可以认为每个人都是自己的老板。在这里,某个人是老板的唯一标志:他的编号与他的老板编号 (头顶上的编号) 相同。

在这里插入图片描述
接下去开始合并。
第一步:A和B进行合并,我们假设编号比较大的人作为老板。那么B就是A的老板,当然,B还是B自己的老板。
第二步:B和C进行合并。C是B的老板。
如果我们这时查找A的老板,我们可以发现A的老板是B,但B说自己已经不当老板好多年了,C才是老板,于是再去找C。C说:“不错,我就是你的老板,赶紧去乖乖的干活。”于是我们确认A的老板是C。这是一个递归的过程。
我们把这个过程用代码表示:

  1. int findBoss(int i) //找i的老板
  2. {
  3. //如果自己和自己的老板是同一个人,说明找到老板了
  4. if(arr[i] == i)
  5. {
  6. return i; //返回老板的编号即可
  7. }
  8. //如果i不是老板,那递归就查找i的老板是不是最终的老板
  9. return findBoss(arr[i]); //递归找老板的编号
  10. }

这个过程的时间复杂度为O(N)。下面对这个过程进行优化:
我们发现,在A => B => C这个过程结束时,可以让A直接记住自己的老板是C,而不是每次都通过B再找到C。这样就相当于压缩了查找路径,提高了查找效率。

在这里插入图片描述
如果在上述B => C <= A的基础上,继续让C与D合并,D变成C的老板。这时如果通过A => C => D这个路径找老板,我们可以把D设置为A的老板。但是这时B的直接领导仍然是C,而不是D。需要再次访问B => C => D之后,才能确定B的真正老板。当所有人的关系都最终确定之后,这个结构就变成了一对多的树状结构:老板是树根,其他人都是叶子。所以从树叶找老板 (树根) 的时间复杂度为 O(1)。但是在确定所有的关系之前,时间复杂度为O(N)。优化之前,任何查找老板的时间复杂度都为 O(N),显然优化之后的查找效率更高。
在这里插入图片描述
上面的过程说起来挺麻烦,但是其实只需要在原来的基础上修改两行内容就可以了,代码实现如下:

  1. int findBoss(int i) //找i的老板
  2. {
  3. //如果自己和自己的老板是同一个人,说明找到老板了
  4. if(arr[i] == i)
  5. {
  6. return i; //返回老板的编号即可
  7. }
  8. //在递归找老板时,顺便把查找路径上的arr[i]的值改成真正的老板的编号
  9. arr[i] = findBoss(arr[i]); //压缩路径,提高效率
  10. return arr[i]; //返回老板的编号
  11. }

有了查找老板的函数之后,我们就可以很容易的判断两个人是不是在同一个公司。只要分别查找两个人对应的老板,然后比较一下这两个人的老板是不是同一个人即可。代码如下:

  1. void isInSameCompany(int a, int b) //判断两个数是不是属于同一个集合
  2. {
  3. int bossA = findBoss(a); //找a的老板
  4. int bossB = findBoss(b); //找b的老板
  5. //如果两个老板是同一个人,说明已经是同一个公司的了
  6. if(bossA == bossB)
  7. {
  8. printf("%d和%d是同一个公司的。\r\n", a, b);
  9. }
  10. else
  11. {
  12. printf("%d和%d不是同一个公司的。\r\n", a, b);
  13. }
  14. }

合并两个人的归属时,我们需要确定一下这两个人的老板是不是同一个人。如果是同一个人,那就说明这两个人已经在同一个公司了,不需要合并了;如果他们的老板不是同一个人,那么就合并一下。代码如下:

  1. int merge(int a, int b) //合并两个人的归属
  2. {
  3. int bossA = findBoss(a); //找a的老板
  4. int bossB = findBoss(b); //找b的老板
  5. //如果两个老板是同一个人,说明已经是同一个公司的了,不需要合并
  6. if(bossA == bossB)
  7. {
  8. return 0; //不合并,返回0
  9. }
  10. //如果两个老板不是同一个人,说明需要合并一下
  11. //就是把其中一个老板(bossB)的值改成bossA
  12. //也就是把bossA设定为bossB的老板
  13. else
  14. {
  15. arr[bossB] = bossA;
  16. return 1; //合并,返回1
  17. }
  18. }

下面是完整的并查集代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int size = 10;
  4. int arr[10];
  5. void init() //初始化
  6. {
  7. // 每一个人都是自己的老板
  8. for(int i = 1; i < size; i++)
  9. {
  10. arr[i] = i;
  11. }
  12. }
  13. int findBoss(int i) //找i的老板
  14. {
  15. //如果自己和自己的老板是同一个人,说明找到老板了
  16. if(arr[i] == i)
  17. {
  18. return i; //返回老板的编号即可
  19. }
  20. //否则就要递归找老板。顺便把arr[i]的值改了,改成真正的老板的编号
  21. arr[i] = findBoss(arr[i]); //压缩路径,提高效率
  22. return arr[i]; //返回老板的编号
  23. }
  24. int merge(int a, int b) //合并两个人的归属
  25. {
  26. int bossA = findBoss(a); //找a的老板
  27. int bossB = findBoss(b); //找b的老板
  28. //如果两个老板是同一个人,说明已经是同一个公司的了,不需要合并
  29. if(bossA == bossB)
  30. {
  31. return 0;
  32. }
  33. //如果两个老板不是同一个人,说明需要合并一下
  34. //就是把其中一个老板(bossB)的值改成bossA
  35. //也就是把bossA设定为bossB的老板
  36. else
  37. {
  38. arr[bossB] = bossA;
  39. return 1;
  40. }
  41. }
  42. void isInSameCompany(int a, int b) //判断两个数是不是属于同一个集合
  43. {
  44. int bossA = findBoss(a); //找a的老板
  45. int bossB = findBoss(b); //找b的老板
  46. //如果两个老板是同一个人,说明已经是同一个公司的了
  47. if(bossA == bossB)
  48. {
  49. printf("%d和%d是同一个公司的。\r\n", a, b);
  50. }
  51. else
  52. {
  53. printf("%d和%d不是同一个公司的。\r\n", a, b);
  54. }
  55. }
  56. int main()
  57. {
  58. int count = 0; //记录有几个公司
  59. init(); //初始化
  60. merge(1, 2); //合并1和2
  61. merge(1, 3);
  62. merge(3, 4);
  63. merge(4, 5);
  64. merge(1, 6);
  65. merge(2, 6);
  66. merge(7, 8);
  67. merge(7, 9);
  68. //遍历。如果某个人就是他自己的老板,那么这个人一定是老板。
  69. //由于一个公司只有一个老板。所有有几个老板,就有几个公司。
  70. for(int i = 1; i < size; i++)
  71. {
  72. if(arr[i] == i) //如果某个人就是他自己的老板
  73. {
  74. count++; //计数,一共有几个老板
  75. }
  76. }
  77. printf("一共有几个公司:count = %d。\r\n", count); //显示一共有几个老板,几个公司
  78. isInSameCompany(3, 9); //判断两个数是不是属于同一个集合
  79. isInSameCompany(4, 6);
  80. return 0;
  81. }

主函数中各元素之间的关系如下:

在这里插入图片描述
运行结果如下:

在这里插入图片描述

并查集算法中的查找老板的算法效率是很高的,而且存在多次查询后,效率进一步提升的情况。时间复杂度最终降低到O(1)。具体的中间状态请读者自己实现吧。

并查集算法主要是用来判断两个元素是否属于同一个集合。它在Kruskal算法中也有应用。Kruskal算法用来计算图的最小生成树,这部分内容我们将在图的章节中进行讲解。

发表评论

表情:
评论列表 (有 0 条评论,21人围观)

还没有评论,来说两句吧...

相关阅读