引论
优先队列英文名为Priority Queue,也常被称为Heaps,也就是堆。
前面已经有了链表,堆栈,队列,树等数据结构,尤其是树,是一个很强大的数据结构,能做很多事情,那么为什么还要引进一个优先队列的东东呢?它和队列有什么本质的不同呢?看一个例子,有一个打印机,但是有很多的文件需要打印,那么这些任务必然要排队等候打印机逐步的处理。这里就产生了一个问题。原则上说先来的先处理,但是有一个文件100页,它排在另一个1页的文件的前面,那么可能我们要先打印这个1页的文件比较合理。因此为了解决这一类的问题,提出了优先队列的模型。
优先队列是一个至少能够提供插入(Insert)和删除最小(DeleteMin)这两种操作的数据结构。对应于队列的操作,Insert相当于Enqueue,DeleteMin相当于Dequeue。
链表,二叉查找树,都可以提供插入(Insert)和删除最小(DeleteMin)这两种操作,但是为什么不用它们而引入了新的数据结构的。原因在于应用前两者需要较高的时间复杂度。对于链表的实现,插入需要O(1),删除最小需要遍历链表,故需要O(N)。对于二叉查找树,这两种操作都需要O(logN);而且随着不停的DeleteMin的操作,二叉查找树会变得非常不平衡;同时使用二叉查找树有些浪费,因此很多操作根本不需要。
因此这里引入一种新的数据结构,它能够使插入(Insert)和删除最小(DeleteMin)这两种操作的最坏时间复杂度为O(N),而插入的平均时间复杂度为常数时间,即O(1)。同时不需要引入指针。
二插堆(Binary Heap)
Heap有两个性质:结构性质(structure property),堆的顺序性(heap order property)。看英文应该比较好理解。
- structure property
Heap(堆)是一个除了底层节点外的完全填满的二叉树,底层可以不完全,左到右填充节点。(a heap is a binary tree that completely filled, with the exception of bottom level, which is filled from left to right.)这样的树叫做完全二叉树。
一个高度为h 的完全二叉树应该有以下的性质:
a) 有2^h到2^h-1个节点
b) 完全二叉树的高度为logN
鉴于完全二叉树是一个很整齐的结构,因此可以不用指针而只用数组来表示一颗完全二叉树。 对于处于位置i 的元素,
a)他的左子节点在2i,右子节点在(2i+1)
b) 它的父节点在【i/2】(向下取整)
下图显示了完全二叉树与数组的对应关系:
- heap order property
堆的顺序性质是指最小的结点应该是根节点,鉴于我们希望子树也是堆,那么每个子树的根节点也应该是最小的,这样就可以立刻找到最小值,然后可以对其进行删除操作。下图是一个堆的例子。
其实从这里可以看出,堆的两条性质:
(a)完全二叉树;
(b)父节点小于后继子节点
代码实现
- 堆的声明
1
2
3
4
5
6
7
8
9typedef struct HeapStruct;
typedef struct HeapStruct *Heap;
struct HeapStruct
{
int Capacity;
int Size;
ElementType *Element;
}
堆元素存放在数组中Element,Capacity是指堆的容量,Size是指堆的实际元素个数。
- 堆的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Heap Initialize(int MaxNum)
{
if(MaxNum<MiniHeapSize)
error("The Heap Size is too small");
Heap H;
H = malloc(sizeof(struct HeapStruct));
if(H==NULL)
Error("Out of Space!!!");
H->Element = malloc(sizeof(ElementType)*(MaxNum+1));
if(H->Element == NULL)
Error("Out of Space!!!");
H->Capacity = MaxNum;
H->Size = 0;
H->Element[0] = MinData;
return H;
}
堆的数组的位置0的值是一个游标哨兵,这个会用到,对元素是从1开始存放的。
- 堆的插入
堆的插入是按照顺序插入到底层的结点上,然后与他的父节点比较,如果小于父节点,那么此结点与父节点交换位置,否则,这个位置就是应该插入的位置,依次循环,如图所示。因此也可以理解堆的插入的平均时间复杂度为O(1),即常数时间,原因就在于只要在最后插入就可,最多是做几个迁移比较,而最坏的时间复杂度为O(logN)是指这个插入节点是最小的结点,要迁移到root。
1 | void Insert(ElementType X, Heap H) |
插入就是一个比较节点和父节点的过程,而对于堆H->Element[i/2]
就是父节点。
- 堆的删除最小操作
找到最小很easy,就是root。但是最关键的是删除了以后的问题。这个可以用插入的思想把一步一步的向上渗透。先选取根节点的最小子节点,然后把这个这点迁移到根节点。然后递归操作。
对于删除最小操作,可与预见的是他的最坏时间复杂度为O(logN),因为删除节点后的渗透是沿着子树走的,类似于二叉查找树的操作,故为O(logN)。
1 | ElementType DeleteMin(Heap H) |
高级语言的封装
在C++或者JAVA中,都已经有成熟的库,我们不需要自己实现堆。
在C++中,优先队列(堆)在头文件#include<queue>
中,他的定义如下:
priority_queue<Type, Container, Functional>
Type
为数据类型Container
为容器类型Functional
为比较的方式
如下所示:1
2
3
4//升序队列,小顶堆
priority_queue <int, vector<int>, greater<int> > q;
//降序队列,大顶堆
priority_queue <int, vector<int>, less<int> > q;