堆分为最大堆和最小堆。最大堆定义:一个二叉树,每个节点的值大于其子节点。最小堆相反。
一般用数组来表示堆,当然,用树,链表都可以,想怎么来怎么来。
数组d,有n个节点,其中一些下标定义:
1)d[k]的子节点为d[2*k+1]和d[2*k+2],当然这里的2*k+1和2*k+2都得小于n
2)最后一个父节点的位置:d[n/2-1]
3)d[k]节点的父节点的位置d[(k-1)/2]
有了如上的坐标索引,就能有定义一些堆的操作。下面的例子都以最大堆为例。
一、堆的建立:
一个数组初始化为堆,需要分为从下至上,然后从上至下。
从下至上指的是,从最下面一个父节点开始,遍历节点到根节点。回顾最大堆的性质:每一个节点,都比其子节点要大,也就是说,每一棵子树的最大节点是根节点。那么从最后一层开始,一直调整到最顶层,就保证了每棵子树都是最大堆。
那如何调整某棵子树?我们从上至下调整。
特殊的,我们先定义,如果一棵树只含有一个节点,那么这棵树也为最大堆。
我们假设当前节点为d[k]处在第i层,由于我们是从下至上遍历的子树,所以当前节点的左右子树已经满足最大堆的条件。
如果满足d[k] > d[2*k+1]且d[k] > d[2*k+2],即可以保证d[k]为根节点的堆也是最大堆。
所以,我们取d[2*k+1]和d[2*k+2]的更大者,和d[k]交换,这样就能保证上面的不等式。比如d[2*k+1] > d[2*k+2],那么交换d[k]和d[2*k+1],然后令k = 2*k+1,也就是向下递归这个过程,直到遇到叶子节点位置。
代码如下:
void top_down(vector<int> &nums, int k){
int n = nums.size();
k = 2 * k + 1;
while(k < n){
if(k + 1 < n && nums[k + 1] > nums[k])
k++;
if(nums[k] > nums[(k - 1) / 2])
swap(nums[k], nums[(k - 1) / 2]);
else
break;
k = 2 * k + 1;
}
}
void buildheap(vector<int> &nums){
int n = nums.size();
//bottom up
for(int i = n / 2 - 1; i >= 0; i--){
top_down(nums, i);
}
}
二、堆的插入
建立好了堆,需要往堆里插入数据,思路和构建堆是一样的。在数组末尾插入了一个元素,也要想办法把当前的数组调整为最大堆。末尾插入元素后,最后一个父节点构成的子树就不一定是最大堆了,所以需要从最后一个父节点开始进行调整。把新插入的元素一直往上和父节点交换位置,直到父节点大于新插入的值为止,这样就能保证堆被调整为最大堆。
证明:
假设新插入元素为d[n-1]。叶子节点就是最大堆。
假设新插入的元素替换到第k层的时候,由新插入的元素构成的子树为最大堆。此时新插入的元素位置为d[m]
那么到k-1层的时候,d[(m-1)/2]为d[m]的父节点,假设d[q]为d[m]的兄弟节点,那么必然有d[(m-1)/2] > d[q]。
如果d[(m-1)/2] < d[m],交换d[m]和d[(m-1)/2],则d[(m-1)/2]根节点的子树为最大堆。如果d[(m-1)/2] > d[m],不交换,则已经是最大堆。数学归纳法知得证。
代码:
void heapinsert(vector<int> &nums, int x){
nums.push_back(x);
int i = nums.size() - 1;
int parent = (i - 1) / 2;
while(parent >= 0 && i != 0){
if(nums[parent] < nums[i])
swap(nums[parent], nums[i]);
else
break;
i = parent;
parent = (i - 1) / 2;
}
}
三、删除堆顶
删除堆顶操作为:把堆顶元素和最后一个元素互换,然后弹出最后一个元素。再把当前堆调整为最大堆。
由于堆顶此时变成了最后一个元素,不一定是最大值,所以从第一个元素开始,进行从上到下的调整。
void heapdelete(vector<int> &nums){
nums[0] = nums.back();
nums.pop_back();
top_down(nums, 0);
}
四、堆的应用。
由于从堆中可以O(1)的时间找到最大值或者最小值,这个特性能够解决很多问题。比如K个有序链表的合并,排序算法等等。
下面举堆排序为例:
1)输入一个乱序数组,先调整为堆
2)把堆顶弹出,相当于删除堆顶,和数组末尾元素交换,这时待处理的数组长度减1
3)从第一个元素进行top_down操作调整堆
4)循环2-3,待处理的子数组长度为0时候,原数组变为有序。
时间复杂度O(nlog(n))(每次调整堆O(logn),调整n次)
void fixdown(vector<int> &nums, int k, int n){
k = 2 * k + 1;
while(k < n){
if(k + 1 < n && nums[k + 1] > nums[k])
k++;
if(nums[k] > nums[(k - 1) / 2])
swap(nums[k], nums[(k - 1) / 2]);
else
break;
k = 2 * k + 1;
}
}
void buildheap(vector<int> &nums){
int n = nums.size();
for(int i = n / 2 - 1; i >= 0; i--){
fixdown(nums, i, n);
}
}
void heapsort(vector<int> &nums){
buildheap(nums);
for(int i = 0; i < n; i++){
swap(nums[0], nums[n - i - 1]);
fixdown(nums, 0, n - i - 1);
}
}