最小堆 / 优先队列(C语言实现)

最近找实习,复习下数据结构方面的内容。

完全二叉树有两种形态,一种是:二叉树的所有子树要么没有孩子,要么一定有左孩子。另一种是:二叉树要么没有子树,要么一定左右子树都有。

堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左孩子和右孩子节点的值。

最大堆和最小堆是二叉堆的两种形式。

最大堆:根结点的键值是所有堆结点键值中最大者。

最小堆:根结点的键值是所有堆结点键值中最小者。

在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高进先出 (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语言描述》。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值