相比队列和栈,很多人可能对堆的概念比较陌生,下面个给出堆的本质概念
一、堆也是一种数据结构,从实际应用意义来说,他是一种最优级别数据永远在第一位的队列,本文皆以最小值为例(小顶堆),即它变相是一种会永远保持最小值先出队的队列。
二、堆的本质是一颗完全二叉树,树根永远为整个树的最小值,这也就是实现了①永远保持最小值先出队的队列这样的功能。
三、为了便于实现②树根为整个树的最小值,堆中某个节点的值总是大于其父节点的值,这样当树根出队以后,次小的值一定是树根的左右子节点的其中一个。并且堆每个结点的左子树和右子树也都是一个堆。即:当某一个节点发生改变之后,取代他的只有可能和他有直系关系的节点。
四、为了保证②的完全二叉树性质,当堆有元素入队时,我们先将其放在队尾,然后再对其做出调整。当有元素出队时,我们把最后一个节点的元素先放在队首,然后再对其做出调整。
五、结合③和④,我们可以得出结论,无论是入队还是出队,我们实际做的操作都是在某一路径下完成若干次子节点与父节点交换的过程(我们称之为上浮和下沉)
下面再说一下堆支持的基本操作
①入队,插入新元素
②出队,删除堆顶
③查找最小值(堆顶),其实也可以查找其次小值
④建堆,这里建队有两种方法,一种是将值一个一个的插入,另一种是先变成一个堆,再对其进行多次调整。
下面具体讲实现方法,首先定义一个小顶堆
/*
* 使用数组实现堆
*
* capacity 数组的最大容量
* size 数组的长度
* elements 堆中的元素存放的数组
*/
struct MinHeap
{
int capacity;
int size;
ElementType *elements;
};
入队:
先将要插入的结点x放在最底层的最右边,插入后满足完全二叉树的特点,然后把x依次向上调整到合适位置满足堆的性质,即“结点上浮”。
for (i = ++pqueue->size; x < pqueue->elements[i / 2]; i /= 2)
pqueue->elements[i] = pqueue->elements[i / 2]; //上浮
pqueue->elements[i] = x;
出队:
树根出队,然后先把最后的叶子节点放在树根,再逐渐下沉到其合适的位置。
// 注意对节点进行下沉操作时,要判断该节点是有两个儿子还是一个儿子
minElement = pqueue->elements[1];
lastElement = pqueue->elements[pqueue->size--];
for (i = 1; i * 2 <= pqueue->size; i = child)
{
child = i * 2;
// 节点i只有一个儿子时必有i * 2 = pqueue->size
if (child < pqueue->size && pqueue->elements[child] > pqueue->elements[child + 1])
child++;
if (lastElement < pqueue->elements[child])
break;
else
pqueue->elements[i] = pqueue->elements[child];
}
pqueue->elements[i] = lastElement;
return minElement; // 返回被删除的元素
查找
直接返回队首就可以了
return pqueue->elements[1];
建堆:
第一种方法:建立一个空堆,然后将一组数据依次做插入操作,时间复杂度O(NlogN)
pqueue = initialize(n * 2);
for (int i = 0; i < n; i++)
insert(arr[i], pqueue);
第二种方法:先将数组转化为堆,再进行调整,时间复杂度为O(N)
for (i = 1; i <= n; i++)
elements[i] = arr[i - 1];
elements[0] = SentinelElement;//数组转换为堆
for (i = n / 2; i > 0; i--)
adjust(elements, n + 1, i);//从最后一个非叶子节点开始,做调整处理
void adjust(int *arr, int len, int i)
{
int n = len - 1;
int tmp;
if(i * 2 >n) //叶子节点跳出
return;
if (i * 2 == n && arr[i] > arr[n]) // 只有左儿子的节点,并且左儿子比本节点的值要小,交换
{
tmp = arr[i];
arr[i] = arr[n];
arr[n] = tmp;
}
else // 有两个儿子的节点
{
if (arr[i * 2] > arr[i * 2 + 1]) // 右儿子较小
{
if (arr[i] > arr[i * 2 + 1]) // 如果本节点比右儿子大,交换
{
tmp = arr[i];
arr[i] = arr[i * 2 + 1];
arr[i * 2 + 1] = tmp;
adjust(arr, len, i * 2 + 1);
}
}
else // 左儿子较小
{
if (arr[i] > arr[i * 2]) // 如果本节点比左儿子大,交换
{
tmp = arr[i];
arr[i] = arr[i * 2];
arr[i * 2] = tmp;
adjust(arr, len, i * 2 );
}
}
}
}
再说一下堆排序,其实可以把他当作是原堆所有值都出队就好了。时间复杂度O(NlogN)
for (i = 1; i <= n; i++)
elements[i] = deleteMin(pqueue);
elements[0] = SentinelElement;
下面给出完整代码
MinHeap.h 头文件
#ifndef DataStructures_MinHeap_h#define DataStructures_MinHeap_hstruct MinHeap;typedef struct MinHeap * MinPriorityQueue;typedef int ElementType;// 初始化堆MinPriorityQueue initialize(int maxElements);// 销毁堆void destroy(MinPriorityQueue pqueue);// 清空堆中的元素void makeEmpty(MinPriorityQueue pqueue);// 插入操作void insert(ElementType x, MinPriorityQueue pqueue);// 删除最小者操作,返回被删除的堆顶元素ElementType deleteMin(MinPriorityQueue pqueue);// 查找最小者(堆顶)ElementType findMin(MinPriorityQueue pqueue);// 判断堆是否为空int isEmpty(MinPriorityQueue pqueue);// 判断堆是否满int isFull(MinPriorityQueue pqueue);// 通过一个数组来建堆,相当于将用数组实现的无序树转换为堆序树——插入法MinPriorityQueue buildHeap_insert(int *arr, int n);// 通过一个数组来建堆,相当于将用数组实现的无序树转换为堆序树——调整法MinPriorityQueue buildHeap_percolate(int *arr, int n);//堆排序MinPriorityQueue buildHeap_sort(MinPriorityQueue pqueue);// 打印堆void printMinPriorityQueue(MinPriorityQueue pqueue);#endif
MinHeap.cpp 实现函数
#include <stdio.h>#include <stdlib.h>#include "MinHeap.h"/* 标记节点,类似于链表中的表头节点 * 该值必须小于所有最小堆中的元素,设其值为-1 */#define SentinelElement -1/* * 使用数组实现堆 * * capacity 数组的最大容量 * size 数组的长度 * elements 堆中的元素存放的数组 */struct MinHeap{int capacity;int size;ElementType *elements; // 堆的元素个数为size,实际上用来存储的数组的长度为size + 1,还包括一个sentinel元素};void PQueueNULLWarning(){printf("Warning: Minimum Priority Queue is NULL");}void outOfSpaceFatalError(){printf("Fatal Error: Out of space");abort();}MinPriorityQueue initialize(int maxElements){MinPriorityQueue pqueue;if (maxElements <= 0){printf("Fail to initialize: maxElements <= 0");return NULL;}pqueue = (MinPriorityQueue)malloc(sizeof(struct MinHeap));if (pqueue == NULL)outOfSpaceFatalError();// 数组的第0个元素是个sentinel标记节点,计入数组容量中,但不计入capcaity或size中pqueue->size = 0;pqueue->capacity = maxElements;pqueue->elements = (ElementType *)malloc(sizeof(ElementType) * (pqueue->capacity + 1));if (pqueue->elements == NULL)outOfSpaceFatalError();elsepqueue->elements[0] = SentinelElement;return pqueue;}void destroy(MinPriorityQueue pqueue){if (pqueue != NULL){// 在GNU99标准中,free(NULL)什么都不做直接返回,所以不用判断pqueue->elements是否为NULLfree(pqueue->elements);free(pqueue);}}void makeEmpty(MinPriorityQueue pqueue){if (pqueue != NULL)pqueue->size = 0;elsePQueueNULLWarning();}/* * 插入的时间复杂度为O(log N),N为最小堆中的元素个数 */void insert(ElementType x, MinPriorityQueue pqueue){if (pqueue == NULL)PQueueNULLWarning();if (isFull(pqueue)){printf("Fail to insert: Priority Queue is Full");return;}else{int i;for (i = ++pqueue->size; x < pqueue->elements[i / 2]; i /= 2)pqueue->elements[i] = pqueue->elements[i / 2]; //上浮pqueue->elements[i] = x;}}/* * 删除操作的平均时间为O(log N) */ElementType deleteMin(MinPriorityQueue pqueue){if (pqueue == NULL){PQueueNULLWarning();return SentinelElement;}if (isEmpty(pqueue)){printf("Fail to delete: Priority Queue is Empty");return SentinelElement;}int i, child;ElementType minElement, lastElement;// 注意对节点进行下沉操作时,要判断该节点是有两个儿子还是一个儿子minElement = pqueue->elements[1];lastElement = pqueue->elements[pqueue->size--];for (i = 1; i * 2 <= pqueue->size; i = child){child = i * 2;// 节点i只有一个儿子时必有i * 2 = pqueue->sizeif (child < pqueue->size && pqueue->elements[child] > pqueue->elements[child + 1])child++;if (lastElement < pqueue->elements[child])break;elsepqueue->elements[i] = pqueue->elements[child];}pqueue->elements[i] = lastElement;return minElement; // 返回被删除的元素}/* * 执行时间:O(1) */ElementType findMin(MinPriorityQueue pqueue){if (pqueue == NULL){PQueueNULLWarning();return SentinelElement;}elsereturn pqueue->elements[1];}int isEmpty(MinPriorityQueue pqueue){if (pqueue == NULL){PQueueNULLWarning();return -1;}elsereturn (pqueue->size == 0);}int isFull(MinPriorityQueue pqueue){if (pqueue == NULL){PQueueNULLWarning();return -1;}elsereturn (pqueue->size == pqueue->capacity);}void adjust(int *arr, int len, int i){int n = len - 1;int tmp; if(i * 2 >n) //叶子节点跳出 return;if (i * 2 == n && arr[i] > arr[n]) // 只有左儿子的节点,并且左儿子比本节点的值要小,交换{tmp = arr[i];arr[i] = arr[n];arr[n] = tmp;}else // 有两个儿子的节点{if (arr[i * 2] > arr[i * 2 + 1]) // 右儿子较小{if (arr[i] > arr[i * 2 + 1]) // 如果本节点比右儿子大,交换{tmp = arr[i];arr[i] = arr[i * 2 + 1];arr[i * 2 + 1] = tmp; adjust(arr, len, i * 2 + 1);}}else // 左儿子较小{if (arr[i] > arr[i * 2]) // 如果本节点比左儿子大,交换{tmp = arr[i];arr[i] = arr[i * 2];arr[i * 2] = tmp; adjust(arr, len, i * 2 );}}}}/*先将数组转化为堆,再进行调整,时间复杂度为O(N)正确的证明方法应当如下:具有n个元素的平衡二叉树,树高为㏒n,我们设这个变量为h。最下层非叶节点的元素,只需做一次线性运算便可以确定大根,而这一层具有2^(h-1)个元素,我们假定O(1)=1,那么这一层元素所需时间为2^(h-1) × 1。由于是bottom-top建立堆,因此在调整上层元素的时候,并不需要同下层所有元素做比较,只需要同其中之一分支作比较,而作比较次数则是树的高度减去当前节点的高度。因此,第x层元素的计算量为2^(x-1) × (h-x)。又以上通项公式可得知,构造树高为h的二叉堆的精确时间复杂度为: S = 2^(h-1) × 1 + 2^(h-2) × 2 + …… +1 × (h-1) ①通过观察第四步得出的公式可知,该求和公式为等差数列和等比数列的乘积,因此用错位想减发求解,给公式左右两侧同时乘以2,可知: 2S = 2^h × 1 + 2^(h-1) × 2+ …… +2 × (h-1) ②用②减去①可知: S =2^h × 1 - h +1 ③将h = ㏒n 带入③,得出如下结论:S = n - ㏒n +1 = O(n)*/MinPriorityQueue buildHeap_percolate(int *arr, int n){if (arr == NULL){printf("Error: Array is NULL");return NULL;}MinPriorityQueue pqueue;pqueue = ( MinPriorityQueue)malloc(sizeof(struct MinHeap));if (pqueue == NULL)outOfSpaceFatalError();ElementType *elements = ( ElementType *)malloc(sizeof(ElementType) * (n + 1));if (elements == NULL)outOfSpaceFatalError();int i;for (i = 1; i <= n; i++)elements[i] = arr[i - 1];elements[0] = SentinelElement;for (i = n / 2; i > 0; i--)adjust(elements, n + 1, i);pqueue->elements = elements;pqueue->size = n;pqueue->capacity = n * 2;return pqueue;}/* * 通过n次插入元素建立堆,由于每次插入的平均执行时间为O(logN),所以建堆平均时间为O(NlogN) */MinPriorityQueue buildHeap_insert(int *arr, int n){MinPriorityQueue pqueue;if (arr == NULL){printf("Array is NULL, fail to build heap");return NULL;}pqueue = initialize(n * 2);for (int i = 0; i < n; i++)insert(arr[i], pqueue);return pqueue;}/* *通过n次出队操作把其值输出到另一个堆里,时间复杂度O(NlogN) */MinPriorityQueue buildHeap_sort(MinPriorityQueue pqueue){ MinPriorityQueue pqueuetemp; if (pqueue == NULL) { PQueueNULLWarning(); } int n=pqueue->size; pqueuetemp = ( MinPriorityQueue)malloc(sizeof(struct MinHeap)); if (pqueuetemp == NULL) outOfSpaceFatalError(); ElementType *elements = ( ElementType *)malloc(sizeof(ElementType) * (n + 1)); if (elements == NULL) outOfSpaceFatalError(); int i; for (i = 1; i <= n; i++) elements[i] = deleteMin(pqueue); elements[0] = SentinelElement; pqueuetemp->elements = elements; pqueuetemp->size = n; pqueuetemp->capacity = n * 2; return pqueuetemp;}void printMinPriorityQueue(MinPriorityQueue pqueue){if (pqueue == NULL){PQueueNULLWarning();return;}if (pqueue->elements == NULL){printf("Fail to print: Elements of priority queue is NULL");return;}if (isEmpty(pqueue)){printf("Empty Prioirty Queue\n");return;}printf("Priority Queue\n");for (int i = 1; i <= pqueue->size; i++)printf("Element[%d] = %d\n", i, pqueue->elements[i]);printf("\n");}
main.cpp 主函数调用
#include <stdio.h>#include <stdlib.h>#include "MinHeap.h"int main(int argc, const char * argv[]){ int a[5] = {5, 4, 3, 2, 1}; MinPriorityQueue pqueue_ins = buildHeap_insert(a, 5); printMinPriorityQueue(pqueue_ins); MinPriorityQueue pqueue_per = buildHeap_percolate(a, 5); printMinPriorityQueue(pqueue_per); MinPriorityQueue pqueue_sort=buildHeap_sort(pqueue_ins); printMinPriorityQueue(pqueue_sort);/* deleteMin(pqueue_ins); printMinPriorityQueue(pqueue_ins); deleteMin(pqueue_ins); printMinPriorityQueue(pqueue_ins); insert(9, pqueue_ins); printMinPriorityQueue(pqueue_ins); insert(1, pqueue_ins); printMinPriorityQueue(pqueue_ins); */ system("pause"); return 0;}/*转载与http://www.2cto.com/kf/201404/293694.html*/
注:代码部分转载与http://www.2cto.com/kf/201404/293694.html,并对其代码做了一些修正与改进。