答:
优先队列可以看做一种特殊的线性表(数组/链表),它是实现堆的底层数据结构,对于大根堆(根节点为最大值),每次队列都能取出其内的最大值,对于小根堆则反之。
对大根堆,堆中的每一个节点都大于其下的子节点,对于小根堆则反之,可将堆看作一个以根节点为起点自上而下递增(递减)的二叉树。
一般来说,对于升序则构建大根堆,对于降序则构建小根堆。
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
vector HeapSort(vector& nums){
heapify(nums); //顺序排序,建大根堆
int len = nums.size();
//循环不变量: nums[0,i]堆有序, nums(i,len-1]有序
//每次都会把堆顶最大元素排到有序部分前
for(int i = len - 1; i >= 1; ){
std::swap(nums[0],nums[i]);
i--; //减小堆有序区间
sink(nums,0,i);
}
return nums;
}
void heapify(vector& nums){
int len = nums.size();
//每次都从nums[i,len-1]开始逐层下移,i初始量取堆的最后一个非叶子结点
for(int i = (len-1) / 2; i >= 0; i--){
sink(nums,i,len-1);
}
}
// 在nums[pre,end]部分将上方较小元素下沉至建堆位置
void sink(vector &nums,int pre,int end){
while(pre * 2 + 1 <= end){
int next = pre * 2 + 1;
if(next + 1 <= end && nums[next] < nums[next + 1]) //对于大根堆,用较大的孩子与父亲交换
next++;
if(nums[pre] >= nums[next])
break;
std::swap(nums[pre],nums[next]);
pre = next;
}
}
只需要修改sink函数的比较,使其构建小根堆即可。
... 其他代码一致
// 在nums[pre,end]部分将上方较小元素下沉至建堆位置
void sink(vector &nums,int pre,int end){
while(pre * 2 + 1 <= end){
int next = pre * 2 + 1;
if(next + 1 <= end && nums[next] > nums[next + 1]) //对于大根堆,用较大的孩子与父亲交换
next++;
if(nums[pre] <= nums[next])
break;
std::swap(nums[pre],nums[next]);
pre = next;
}
}
... 其他代码一致
**时间复杂度:**要遍历数组,每个元素都要进行一次下沉操作,下沉操作的处理方式类似二叉树,故时间复杂度为 O ( l o g N ) O(logN) O(logN)
**空间复杂度:**属于原地算法,故为 O ( 1 ) O(1) O(1)
**稳定性:**每次堆顶元素和堆有序末尾元素交换是强制的,若堆顶元素值和堆有序末尾元素值相等,则该算法不稳定。
初始次序:
移动次数有关:取决于所选元素的深度,可能升到顶端后就不会下沉,也可能升到顶端后就会下沉。
比较次数有关:sink函数若出现部分有序就会break, 如果一个元素沉到堆中间就停止下沉的话,比较次数就少了一半。
时间复杂度无关:不管最好情况还是最坏情况时间复杂度都是 O ( l o g N ) O(log N) O(logN),故时间复杂度无关
排序趟数无关:不管你数据有不有序,堆排序建堆都会调用logN
次sink函数,后期交换排序调用N-1次sink函数 (当然如果你加了检测数据是否预习排好序的检测函数那么另行讨论)
优先队列的原理就是堆排序,以前看算法4用Java实现过一个优先队列,我尝试用C++再次实现了下,发现算法4实现优先队列的堆排序思想和LC题解的堆排序思想不太一样,LC题解的堆排序是通过每次顶部元素的交换排定来实现的,也就是升序输出配小根堆,降序输出配大根堆,而这边优先队列为了能够o(1)获取元素,反而是降序输出配小根堆,升序输出配大根堆的,但排序本质思路基本一致。
#include
using namespace std;
class MinPQ{
public:
MinPQ(int cap):arr(vector(cap+1)),top(-1){
}
void add(int data){
arr[++top] = data;
swim(arr,top);
}
int poll(){
int key = arr[0];
std::swap(arr[0],arr[top--]);
sink(arr,0,top);
return key;
}
void swim(vector &arr,int k){
while(k > 0 && arr[k] < arr[k / 2]){
std::swap(arr[k],arr[k / 2]);
k = k / 2;
}
}
void sink(vector &arr,int pre,int top){
while(pre * 2 + 1 <= top){
int next = pre * 2 + 1;
if(next + 1 <= top && arr[next] > arr[next + 1])
next++;
if(arr[pre] <= arr[next])
break;
std::swap(arr[pre],arr[next]);
pre = next;
}
}
bool isEmpty(){
return top == -1;
}
void print(){
printf("top=%d |",top);
for(int i : arr){
cout << i << " ";
}
cout << endl;
}
private:
vector arr;
int top;
};
int main(){
vector vec{8,4,1,2,7,5,3,9};
MinPQ pq(10);
for(int i:vec){
pq.add(i);
}
pq.print();
while(!pq.isEmpty()){
cout << pq.poll() << " ";
}
cout << endl;
}
将swim和sink比较和交换操作的值比较反过来即可。
... 其他代码相同
void swim(vector &arr,int k){
while(k > 0 && arr[k] > arr[k / 2]){ //change
std::swap(arr[k],arr[k / 2]);
k = k / 2;
}
}
void sink(vector &arr,int pre,int top){
while(pre * 2 + 1 <= top){
int next = pre * 2 + 1;
if(next + 1 <= top && arr[next] < arr[next + 1]) //change
next++;
if(arr[pre] >= arr[next]) //change
break;
std::swap(arr[pre],arr[next]);
pre = next;
}
}
... 其他代码相同
(1)从上面的内容看,好像我们知道这个索引也没什么实际用处,真的是这样吗?
考虑下面一种情况,比如李雷考了全班第一,韩梅梅考了第二。我们把全班四十个人的成绩按照高低排了优先队列。但是复核的时候,突然发现韩梅梅的成绩少算了10分,加上10分应该她是第一。那么,如果没有这个索引,我们要怎么修改已经形成的优先队列呢?
有的人可能说,很简单啊,把李雷和韩梅梅出队列,然后更改成绩,重新加入队列。
好,那么,假如韩梅梅成绩统计错了,她是全班第三十九人呢?难道要把39个人的成绩重新出队列,然后重新加入吗?这个成本代价似乎有点高。
更进一步,如果是全校四千人的队列呢?如果有一千人的成绩全算错了呢?我们要重新生成这个队列一千次?
这时候,索引优先队列就有了用武之地。如果韩梅梅的成绩错了,我们从索引里知道她是优先队列里的第二个,那么我们直接修改她的成绩,然后上浮或者下沉就可以了,要付出的代价非常小。
(2)再进一步,如果要按成绩排队之后,依次请家长上台传授家教经验呢?我们怎么知道每个人的家长是谁啊?
这时候,我们再用一个数组key[],其中保存了每个人和家长的名字对应关系。这样来看,有一个索引的用处是不是更大了?
把这些合起来,就得到第三部分的索引优先队列IndexedPQ的实现。
摘自某博客,还算通俗理解,关于索引优先队列的应用有对数据库中已排序表格中若干元素值修改后的再排序,在多任务的应用系统中对已经加入处理队列的任务调整优先级等。
但是用数组也能实现同样的目标啊,为什么还要搞得如此麻烦?而且一看LeetCode上并没有相关的应用题,B站也没有对这个数据结构的讲解,许是个冷门的知识,故跳过了。
参考:
小橙书阅读指南(七)——优先队列和索引优先队列
[图解排序算法(三)之堆排序]https://www.cnblogs.com/chengxiao/p/6129630.html
复习基础排序算法(Java)