1.并查集
1.1需求分析
假设现在有这样一个需求,如下图的每一个点代表一个村庄,每一条线就代表一条路,所以有些村庄之间有连接的路,有
些村庄没有连接的路,但是有间接连接的路,根据上面的条件,能设计出一个数据结构,能快速执行下面2个操作:
- 查询两个村庄之间是否有连接的路
- 连接两个村庄
如何判断图中两点是否有连接
1.2并查集概念
并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:
- 查询(Find):查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作是为了判断两个元素是 否在同一个集合之中。
- 合并(Union):将两个集合合并为一个。
- 添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。
由于支持查询和合并这两种操作,并查集在英文中也被称为联合-查找数据结构(Union-find data structure)或者
合并-查找集合(Merge-find set)。
1.3并查集算法介绍
- 并查集有两种常见实现思路
- a.Quick Find
- 查找效率:O(1)
- 合并效率:O(N)
- b.Quick Union
- 查找效率:O(logN)
- 合并效率:O(logN)
1.3.1QuickFind的思路
- 核心是将所有元素进行ID分组管理,每个元素对应一个ID号
- find的时候,只需要返回这个元素对应的分组ID
- 合并的时候Union(a,b)时候,将是属于a分组的元素,都改为b分组的ID号
1.3.2QuickUnion的思路
- 将集合分为根结点和父节点的思想,所有节点保存他的父节点信息,当发现某个节点的父节点就是他自己的时候,这个节点就是根结点
- find操作就是找到这个元素的根结点,判断两个元素的根结点是不是一致,来判断是否连通。
- 合并操作的时候,Union(a, b)的时候,不是合并a和b,而是将a的根结点和b的根节点进行合并
1.3.3基于size的算法改进
- 在Union过程中,可能会出现不平衡的情况,甚至退化成为链表,Union(1,3)
- 将元素少的树,嫁接到元素多的树
1.3.4 基于rank的算法改进
- 矮的树,嫁接到高的树
#ifndef DATA_STRUCTURE_BASE_H
#define DATA_STRUCTURE_BASE_H
typedef int Element;
#endif //DATA_STRUCTURE_BASE_H
#ifndef DATA_STRUCTURE_QUICKFINDSET_H
#define DATA_STRUCTURE_QUICKFINDSET_H
#include "base.h"
/* 并查集的QuickFind算法下的结构定义
* 每一个元素都存有一个编号,只要编号一样,就代表一个集团的人
* 合并时,就是把另外一个团队的人的老大改为这个团队的老大
* */
typedef struct{
Element *data; // 存放的具体数据,利用索引来建立数据的关系
int n; // 并查集的元素个数
int *groupID; // 每个元素的编号
}QuickFindSet;
//产生n个元素的并查集
QuickFindSet *createQuickFindSet(int n);
//释放并查集
void releaseQuickFindSet(QuickFindSet *setQF);
// 初始化并查集,设置元素值和对应的编号
void initQuickFindSet(QuickFindSet *setQF, const Element *data, int n);
// 查:判断两个元素是不是同一个组ID
int isSameQF(QuickFindSet *setQF, Element a, Element b);
// 并:合并两个元素
void unionQF(QuickFindSet *setQF, Element a, Element b);
#endif //DATA_STRUCTURE_QUICKFINDSET_H
#include "quickFindSet.h"
#include<iostream>
//产生n个元素的并查集
QuickFindSet *createQuickFindSet(int n){
QuickFindSet* setQF=new QuickFindSet ;
setQF->data=new Element[n] ;
setQF->groupID=new int[n];
setQF->n=n;
return setQF;
}
//释放并查集
void releaseQuickFindSet(QuickFindSet *setQF){
if(setQF){
if(setQF->groupID){
delete setQF->groupID;
}
if(setQF->data){
delete setQF->data;
}
delete setQF;
}
}
// 初始化并查集,设置元素值和对应的编号
void initQuickFindSet(QuickFindSet *setQF, const Element *data, int n){
for(int i=0;i<n;i++){
setQF->data[i]=data[i]; //元素赋值
setQF->groupID[i]=i; //第i个元素的组ID就是i,自己管理自己
}
}
static int findIndex(QuickFindSet *setQF, Element a){
for(int i=0;i<setQF->n;i++){
if(setQF->data[i]==a){
return i;
}
}
return -1;
}
// 查:判断两个元素是不是同一个组ID
int isSameQF(QuickFindSet *setQF, Element a, Element b){
// 把元素转换为并查集中对应的索引号
int aIndex= findIndex(setQF,a);
int bIndex= findIndex(setQF,b);
if(aIndex==-1||bIndex==-1){ // 并查集中没有a或b,返回假
return 0;
}
return aIndex==bIndex;
}
// 并:合并两个元素
void unionQF(QuickFindSet *setQF, Element a, Element b){
int aIndex= findIndex(setQF,a);
int bIndex= findIndex(setQF,b);
int bGrapId=setQF->groupID[bIndex]; // 备份b的老大,后面就判断所有元素是不是老大是b
for(int i=0;i<setQF->n;i++){
if(setQF->groupID[i]==bGrapId){
setQF->groupID[i]=setQF->groupID[aIndex]; // b的组ID,替换成a的组ID
}
}
}
#ifndef DATA_STRUCTURE_QUICKUNIONSET_H
#define DATA_STRUCTURE_QUICKUNIONSET_H
/* QuickUnion方法
* 每个元素只保存他的父元素的编号,通过不断找父元素,最终找到根编号
* find : 两个元素的根一样
* union : a元素的根 和 b元素的根 进行合并
* */
#include "base.h"
typedef struct {
Element *data;
int n;
int *parent; // 每个元素只知道他的父节点,通过父节点不断去找根节点
int *size; // 根节点位置存放这个集合的个数
}QuickUnionSet;
//产生n个元素的并查集
QuickUnionSet *createQuickUnionSet(int n);
//释放并查集
void releaseQuickUnionSet(QuickUnionSet *setQU);
//初始化并查集
void initQuickUnionSet(QuickUnionSet *setQU, const Element *data, int n);
// 查:判断两个元素是不是同一个组ID
int isSameQU(QuickUnionSet *setQU, Element a, Element b);
// 并:合并两个元素
void unionQU(QuickUnionSet *setQU, Element a, Element b);
#endif //DATA_STRUCTURE_QUICKUNIONSET_H
#include "quickUnionSet.h"
#include<iostream>
//产生n个元素的并查集
QuickUnionSet *createQuickUnionSet(int n){
QuickUnionSet *setQU=new QuickUnionSet ;
setQU->data=new Element[n];
setQU->parent=new int[n];
setQU->size=new int[n];
setQU->n=n;
return setQU;
}
//释放并查集
void releaseQuickUnionSet(QuickUnionSet *setQU){
if(setQU){
if(setQU->size){
delete setQU->size;
}
if(setQU->parent){
delete setQU->parent;
}
if(setQU->data){
delete setQU->data;
}
delete setQU;
}
}
//初始化并查集
void initQuickUnionSet(QuickUnionSet *setQU, const Element *data, int n){
for(int i=0;i<setQU->n;i++){
setQU->data[i]=data[i];
setQU->parent[i]=i;
setQU->size[i]=1;
}
}
static int findIndex(QuickUnionSet *setQF, Element a){
for(int i=0;i<setQF->n;i++){
if(setQF->data[i]==a){
return i;
}
}
return -1;
}
static int findRootIndex(QuickUnionSet *setQU, Element a){
// 找e的父亲,再找这个父亲的父亲,直到发现父亲的父亲是自己,那就是根了
int curIndex= findIndex(setQU,a); // 如果为-1 补
// 向上遍历
while(setQU->parent[curIndex]!=curIndex){
curIndex = setQU->parent[curIndex]; // 将父节点ID作为下一个寻找的ID号
}
return curIndex;
}
// 查:判断两个元素是不是同一个组ID
int isSameQU(QuickUnionSet *setQU, Element a, Element b){
// a和b的根节点
int aRoot = findRootIndex(setQU, a);
int bRoot = findRootIndex(setQU, b);
if (aRoot == -1 || bRoot == -1) {
return 0;
}
return aRoot == bRoot;
}
/* 在QuickUnion集合中,将a元素和b元素合并
* 1. 找到a和b的根节点
* 2. 将b的父节点指向a的索引
* 根据size做优化,谁的元素多,就让另一根接入元素多的根
* */
// 并:合并两个元素
void unionQU(QuickUnionSet *setQU, Element a, Element b){
int aRoot = findRootIndex(setQU, a);
int bRoot = findRootIndex(setQU, b);
if (aRoot == -1 || bRoot == -1) {
return;
}
if(aRoot!=bRoot){
// 根据根节点元素个数来决定合并顺序
int aSize=setQU->size[aRoot];
int bSize=setQU->size[bRoot];
if(aSize>bSize){
setQU->parent[bRoot]=aRoot;
setQU->size[aRoot]+=bSize;
}else{
setQU->parent[aRoot]=bRoot;
setQU->size[bRoot]+=aSize;
}
}
}
#include <stdio.h>
#include "quickFindSet.h"
#include "quickUnionSet.h"
// QuickFind的测试
int test01() {
int n = 9;
QuickFindSet *QFSet = createQuickFindSet(n);
Element data[9];
for (int i = 0; i < sizeof(data) / sizeof(data[0]); ++i) {
data[i] = i;
}
initQuickFindSet(QFSet, data, n);
unionQF(QFSet, 3, 4);
unionQF(QFSet, 8, 0);
unionQF(QFSet, 2, 3);
unionQF(QFSet, 5, 6);
if (isSameQF(QFSet, 0, 2)) {
printf("Yes\n");
} else {
printf("No\n");
}
if (isSameQF(QFSet, 2, 4)) {
printf("Yes\n");
} else {
printf("No\n");
}
unionQF(QFSet, 5, 1);
unionQF(QFSet, 7, 3);
unionQF(QFSet, 1, 6);
unionQF(QFSet, 4, 8);
if (isSameQF(QFSet, 0, 2)) {
printf("Yes\n");
} else {
printf("No\n");
}
if (isSameQF(QFSet, 2, 4)) {
printf("Yes\n");
} else {
printf("No\n");
}
releaseQuickFindSet(QFSet);
return 0;
}
int test02() {
int n = 9;
QuickUnionSet *QUSet = createQuickUnionSet(n);
Element data[9];
for (int i = 0; i < sizeof(data) / sizeof(data[0]); ++i) {
data[i] = i;
}
initQuickUnionSet(QUSet, data, n);
unionQU(QUSet, 3, 4);
unionQU(QUSet, 8, 0);
unionQU(QUSet, 2, 3);
unionQU(QUSet, 5, 6);
if (isSameQU(QUSet, 0, 2)) {
printf("Yes\n");
} else {
printf("No\n");
}
if (isSameQU(QUSet, 2, 4)) {
printf("Yes\n");
} else {
printf("No\n");
}
unionQU(QUSet, 5, 1);
unionQU(QUSet, 7, 3);
unionQU(QUSet, 1, 6);
unionQU(QUSet, 4, 8);
if (isSameQU(QUSet, 0, 2)) {
printf("Yes\n");
} else {
printf("No\n");
}
if (isSameQU(QUSet, 2, 4)) {
printf("Yes\n");
} else {
printf("No\n");
}
// releaseQuickFindSet(QFSet);
return 0;
}
int main() {
test01();
return 0;
}
1.3.5 路径压缩
- 在find时使路径上的所有节点都指向根节点,从而降低树的高度
- 指向父节点的父节点
- 都指向根结点
2. 哈夫曼树
2.1 哈夫曼树相关的几个名词
- 路径
在一棵树中,一个结点到另一个结点之间的通路,称为路径。
从根结点到结点 a 之间的通路就是一条路径。
- 路径长度
在一条路径中,每经过一个结点,路径长度都要加 1 。
例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。
从根结点到结点 c 的路径长度为 3。
- 节点的权
给每一个结点赋予一个新的数值,被称为这个结点的权。
例如,结点 a 的权为 7,结点 b 的权为 5。
- 节点的带权路径长度
指的是从根结点到该结点之间的路径长度与该结点的权的乘积。
例如,结点 b 的带权路径长度为 2 * 5 = 10 。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。
例如图中所示的这颗树的带权路径长度为:
WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
2.2 什么是哈夫曼树
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称
这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。
2.3 构建哈夫曼树
- 对于给定的有各自权值的 n 个结点,构建哈夫曼树有一个行之有效的办法:
- 1.在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权
值的和; -
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
- 3.重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。