时间复杂度:O(nlogn)
优先队列:出队顺序和入队顺序无关,和优先级相关
比如在医院看病,急诊病人优先,再比如在操作系统中执行任务,操作系统将动态的选择每一次优先级最高的任务进行执行
为什么选择优先队列:例如在1000000个元素中选出前一百名,在 n 个元素中选出前 m 个元素,排序算法的算法复杂度是O(nlog n)级别的,而使用优先队列,可以将其算法复杂度降低到O(n*log m)级别
优先队列的主要操作:入队,出队(取出优先级最高的元素)
优先队列在堆中的实现:
最大堆:堆中的某个节点的值总是不大于其父节点的值;堆总是一棵完全二叉树
最小堆:堆中的某个节点的值总是不小于其父节点的值;堆总是一棵完全二叉树
(并不意味着层数越高数值越大)
最大堆原理:将一个一个元素插入堆的同时,进行 shiftUp() 函数,使堆满足最大堆性质
最大堆代码:
template<typename Item>
class MaxHeap{
private:
Item *data;//存储堆内容的数组
int count;//元素的个数
int capacity;//堆的容量
void shiftUp(int k){
//与父节点相比,若父节点小于其优先性,交换二者位置 ,直到满足最大堆的定义
//k的值最小为2
while( k > 1 && data[k/2] < data[k] ){
swap( data[k/2], data[k] );
k /= 2;
}
}
void shiftDown(int k){
//当前节点 k 有左孩子则一定有孩子
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
//有右孩子
if( j+1 <= count && data[j+1] > data[j] )
j ++;
// 使得data[j] 是 data[2*k]和data[2*k+1]中的最大值
if( data[k] >= data[j] )
break;
//data[k] < data[j],交换位置
swap( data[k] , data[j] );
k = j;
}
}
public:
MaxHeap(int capacity){//堆的容量由用户指定
data = new Item[capacity+1];//数组存储堆的内容是从索引 1 开始的
count = 0;
this->capacity = capacity;
}
~MaxHeap(){
delete[] data;
}
int size(){
return count;
}
bool isEmpty(){
return count == 0;
}
//新添元素
//将元素添加到数组末尾,count++ ,判断是否保持堆得性质
//不满足,与其父节点交换位置,接着继续观察是否满足堆的性质,循环此步骤,直到满足
void insert(Item item){
//检测堆是否还有空间,没有空间,程序结束
assert( count + 1 <= capacity );
data[count+1] = item;
count ++;
//调整新添元素,使二叉树仍然保持堆的性质
shiftUp(count);
}
//取出堆顶元素
//将数组末尾元素交换到堆首,count--, 判断是否保持堆得性质
//不满足,跟优先性大的子孩子交换位置,循环此步骤,直到满足
Item extractMax(){
assert( count > 0 );
Item ret = data[1];
swap( data[1] , data[count] );
count --;
shiftDown(1);
return ret;
}
};
template<typename T>
void heapSort1(T arr[], int n){
//实例化一个最大堆
MaxHeap<T> maxheap = MaxHeap<T>(n);
for( int i = 0 ; i < n ; i ++ )
maxheap.insert(arr[i]);
//反向遍历,使之从小到大排序
for( int i = n-1 ; i >= 0 ; i-- )
arr[i] = maxheap.extractMax();
}
优化1:(建堆方法有差别)将数组赋值给堆,在堆中从后向前的考察每一个不是叶子节点的节点(如图示,考察顺序为 5 4 3 2 1),使以它为根所构成的子树满足最大堆的性质,即在这个位置上执行 shiftDown() 函数
实现优化原因:将 n 个元素逐个插入到一个空堆中,算法复杂度是O(n*logn)级别
heapify 的过程,算法复杂度为O(n)级别
补充:1.完全二叉树所有的叶子节点(没有孩子的节点)本身就是一个最大堆
2.对于一棵完全二叉树来说,第一棵非叶子节点的索引是这棵完全二叉树的元素个数/2得到的值
代码实现:
#include <algorithm>
#include <cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item *data;
int count;
int capacity;
void shiftUp(int k){
while( k > 1 && data[k/2] < data[k] ){
swap( data[k/2], data[k] );
k /= 2;
}
}
void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[j+1] > data[j] ) j ++;
if( data[k] >= data[j] ) break;
swap( data[k] , data[j] );
k = j;
}
}
public:
MaxHeap(int capacity){
data = new Item[capacity+1];
count = 0;
this->capacity = capacity;
}
//传入 n 个元素的数组
MaxHeap(Item arr[], int n){
data = new Item[n+1];
capacity = n;
//data[]数组从 1 开始
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
//从第一个不是叶子节点的结点开始进行shiftDown()操作
for( int i = count/2 ; i >= 1 ; i -- )
shiftDown(i);
}
~MaxHeap(){
delete[] data;
}
int size(){
return count;
}
bool isEmpty(){
return count == 0;
}
void insert(Item item){
assert( count + 1 <= capacity );
data[count+1] = item;
shiftUp(count+1);
count ++;
}
Item extractMax(){
assert( count > 0 );
Item ret = data[1];
swap( data[1] , data[count] );
count --;
shiftDown(1);
return ret;
}
Item getMax(){
assert( count > 0 );
return data[1];
}
};
template<typename T>
void heapSort2(T arr[], int n){
//构造堆
MaxHeap<T> maxheap = MaxHeap<T>(arr,n);
for( int i = n-1 ; i >= 0 ; i-- )
arr[i] = maxheap.extractMax();
}
优化2:原地堆排序:一个数组,我们可以通过 heapify 过程使之变成一个最大堆,在这个最大堆中,第一个元素 v 的位置就是整个数组中最大值的位置,我们在具体排序过程中,v 显然应该排在数组末尾的位置,则交换第一个元素 v 和最后一个元素 w ,此时 w 所在的数组显然不再满足最大堆的性质,则将 w 所在数组执行 shiftDown2()函数,此时第一个元素又成为最大值,再次执行交换、shiftDown2() 过程,直到完成排序
注意:数组下标从零开始,顾改变堆得索引值,但节点的左右节点以及父节点仍有规律可循
代码:
template<typename T>
//实现在arr[n]所定义的堆中 k 这个位置的元素找到它合适的位置
void shiftDown2(T arr[], int n, int k){
T e = arr[k];
//左孩子变成2*k+1
while( 2*k+1 < n ){
int j = 2*k+1;
//拥有右孩子
if( j+1 < n && arr[j+1] > arr[j] )
j ++;
//此时 j 为左右孩子中较大孩子的位置
if( e >= arr[j] ) break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
template<typename T>
void heapSort(T arr[], int n){
//进行一次heapify 的过程,第一个非叶子节点下标 i = (n-1)/2
for( int i = (n-1)/2 ; i >= 0 ; i -- )
shiftDown2(arr, n, i);
//把当前最大的元素放到它合适的位置上
//对他们第 0 位上的元素执行 shiftshiftDown2()
//注意,每次循环数组 w 元素个数 -1,即为 i 的个数,
for( int i = n-1; i > 0 ; i-- ){
swap( arr[0] , arr[i] );
shiftshiftDown2(arr, i, 0);
}
}