数据结构--树(下) 堆、哈夫曼树、哈夫曼编码、并查集

ぃ、小莉子 提交于 2020-02-04 03:13:26

堆(heap)

讲线性结构队列中有提到一种队列叫优先队列(Priority Queue),取出元素是依照元素的优先权大小,而不是元素进入队列的优先顺序。

如何组织优先队列?

我们用完全二叉树去表示优先队列,任一结点都是其子树的最大值/最小值,这就叫堆,堆分为大顶堆和小顶堆,从名字也可看出,大顶对对应的结点顶部为最大值,小顶堆同理。

大顶堆:
在这里插入图片描述
小顶堆
在这里插入图片描述
堆的两个特性

  • 结构性:用数组表示的完全二叉树
  • 有序性:任一结点的关键字是其子树所有结点的最大值/最小值
    在这里插入图片描述

堆的抽象数据类型描述

类型名称:最大堆

数据对象集:完美二叉树,每个结点的元素值不小于其他子结点的元素值

操作集:

•MaxHeap Create( int MaxSize ):创建一个空的最大堆。

•Boolean IsFull( MaxHeap H ):判断最大堆H是否已满。

•Insert( MaxHeap H, ElementType item ):将元素item插入最大堆H。

•Boolean IsEmpty( MaxHeap H ):判断最大堆H是否为空。

•ElementType DeleteMax( MaxHeap H ):返回H中最大元素(高优先级)。

堆结构

typedef struct heapsturct
{
	ElementType *Elements ;//0位置存放一个值为无穷大的哨兵。便于后面插入操作
	int Size ;//储存当前堆存放元素的个数
	int Capacity ;//堆存放的最大个数
}*MaxHeap;

创建堆

MaxHeap Create( int MaxSize )//创建一个空的最大堆。
{
	MaxHeap H = (MaxHeap)malloc(sizeof(struct heapsturct));
	H->Elements = (ElementType *)malloc(sizeof (ElementType) * (MaxSize+1)); //分配数组的大小
	H->Capacity = MaxSize ; //记录内存大小
	H->Size = 0 ;
	H->Elements[0] = Maxn ;//设立哨兵
	return H;
}

堆插入

  1. 先把新加入的元素放到二叉树的最后,也就是数组的最后位置。
  2. 然后和新元素位置的父节点比较大小,大则交换
i= ++H->Size ;
for (;item > H->Elements[i/2] ; i=i/2  ) // 0位置有一个很大的哨兵,到顶时结束 
		H->Elements[i] = H->Elements[i/2];

i一开始为插入元素的位置,这条语句就能使得比新元素的结点一直往下移,直到i到达的位置是的新元素的父节点比它大位置,这里哨兵的作用就是当插入的元素是当前的最大元素,i到1(树根)位置时,再比较时会比哨兵小(哨兵设置为很大),这样循环会自动停止,这就是哨兵的作用。

  1. 找到合适的位置把新元素存入
void Insert( MaxHeap H, ElementType item )//将元素item插入最大堆H
{
	if (IsFull(H))
	{
		cout <<"堆满"<<endl; 
		return ;
	}
	int i= ++H->Size;
	
	for (;item > H->Elements[i/2] ; i=i/2  ) // 0位置有一个很大的哨兵,到顶时结束 
		H->Elements[i] = H->Elements[i/2];
	
	H->Elements[i] = item ;	
}

堆删除最大元素

  1. 先把最大值保存起来,函数结束时返回
  2. 把最后一个元素放到树根的位置,也就是删除的位置,此时堆长度减一
  3. 一直选择左右子结点最大的跟放上去的值比较,如果比这个值大,则交换位置,直到左右结点都比这个元素小为止。
for (parent=1 ; parent*2<H->Size ;parent = child)
	{
		child = parent*2 ;
		if (child<H->Size && H->Elements[child]<H->Elements[child+1])//如果child==H->Size则不存在右结点了
			child ++ ;
		if (item > H->Elements[child]) break ;//parent位置符合条件
		else 
		H->Elements[parent] = H->Elements[child]; //交换位置
	}

这条循环结束后parent的位置就是符合item元素插入的位置

ElementType DeleteMax( MaxHeap H )//返回H中最大元素(高优先级)
{
	if (IsEmpty(H))
	{
		cout <<"最大堆以为空"<<endl;
		return H->Elements[0]; 
	}
	int item  = H->Elements[H->Size--];
	int value = H->Elements[1];
	int parent , child ;
	for (parent=1 ; parent*2<H->Size ;parent = child)
	{
		child = parent*2 ;
		if (child<H->Size && H->Elements[child]<H->Elements[child+1])
			child ++ ;
		if (item > H->Elements[child]) break ;
		else 
		H->Elements[parent] = H->Elements[child];
	}
	H->Elements[parent] = item ;
	return value ;
}

测试代码:

#include <iostream>
#include <stdlib.h>
#define ElementType int 
#define Maxn  1<<30 
using namespace std ;

typedef struct heapsturct
{
	ElementType *Elements ;
	int Size ;
	int Capacity ;
}*MaxHeap;


MaxHeap Create( int MaxSize )//创建一个空的最大堆。
{
	MaxHeap H = (MaxHeap)malloc(sizeof(struct heapsturct));
	H->Elements = (ElementType *)malloc(sizeof (ElementType) * (MaxSize+1)); 
	H->Capacity = MaxSize ;
	H->Size = 0 ;
	H->Elements[0] = Maxn ;
	return H;
}

bool IsFull( MaxHeap H )//判断最大堆H是否已满。
{
	if (H->Capacity <= H->Size)
		return true ;
	else 
		return false ;
}


void Insert( MaxHeap H, ElementType item )//将元素item插入最大堆H。
{
	if (IsFull(H))
	{
		cout <<"堆满"<<endl; 
		return ;
	}
	int i= ++H->Size;
	
	for (;item > H->Elements[i/2] ; i=i/2  ) // 0位置有一个很大的哨兵,到顶时结束 
		H->Elements[i] = H->Elements[i/2];
	
	H->Elements[i] = item ;	
}
bool IsEmpty( MaxHeap H )//判断最大堆H是否为空。
{
	if (H->Size == 0)
		return true ;
	else 
		return false ;
}

ElementType DeleteMax( MaxHeap H )//返回H中最大元素(高优先级)
{
	if (IsEmpty(H))
	{
		cout <<"最大堆以为空"<<endl;
		return H->Elements[0]; 
	}
	int item  = H->Elements[H->Size--];
	int value = H->Elements[1];
	int parent , child ;
	for (parent=1 ; parent*2<H->Size ;parent = child)
	{
		child = parent*2 ;
		if (child<H->Size && H->Elements[child]<H->Elements[child+1])
			child ++ ;
		if (item > H->Elements[child]) break ;
		else 
		H->Elements[parent] = H->Elements[child];
	}
	H->Elements[parent] = item ;
	return value ;
}


int main ()
{
	MaxHeap H ;
	H = Create(10);
	cout <<"插入元素:";
	for (int i=0;i<11;i++) // 插入11个元素 第11个元素堆满 
	{
		int item = rand();
		cout <<item <<" "; 
		Insert(H,item); 
	} 
	cout <<endl;
	for (int i=0;i<10;i++)
	cout <<DeleteMax(H)<<endl;
	cout <<DeleteMax(H)<<endl; //堆空! 
}

在这里插入图片描述

如果按上述方法一个一个元素相继插入建立的堆,时间复杂度为O(nlogn)

一个线性建堆的方法

  1. 将n个元素按输入顺序存入。先满足完全二叉树的结构性质
  2. 调整各个结点 的位置。以满足最大堆的有序特性

调整策略是从最后一个结点的父节点开始,到根结点1从最底部的元素逐步往上调整

void PerDown(MaxHeap H , int p) // 调整位置为p的结点位置  方法同删除最大值的方法类似 
{
	int parent,child ;
	ElementType x;
	x = H->Elements[p];
	for (parent = p ; parent*2<=H->Size;parent = child)
	{
		 child = parent*2 ;
		 if (child <H->Size && H->Elements[child]<H->Elements[child+1])
		 	child ++;
		if (x > H->Elements[child]) break ;
		else 
			H->Elements[parent] = H->Elements[child];
	 } 
	 H->Elements[parent] = x ;
} 

测试代码:

#include <iostream>
#include <stdlib.h>
#define ElementType int 
#define Maxn  1<<30 
using namespace std ;

typedef struct heapsturct
{
	ElementType *Elements ;
	int Size ;
	int Capacity ;
}*MaxHeap;


MaxHeap Create( int MaxSize )//创建一个空的最大堆。
{
	MaxHeap H = (MaxHeap)malloc(sizeof(struct heapsturct));
	H->Elements = (ElementType *)malloc(sizeof (ElementType) * (MaxSize+1)); 
	H->Capacity = MaxSize ;
	H->Size = 0 ;
	H->Elements[0] = Maxn ;
	return H;
}

bool IsFull( MaxHeap H )//判断最大堆H是否已满。
{
	if (H->Capacity <= H->Size)
		return true ;
	else 
		return false ;
}

bool IsEmpty( MaxHeap H )//判断最大堆H是否为空。
{
	if (H->Size == 0)
		return true ;
	else 
		return false ;
}

ElementType DeleteMax( MaxHeap H )//返回H中最大元素(高优先级)
{
	if (IsEmpty(H))
	{
		cout <<"最大堆以为空"<<endl;
		return H->Elements[0]; 
	}
	int item  = H->Elements[H->Size--];
	int value = H->Elements[1];
	int parent , child ;
	for (parent=1 ; parent*2<H->Size ;parent = child)
	{
		child = parent*2 ;
		if (child<H->Size && H->Elements[child]<H->Elements[child+1])
			child ++ ;
		if (item > H->Elements[child]) break ;
		else 
		H->Elements[parent] = H->Elements[child];
	}
	H->Elements[parent] = item ;
	return value ;
}

void PerDown(MaxHeap H , int p) // 调整位置为p的结点位置  方法同删除最大值的方法类似 
{
	int parent,child ;
	ElementType x;
	x = H->Elements[p];
	for (parent = p ; parent*2<=H->Size;parent = child)
	{
		 child = parent*2 ;
		 if (child <H->Size && H->Elements[child]<H->Elements[child+1])
		 	child ++;
		if (x > H->Elements[child]) break ;
		else 
			H->Elements[parent] = H->Elements[child];
	 } 
	 H->Elements[parent] = x ;
} 


void BuildGeap(MaxHeap H)
{
	int i ;
	for (i = H->Size/2;i>0;i--)//从最后一个父结点开始,到根节点1 逐个调整 
		PerDown(H,i);
}

int main ()
{
	MaxHeap H ;
	H = Create(10);
	cout <<"插入元素:";
	for (int i=0;i<10;i++)
	{
		int item = rand();
		cout <<item <<" "; 
		H->Elements[++H->Size] = item ;  //直接按顺序放入元素
	} 
	cout <<endl;
	BuildGeap(H); 
	for (int i=0;i<10;i++)
	cout <<DeleteMax(H)<<endl;
}

在这里插入图片描述

哈夫曼树与哈夫曼编码

哈夫曼树定义:
在这里插入图片描述
哈夫曼树也叫最优二叉树:WPL最小的二叉树

构造方法:每次把权值最小的两颗树二叉树合并
这里以构造:1 2 3 4 5为例

在这里插入图片描述每次都要从有序列拿两个最小的元素出来,然乎放入一个新的元素组成新的有序列,如果用数组的话,每次排序都需要O(nlong)的时间,而如果用上面提到的堆中的最小堆去存放数据的话,插入和删除都是O(logn)的时间复杂度。这样进行n次,构建一棵哈夫曼树的的时间为O(nlogn),而如果用数组每次都排序的话是O(n^2logn)。

下面用最小堆去构造一棵哈夫曼树。

#include <iostream>
#include <stdlib.h>

#define Maxn  1<<30 
using namespace std;

typedef struct Tree {
	int weight;
	struct Tree* left, * right;
}*BiTree, TreeNode;

#define ElementType BiTree 

typedef struct heapsturct
{
	ElementType* Elements; //此时存放的数据因该是树结点的指针
	int Size;
	int Capacity;
}*MinHeap;


MinHeap Create(int MaxSize)//创建一个空的最大堆。
{
	MinHeap H = (MinHeap)malloc(sizeof(struct heapsturct));
	H->Elements = (ElementType*)malloc(sizeof(ElementType) * (MaxSize + 1));
	H->Capacity = MaxSize;
	H->Size = 0;
	BiTree T = (BiTree)malloc(sizeof(TreeNode));
	T->weight = -1 * Maxn;
	T->left = NULL;
	T->right = NULL;
	H->Elements[0] = T;
	return H;
}

bool IsFull(MinHeap H)//判断最大堆H是否已满。
{
	if (H->Capacity <= H->Size)
		return true;
	else
		return false;
}


bool IsEmpty(MinHeap H)//判断最大堆H是否为空。
{
	if (H->Size == 0)
		return true;
	else
		return false;
}

BiTree DeleteMin(MinHeap H)//返回H中最小元素(高优先级)
{
	if (IsEmpty(H))
	{
		cout << "最大堆以为空" << endl;
		return H->Elements[0];
	}
	ElementType item = H->Elements[H->Size--];
	ElementType value = H->Elements[1];
	int parent, child;
	for (parent = 1; parent * 2 <= H->Size; parent = child)
	{
		child = parent * 2;
		if (child < H->Size && H->Elements[child]->weight > H->Elements[child + 1]->weight)
			child++;
		if (item->weight < H->Elements[child]->weight) break;
		else
			H->Elements[parent] = H->Elements[child];
	}
	H->Elements[parent] = item;
	return value;
}

void PerDown(MinHeap H, int p) // 调整位置为p的结点位置  方法同删除最大值的方法类似 
{
	int parent, child;
	ElementType x;
	x = H->Elements[p];
	for (parent = p; parent * 2 <= H->Size; parent = child)
	{
		child = parent * 2;
		if (child < H->Size && H->Elements[child]->weight>(H->Elements[child + 1])->weight)
			child++;
		if (x->weight < H->Elements[child]->weight) break;
		else
			H->Elements[parent] = H->Elements[child];
	}
	H->Elements[parent] = x;
}


void BuildGeap(MinHeap H)
{
	int i;
	for (i = H->Size / 2; i > 0; i--)//从最后一个父结点开始,到根节点1 逐个调整 
		PerDown(H, i);
}

void Insert(MinHeap H, BiTree item)//将元素item插入最小堆H。
{
	if (IsFull(H))
	{
		cout << "堆满" << endl;
		return;
	}
	int i = ++H->Size;

	for (; item->weight < H->Elements[i / 2]->weight; i = i / 2) // 0位置有一个很小的哨兵,到顶时结束 
		H->Elements[i] = H->Elements[i / 2];

	H->Elements[i] = item;
}
BiTree Huffuman(MinHeap T)
{
	int n = T->Size;
	for (int i = 0; i < n-1; i++)
	{
		BiTree BT = (BiTree)malloc(sizeof(TreeNode));
		BiTree Min1, Min2;
		Min1 = DeleteMin(T);
		Min2 = DeleteMin(T);
		BT->weight = Min1->weight + Min2->weight;
		BT->left = Min1;
		BT->right = Min2;
		Insert(T, BT);
	}
	return DeleteMin(T);
}

void preTraverse(BiTree T)
{
	if (!T)
		return;
	cout << T->weight << " ";
	preTraverse(T->left);
	preTraverse(T->right);
}

void inTraverse(BiTree T)
{
	if (!T)
		return;
	inTraverse(T->left);
	cout << T->weight << " ";
	inTraverse(T->right);
}

int main()
{
	MinHeap H;
	H = Create(10);
	cout << "插入元素:";
	for (int i = 0; i < 5; i++)
	{
		int item = rand();
		cout << item << " ";
		H->Elements[++H->Size] = (ElementType)malloc(sizeof(TreeNode)); //使用要先分配空间
		H->Elements[H->Size]->weight = item;
		H->Elements[H->Size]->left = NULL;
		H->Elements[H->Size]->right = NULL;
	}
	cout << endl;
	BuildGeap(H);
	BiTree  Root = Huffuman(H);
	cout << "先序:"; preTraverse(Root); cout << endl;
	cout << "后续:"; inTraverse(Root); cout << endl;
}

在这里插入图片描述
上面给出随机数构成的哈夫曼树如下:
在这里插入图片描述
可以看到权值越小,所在位置的深度越大。权值越大,深度越小。

用上面的哈夫曼树进行哈夫曼编码
给定一段字符串,对字符进行编码,可以使得该字符串的编码存储空间最少

设 :
A出现的次数:26500
B出现的次数:19169
C出现的次数:18467
D出现的次数:41
E出现的次数:6334
在这里插入图片描述
对应的编码为
D ->0011
E ->0010
C ->000
B ->01
A ->1
哈夫曼编码:Cost = 126500+219169+318467+441+46334 = 145793
等长编码:3
(26500+19169+18467+41+6334) = 211533
ASSIC:8*(26500+19169+18467+41+6334) = 564088
通过上面空间使用的对比,可以看出使用哈夫曼编码可以大大的节省空间。

具体用代码实现,我会在邓俊辉算法训练营学习笔记中实现,直接使用STL中的优先队列去替代最小堆实现哈夫曼编码。

集合及运算

集合运算:交、并、补、差,判定一个元素是否属于某一集合
并查集:集合并、查某元素属于什么集合

用数组实现并查集
路径未压缩:

#include <iostream>

using namespace std ;
int find (int a[],int x)
{
	int root =x ;
	while (root !=a[root])
		root = a[root];
/*	while (root !=x)  //路径压缩 
	{
		int item = a[x];
		a[item] = root ;
		x= item ;
	}
*/
	return item ;
}

void merge(int a[],int x1, int x2)
{
	a[find(a,x2)]=find(a,x1);
}

int main ()
{
	int a[11];
	for (int i=0;i<=10;i++)
		a[i] =i ;
	int n;
	cin >> n ;
	while (n--)
	{
		int x1 , x2 ;
		cin>> x1 >>x2;	
		if (find(a,x1)!=find(a,x2))
		{
			merge(a,x1,x2);
		}
	}	
	for (int i=0;i<=10;i++)
		cout << i<<"->"<<a[i]<<endl;
	return 0;
}

在这里插入图片描述
未压缩时的联通情况
在这里插入图片描述
我们可以发现,从1指向7的路径太长,这样会导致寻找根时浪费过多的时间,导致查找效率变低,如果我们把路径压缩后,使路径变短,查找的效率就会高很多。

压缩路径后:

while (root !=x)  //路径压缩 
	{
		int item = a[x];
		a[item] = root ;
		x= item ;
	}

在这里插入图片描述
在这里插入图片描述
路径压缩后路径都变成了1,这样可以提高并查集查找的效率。

并查集功能

  1. 判环,如果两个结点的根相同,这两个结点又相连,那一定存在环
  2. 可以判断两个元素是否属于同一类
  3. 判断一共有几类

##习题:7-8 堆中的路径 ##

将一系列给定数字插入一个初始为空的小顶堆H[]。随后对任意给定的下标i,打印从H[i]到根结点的路径。

输入格式:
每组测试第1行包含2个正整数N和M(≤1000),分别是插入元素的个数、以及需要打印的路径条数。下一行给出区间[-10000, 10000]内的N个要被插入一个初始为空的小顶堆的整数。最后一行给出M个下标。

输出格式:
对输入中给出的每个下标i,在一行中输出从H[i]到根结点的路径上的数据。数字间以1个空格分隔,行末不得有多余空格。

输入样例:
5 3
46 23 26 24 10
5 4 3

输出样例:
24 23 10
46 23 10
26 10

这题只能边插入边建堆的方法,不能用先存数据再建堆的方法,因为会改变输出,和题目不符合!

#include <iostream>
#include <stdlib.h>
using namespace std ;

typedef struct node{
	int date[1005] ;
	int size;
}*MinTree,Node;

void Insert (MinTree T , int item)
{
	int i = ++T->size ;
	for ( ; T->date[i/2] > item; i/=2)
		T->date[i] = T->date[i/2];
	T->date[i] = item ;
}

int main ()
{
	MinTree T ;
	T = (MinTree)malloc (sizeof(Node));
	T->size = 0 ;
	T->date[0] = -1*(1<<30);//哨兵
	int n , m ;
	int item ;
	cin >> n>> m;
	for (int i=1;i<=n;i++)
		{
			cin >> item ;
			Insert (T,item ); 	
		}

	for (int i=0;i<m;i++)
	{
		cin >> item ;
		cout << T->date[item];
		item /=2 ;
		while (item)
		{
			cout <<" "<<T->date[item];
			item /=2 ;
		}
		cout <<endl;
	}
	
		
	return 0;
} 

习题:7-9 列出连通集

给定一个有N个顶点和E条边的无向图,请用DFS和BFS分别列出其所有的连通集。假设顶点从0到N−1编号。进行搜索时,假设我们总是从编号最小的顶点出发,按编号递增的顺序访问邻接点。

输入格式:
输入第1行给出2个整数N(0<N≤10)和E,分别是图的顶点数和边数。随后E行,每行给出一条边的两个端点。每行中的数字之间用1空格分隔。

输出格式:
按照"{ v
​1
​​ v
​2
​​ … v
​k
​​ }"的格式,每行输出一个连通集。先输出DFS的结果,再输出BFS的结果。

输入样例:
8 6
0 7
0 1
2 0
4 1
2 4
3 5

输出样例:
{ 0 1 4 2 7 }
{ 3 5 }
{ 6 }
{ 0 1 2 7 4 }
{ 3 5 }
{ 6 }

这题我没有用到并查集,也没有用到堆,因为数据小,直接开二维数组保存关系的,然后直接深搜宽搜就可以了!

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <queue>
using namespace std ;

int a[11][11];
int Hash[11]; 
	int n, m ;
void dfs (int i)
{
	if (Hash[i]==0)
	{
		cout <<" "<<i;
		Hash[i] = 1;
		for (int j=i;j<n;j++)
		{
			if (a[i][j] ==1 && Hash[j]==0)
			{
				dfs(j);
			}
		}
	}	
}
int main ()
{
	memset(a,0,sizeof (a));
	memset(Hash,0,sizeof (Hash));
	cin >> n >> m ;
	int x1 , x2;
	for (int i=0;i<m;i++)
	{
		cin >>x1 >>x2 ;
		a[x1][x2] = 1 ;
		a[x2][x1] = 1 ;
	}
	for (int i=0;i<n;i++)
	{
		if (Hash[i]==0)
		{
		cout <<"{";
		dfs(i);
		cout <<" }"<<endl;
		}
	}
	memset(Hash,0,sizeof (Hash));
	for (int i=0;i<n;i++)
	{
		queue<int> q ;
		if (Hash[i] == 0)
		{
			cout << "{";
			q.push (i);
			while (!q.empty())
			{
				int item = q.front(); q.pop();
				if (Hash[item] ==0)
				cout <<" "<<item;
				Hash[item] = 1 ;
				for (int j=item;j<n;j++)
					if (Hash[j]==0 && a[item][j]==1 )
						q.push(j);
			}
			cout <<" }"<<endl;
		}
	}
	return 0;
} 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!