优先队列与堆排序

概述

我所知道的,广义的队列有三种: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);						//初始容量为max
	MinPQ(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 swap
		if ((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;
		}
		else
			break;
	}
}
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>;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值