优先队列与堆排序

来源:互联网 发布:二维数组定义 编辑:程序博客网 时间:2024/05/23 18:16

概述

我所知道的,广义的队列有三种:stack栈是一种,queue队列是一种,还有一种比较特殊,priority queue优先队列微笑


为什么特殊,因为前面两种队列,往队列push元素的时候,push过程就是很简单的把元素放在头部或者尾部就行了,pop的时候也是简单的取出头尾数据。而优先队列更加“智能化”,你把数据push进队尾的时候,push过程要在内部把数据进行整理,使队首元素总保持是最大值,所以你pop元素的时候都能拿到队列的最大元素了,另外pop的时候也需要整理数组,毕竟还得从剩下的数据中选一个最大的数顶上来,不然下一次pop队首元素,怎么能保证它是最大元素呢。

如果你要对一组数据进行排序,除了插入排序、快速排序等方式,还可以借助优先队列这个性质,把数据全部装进去,一个一个取出来不就有序了么。这就是堆排序。为啥是堆排序不叫“优先队列排序”呢,前面说,priority queue的核心算法就是push与pop,其实就是赋予普通数组堆的性质,也可以说是基于堆的性质来整理数组,这样才能让你每次pop得到的都是排好序的元素啊。什么是堆的性质呢,下面就来讲下这个堆到底是什么。


堆的性质

堆是一种抽象数据结构,同时也是一种性质,是普通数组 + 一种关联 微笑

这种关联以数组下标表示:下标n的元素为父节点,2n、2n+1为它左右两个子节点。

说得像是二叉树一样是吧,不过他就是内置数据类型---数组,不是二叉树结构,只是加上了元素间的一层关系而已。

通过这种关系,元素i就可以直接访问到距离它很远的2i、2i+1的元素了。

如果我们在这层关联上面建立一个大小关系(n > 2n && n > 2n + 1),那么数组就成为了堆。


用堆排序

假设数组已满足了堆定义,添加队尾元素后,如何让首元素保持是最大元素?

这个不就是push接口里面该做的事情么,答案是对其进行排序么?单独看排序的话,想到的排序肯定是快排之类的nlogn级别的排序,不过没有这个必要,因为我们想的是只找出最大元素更新到数组的首位,而不是把整个数组排序,显然这个工作比兴师动众的快排更加轻量级,时间复杂度肯定比nlogn小得多,怎么实现这个push呢,假设这个新的尾部元素索引是8并且他就是最大元素,他需要如何“上位”呢,一路swap,他需要经过的索引是:4->2->1,最后就替代了1这个首元素成为老大了,为什么可以这样跳着排,正是因为数组原本就满足了堆的性质,并且“上位”之后,这个性质依然保持。每次交换,索引都会折半,那么时间复杂度就和折半查找一样是logn了,注意logn只是使首元素成为最大/小,并没有对数组进行排序。


假设数组已满足了堆定义,取出队首元素后,如何让首元素保持是最大元素?

其实就是pop的实现,队首元素没有了,肯定要让其他元素去填充它,最方便的就是让队尾元素去填充,填充了之后它变成队首元素,为了最大的元素“上位”,它还得与左右子节点比较,如果它本身就是数组中最小的元素,它得从队首位置开始一路swap,比如经过1->3->7,最后7这个位置就是它最终位置,最后数组保持了堆的性质。


这样一来,就实现了概述里面的的排序功能了。


以最小堆为例,来看一个优先队列的接口:

template<typename T>class MinPQ{public:public:MinPQ();//默认构造空队列MinPQ(int max);//初始容量为maxMinPQ(T a[], int n);//用a[]数组创建void insert(T v);//末尾插入 T delMin();//头部删除 T min();//返回最小元素bool isEmpty();//判空int  size();//返回队列元素个数private:void swim(int k);//尾部新数据重排void sink(int k);//头部新数据重排void minPQCheck();//检查队列是否满足最小堆T* pq;//内置数组int N;//当前数据量\当前队尾索引int C;//数据容量public:static int MinPQ<T>::testMinPQ();};


实际用法

如果你真的用优先队列来排序整个数组的话,并没有与它的核心功能完全匹配,它的应用场景不是排序,而是提取数组中前k大的数据。
此时又想到了使用排序,不过排序是在提取前面k个元素基础上还对其进行了大小排列,而提取前k个元素并没有叫我们排列数据!所以我们不需要用排序,用优先队列更快。
假如n为10000,k为100,提取前面100个元素:
只需要建立大小为101的最小堆,往里面不断push数据直到装满,然后重复pop、push多次直到10000个元素全部使用完,最后剩在堆里面的100个数据就是前100大小的数据。
一般地,提取前k个数据,堆大小为k,我往里面push了n个元素,每次push、pop时间复杂度为logk,整个过程时间复杂度为n*logk。


以下给出实现代码:

template<typename T>MinPQ<T>::MinPQ() {////}template<typename T>MinPQ<T>::MinPQ(int max) :N(0), C(max) {pq = new T[max + 1];//索引要从1开始memset(pq, 0, sizeof(int) *(max + 1));}template<typename T>MinPQ<T>::MinPQ(T a[], int n) : N(n), C(n) {////}template<typename T>void MinPQ<T>::insert(T v) {pq[++N] = v;swim(N);minPQCheck();}template<typename T>T MinPQ<T>::delMin() {T ret = min();pq[1] = pq[N];pq[N--] = NULL;sink(1);minPQCheck();return ret;}template<typename T>bool MinPQ<T>::isEmpty() {return N == 0;}template<typename T>int MinPQ<T>::size() {return N;}template<typename T>T MinPQ<T>::min() {return pq[1];}template<typename T>void MinPQ<T>::swim(int k) {while ((k > 1) && (pq[k] < pq[k / 2])) {swap(pq[k], pq[k / 2]);k = k / 2;}}template<typename T>void MinPQ<T>::sink(int k) {while (2 * k <= N) {int s = 2 * k;//s is the final child element in swapif ((2 * k < N) && (pq[2 * k + 1] < pq[2 * k])) {if (pq[k] > pq[2 * k + 1]) {s++;}}if (pq[k] > pq[s]) {swap(pq[k], pq[s]);k = s;}elsebreak;}}template<typename T>void MinPQ<T>::minPQCheck(void) {for (int i = 1; i <= N / 2; i++) {if ((2 * i + 1 <= N) && (pq[i] > pq[2 * i] || pq[i] > pq[2 * i + 1])) {assert(false);}}}template<typename T>int MinPQ<T>::testMinPQ() {MinPQ<int>* pObj = new MinPQ<int>(2);srand(time(NULL));for (int i = 0; i < 3; i++)pObj->insert(rand() % 2000);vector<int> arr;for (int i = 0; i < 2000; i++) {arr.push_back(pObj->delMin());}for (auto n : arr) {cout << n << " ";}return 0;}template class MinPQ<int>;


原创粉丝点击