堆
一般我们常用的堆指的是大顶堆。
堆是一棵完全二叉树(简单的说,就是前n-1层是满二叉树,最后一层可以没有填充满,全部聚集在左侧的二叉树),树中的每个结点的值都不小(或不大于)其左右孩子结点的值。
其中,如果父结点的值大于或等于孩子结点的值,那么程这样的堆为大顶堆,反之则为小顶堆。
这里我们以大顶堆为例。
我们调整初始堆来构造大顶堆的方式如下:
从堆的最后一个位置开始,从下往上,从左往右。假设当前结点为i,那么让i与它的左右孩子结点(如果有的话)比较,如果发现存在更大的结点,选择其中最大的那一个孩子结点j与结点i交换。交换之后让i继续与它新的孩子结点(如果有的话)比较,直到它的孩子结点都比它小或者它没有孩子结点为止。
把建堆操作细分成几个模块:
存储方式
对于完全二叉树而言,比较简洁的实现方式是使用数组存储,这样结点会按照层序存储于数组中,其中第一个结点存储于数组的1号位,并且数组i号位表示的左孩子是第2i号位,而右孩子则是第(2i+1)号位(如果第一个结点存储于数组的第0号,那么它的左孩子是第(2i+1)号位置,右孩子是第(2i+2)号位置)。于是可以像下面这样来定义表示堆:
const int maxn = 100;
// heap为堆,n为元素个数
int heap[maxn], n = 10;
向下调整
回顾之前的建堆过程可以发现,对于每一个结点的调整都是把它从上往下的调整,这对这种向下调整,调整方法如下:对于每一个结点U,我们都把它和左右孩子结点比较,如果存在更大的孩子结点,我们选择其中最大的孩子结点V,将U和V的位置交换,随后继续把U和它的孩子结点比较,直到U没有孩子结点或者孩子结点全部小于U为止。
于是可以写出向下调整的代码如下(其时间复杂度为O(logn)):
// 对heap数组在[low, high]范围进行向下调整
// 其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标
void downAdjust(int low, int high){
int i = low, j = i*2; // i为欲调整结点,j为其左孩子
while(j <= high){ // 存在孩子结点
//如果右孩子存在,且右孩子的值大于左孩子
if(j+1 <= high && heap[j+1]>heap[j]){
j = j+1; // 让j存储右孩子下标
}
// 如果孩子中最大的权值比欲调整结点i大
if(heap[j] > heap[i]){
swap(heap[j],heap[i]); //交换i和j两个结点
i=j; // 保持i为欲调整结点,j为i的左孩子
j*=2;
}else{
break; //孩子的权值均比欲调整结点i小,调整结束
}
}
}
建堆
代码如下,值得一提的是这里为何要倒着枚举呢?这是因为每次调整完一个结点后,当前子树中权值最大的结点就会处在根结点的位置,这样当遍历到其父结点时,就可以直接使用这个结果。也就是说,这种做法保证每个结点都是以其为根结点的子树中的权值最大的结点。
void createHeap(){
for(int i=n/2;i>=1;i--){
downAdjust(i,n);
}
}
删除最大元素(堆顶元素)
如果要删除最大元素(即堆顶元素),并让其仍然保持堆的结构,那么只需要让最后一个元素覆盖堆顶元素,然后对根结点进行调整即可。代码如下,时间复杂度为O(logn)
void deleteTop(){
heap[1] = heap[n--]; // 用最后一个元素覆盖堆顶元素,并让元素个数减1
downAdjust(1,n); // 向下调整堆顶元素
}
添加元素
如果想要往堆里添加一个元素,那么就将想要添加到元素放在数组最后(也就是完全二叉树的最后一个结点后面),然后进行向上调整操作。向上调整总是把插入结点与父结点比较,如果权值比父结点大,那么就交换其与父结点,这样反比比较,直到到达堆顶或父结点的权值较大为止。向上调整的代码如下,时间复杂度为O(logn):
//对heap在[low,high]范围进行向上调整
//其中low一般设置为1,high表示欲调整结点的数组下标
void upAdjust(int low, int high){
int i = high, j = i/2; // i为欲调整结点,j为其父亲
while(j >= low){ //父亲在[low,high]范围内
//父亲权值小于欲调整结点i的权值
if(heap[j] < heap[i]){
swap(heap[j],heap[i]); //交换父亲和欲调整结点
i=j; //保持i为欲调整结点,j为i的父亲
j/=2;
}else{
break; ///父亲权值比欲调整结点i的权值大,调整结束
}
}
}
在此基础上就可以很容易的实现添加元素的代码:
//添加元素
void insert(int x){
heap[++n]=x; //让元素个数加1,然后将数组末位赋值为x
upAdjust(1,n); //向上调整新加入的结点n
}
堆排序
有了上面的基础,就可以进行堆排序的操作。这里讨论以递增排序为例。
考虑对一个堆来说,堆顶元素是最大的,因此在建堆完毕后,堆排序的直观思路就是取出堆顶元素,然后将堆的最后一个元素替换至堆顶,再进行一次针对堆顶元素的向下调整——如此重复,直到堆中只有一个元素为止。
在具体实现时,为了节省空间,可以倒着遍历数组,假设当前访问到i号位,那么将堆顶元素与i号位的元素交换,接着在[1,i-1]范围内对堆顶元素进行一次向下调整即可。
堆排序代码如下:
void heapSort() {
createHeap(); //建堆
for(int i=n; i>1;i--){ //倒着枚举,直到堆中只有一个元素
swap(heap[i], heap[1]); //交换Heao[i]与堆顶
downAdjust(1,i-1); //调整堆顶
}
}
内容来自《算法笔记》