堆(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;
}
堆插入
- 先把新加入的元素放到二叉树的最后,也就是数组的最后位置。
- 然后和新元素位置的父节点比较大小,大则交换
i= ++H->Size ;
for (;item > H->Elements[i/2] ; i=i/2 ) // 0位置有一个很大的哨兵,到顶时结束
H->Elements[i] = H->Elements[i/2];
i一开始为插入元素的位置,这条语句就能使得比新元素的结点一直往下移,直到i到达的位置是的新元素的父节点比它大位置,这里哨兵的作用就是当插入的元素是当前的最大元素,i到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 ;
}
堆删除最大元素
- 先把最大值保存起来,函数结束时返回
- 把最后一个元素放到树根的位置,也就是删除的位置,此时堆长度减一
- 一直选择左右子结点最大的跟放上去的值比较,如果比这个值大,则交换位置,直到左右结点都比这个元素小为止。
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)
一个线性建堆的方法
- 将n个元素按输入顺序存入。先满足完全二叉树的结构性质
- 调整各个结点 的位置。以满足最大堆的有序特性
调整策略是从最后一个结点的父节点开始,到根结点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,这样可以提高并查集查找的效率。
并查集功能
- 判环,如果两个结点的根相同,这两个结点又相连,那一定存在环
- 可以判断两个元素是否属于同一类
- 判断一共有几类
##习题: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;
}
来源:CSDN
作者:闽院ACMer
链接:https://blog.csdn.net/qq_20225851/article/details/104148777