最近找实习,复习下数据结构方面的内容。
完全二叉树有两种形态,一种是:二叉树的所有子树要么没有孩子,要么一定有左孩子。另一种是:二叉树要么没有子树,要么一定左右子树都有。
堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左孩子和右孩子节点的值。
最大堆和最小堆是二叉堆的两种形式。
最大堆:根结点的键值是所有堆结点键值中最大者。
最小堆:根结点的键值是所有堆结点键值中最小者。
在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高进先出 (largest-in,first-out)的行为特征。优先队列常用于模拟操作系统的进程调度,可以用堆来实现。
下面我们用数组来实现一个最小堆。
代码如下:
MinHeap.h
#ifndef DataStructures_MinHeap_h
#define DataStructures_MinHeap_h
struct 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);
// 打印堆
void printMinPriorityQueue(MinPriorityQueue pqueue);
#endif
MinHeap.c
#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 = malloc(sizeof(struct MinHeap));
if (pqueue == NULL)
outOfSpaceFatalError();
// 数组的第0个元素是个sentinel标记节点,计入数组容量中,但不计入capcaity或size中
pqueue->size = 0;
pqueue->capacity = maxElements;
pqueue->elements = malloc(sizeof(ElementType) * (pqueue->capacity + 1));
if (pqueue->elements == NULL)
outOfSpaceFatalError();
else
pqueue->elements[0] = SentinelElement;
return pqueue;
}
void
destroy(MinPriorityQueue pqueue)
{
if (pqueue != NULL)
{
// 在GNU99标准中,free(NULL)什么都不做直接返回,所以不用判断pqueue->elements是否为NULL
free(pqueue->elements);
free(pqueue);
}
}
void
makeEmpty(MinPriorityQueue pqueue)
{
if (pqueue != NULL)
pqueue->size = 0;
else
PQueueNULLWarning();
}
/*
* 插入时,堆中的元素执行下滤操作
* 删除时,堆中的元素执行上滤操作
*/
/*
* 插入的时间复杂度为O(log N),N为最小堆中的元素个数
* 实际上,其平均执行时间为O(1)
*/
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;
// sentinel element在这里作为elements[0]被比较,是循环的终止条件
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->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; // 返回被删除的元素
}
/*
* 执行时间:O(1)
*/
ElementType
findMin(MinPriorityQueue pqueue)
{
if (pqueue == NULL)
{
PQueueNULLWarning();
return SentinelElement;
}
else
return pqueue->elements[1];
}
int
isEmpty(MinPriorityQueue pqueue)
{
if (pqueue == NULL)
{
PQueueNULLWarning();
return -1;
}
else
return (pqueue->size == 0);
}
int
isFull(MinPriorityQueue pqueue)
{
if (pqueue == NULL)
{
PQueueNULLWarning();
return -1;
}
else
return (pqueue->size == pqueue->capacity);
}
void
percolateDown(int *arr, int len, int i)
{
int child;
int n = len - 1;
ElementType tmp;
for (tmp = arr[i]; i * 2 < n; i = child)
{
child = i * 2;
if (child < n && arr[child] > arr[child + 1])
child++;
if (tmp > arr[child])
arr[i] = arr[child];
else
break;
}
arr[i] = tmp;
}
MinPriorityQueue
buildHeap_percolate(int *arr, int n)
{
if (arr == NULL)
{
printf("Error: Array is NULL");
return NULL;
}
MinPriorityQueue pqueue;
pqueue = malloc(sizeof(struct MinHeap));
if (pqueue == NULL)
outOfSpaceFatalError();
ElementType *elements = 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--)
percolateDown(elements, n + 1, i);
pqueue->elements = elements;
pqueue->size = n;
pqueue->capacity = n * 2;
return pqueue;
}
/*
* 通过n次插入元素建立堆,由于每次插入的平均执行时间为O(1),所以建堆平均时间为O(N)
*/
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;
}
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");
}
建堆的测试代码:
void
buildHeapTest()
{
int a[9] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
MinPriorityQueue pqueue_ins = buildHeap_insert(a, 9);
MinPriorityQueue pqueue_per = buildHeap_percolate(a, 9);
printMinPriorityQueue(pqueue_ins);
printMinPriorityQueue(pqueue_per);
}
int main(int argc, const char * argv[])
{
buildHeapTest();
return 0;
}
分别使用插入和下滤两种方式建堆,所以建立的结果是不同的,输出如下:
Priority Queue
Element[1] = 1
Element[2] = 2
Element[3] = 4
Element[4] = 3
Element[5] = 7
Element[6] = 8
Element[7] = 5
Element[8] = 9
Element[9] = 6
Priority Queue
Element[1] = 1
Element[2] = 2
Element[3] = 3
Element[4] = 6
Element[5] = 5
Element[6] = 4
Element[7] = 7
Element[8] = 8
Element[9] = 9
最大堆实现类似。
下面用最小堆解决“在数组中寻找第k大元素”的问题。
测试代码如下:
void
findKMaxElementTest(int k)
{
int a[10] = {20, 10, 11, 21, 36, 90, 100, 200, 25, 9};
MinPriorityQueue pqueue = buildHeap_insert(a, 10);
for (int i = 1; i < k; i++)
deleteMin(pqueue);
printf("第%d大元素为:%d\n", k, findMin(pqueue));
}
int main(int argc, const char * argv[])
{
findKMaxElementTest(5);
return 0;
}
由于建堆最多需要花费O(N)的时间,每次删除操作最多需要花费O(log N)的时间,所以该算法的时间复杂度为O(N + klog N),如果k = [N / 2],那么时间复杂度为Θ(N log N)
主要参考了Mark Allen Weiss的《数据结构与算法分析 —— C语言描述》。