算法与数据结构番外(1):优先队列

这是算法与数据结构番外系列的第一篇,这个系列将来的主要内容是补充一些与算法与数据结构相关的知识,这些知识比较零碎,同时也与正传关系密切,每每须要阅读了正传的相关内容之后,才能较好的理解这部份内容。若是对番外系列不感兴趣的话,是能够跳过此系列内容的,不会影响理解其余文章的内容。git

阅读本文前,须要首先了解队列的相关知识。github

此文优先队列的代码能够在个人 github 上查看。算法

优先队列

优先队列是一种特殊的队列。队列具备先进先出的特性,对于普通队列而言,首先出队的元素是首先入队的元素,而优先队列中,首先出队的元素是目前队列中优先级最高的元素数组

优先队列分为两类,最大优先队列和最小优先队列。一个最大优先队列的严格定义以下:数据结构

最大优先队列是一种用来维护由一组元素构成的集合 S 的数据结构,其中的每个元素都有一个相关的值,称为关键字(key,也就是咱们上文所说的优先级),一个最大优先队列应该支持如下操做:函数

INSERT(S, x):把元素 x 插入集合 S 中。spa

MAXIMUM(S):返回 S 中具备最大关键字的元素。code

EXTRACT-MAX(S):去掉并返回 S 中的具备最大关键字的元素。blog

最大优先队列最为著名的应用场景,是共享计算机系统的做业调度。最大优先队列将会记录将要执行的各个做业以及它们之间的相对优先级。当一个做业完成或者被中断后,调度器调用 EXTRACT-MAX 从全部的等待做业中,选出具备最高优先级的做业来执行,在任什么时候候,调度器均可以调用 INSERT 把一个新做业加入到队列中来。排序

相应的,最小优先队列则是选择集合中具备最小关键字的元素。最小优先队列常常被用于模拟。队列中保存要模拟的事件,每一个事件都有一个发生时间做为其关键字,事件将会按照时间前后顺序依次发生。模拟程序调用 EXTRACT-MIN (即与 EXTRACT-MAX 正好相反的功能:去掉并返回 S 中的具备最小关键字的元素)选择下一个要模拟的事件,当一个新事件产生时,模拟器经过调用 INSERT 将其插入最小优先队列。

优先队列可使用堆来实现,最大优先队列对应最大堆,最小优先队列对应最小堆。下面咱们将以最大优先队列为例进行介绍。

优先队列的实现

优先队列的定义

优先队列的定义与队列几乎彻底同样:

// 优先队列定义
typedef struct PriorityQueue{
    int* array;
    int max_size;
}PriorityQueue;

辅助函数

这些辅助函数咱们在堆排序的那一章中已经写好,这里能够直接使用。

获取堆中节点的父节点,左孩子和右孩子节点下标

#define PARENT(i) (i / 2)
#define LEFT(i) (2 * i)
#define RIGHT(i) (2 * i + 1)

交换数组两个元素的值:

// 交换数组 下标i 和 下标j 对应的值
int swap(int *array, int i, int j){
    int temp;
    temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    return 0;
}

递归维护最大堆:

// 递归维护最大堆
int MaintainMaxHeap(int *heap, int i){
    int largest;
    int left = LEFT(i);
    int right = RIGHT(i);
    if(left <= heap[0] && heap[left] > heap[i]){
        largest = left;
    } else{
        largest = i;
    }
    if(right <= heap[0] && heap[right] > heap[largest]){
        largest = right;
    }
    if(largest != i){
        swap(heap, largest, i);
        MaintainMaxHeap(heap, largest);
    }
    return 0;
}

这些辅助函数是直接采用的堆排序所用代码,因为篇幅有限,故再也不重复解释,能够点此查看相关解释。

初始化函数

// 初始化优先队列
PriorityQueue* PriorityQueueInit(int max_size){
    PriorityQueue* priority_queue = (PriorityQueue*)malloc(sizeof(PriorityQueue));
    priority_queue->array = (int*)malloc(sizeof(int) * (max_size + 1));
    priority_queue->array[0] = 0;
    priority_queue->max_size = max_size;
    return priority_queue;
}

咱们在这里,依然使用堆排序中的数组解释方法:array[0] 用于储存堆中的有效数据个数,故数组的实际长度为 max_size + 1,堆顶元素是 array[1]。

入队函数

// 优先队列入队
int PriorityQueueEnqueue(PriorityQueue *priority_queue, int number_to_enqueue){
    int i;
    priority_queue->array[0] += 1;
    i = priority_queue->array[0];
    priority_queue->array[priority_queue->array[0]] = number_to_enqueue;
    while(i > 1 && priority_queue->array[PARENT(i)] < priority_queue->array[i]){
        swap(priority_queue->array, PARENT(i), i);
    }
    return 0;
}

整个最大优先队列本质上是一个最大堆,当咱们插入一个数据时,首先将其插入至堆的尾部,此时可能会违背最大堆的性质,故咱们将此元素不断与其父节点的值进行比较,若其小于父节点的值,说明此时整个堆已是一个最大堆了;若其大于父节点的值,则将此节点与父节点交换,重复此步骤,直到此元素小于其父节点的值或此元素成为了堆顶节点。

显然,入队操做的时间复杂度是 \(O(lgn)\) ,由于整个函数中影响其时间复杂度的过程为 while 循环,其最差状况是将此元素从叶节点一步一步交换至根节点,而树的高度为 \(O(lgn)\)

整个过程以下图所示:

出队函数

// 优先队列队首元素
int PriorityQueueHead(PriorityQueue *priority_queue){
    return priority_queue->array[1];
}

// 优先队列出队
int PriorityQueueDequeue(PriorityQueue *priority_queue){
    int return_number = priority_queue->array[1];
    priority_queue->array[1] = priority_queue->array[priority_queue->array[0]];
    priority_queue->array[0] -= 1;
    MaintainMaxHeap(priority_queue->array, 1);
    return return_number;
}

在理解了堆排序之后,优先队列的出队操做很简单了。

一个最大优先队列出队时返回的值为队列中的最大值,即 array[1],那么咱们只须要像堆排序时那样,将堆中最后一个有效数据复制到堆顶(array[1]),此时新的堆顶可能会违反最大堆的性质,为此咱们只需对堆顶元素调用一次 MaintainMaxHeap() ,便可保证出队后,此堆依然是一个最大堆。

关于此过程的时间复杂度和正确性分析,咱们已经在堆排序一章中介绍过了,在此就不赘述了,直接给出结果 \(O(lgn)\)

总结

在理解了堆排序之后,优先队列就很是简单了,几乎连代码都不须要太多修改。从上文的分析,咱们也能得出一个结论,优先队列最为关键的两个步骤(入队、出队)的时间复杂度均为 \(O(lgn)\)

原文连接:albertcode.info

我的博客:albertcode.info

相关文章
相关标签/搜索