堆与队列基本概念
普通队列: FIFO
优先队列:有优先级的队列,可以用堆来实现。
二叉堆:最大堆MaxHeap满足父节点的值大于任一子节点,最小堆MinHeap反之。且二叉堆必须是一个完全二叉树,这个性质让它可以用数组来实现。
堆的基本存储
由于有完全二叉树的性质,我们把它们按层序遍历的方式编号,根节点是1(不是0!),以此类推。
设某节点的编号为k,则它的parent = k/2,左子树left_child = 2k, 右子树right_child = 2k+1。
注意边界条件!
向一个最大堆添加一个元素 Shift Up
思路:
把新元素添加到堆的末尾,此时count++,但是此时这个新元素未必在正确的位置,所以要将它shift up,将它与它的parent比较,如果大于parent,则和parent swap一下,由此循环,直到不比它的parent大为止。同时,要注意shift到根节点是边界条件:k/2==1。
public:
void insert(Item item){
assert( count + 1 <= capacity );//有空间存储新插入的元素
data[count+1] = item;
shiftUp(count+1);
count ++;
}
Private:
void shiftUp(int k){
while( k > 1 && data[k/2] < data[k] ){ //k>1保证k/2最少到1,不会超越根节点
swap( data[k/2], data[k] );
k /= 2;
}
}
从一个最大堆取出一个元素 Shift Down
思路:
把根节点的元素取出来,相应的操作是用一个变量先存储这个要取出来的元素,然后count–,这样之后也不会遍历到第count个元素,相当于把一个元素删了。然后把最后一个元素和根节点元素swap,之后为了保证我们的二叉堆还是满足最大堆性质,需要现在这个根节点的元素shift down到它适合的位置。先判断是否有孩子(2k<=count,因为如果有孩子则必有左孩子)跟它的左右child相比,如果都大于,则说明在正确的位置,如果小于,则将它与大的那个子节点swap,然后继续循环。
注意边界条件:判断是否有右孩子。
设一个变量j来存与data[k]交换的位置,初值设为2k,如果2k+1<=count且data[2k+1]>data[2k],则和右孩子交换。
如果有swap记得更新k。
public:
// 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
Item extractMax(){
assert( count > 0 );//保证堆中有元素
Item ret = data[1];//取根节点
swap( data[1] , data[count] );//最后一个元素swap到根结点然后shiftdown
count --;
shiftDown(1);
return ret;
}
private:
void shiftDown(int k){
while(2k <= count){
int j = 2k;//要交换的元素的位置,初始化为2k
if(j + 1 <= count && data[j] > data[i] )
j += 1;
if(data[k] >= data[j])
break;
swap(data[k], data[j]);
k = j;
}
}
基础堆排序 (heapSort1)
需要开辟一个空堆。
把数组中的元素按顺序insert到一个空堆里,由于是最大堆,所以依次取出根节点的值(当前最大)存到数组中n-1, n-2, …, 0的位置里。
//不定义在类里
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();
}
}
将n个元素逐个插入到一个空堆中,算法复杂度是 O(nlogn) O ( n l o g n )
注意对比不同方法的算法复杂度!
Heapify (heapSort2)
Heapify与基础堆排的不同点在于它不是直接创建一个空堆,而是创建一个数组元素按顺序依次存入堆(未排序),然后从第一个非叶子节点的位置开始shiftdown,(叶子节点已经是二叉堆了),由第一个非叶子节点逐个向上处理非二叉堆,这是在构造函数的工作,不是只传入一个元素个数n,而是还要传入这个数组。
由此就构造了二叉堆。(排好序的堆)
public:
MaxHeap(Item arr[], int n){
data = new Item[n+1];
capacity = n;
for(int i = 0; i < n; i++)
data[i+1] = arr[i];
count = n;
for(int i=count/2; i >= 0; i--)
shiftDown(i);
}
//不定义在类里
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();
}
heapify的过程,算法复杂度为 O(n) O ( n )
重要性质:
在上面的做法,我们的方法都是用数组来表示堆下标从1开始,这种情况下有以下性质:
下标从1开始,对于下标k的节点来说,
1. parent = k/2
2. left_child = 2k
3. right_child = 2k+1
4. 最后一个非叶子节点的索引:count/2
下标从0开始,对于下标k的节点来说:
1. parent = (k-1)/2
2. left_child = 2k+1
3. right_child = 2k+2
4. 最后一个非叶子节点的索引:(count-1)/2
原地堆排序 (heapSort)–优化的堆排序
在原地排序,把整个数组当作一个未排好序的堆结构,之所以上面的heapSort要构建另一个堆是因为数组元素下标从0开始,而堆是从1开始的。那么这里,我们就把这个数组看成一个下标从0开始的堆结构。
n是数组中元素个数。一开始从第一个非叶子节点(下标(n-1)/2)开始往上逐个shiftdown,获得一个二叉堆,数组第一个元素为根节点,是最大值,应该放在数组末尾,于是和最后一个元素swap。之后只考虑除了已就位的最大数的剩下的n-1个元素即可。此时只需shiftdown第一个元素,因为其他位置都满足二叉堆定义。由此循环,直到到i=1。
Time:
O(nlogn)
O
(
n
l
o
g
n
)
Space:
O(1)
O
(
1
)
void heapSort(T arr[], int n){
//heapify
for(int i = (n-1)/2; i >= 0; i--)
__shiftDown(arr, n, i);//完整的二叉堆
for(int i = n-1; i > 0; i--){
swap(arr[0],arr[i]);
__shiftDown(arr, i, 0);
}
}
void __shiftDown(T arr[], int n, int k){
while(2*k+1 < n){ 因为数组下标是从0...n-1,故n不能取
int j = 2*k+1;
if(j+1 < n && arr[j+1] > arr[j])
j += 1;
if(arr[k] >= arr[j])
break;
swap(arr[k],arr[j]);
k = j;
}
}
最大堆的完整代码实现
#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cmath>
#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] ){ //k>1保证k/2最少到1,不会超越根节点
swap( data[k/2], data[k] );
k /= 2;
}
}
void shiftDown(int 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;
swap( data[k] , data[j] );
k = j;
}
}
public:
// 构造函数, 构造一个空堆, 可容纳capacity个元素
MaxHeap(int capacity){
data = new Item[capacity+1];//考虑数组越界
count = 0;
this->capacity = capacity;//把用户传来的capacity赋值给capacity这个这个属性,创建这个属性是为了成员函数都能查看堆的容量capacity
}
~MaxHeap(){
delete[] data;
}
// 返回堆中的元素个数
int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
bool isEmpty(){
return count == 0;
}
// 像最大堆中插入一个新的元素 item
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];
}
};
// 测试最大堆
int main() {
MaxHeap<int> maxheap = MaxHeap<int>(100);
srand(time(NULL));
int n = 100; // 随机生成n个元素放入最大堆中
for( int i = 0 ; i < n ; i ++ ){
maxheap.insert( rand()%100 );
}
int* arr = new int[n];
// 将maxheap中的数据逐渐使用extractMax取出来
// 取出来的顺序应该是按照从大到小的顺序取出来的
for( int i = 0 ; i < n ; i ++ ){
arr[i] = maxheap.extractMax();
cout<<arr[i]<<" ";
}
cout<<endl;
// 确保arr数组是从大到小排列的
for( int i = 1 ; i < n ; i ++ )
assert( arr[i-1] >= arr[i] );
delete[] arr;
return 0;
}