1. 概述
堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别称为大顶堆和小顶堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。
堆结构是一类很重要的数据结构,特别是在实现优先队列方面。堆结构实际上都是树(或森林),不过按节点的存储方式又可以分为两种
- 顺序存储
这种情况下,必须保持是完全树。 (在插入,删除后及时调整)
可以通过下标运算找到父节点,字节点。 - 链接存储
如果是二叉树,一般就用LeftChild,RightChild 两个指针。
否则一般用环链表表示一个 节点的子女。
数据结构 | 存储方式 | 定义与性质 | 支持的操作 及 复杂度(分摊) |
最大堆 | 顺序存储 | 完全二叉树,最大树 | 插入 O(log n) 删除最大元素 O(log n) |
最小最大堆 | 顺序存储 | 完全二叉树,树的各层交替为最小层和最大层 | 插入 O(log n) 删除最大元素 O(log n) 删除最小元素 O(log n) |
双堆 | 顺序存储 | 完全二叉树 或者为空,或者同时满足: 1.根结点不含元素 2.左子树是最小堆 3.右子树为最大堆 4.若右子树不为空,设i 为左子树中的任意结点, j为右子树的对应结点. 若j不存在,则令j为右子树中对应i的父结点的结点。 必有结点i的key 小于 结点j的key值 | 插入 O(log n) 删除最大元素 O(log n) 删除最小元素 O(log n) |
左偏树 (最小/最大) | 链接存储 | 1.是左偏树 每一个内部结点x 有 shortest(LeftChild(x)) >=shortes(RightChild(x)) 2.是最小/最大树 | 插入 O(log n) 删除最小/最大元素 O(log n) 合并 O(log n) |
二项式堆 (最小/最大) | 链接存储 | 最小/最大二项式树的集合. 度为k的二项式树(记作Bk)定义为: 若k=0,该树只有1个结点. 若k>0,该树的根的度为k,其子树为B0,B1,…..,Bk-1 | 插入 O(1) 删除最小/最大元素 O(log n) 合并 O(1) |
斐波纳契堆 (最小/最大) | 链接存储 | 最小/最大树的集合. 二项式堆是斐波纳契堆的特殊情况. 使用双环链表,并在结点中加入parent和childcut. 关键是瀑布修剪 | 插入 O(1) 删除最小/最大元素 O(log n) 合并 O(1) 减少指定结点key值 O(1) 删除指定结点元素 O(log n) |
2. 堆的基本操作
堆是一棵完全二叉树,高度为O(lg n),其基本操作至多与树的高度成正比。在介绍堆的基本操作之前,先介绍几个基本术语:
A:用于表示堆的数组,下标从1开始,一直到n
PARENT(t):节点t的父节点,即floor(t/2)
RIGHT(t):节点t的左孩子节点,即:2*t
LEFT(t):节点t的右孩子节点,即:2*t+1
HEAP_SIZE(A):堆A当前的元素数目
下面给出其主要的四个操作(以大顶堆为例):
1.一个是他是一个数组(当然你也可以真的用链表来做。)。
2.他可以看做一个完全二叉树。注意是完全二叉树。所以他的叶子个数刚好是nSize / 2个。
3.我使用的下标从1开始,这样好算,如果节点的位置为i,他的父节点就是i/2,他的左孩子结点就是i*2,右孩子结点就是i*2+1,如果下标从0开始,要复杂一点。
4.他的父节点一定不比子节点小(我所指的是最大堆)。
由这些性质就可以看出堆得一些优点:
1.可以一下找到最大值,就在第一个位置heap[1].
2.维持堆只需要log(2,n)(n是数据个数)的复杂度,速度比较快。他只需要比较父与子之间的大小关系,所以比较次数就是树的高度,而他是一个完全二叉树,所以比较次数就是log(2,n)。
代码如下:
binheap.h
typedef int ElementType;
#ifndef BINHEAP_H_INCLUDED
#define BINHEAP_H_INCLUDED
struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;
PriorityQueue Initialize( int MaxElements );
void Destroy( PriorityQueue H );
void MakeEmpty( PriorityQueue H );
void Insert( ElementType X, PriorityQueue H );
ElementType DeleteMin( PriorityQueue H );
ElementType FindMin( PriorityQueue H );
int IsEmpty( PriorityQueue H );
int IsFull( PriorityQueue H );
#endif // BINHEAP_H_INCLUDED
binheap.c
#include "binheap.h"
#include "../lib/fatal.h"
#include <stdlib.h>
#define MinPQSize 10
#define MinData -32767
struct HeapStruct
{
int Capacity;
int Size;
ElementType *Elements;
};
PriorityQueue Initialize( int MaxElements )
{
PriorityQueue H;
if( MaxElements < MinPQSize )
Error( "Priority queue size is too small" );
H = malloc( sizeof( struct HeapStruct ) );
if( H ==NULL )
FatalError( "Out of space!!!" );
H->Elements = malloc( ( MaxElements + 1 ) * sizeof( ElementType ) );
if( H->Elements == NULL )
FatalError( "Out of space!!!" );
H->Capacity = MaxElements;
H->Size = 0;
H->Elements[ 0 ] = MinData;
return H;
}
void MakeEmpty( PriorityQueue H )
{
H->Size = 0;
}
void Insert( ElementType X, PriorityQueue H )
{
int i;
if( IsFull( H ) )
{
Error( "Priority queue is full" );
return;
}
for( i = ++H->Size; H->Elements[ i / 2 ] > X; i /= 2 )
H->Elements[ i ] = H->Elements[ i / 2 ];
H->Elements[ i ] = X;
}
ElementType DeleteMin( PriorityQueue H )
{
int i, Child;
ElementType MinElement, LastElement;
if( IsEmpty( H ) )
{
Error( "Priority queue is empty" );
return H->Elements[ 0 ];
}
MinElement = H->Elements[ 1 ];
LastElement = H->Elements[ H->Size-- ];
for( i = 1; i * 2 <= H->Size; i = Child )
{
Child = i * 2;
if( Child != H->Size && H->Elements[ Child + 1 ] < H->Elements[ Child ] )
Child++;
if( LastElement > H->Elements[ Child ] )
H->Elements[ i ] = H->Elements[ Child ];
else
break;
}
H->Elements[ i ] = LastElement;
return MinElement;
}
ElementType FindMin( PriorityQueue H )
{
if( !IsEmpty( H ) )
return H->Elements[ 1 ];
Error( "Priority Queue is Empty" );
return H->Elements[ 0 ];
}
int IsEmpty( PriorityQueue H )
{
return H->Size == 0;
}
int IsFull( PriorityQueue H )
{
return H->Size == H->Capacity;
}
void Destroy( PriorityQueue H )
{
free( H->Elements );
free( H );
}
2.1 Heapify(A,n,t)
该操作主要用于维持堆的基本性质。假定以RIGHT(t)和LEFT(t)为根的子树都已经是堆,然后调整以t为根的子树,使之成为堆。
void Heapify(int A[], int n, int t)
{
int left = LEFT(t);
int right = RIGHT(t);
int max = t;
if(left <= n) max = A[left] > A[max] ? left : max;
if(right <= n) max = A[right] > A[max] ? right : max;
if(max != A[t])
{
swap(A, max, t);
Heapify(A, n, max);
}
}
2.2 BuildHeap(A,n)
该操作主要是将数组A转化成一个大顶堆。思想是,先找到堆的最后一个非叶子节点(即为第n/2个节点),然后从该节点开始,从后往前逐个调整每个子树,使之称为堆,最终整个数组便是一个堆。
void BuildHeap(int A[], int n)
{
int i;
for(i = n/2; i<=n; i++)
Heapify(A, n, i);
}
2.3 GetMaximum(A,n)
该操作主要是获取堆中最大的元素,同时保持堆的基本性质。堆的最大元素即为第一个元素,将其保存下来,同时将最后一个元素放到A[1]位置,之后从上往下调整A,使之成为一个堆。
void GetMaximum(int A[], int n)
{
int max = A[1];
A[1] = A[n];
n--;
Heapify(A, n, 1);
return max;
}
2.4 Insert(A, n, t)
向堆中添加一个元素t,同时保持堆的性质。算法思想是,将t放到A的最后,然后从该元素开始,自下向上调整,直至A成为一个大顶堆。
void Insert(int A[], int n, int t)
{
n++;
A[n] = t;
int p = n;
while(p >1 && A[PARENT(p)] < t)
{
A[p] = A[PARENT(p)];
p = PARENT(p);
}
A[p] = t;
return max;
}
3. 堆的应用
3.1 堆排序
堆的最常见应用是堆排序,时间复杂度为O(N lg N)。如果是从小到大排序,用小顶堆;从大到小排序,用大顶堆。
3.2 在O(n lg k)时间内,将k个排序表合并成一个排序表,n为所有有序表中元素个数。
【解析】取前100 万个整数,构造成了一棵数组方式存储的具有小顶堆,然后接着依次取下一个整数,如果它大于最小元素亦即堆顶元素,则将其赋予堆顶元素,然后用Heapify调整整个堆,如此下去,则最后留在堆中的100万个整数即为所求 100万个数字。该方法可大大节约内存。
3.3 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k))
4. 总结
堆是一种非常基础但很实用的数据结构,很多复杂算法或者数据结构的基础就是堆,因而,了解和掌握堆这种数据结构显得尤为重要。
5. 参考资料
(1)经典算法教程《算法导论》