【算法导论】第六章 堆 学习笔记
堆
二叉堆是一个 数 组 ,可以看做一个近似完全二叉树,对于一个长度为n的数组给定一个数组下标i,~i=0,1,2,…,n-1,则:
最大堆
除根节点外,所有节点满足:
最小堆
除根节点外,所有节点满足:
堆的高度
定义:叶节点到根的最短路径上边的长度为堆的高。
定理6.1
对于节点数为的堆,其所有节点的可能取值高度(即,以节点i为 根节点 的子树的高)为:h=0,1,…lgn取下界,且高度为h的节点,至多有{n}/{n^{h+1}}取上界个。
堆排序
以最大堆为例:
排序的整个过程可以看做如下:
建堆 BUILD_MAX_HEAP
2. 堆维护 MAX_HEAPITY
3. 断尾重构 HEAPSORT
AI写代码
堆维护 MAX_HEAPITY
MAX_HEAPITY堆维护是建堆过程中至关重要的一步,它负责维护最大堆的性质不变,即根节点一定大于等于子节点。
代码思想(伪代码)
MAX_HEAPITY(A,i):
left=i*2+1;
right=i*2+2;
//假设以left和right均为最大堆
//判断left和right的合理性 < heap_size
//比较节点与left、right的大小,
//将大的节点与i交换,并递归调用MAX_HEAPITY(A,max_index)
AI写代码
复杂度分析
从上可知,对于节点至多交换h_i次,即被从堆顶被交换到堆底。所以时间复杂度为O(h)=O(lgn)。
也可用递推公式T(n)\le T(2n/3)+\Theta(1),通过主定理求解得T(n)=O(lgn)。
建堆 BUILD_MAX_HEAP
建堆主要是将无序数组按照树顺序,建立成最大(小)堆的过程。
已知对于节点数为n,~i=0,1,2…n-1的二叉树,其叶子节点的序号一定从(n+1)/2开始,则在建堆初始化时,可以把第(n+1)/2,(n+1)/2+1,…,n-1个叶子节点看做单节点最大堆。故建堆循环应从后往前,直到根节点i=0。
代码思想(伪代码)
BUILD_MAX_HEAP(A):
for i=(n+1)/2-1 to 0:
MAX_HEAPITY(A,i)
AI写代码
复杂度分析
结论:O(n)的线性复杂度。
分析:由伪代码可知,时间复杂度的上界为(n+1)/2*O(MAX\_HEAPITY),由于MAX_HEAPITY的复杂度与节点i所处的高度有关,我们可以利用定理6.1得到一个更紧确的界:即把要经过MAX_HEAPIFY的节点复杂度按其高度进行复杂度求和。通过变换和得线性复杂度。详见算法导论p88。
堆排序算法
用断尾重构的方法:每次把堆顶元素放至数组尾部,相应的堆size减小1
代码思想(伪代码)
HEAPSORT(A):
for i=A.length to 1:
exchange A[0],A[i]
--A.heap_size;
MAX_HEAPIFY(A,0);//对新的堆顶进行排序维护
AI写代码
复杂度分析
结论:O(nlgn)。
完整代码
//MaxHeap.h
#ifndef LINTCODE_MAXHEAP_H
#define LINTCODE_MAXHEAP_H
#include <vector>
using namespace std;
//原地排序算法
class MaxHeap {
private:
void exchangeNum(vector<int> &A, int i, int j);
void maxHeapity(vector<int> &A, int index, int &heap_size);
void buildMaxHeap(vector<int>& A, int &heap_size);
public:
void heapSort(vector<int>& A);
};
#endif //LINTCODE_MAXHEAP_H
AI写代码
//MaxHeap.cpp
#include "MaxHeap.h"
using namespace std;
void MaxHeap::exchangeNum(vector<int> &A, int i, int j) {
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
//数组下标从0开始
void MaxHeap::maxHeapity(vector<int> &A, int index, int &heap_size) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int i = index;
i = (left < heap_size && A[left] > A[i]) ? left : i;
i = (right < heap_size && A[right] > A[i]) ? right : i;
if (i != index) {
exchangeNum(A, i, index);
//递归调用
maxHeapity(A, i, heap_size);
}
}
void MaxHeap::buildMaxHeap(vector<int> &A, int &heap_size) {
int n = A.size();
int leaf_num = (n + 1) / 2; //节点数为n的堆,的叶子节点号为(n+1)/2,(n+1)/2+1,...,n
for (int i = leaf_num - 1; i >= 0; --i) {
maxHeapity(A, i, heap_size);
}
}
//断尾法,得排序数组
void MaxHeap::heapSort(vector<int> &A) {
int heap_size = A.size();
buildMaxHeap(A, heap_size);
for (int i = A.size() - 1; i > 0; --i) {
exchangeNum(A, 0, i); //交换尾部和当前堆顶元素
--heap_size;
maxHeapity(A, 0, heap_size);
}
}
AI写代码
优先队列
我们常用最大堆和最小堆来维护一个优先队列。
总结
- 堆排序是基于比较的排序,性能稳定,排序不稳定。
- 性能稳定:堆排序的(最好/平均/最差)时间复杂度均为。
- 只需要O(1)的辅助空间,既最高效率又节省空间
- 缺点:堆维护比较频繁,对于数组元素经常变动的情况下,不适合采用堆排序
