1.堆
(1)概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组(或链表)中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
(特殊的“队列”取出元素的顺序是按照元素的优先级|关键词的大小,而不是元素进入堆的先后顺序)
(2)两个特性
1.结构性:用数组表示完全二叉树
2.有序性:任意一节点的关键字是其子树的最大值(最小值)。
最大堆(MaxHeap)也称作“大顶堆”:最大值;
最小堆(MinHeap)也称作“小顶堆”:最小值;
(3)优先队列的完全二叉树表示
(4)基本操作代码实现
1.堆的定义
typedef struct HNode *Heap; /* 堆的类型定义 */
struct HNode {
ElementType *Data; /* 存储元素的数组 */
int Size; /* 堆中当前元素个数 */
int Capacity; /* 堆的最大容量 */
};
typedef Heap MaxHeap; /* 最大堆 */
typedef Heap MinHeap; /* 最小堆 */
#define MAXDATA 1000 /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */
2.创建一个空的最大堆
MaxHeap CreateHeap( int MaxSize )
{ /* 创建容量为MaxSize的空的最大堆 */
MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType));//数组下标为0的位置不存储数据或存储“哨兵”
H->Size = 0;
H->Capacity = MaxSize;
H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/
return H;
}
3.判断最大堆是否已满
bool IsFull( MaxHeap H )
{
return (H->Size == H->Capacity);
}
4.将元素item插入到最大堆H
bool Insert( MaxHeap H, ElementType X )
{ /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */
int i;
if ( IsFull(H) ) {
printf("最大堆已满");
return false;
}
i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
for ( ; H->Data[i/2] < X; i/=2 )
H->Data[i] = H->Data[i/2]; /* 上滤X */
H->Data[i] = X; /* 将X插入 */
return true;
}
5.判断最大堆H是否为空
bool IsEmpty( MaxHeap H )
{
return (H->Size == 0);
}
6.返回H中最大元素(高优先级)
ElementType DeleteMax( MaxHeap H )
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
int Parent, Child;
ElementType MaxItem, X;
if ( IsEmpty(H) ) {
printf("最大堆已为空");
return ERROR;
}
MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
/* 用最大堆中最后一个元素从根结点开始“向上过滤”下层结点 */
X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
//删除最大根节点 下标为1
for( Parent=1; Parent*2<=H->Size; Parent=Child ) {//判断Parent有没有儿子
Child = Parent * 2;
if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )//有右节点并且右节点大于左节点
Child++; /* Child指向左右子结点的较大者 */
if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
return MaxItem;
}
(5)最大堆的创建
1.创造最大堆
将已经存在的N个元素,按最大堆的要求存放在一个一维数组中
2.代码实现
void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = H->Data[p]; /* 取出根结点存放的值 */
for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
Child = Parent * 2;
if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
Child++; /* Child指向左右子结点的较大者 */
if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性 */
/* 这里假设所有H->Size个元素已经存在H->Data[]中 */
int i;
/* 从最后一个结点的父节点开始,到根结点1 */
for( i = H->Size/2; i>0; i-- )
PercDown( H, i );
}
(6) 堆中的路径
1.题意理解
将一系列给定数字插入一个初始为空的小顶堆H[]。随后对任意给定的下标‘i’,打印从H[i]到根节点的路径。
###输入样例:
5 3
46 23 26 24 10
5 4 3
###输出样例:
24 23 10
46 23 10
26 10
2.堆的表示及操作
创建堆
#define MAXN 1001
#define MINH -10001
int H[MAXN],::size;//这里size前加::是因为VSC的内库中也有叫size的函数,所以会造成size不明确。解决方法有两个,1是在size前加::。2是给size改名。我这里用了前者
void Create() {
::size = 0;
H[0] = MINH;
}
向堆中插入元素
void Insert(int X) {
int i;
for (i = ++::size; H[i / 2] > X; i /= 2) {
H[i] = H[i / 2];
}
H[i] = X;
}
主函数
int main() {
int n, m,x,i,j;//n表示读入元素个数,m表示层数
cin >> n >> m;
Create();//初始化堆
for (i = 0; i < n; i++) {
cin >> x;
Insert(x);
}
for (i = 0; i < m; i++) {
cin >> j;
cout << H[j];
while (j > 1) {
j /= 2;
cout << H[j];
}
cout << "\n";
}
return 0;
}
2.哈夫曼树(最优二叉树),哈夫曼编码
1.概念
哈夫曼树,又称最优二叉树,是带权路径长度最短的数,可用来构造最优编码,用于信息传输,数据压缩等方面,是一种应用广泛的二叉树。
2.哈夫曼树的特点
1.没有度为1的节点。(叶节点的度为0,其他节点度为2)
2.n个叶子节点的哈夫曼树共有2n-1个节点。
3.哈夫曼树的任意非叶子节点交换还是哈夫曼树。
4同一组权值可能会有不同构的两棵哈夫曼树。
3.哈夫曼编码
赫夫曼树的应用十分广泛,比如众所周知的在通信电文中的应用。在等传送电文时,我们希望电文的总长尽可能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。为了保证在译码时不出现歧义,我们可以采取如下图所示的编码方式:
即左分支编码为字符0,右分支编码为字符1,将从根节点到叶子节点的路径上分支字符组成的字符串作为叶子节点字符的编码,这便是赫夫曼编码。我们根据上面左图可以得到各叶子节点的赫夫曼编码如下:
权值为5的节点的赫夫曼编码为:11
权值为4的节点的赫夫曼编码为:10
权值为3的节点的赫夫曼编码为:00
权值为2的节点的赫夫曼编码为:011
权值为1的节点的赫夫曼编码为:010
而对于上面右图,则可以得到各叶子节点的赫夫曼编码如下:
权值为5的节点的赫夫曼编码为:00
权值为4的节点的赫夫曼编码为:01
权值为3的节点的赫夫曼编码为:10
权值为2的节点的赫夫曼编码为:110
权值为1的节点的赫夫曼编码为:111
最小带权路径长度(左图):WPL=2*3+3*1+3*2+2*4+2*5=33
最小带权路径长度(右图):WPL=2*5+2*4+2*3+3*2+3*1=33
由此可见,不管是左子树权值大于右子树权值还是小于右子树权值的哈夫曼树的最小带权路径长度不变。
4.哈夫曼编码的实现
由于赫夫曼树中没有度为1的节点,则一棵具有n个叶子节点的的赫夫曼树共有2n-1个节点(最后一条特性),因此可以将这些节点存储在大小为2n-1的一维数组中。我们可以用以下数据结构来表示赫夫曼树和赫夫曼编码:
/*
赫夫曼二叉树的存储结构,它也是一种二叉树结构,
这种存储结构既适合表示树,也适合表示森林。
*/
typedef struct Node
{
int weight; //权值
int parent; //父节点的序号,为-1的是根节点
int lchild, rchild; //左右孩子节点的序号,为-1的是叶子节点
}HTNode, *HuffmanTree; //用来存储赫夫曼树中的所有节点
从叶子结点开始遍历二叉树求最小带权路径长度WPL
/*
从叶子结点开始遍历二叉树直到根结点,根结点为HT[2n-1],且HT[2n-1].parent=-1;
各叶子结点为HT[0]、HT[1]...HT[n-1]。
关键步骤是求出各个叶子结点的路径长度,用此路径长度*此结点的权值就是
此结点带权路径长度,最后将各个叶子结点的带权路径长度加起来即可。
*/
int countWPL1(HuffmanTree HT, int n)
{
int i,countRoads,WPL=0;
/*
由creat_huffmanTree()函数可知,HT[0]、HT[1]...HT[n-1]存放的就是各个叶子结点,
所以挨个求叶子结点的带权路径长度即可
*/
for (i = 0; i < n; i++)
{
int father = HT[i].parent; //当前节点的父节点
countRoads = 0;//置当前路径长度为0
//从叶子节点遍历赫夫曼树直到根节点
while (father != -1)
{
countRoads++;
father = HT[father].parent;
}
WPL += countRoads * HT[i].weight;
}
return WPL;
}
从根结点开始遍历二叉树求最小带权路径长度WP:
/*
以下是从根结点开始遍历二叉树,求最小带权路径长度。关键步骤是求出各个叶子
结点的路径长度,用此路径长度*此结点的权值就是此结点带权路径长度,最后将
各个叶子结点的带权路径长度加起来即可。
*/
int countWPL2(HuffmanTree HT, int n)
{
int cur = 2 * n - 2; //当前遍历到的节点的序号,初始时为根节点序号
int countRoads=0, WPL=0;//countRoads保存叶子结点的路径长度
//构建好赫夫曼树后,把visit[]用来当做遍历树时每个节点的状态标志
//visit[cur]=0表明当前节点的左右孩子都还没有被遍历
//visit[cur]=1表示当前节点的左孩子已经被遍历过,右孩子尚未被遍历
//visit[cur]=2表示当前节点的左右孩子均被遍历过
int visit[maxSize] = { 0 };//visit[]是标注数组,初始化为0
//从根节点开始遍历,最后回到根节点结束
//当cur为根节点的parent时,退出循环
while (cur != -1)
{
//左右孩子均未被遍历,先向左遍历
if (visit[cur]==0)
{
visit[cur] = 1; //表明其左孩子已经被遍历过了
if (HT[cur].lchild != -1)
{ //如果当前节点不是叶子节点,则路径长度+1,并继续向左遍历
countRoads++;
cur = HT[cur].lchild;
}
else
{ //如果当前节点是叶子节点,则计算此结点的带权路径长度,并将其保存起来
WPL += countRoads * HT[cur].weight;
}
}
//左孩子已被遍历,开始向右遍历右孩子
else if (visit[cur]==1)
{
visit[cur] = 2;
if (HT[cur].rchild != -1)
{ //如果当前节点不是叶子节点,则记下编码,并继续向右遍历
countRoads++;
cur = HT[cur].rchild;
}
}
//左右孩子均已被遍历,退回到父节点,同时路径长度-1
else
{
visit[cur] = 0;
cur = HT[cur].parent;
--countRoads;
}
}
return WPL;
}
c语言版完整代码(第一种,如上图左图,左孩子权值小于右孩子权值):
#include<iostream>
using namespace std;
#pragma warning (disable:4996)
#define maxSize 100
/*
赫夫曼树的存储结构,它也是一种二叉树结构,
这种存储结构既适合表示树,也适合表示森林。
*/
typedef struct Node
{
int weight; //权值
int parent; //父节点的序号,为-1的是根节点
int lchild, rchild; //左右孩子节点的序号,为-1的是叶子节点
}HTNode, *HuffmanTree; //用来存储赫夫曼树中的所有节点
typedef char **HuffmanCode; //用来存储每个叶子节点的赫夫曼编码
HuffmanTree create_HuffmanTree(int *wet, int n);
void select_minium(HuffmanTree HT, int k, int &min1, int &min2);
int min(HuffmanTree HT, int k);
void HuffmanCoding1(HuffmanTree HT, HuffmanCode &HC, int n);
void HuffmanCoding2(HuffmanTree HT, HuffmanCode &HC, int n);
int countWPL1(HuffmanTree HT, int n);
int countWPL2(HuffmanTree HT, int n);
int main()
{
int w[] = { 5,4,3,2,1 };//用数组w存储各个权值
int n=5;//表示数组w中的个数
HuffmanCode HC = NULL;
HuffmanTree hTree = create_HuffmanTree(w, n);
int wpl1 = countWPL1(hTree, n);
printf("从叶子结点开始遍历二叉树求最小带权路径长度WPL=%d\n", wpl1);
int wpl2 = countWPL2(hTree, n);
printf("从根结点开始遍历二叉树求最小带权路径长度WPL=%d\n", wpl2);
printf("\n从叶子到根结点编码结果为:\n");
HuffmanCoding1(hTree, HC, n);
printf("\n从根结点到叶子结点编码结果为:\n");
HuffmanCoding2(hTree, HC, n);
system("pause");
return 0;
}
/*根据给定的n个权值构造一棵赫夫曼树,wet中存放n个权值*/
HuffmanTree create_HuffmanTree(int *wet, int n)
{
//一棵有n个叶子节点的赫夫曼树共有2n-1个节点
int total = 2 * n - 1;
HuffmanTree HT = (HuffmanTree)malloc(total * sizeof(HTNode));
if (!HT)
{
printf("HuffmanTree malloc faild!");
exit(-1);
}
int i;
//以下初始化序号全部用-1表示,
//这样在编码函数中进行循环判断parent或lchild或rchild的序号时,
//不会与HT数组中的任何一个下标混淆
//HT[0],HT[1]...HT[n-1]中存放需要编码的n个叶子节点
for (i = 0; i < n; i++)
{
HT[i].parent = -1;
HT[i].lchild = -1;
HT[i].rchild = -1;
HT[i].weight = *wet;
wet++;
}
//HT[n],HT[n+1]...HT[2n-2]中存放的是中间构造出的每棵二叉树的根节点
for (; i < total; i++)
{
HT[i].parent = -1;
HT[i].lchild = -1;
HT[i].rchild = -1;
HT[i].weight = 0;
}
int min1, min2; //用来保存每一轮选出的两个weight最小且parent为0的节点
//每一轮比较后选择出min1和min2构成一课二叉树,最后构成一棵赫夫曼树
for (i = n; i < total; i++)
{
select_minium(HT, i, min1, min2);
HT[min1].parent = i;
HT[min2].parent = i;
//这里左孩子和右孩子可以反过来,构成的也是一棵赫夫曼树,只是所得的编码不同
HT[i].lchild = min1;
HT[i].rchild = min2;
HT[i].weight = HT[min1].weight + HT[min2].weight;
}
return HT;
}
/*
从HT数组的前k个元素中选出weight最小且parent为-1的两个,分别将其序号保存在min1和min2中
*/
void select_minium(HuffmanTree HT, int k, int &min1, int &min2)
{
min1 = min(HT, k);
min2 = min(HT, k);
}
/*
从HT数组的前k个元素中选出weight最小且parent为-1的元素,并将该元素的序号返回
*/
int min(HuffmanTree HT, int k)
{
int i = 0;
int min; //用来存放weight最小且parent为-1的元素的序号
int min_weight; //用来存放weight最小且parent为-1的元素的weight值
//先将第一个parent为-1的元素的weight值赋给min_weight,留作以后比较用。
//注意,这里不能按照一般的做法,先直接将HT[0].weight赋给min_weight,
//因为如果HT[0].weight的值比较小,那么在第一次构造二叉树时就会被选走,
//而后续的每一轮选择最小权值构造二叉树的比较还是先用HT[0].weight的值来进行判断,
//这样又会再次将其选走,从而产生逻辑上的错误。
while (HT[i].parent != -1)
i++;
min_weight = HT[i].weight;
min = i;
//选出weight最小且parent为-1的元素,并将其序号赋给min
for (; i < k; i++)
{
if (HT[i].weight < min_weight && HT[i].parent == -1)
{
min_weight = HT[i].weight;
min = i;
}
}
//选出weight最小的元素后,将其parent置1,使得下一次比较时将其排除在外。
HT[min].parent = 1;
return min;
}
/*
从叶子节点到根节点逆向求赫夫曼树HT中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding1(HuffmanTree HT, HuffmanCode &HC, int n)
{
//用来保存指向每个赫夫曼编码串的指针
HC = (HuffmanCode)malloc(n * sizeof(char *));
if (!HC)
{
printf("HuffmanCode malloc faild!");
exit(-1);
}
//临时空间,用来保存每次求得的赫夫曼编码串
//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
//外加一个'\0'结束符,因此分配的数组长度最长为n即可
char *code = (char *)malloc(n * sizeof(char));
if (!code)
{
printf("code malloc faild!");
exit(-1);
}
code[n - 1] = '\0'; //编码结束符,亦是字符数组的结束标志
//求每个字符的赫夫曼编码
int i;
for (i = 0; i < n; i++)
{
int current = i; //定义当前访问的节点
int father = HT[i].parent; //当前节点的父节点
int start = n - 1; //每次编码的位置,初始为编码结束符的位置
//从叶子节点遍历赫夫曼树直到根节点
while (father != -1)
{
if (HT[father].lchild == current) //如果是左孩子,则编码为0
code[--start] = '0';
else //如果是右孩子,则编码为1
code[--start] = '1';
current = father;
father = HT[father].parent;
}
//为第i个字符的编码串分配存储空间
HC[i] = (char *)malloc((n - start) * sizeof(char));
if (!HC[i])
{
printf("HC[i] malloc faild!");
exit(-1);
}
//将编码串从code复制到HC
strcpy(HC[i], code + start);
}
for (int i = 0; i < n; ++i) {
printf("%s\n", HC[i]);
}
free(code); //释放保存编码串的临时空间
}
/*
从根节点到叶子节点无栈非递归遍历赫夫曼树HT,求其中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding2(HuffmanTree HT, HuffmanCode &HC, int n)
{
//用来保存指向每个赫夫曼编码串的指针
HC = (HuffmanCode)malloc(n * sizeof(char *));
if (!HC)
{
printf("HuffmanCode malloc faild!");
exit(-1);
}
//临时空间,用来保存每次求得的赫夫曼编码串
//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
//外加一个'\0'结束符,因此分配的数组长度最长为n即可
char *code = (char *)malloc(n * sizeof(char));
if (!code)
{
printf("code malloc faild!");
exit(-1);
}
int cur = 2 * n - 2; //当前遍历到的节点的序号,初始时为根节点序号
int code_len = 0; //定义编码的长度
//构建好赫夫曼树后,把weight用来当做遍历树时每个节点的状态标志
//weight=0表明当前节点的左右孩子都还没有被遍历
//weight=1表示当前节点的左孩子已经被遍历过,右孩子尚未被遍历
//weight=2表示当前节点的左右孩子均被遍历过
int i;
for (i = 0; i < cur + 1; i++)
{
HT[i].weight = 0;
}
//从根节点开始遍历,最后回到根节点结束
//当cur为根节点的parent时,退出循环
while (cur != -1)
{
//左右孩子均未被遍历,先向左遍历
if (HT[cur].weight == 0)
{
HT[cur].weight = 1; //表明其左孩子已经被遍历过了
if (HT[cur].lchild != -1)
{ //如果当前节点不是叶子节点,则记下编码,并继续向左遍历
code[code_len++] = '0';
cur = HT[cur].lchild;
}
else
{ //如果当前节点是叶子节点,则终止编码,并将其保存起来
code[code_len] = '\0';
HC[cur] = (char *)malloc((code_len + 1) * sizeof(char));
if (!HC[cur])
{
printf("HC[cur] malloc faild!");
exit(-1);
}
strcpy(HC[cur], code); //复制编码串
}
}
//左孩子已被遍历,开始向右遍历右孩子
else if (HT[cur].weight == 1)
{
HT[cur].weight = 2; //表明其左右孩子均被遍历过了
if (HT[cur].rchild != -1)
{ //如果当前节点不是叶子节点,则记下编码,并继续向右遍历
code[code_len++] = '1';
cur = HT[cur].rchild;
}
}
//左右孩子均已被遍历,退回到父节点,同时编码长度减1
else
{
HT[cur].weight = 0;
cur = HT[cur].parent;
--code_len;
}
}
for (int i = 0; i < n; ++i) {
printf("%s\n", HC[i]);
}
free(code);
}
/*
从叶子结点开始遍历二叉树直到根结点,根结点为HT[2n-1],且HT[2n-1].parent=-1;
各叶子结点为HT[0]、HT[1]...HT[n-1]。
关键步骤是求出各个叶子结点的路径长度,用此路径长度*此结点的权值就是
此结点带权路径长度,最后将各个叶子结点的带权路径长度加起来即可。
*/
int countWPL1(HuffmanTree HT, int n)
{
int i,countRoads,WPL=0;
/*
由creat_huffmanTree()函数可知,HT[0]、HT[1]...HT[n-1]存放的就是各个叶子结点,
所以挨个求叶子结点的带权路径长度即可
*/
for (i = 0; i < n; i++)
{
int father = HT[i].parent; //当前节点的父节点
countRoads = 0;//置当前路径长度为0
//从叶子节点遍历赫夫曼树直到根节点
while (father != -1)
{
countRoads++;
father = HT[father].parent;
}
WPL += countRoads * HT[i].weight;
}
return WPL;
}
/*
以下是从根结点开始遍历二叉树,求最小带权路径长度。关键步骤是求出各个叶子
结点的路径长度,用此路径长度*此结点的权值就是此结点带权路径长度,最后将
各个叶子结点的带权路径长度加起来即可。
*/
int countWPL2(HuffmanTree HT, int n)
{
int cur = 2 * n - 2; //当前遍历到的节点的序号,初始时为根节点序号
int countRoads=0, WPL=0;//countRoads保存叶子结点的路径长度
//构建好赫夫曼树后,把visit[]用来当做遍历树时每个节点的状态标志
//visit[cur]=0表明当前节点的左右孩子都还没有被遍历
//visit[cur]=1表示当前节点的左孩子已经被遍历过,右孩子尚未被遍历
//visit[cur]=2表示当前节点的左右孩子均被遍历过
int visit[maxSize] = { 0 };//visit[]是标注数组,初始化为0
//从根节点开始遍历,最后回到根节点结束
//当cur为根节点的parent时,退出循环
while (cur != -1)
{
//左右孩子均未被遍历,先向左遍历
if (visit[cur]==0)
{
visit[cur] = 1; //表明其左孩子已经被遍历过了
if (HT[cur].lchild != -1)
{ //如果当前节点不是叶子节点,则路径长度+1,并继续向左遍历
countRoads++;
cur = HT[cur].lchild;
}
else
{ //如果当前节点是叶子节点,则计算此结点的带权路径长度,并将其保存起来
WPL += countRoads * HT[cur].weight;
}
}
//左孩子已被遍历,开始向右遍历右孩子
else if (visit[cur]==1)
{
visit[cur] = 2;
if (HT[cur].rchild != -1)
{ //如果当前节点不是叶子节点,则记下编码,并继续向右遍历
countRoads++;
cur = HT[cur].rchild;
}
}
//左右孩子均已被遍历,退回到父节点,同时路径长度-1
else
{
visit[cur] = 0;
cur = HT[cur].parent;
--countRoads;
}
}
return WPL;
}
- 测试结果为:
从叶子结点开始遍历二叉树求最小带权路径长度WPL=33 从根结点开始遍历二叉树求最小带权路径长度WPL=33 从叶子到根结点编码结果为: 11 10 00 011 010 从根结点到叶子结点编码结果为: 11 10 00 011 010 Press any key to continue . . .