什么是堆
定义:“优先队列”是特殊的“队列”,从堆中取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。采用完全二叉树存储的优先队列 称为堆。
数据对象集:一个有N>0个元素的最大堆H是一棵完全二叉树,每个结点上的元素值不小于其子结点元素的值。
若采用数组或链表实现优先队列
数组
插入 — 元素总是插入尾部 ~O ( 1 )
删除 — 查找最大(或最小)关键字 ~O( n )
从数组中删去需要移动元素 ~ O( n )
链表
插入 — 元素总是插入链表的头部 ~ O( 1 )
删除 — 查找最大(或最小)关键字 ~ O ( n )
删去结点 ~ O( 1 )
有序数组
插入 — 找到合适的位置 ~ O( n ) 或 O(log2 n )
移动元素并插入 ~ O( n )
删除 — 删去最后一个元素 ~ O( 1 )
有序链表
插入 — 找到合适的位置~ O( n )
插入元素 ~ ( 1 )
删除 — 删除首元素或最后元素 ~ O( 1 )
二叉树存储结构
完全二叉树表示优先队列
堆的两个特性
结构性:用数组表示的完全二叉树;
有序性:根结点到任一结点的关键字序列保持非递增(称“最大堆”,也称“大顶堆” )或者非递减(称“最小堆,也称“小顶堆” )。
堆的顶端一定是“最大”,最小”的,但是要注意一个点,这里的大和小并不是传统意义下的大和小,它是相对于优先级而言的。
最大堆的操作
1.创建:
typedef struct HNode *Heap; /* 堆的类型定义 */
struct HNode
{
ElementType *Data; /* 存储元素的数组 */
int Size; /* 堆中当前元素个数 */
int Capacity; /* 堆的最大容量 */
};
typedef Heap MaxHeap;
2.插入
根据完全二叉树的定义,可实现堆的插入
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;
}
3.删除
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--]; /* 注意当前堆的规模要减小 */
for( Parent=1; 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;
return MaxItem;
}
最大堆的建立
建立最大堆”是指如何将已经存在的N个元素按最大堆的要求存放在一个一维数组中。
方法一: 可以通过最大堆的插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为O(N logN)。
方法二:
在线性时间复杂度下建立最大堆。具体分两步进行:
第一步,将N个元素按输入顺序存入二叉树中,这一步只要求满足完全二叉树的结构特性,而不管其有序性。
第二步,调整各结点元素,以满足最大堆的有序特性。
讨论:减堆中某个元素值的操作如何实现?
需要先找到要减的值在数组中的序号,基础方法是遍历整个数组,无视了堆的特性。
考虑堆的特性,尝试用递归。
以最小堆为例,比较顶节点(1)与要找的值,
1) 如果相等,就返回当前节点的index
2)如果当前节点小于要找的值,就先找左子树,左子树找不到,再找右子树。
3)如果当前节点大于要找的值,已经超出范围了,返回0
哈夫曼树与哈夫曼编码
定义;设一棵二叉树有n个叶子结点,每个叶子结点带有权值 wk,从根结点到每个叶子结点的长度为 lk,则每个叶子结点的带权路径长度之和就是这棵树的“带权路径长度”。
最优二叉树或哈夫曼树:WPL最小的二叉树。
定义:假设有n个权值{w1 ,w2 , …… , wn} ,构造有n个叶子的二叉树,每个叶子的权值是n个权值之一。这样的二叉树也许可以构造多个,其中必有一个(或几个)是带权路径长度WPL最小的。达到WPL最小的二叉树就称为最优二叉树或哈夫曼树
哈夫曼树的构造
每次把权值最小的两颗二叉树合并
typedef struct HTNode *HuffmanTree; /* 哈夫曼树类型 */
struct HTNode /* 哈夫曼树结点定义 */
{
int Weight;
HuffmanTree Left, Right;
};
HuffmanTree Huffman( MinHeap H )
{
/* 这里最小堆的元素类型为HuffmanTree */
/* 假设H->Size个权值已经存在H->Data[]->Weight里 */
int i, N;
HuffmanTree T;
BuildHeap(H); /* 将H->Data[]按权值Weight调整为最小堆 */
N = H->Size;
for ( i=1; i<N; i++ ) /* 做H->Size-1次合并 */
{
T = (HuffmanTree)malloc(sizeof(struct HTNode));
T->Left = DeleteMin(H);
T->Right = DeleteMin(H);
T->Weight = T->Left->Weight+T->Right->Weight;
Insert( H, T ); /* 将新T插入最小堆 */
}
return DeleteMin(H);
}
哈夫曼树的特点:
没有度为1的结点;
n个叶子结点的哈夫曼树共有2n-1个结点;
哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树;
哈夫曼编码
给定一段字符串,对字符进行编码,使得该字符串的编码存储空间最少;
怎样进行不等长编码?
如何避免二义性?
前缀码:任何字符的编码都不是另一个字符的前缀
可以无二义的解码
二叉树用于编码
用二叉树进行编码:
(1)左右分支:0,1;
(2)字符只在叶结点上
集合的表示
**集合运算:**交,并,补,差。
并查集:
**定义:**并查集通俗地说的话就是查询确定两个人是否有关系, 或许他们之间很陌生, 但是他们的朋友的朋友的朋友可能就是朋友, 这样也是存在关系。
采用数组存储
(1)查找某个元素所在的集合(用根结点表示)
int Find( SetType S[ ], ElementType X )
{
/* 在数组S中查找值为X的元素所属的集合 */ /* MaxSize是全局变量,为数组S的最大长度 */
int i;
for ( i=0; i < MaxSize && S[i].Data != X; i++) ;
if( i >= MaxSize )
return -1; /* 未找到X,返回-1 */
for( ; S[i].Parent >= 0; i = S[i].Parent ) ;
return i; /* 找到X所属集合,返回树根结点在数组S中的下标 */
}
集合的并运算
(2) 集合的并运算
分别找到X1和X2两个元素所在集合树的根结点
如果它们不同根,则将其中一个根结点的父结点指针设置成 另一个根结点的数组下标。
void Union( SetType S[ ], ElementType X1, ElementType X2 )
{
int Root1, Root2;
Root1 = Find(S, X1);
Root2 = Find(S, X2);
if( Root1 != Root2 )S[Root2]
.Parent = Root1;
}