优先队列及(二叉)堆

  数据结构书籍与算法书(包括算法导论算法设计)通常将优先队列(Priority Queue)与堆(Heap)放在一起讲,算法导论上先讲堆这个特殊的数据结构,后讲堆的两个应用,堆排序与优先队列。算法设计这本书先讲优先队列是个什么样的数据结构,有什么性质,为什么需要优先队列这种数据结构,然后讲实现优先队列有什么样的要求,而这些要求数组(Array)和链表(Linked List)都不能满足,所以我们需要设计一种新的数据结构来满足这些要求,那就是堆。个人更喜欢算法设计书上这种顺序。

  某些特定的算法,只需要数据的一部分信息,而不需要全部的信息,这个时候为了提升算法的效率,可能需要设计某个特定的数据结构,这个特定的数据结构只保留了该算法需要的那部分信息,而舍弃了其余的信息,舍弃这部分信息换来了效率上的提升,这正是我们所需要的。举个例子直观一点,数组这种数据结构,你可以知道数组中每个元素(element)的值,这相当于知道所有信息,而堆这种数据结构,譬如最小堆,你只知道堆顶的元素是多少,而堆中其它的元素你是不知道的,相当于你只知道部分信息。而如果某个算法,你只关心一批数据中的最小值,而不关心具体每个数据的值,那最小堆就正能满足你的需求。效率方面而言,对于大小为n的数组,求最小值需要遍历整个数组,时间为\(\mathcal{O}(n)\),而最小堆的堆顶元素即堆中数据的最小值,只需要\(\mathcal{O}(1)\)时间。

  在【待填坑】稳定匹配问题中,需要维护一个集合S,对集合S的操作包括:插入元素、删除元素,访问最高优先级(优先级自己定义)的元素,而优先队列正是为此设计的。

优先队列

定义

优先队列是一种数据结构,其维护一个集合S,每一个元素\(v\in S\)都有对应的键值\(key(v)\)表示该元素的优先级,小键值对应高优先级。优先队列支持插入元素、删除元素、访问最小键值元素\(^{[1]}\)。

  优先队列的一个典型应用是简化的计算机进程调度(process scheduling)问题,每一个进程有一个优先级。每个进程的产生不是按照优先级顺序,我们维护一个进程的集合,每次我们在集合中选取一个最高优先级的进程去运行,同时从集合中删除该进程,另外我们还会往这个集合增加新的进程,这些正对应着优先队列的功能。

期望复杂度

  那么我们期望大小为n的优先队列的时间复杂度达到多少呢?
  我们知道基于比较的排序算法的时间复杂度的下界为\(\mathcal{O}(n\log n)\),从这个下界出发,我们可以得出优先队列每次插入元素、删除元素、访问最小键值元素的期望时间复杂度。设想我们有一个大小为n的数组,我们依次将每个数组元素都加入到优先队列中,然后再将优先队列的元素依次都取出来,那么取出来元素就已经有顺序了,我们实现了对一个数组的排序。以上操作共有n次插入、n次取出、n次删除操作,那么可知,优先队列的这些基本操作的时间复杂度的(大概)下界应该是\(\mathcal{O}(\log n)\)。但实际情况中,由于优先队列的实现方法不一样,基本操作的时间复杂度下界也不同\(^{[3]}\),但是对于数组排序这个问题而言,采用优先队列的方法进行排序(实际上就是堆排序)的时间复杂度下界是\(\mathcal{O}(n\log n)\)。

数组和链表的局限

  对于数组或者链表而言,基本操作能否达到\(\mathcal{O}(\log n)\)?
  答案是否定的。以进程调度问题举例,假如我们按照优先级顺序把进程放在不同的位置,那么访问操作和删除操作的时间都可以是\(\mathcal{O}(1)\)。但插入操作就不符合要求了,对于数组而言,找到要插入的位置可通过二分查找达到\(\mathcal{O}(\log n)\)的时间,但插入元素的时间是\(\mathcal{O}(n)\),而对于链表而言,插入元素的时间是\(\mathcal{O}(1)\),但我们要找到插入的位置需要\(\mathcal{O}(n)\)的时间。综上,数组和链表都不符合我们的要求,需要设计新的数据结构——堆。

定义

  堆有很多种类型,二叉堆、二项堆、斐波那契堆等,在这里讲的是二叉堆。二叉堆可以看作是平衡二叉树或近似的完全二叉树,平衡二叉树中任意一个节点的左右子树的深度之差不超过1,完全二叉树的叶节点的深度相同,内部节点的度(degree,孩子节点的数量)相同。

图1 完全二叉树
下图是一个堆的示意图,同时也是一个平衡二叉树,可以看出,堆之所以叫做近似的完全二叉树是因为不是所有内部节点的度都相同。

图2 堆(平衡二叉树)
  堆有一个性质,称作heap order, 对于最小堆而言,即树中任意一个节点的键值key要大于等于其父节点的键值key,最大堆反之。图2表示的是最小堆。
  通常采用数组来存储堆,图2所示的最小堆可以存储如下图3所示:

图3 堆的数组表示
其中,数组A下标从1到N,N为堆的大小,A[1]是根节点,A[2]是根节点的左子孩节点,A[3]是根节点的右子孩节点。实际上,对于任何一个节点,若其在数组中的位置是i,则它的左子孩节点位置\(left\_child(i)=2i\),右子孩节点位置\(right\_child=2i+1\),它的父节点(假如有)的位置\(parent(i)=\lfloor i \rfloor\),\(\lfloor i \rfloor\)表示对i向下取整。图3中的箭头从父节点分别指向左右子孩节点。

用堆实现优先队列

基本操作

  我们回顾优先队列的基本操作,并看看用数组表示的最小堆怎么实现这些操作。

  • 访问优先级最高(键值key最小)的元素

  由堆的heap order性质可以知道,A[1]即是键值最小的元素,所以只需要返回A[1]的值即可。

  • 插入元素

  我们维护一个变量\(length\)表示堆的大小,每次往堆里添加元素的时候,将\(length\)加1,然后将元素的值赋给数组A中\(length\)位置。

  • 删除元素

  优先队列的许多应用通常只会在访问优先级最高的元素后删除该元素。对于数组A而言,只需要把A[1]删除即可,具体实现时,我们将A[length]赋值给A[1],然后length减一。

  我们需要注意一点,插入元素删除元素会改变数组的值,而改变之后该数组是否还能表示一个堆呢?答案是不一定,因为数组值改变后不一定符合heap order,所以我们需要做一些操作,来维护堆的heap order性质。

维护堆的性质

  以维护最大堆的heap order性质为例,插入元素后,A[length]的值有可能大于A[parent(length)]的值,所以需要将A[length]的值调整到合适的位置。需要heap_increase_key来实现插入操作,伪代码\(^{[2]}\)如下:

heap_increase_key(A, i, key){
    A[i] = key
    while(i > 1 && A[parent(i)] < A[i]) {
        exchange A[i] and A[parent(i)]
        i = parent(i)
    }
}

  简单来说,就是若A[i]的值大于其父节点的值,则交换二者,直到A[i]的值小于等于父节点的值或已到达根节点。图4是heap_increase_key(A, 9, 15)的示意图:

图4 heap_increase_key(A, 9, 15)
  类似地,删除元素后,A[length]的值赋值给A[1],而此时A[1]可能小于A[left_child(1)]或A[right_child(1)],所以需要将A[1]的值调整到合适的位置。采用max_heapify函数来实现删除操作,输入数组A和下标i,我们假设A[i]是唯一违反堆性质的值,调用max_heapify(A, i)使得A[i]的值在最大堆中“逐级下降”,从而维护堆的性质。伪代码\(^{[2]}\)如下:

max_heapify(A, i){
    l = left_child(i)
    r = right_child(i)
    largest = i
    if(l <= length && A[l] > A[i])
        largest = l
    if(r <= length && A[r] > A[largest])
        largest = r
    if(largest != i){
        exchange A[i] and A[largest]
        max_heapify(A, largest);
    }
}

  简单来说,就是在i节点及左右子孩节点中,选出键值最大的节点largest,若largest不是i,则交换A[i]和A[largest]的值,此时这三个节点是符合堆性质的,但A[largest]可能违反堆性质,所以我们递归调用max_heapify(A, largest)函数。图5是max_heapify(A, 2)的示意图。

图5 max_heapify(A, 2)
  heap_increase_key和max_heapify都是沿着树的路径走,最坏情况下从叶节点走到根节点(max_heapify从根节点走到叶节点),则时间复杂度为\(\mathcal{O}(\log n)\)。

  类似heap_increase_key和max_heapify,不难得到heap_decrese_key和min_heapify,从而我们可以将优先队列的插入元素和删除元素操作完善如下:

  • 插入元素

  我们维护一个变量\(length\)表示堆的大小,每次往堆里添加元素的时候,将\(length\)加1,然后将INT_MIN赋给A[length],然后调用1次heap_decrease_key(A, length, key)。

  • 删除元素

  优先队列的许多应用通常只会在访问优先级最高的元素后删除该元素。对于数组A而言,只需要把A[1]删除即可,具体实现时,我们将A[length]赋值给A[1],然后length减一。然后调用1次min_heapify(A, 1)。

优先队列基本操作的时间复杂度

操作 时间复杂度
插入元素 \(\mathcal{O}(\log n)\)
删除元素 \(\mathcal{O}(\log n)\)
访问优先级最高的元素 \(\mathcal{O}(1)\)

  

具体实现

  根据上面的基本操作,给出基于最小堆的优先队列的伪代码如下:

  • 访问最小键值元素
heap_minimum(A){
    return A[1]
}
  • 插入操作
min_heap_insert(key){
    length = length + 1
    A[length] = INT_MAX
    heap_decrease_key(length, key)
}
  • 访问键值最大元素后删除该元素
heap_extract_min(A){
    min = A[1]
    A[1] = A[length]
    length = length - 1
    min_heapify(A, 1)
    return min
}

例子

算法课的练习题:Dynamic Median

  防止链接失效截一张图放这:

图6 Dynamic Median

算法思路

  分别实现一个最大堆、一个最小堆,最大堆中的所有元素小于等于最小堆中的任何元素。最大堆最小堆的大小相差不超过1,当最小堆的大小比最大堆的大小大1时,中位数为最小堆的堆顶元素,其余情况,中位数均为最大堆的堆顶元素(与题目要求一致)。插入新元素时,若元素值大于中位数则插入到最小堆,反之,插入到最大堆,同时应保持两个堆的大小相差不超过1。

自己编写堆实现

  23596kB, 1084ms

  代码比较长,因为为了与上文中伪代码的函数名对应,分开实现最大堆最小堆。

#include <stdio.h>
#include <math.h>
#include <limits.h>
#include <algorithm>
#include <iostream>

#define parent(i) (int)std::floor(i/2)
#define left(i) i * 2
#define right(i) i * 2 + 1

int A[5005], B[5005];//分别存储最大堆、最小堆
int max_heap_size, min_heap_size;

void exchange(int* array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

//最大堆
void heap_increase_key(int i, int key) {
    if (key < A[i])
        printf("error: new key is smaller than current key.");
    A[i] = key;
    while (i > 1 && A[parent(i)] < A[i])
    {
        exchange(A, i, parent(i));
        i = parent(i);
    }
}

void max_heap_insert(int key) {
    max_heap_size++;
    A[max_heap_size] = INT_MIN;
    heap_increase_key(max_heap_size, key);
}

int heap_maximum(void) {
    return A[1];
}

void max_heapify(int i) {
    int l = left(i), r = right(i);
    int largest = i;
    if (l <= max_heap_size && A[l] > A[i])
        largest = l;
    if (r <= max_heap_size && A[r] > A[largest])
        largest = r;
    if (largest != i) {
        exchange(A, i, largest);
        max_heapify(largest);
    }
}

int heap_extract_max(void) {
    int max = A[1];
    A[1] = A[max_heap_size];
    max_heap_size--;
    max_heapify(1);
    return max;
}

//最小堆
void heap_decrease_key(int i, int key) {
    if (key > B[i])
        printf("error: new key is bigger than current key.");
    B[i] = key;
    while (i > 1 && B[parent(i)] > B[i])
    {
        exchange(B, i, parent(i));
        i = parent(i);
    }
}

void min_heap_insert(int key) {
    min_heap_size++;
    B[min_heap_size] = INT_MAX;
    heap_decrease_key(min_heap_size, key);
}

int heap_minimum(void) {
    return B[1];
}

void min_heapify(int i) {
    int l = left(i), r = right(i);
    int smallest = i;
    if (l <= min_heap_size && B[l] < B[i])
        smallest = l;
    if (r <= min_heap_size && B[r] < B[smallest])
        smallest = r;
    if (smallest != i) {
        exchange(B, i, smallest);
        min_heapify(smallest);
    }
}

int heap_extract_min(void) {
    int min = B[1];
    B[1] = B[min_heap_size];
    min_heap_size--;
    min_heapify(1);
    return min;
}

int quary(void) {
    if (min_heap_size == max_heap_size + 1)
        return heap_minimum();
    else//max_heap_size = min_heap_size + 1或size相等
        return heap_maximum();
}

void insert(int x) {
    if ((!min_heap_size) && (!max_heap_size))//第一个数据
        max_heap_insert(x);
    else {
        int median = quary();
        if (x < median) {
            max_heap_insert(x);
            if (max_heap_size == min_heap_size + 2)//保持最大堆和最小堆的size相差不超过1
                min_heap_insert(heap_extract_max());
        }
        else {
            min_heap_insert(x);
            if (min_heap_size == max_heap_size + 2)
                max_heap_insert(heap_extract_min());
        }
    }

}

void del(void) {//del操作后,最大堆最小堆的size相差不超过1的性质不变
    if (min_heap_size == max_heap_size + 1)
        heap_extract_min();
    else
        heap_extract_max();
}
int main() {
    int t, n, x;
    char op;
    scanf("%d", &t);
    while (t--) {
        max_heap_size = 0;
        min_heap_size = 0;
        scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf(" %c", &op);
            if (op == 'I') {
                scanf("%d", &x);
                insert(x);
            }
            else if (op == 'Q')
                printf("%d\n", quary());
            else
                del();
        }
    }
    return 0;
}

库函数实现

23724kB, 1288ms

#include <stdio.h>
#include <queue>
#include <functional>

std::priority_queue<int> max_heap;
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;

int quary(void) {
    if (min_heap.size() == max_heap.size() + 1)
        return min_heap.top();
    else//max_heap_size = min_heap_size + 1或size相等
        return max_heap.top();
}

void insert(int x) {
    if (min_heap.empty() && max_heap.empty())
        max_heap.push(x);
    else {
        int median = quary();
        if (x < median) {
            max_heap.push(x);
            if (max_heap.size() == min_heap.size() + 2) {
                min_heap.push(max_heap.top());
                max_heap.pop();
            }
        }
        else {
            min_heap.push(x);
            if (min_heap.size() == max_heap.size() + 2) {
                max_heap.push(min_heap.top());
                min_heap.pop();
            }
        }
    }
}

void del(void) {
    if (min_heap.size() == max_heap.size() + 1)
        min_heap.pop();
    else
        max_heap.pop();
}
int main() {
    int t, n, x;
    char op;
    scanf("%d", &t);
    while (t--) {
        while (!min_heap.empty())
            min_heap.pop();
        while (!max_heap.empty())
            max_heap.pop();
        scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf(" %c", &op);
            if (op == 'I') {
                scanf("%d", &x);
                insert(x);
            }
            else if (op == 'Q')
                printf("%d\n", quary());
            else
                del();
        }
    }
    return 0;
}

参考:

[1] 算法设计
[2] 算法导论
[3] Algorithm Design lecture slides: binary and binomial heaps

原文地址:https://www.cnblogs.com/wtyuan/p/12075500.html

时间: 2024-11-05 22:02:15

优先队列及(二叉)堆的相关文章

优先队列之二叉堆与d-堆

二叉堆简介 平时所说的堆,若没加任何修饰,一般就是指二叉堆.同二叉树一样,堆也有两个性质,即结构性和堆序性.正如AVL树一样,对堆的以此操作可能破坏者两个性质中的一个,因此,堆的操作必须要到堆的所有性质都被满足时才能终止. 结构性质 堆是一棵完全填满的二叉树,因为完全二叉树很有规律,所以它可以用一个数组表示而不需要指针.如下图所示,图2中的数组对应图1中的堆.                   图1:二叉堆                                            

算法学习 - 优先队列的二叉堆实现

PriorityQuenue 优先队列就是作业调度类的ADT,这里用二叉堆来实现. 优先队列最少有两个操作:插入(Insert)和删除最小者(DeleteMin). 插入操作图解: 图片来源:www.educity.cn 删除操作图解: 图片来源:www.cfanz.cn 代码实现: // // main.cpp // binaryHeap // // Created by Alps on 14-8-17. // Copyright (c) 2014年 chen. All rights rese

《Algorithms算法》笔记:优先队列(2)——二叉堆

二叉堆 1 二叉堆的定义 堆是一个完全二叉树结构(除了最底下一层,其他层全是完全平衡的),如果每个结点都大于它的两个孩子,那么这个堆是有序的. 二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不用数组的第一个位置) 2 二叉堆的性质 最大的元素在a[1] (root结点) 每个k的父亲在k/2 每个k的孩子在k*2和k*2+1 3 二叉堆的操作 3.1 上浮(孩子大于父亲)--对应插入操作 循环,每次比较自己和父亲,如果比父亲大就交换,直到root. 3.2 插入 先把元

优先队列 - 数据结构 (二叉堆)

优先队列包括二叉堆.d-堆.左式堆.斜堆.二项队列等 1.二叉堆 堆是一棵被完全填满的二叉树,有可能例外的是在底层,底层上的元素从左到右填入.这样的树称为完全二叉树. 堆序的性质:在一个堆中,对于每一个节点X,X的父亲的关键字小于(或等于)X中的关键字,根节点除外(它没有父节点).完全二叉树可以用数组实现. //关于二叉堆的头文件定义 如果要插入的元素是新的最小值,那么它将一直被推向堆顶.这样在某一个时刻,i将是1,我们就需要另Insert函数令程序跳出while循环,这个值必须保证小于或者至少

纯数据结构Java实现(6/11)(二叉堆&amp;优先队列)

堆其实也是树结构(或者说基于树结构),一般可以用堆实现优先队列. 二叉堆 堆可以用于实现其他高层数据结构,比如优先队列 而要实现一个堆,可以借助二叉树,其实现称为: 二叉堆 (使用二叉树表示的堆). 但是二叉堆,需要满足一些特殊性质: 其一.二叉堆一定是一棵完全二叉树 (完全二叉树可以用数组表示,见下面) 完全二叉树缺失的部分一定是在右下方.(每层一定是从左到右的顺序优先存放) 完全二叉树的结构,可以简单理解成按层安放元素的.(所以数组是不错的底层实现) 其二.父节点一定比子节点大 (针对大顶堆

二叉堆,优先队列,二叉树的理解

1. 二叉堆是完全二叉树,即它的N级子节点放慢之后才会去放N+1级子节点 2. 二叉堆用数组实现,每个子节点通过固定的索引找到(由完全二叉树保证) 3. 二叉堆排序只保证堆顶有效,即堆顶是最大值或最小值,是优先队列实现的不二选择 4. 二叉堆删除节点,需要重新组织堆内结构,不太高效 5. 二叉树,也叫二叉搜索树,用关系型容器实现,适用于普通的查找,排序

结构之美——优先队列基本结构(四)——二叉堆、d堆、左式堆、斜堆

实现优先队列结构主要是通过堆完成,主要有:二叉堆.d堆.左式堆.斜堆.二项堆.斐波那契堆.pairing 堆等. 1. 二叉堆 1.1. 定义 完全二叉树,根最小. 存储时使用层序. 1.2. 操作 (1). insert(上滤) 插入末尾 26,不断向上比较,大于26则交换位置,小于则停止. (2). deleteMin(下滤) 提取末尾元素,放在堆顶,不断下滤: (3). 其他操作: 都是基于insert(上滤)与deleteMin(下滤)的操作. 减小元素:减小节点的值,上滤调整堆. 增大

【转】二叉堆与优先队列

目录 1.插入 2.删除 3.查询 1.堆排序 2.用两个堆来维护一些查询第k小/大的操作 中位数 3.利用堆来维护可以“反悔的贪心” 如题,二叉堆是一种基础数据结构 事实上支持的操作也是挺有限的(相对于其他数据结构而言),也就插入,查询,删除这一类 对了这篇文章中讲到的堆都是二叉堆,而不是斜堆,左偏树,斐波那契堆什么的 我都不会啊 更新概要: 无良博主终于想起来要更新辣 upd1:更新5.2.2-对于该子目所阐述的操作“用两个堆来维护一些查询第k小/大的操作”更新了一道例题-该操作对于中位数题

优先队列(一)——二叉堆

二叉堆的抽象数据类型: 1 #ifndef _BinHeap_H 2 #define ElementType int 3 #define MinElement -1000 4 struct HeapStruct; 5 typedef struct HeapStruct *PriorityQueue; //结构体指针 6 PriorityQueue Initialize(int MaxElements); //初始化一个二叉堆 7 void Destroy(PriorityQueue H); //

二叉堆(binary heap)

堆(heap) 亦被称为:优先队列(priority queue),是计算机科学中一类特殊的数据结构的统称.堆通常是一个可以被看做一棵树的数组对象.在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权.堆即为解决此类问题设计的一种数据结构. 本文地址:http://www.cnblogs.com/archimedes/p/binary-heap.html,转载请注明源地址. 逻辑定义 n个