在一些用户环境中,需要优先处理队列中的一些元素。例如,在一堆文件中有一项文件非常重要,需要对它进行优先处理,如果使用队列的话,则必须逐个出队直到该文件,这是没有办法对该文件进行优先处理的,所以就提出了一中特殊的队列(优先队列)来解决这个问题。
优先队列至少允许下面两种操作:Insert(插入)、DeleteMin(删除最小者)。
简单的实现方法:使用一个带有表头的链表,插入在表头以O(1)时间复杂度实现,删除则需要遍历该链表,时间复杂度为O(N)。另一种方法是保持表是有序的,那么删除则需要O(1)时间,插入则需要遍历链表,时间复杂度为O(N)。如果使用链表来实现优先队列,当删除操作次数小于插入时,应该使用前一种方法,反之则使用后一种方法。还可以用二叉查找树来实现优先队列,那么插入和删除都花费O(logN)时间,但删除操作会一直删除左子树中的元素,这会导致树的不平衡,还需要对树进行重新平衡。但还有一种更见简便的方法,使用二叉堆(数组实现)来实现优先队列。
二叉堆:
二叉堆具有结构性和堆序性。
结构性:
二叉堆是一棵完全二叉树。完全二叉树是指一棵从上到下、从左到右完全填满的二叉树,唯一的例外可能出现在最底层。二叉堆的任意子树也是一个二叉堆。
完全二叉树可以使用数组来表示,所以二叉堆也就可以用数组来实现。二叉堆的根节点在数组下标为1的地方,并且任意节点i的左儿子下表为2*i,右儿子下标为2*i+1.并且在数组最后一个关键字的后面插入新元素就可以满足完全二叉树的条件,这使得插入就可以直接在上一个元素之后进行。
堆序性:
对于二叉堆中所有的节点而言,它们的儿子节点中的关键字均大于(或小于)该节点。
二叉堆实现优先队列:
小根堆的实现:
//二叉堆
#include <stdio.h>
#include <stdlib.h>
typedef struct dui {
int capacity;//堆的容量
int num;//成员数量
int ele[10];//二叉堆
}d;
d* CreatDui() {
d* p = (d*)malloc(sizeof(d));
if (p == NULL) {
printf("内存不足\n");
return NULL;
}
p->capacity = 10;
p->num = 0;
p->ele[0] = -1;
return p;
}
void insert(d* p, int x) {
int i = ++p->num;//要在数组下标为p->num+1处插入新元素,也就是第一个空位置处
for (; p->ele[i / 2] > x; i /= 2) {//插入元素要与父节点比较,如果小于,则需要上浮
p->ele[i] = p->ele[i / 2];//父节点下沉
}
p->ele[i] = x;//当发现父节点小于插入元素时,说明找到了要插入的位置
}
int deletemin(d* p) {
int minele = p->ele[1];//用于返回最小元素
int child = 0;
int lastele = p->ele[p->num--];//将最后一个元素前移,以满足完全二叉树,但要满足堆序性,并且队中的元素个数-1
int i = 0;
for (i = 1; i * 2 <= p->num; i=child) {//i代表空穴
child = 2 * i;//找到空穴的孩子节点
if (child <= p->num && p->ele[child] > p->ele[child + 1]) {//如果只有一个孩子节点,那么child>p->num(因为之前num已经-1),则不会出现越界问题,并且下面的if中的条件会判断为真,直接跳出循环,如果有两个孩子节点,则找到小的那个孩子节点
child++;
}
if (lastele <= p->ele[child]) {//如果小的孩子节点大于或等于最后一个元素,就跳出循环,直接用最后一个元素填补空穴
break;
}
else {//如果不是,则使空穴下沉
p->ele[i] = p->ele[child];
}
}
p->ele[i] = lastele;//用最后一个元素填补空穴
return minele;
}
int main() {
d* pd = CreatDui();
for (int i = 10; i >= 1; i--) {
insert(pd, i);
}
for (int i = 1; i <= 10; i++) {
printf("%d ", deletemin(pd));
}
printf("\n-----------------\n");
return 0;
}
大根堆的实现:
#include <stdio.h>
#include <stdlib.h>
typedef struct heap {
int num;
int arr[11];
}heap;
heap* CreatHeap() {
heap* p = (heap*)malloc(sizeof(heap));
p->num = 0;
p->arr[0] = 100;//将下标0处设置为一个标记,用来在插入时终止循环
return p;
}
void insert(heap* p, int x) {
int i = ++p->num;
for (; p->arr[i / 2] < x; i = i / 2) {//当插入值上浮到根节点时,将它与0下标的值比较,就可以将其插入在根节点
p->arr[i] = p->arr[i / 2];
}
p->arr[i] = x;
}
int deletemax(heap* p) {
int max = p->arr[1];
int lastele = p->arr[p->num--];
int child = 0;
int i = 1;
for (; i <= p->num; i = child) {
child = 2 * i;
if (child <= p->num && p->arr[child] < p->arr[child + 1]) {
child++;
}
if (p->arr[child] <= lastele) {
break;
}
else {
p->arr[i] = p->arr[child];
}
}
p->arr[i] = lastele;
return max;
}
int main() {
heap* h = CreatHeap();
for (int i = 1; i <= 10; i++) {
insert(h, i);
}
for (int i = 1; i <= 10; i++) {
printf("%d ", deletemax(h));
}
return 0;
}
这种算法的最坏运行时间为O(logN),平均运行时间也为O(logN).
d堆:
d堆和二叉堆的关系就像B树和二叉树,d堆是二叉堆的简单推广,只是它的每一个节点可以有d个孩子,如果d=2,那么d堆就是一个二叉堆。d堆将插入时间改良为O(logdN),但它的删除操作比较费时,因为每一次选出最小值需要进行d-1次比较,所以一次deletemin需要O(dlogdN)时间,当d是常数时,还是O(logN)时间。由于d堆减少了树的深度,所以在一定程度上也改善了性能,当二叉堆大到主存无法存放时,d堆也能发挥作用。d堆是一棵完全d叉树。d堆也有着二叉堆的缺陷,除了不能执行find操作外,两个堆的合并也是一个难题,为了解决两个堆的合并问题,就提出了左式堆。
左式堆:
左式堆的堆序性与二叉堆一致。
左式堆的结构性:左式堆是一棵不平衡的树。我们把任意节点x的零路径长npl定义为从x到一个没有两个儿子节点的最短距离。具有一个或没有儿子节点的npl就为0,NULL为-1.任意节点的npl比它孩子中最小的npl多1.左式堆就是堆中每一个节点的左子树的npl最小等于右子树的npl的堆。
对于左式堆,右路径有r个节点的堆至少有2^r-1个节点。
左式堆的基本操作是合并,共分为三步:
1.将具有较大根植的堆与较小根植堆的右子堆合并
2.使这个新堆成为较小根植根的右孩子
3.将根的左右儿子互换,并更新npl
左式堆有可能退化到O(N)时间复杂度。
斜堆:
斜堆是左式堆的推广,就像AVL树与伸展树一样。斜堆不要求堆的结构性,只要求堆序性。每个节点并不保留npl值,所以斜堆的右子堆可以任意长,最坏时间复杂度为O(N)。对于斜堆的M次操作的平均时间为O(MlogN),摊还时间为O(logN)。
斜堆的基本操作也是merge。但不同于左式堆,除了右路经中的最大者外(没有儿子就不用交换),交换左右儿子是无条件的,这是递归中自然发生的。斜堆并不一定是左式的。