如有误欢迎指出
基本概念
大顶堆是一颗完全二叉树,同时保证所有双亲都比自己的孩子大(可以相等
小顶堆反之
堆的维护
使用数组存储,长度为 n+1,从 1 索引开始存储,对 i i i 结点, 2 i 2i 2i 和 2 i + 1 2i+1 2i+1 是孩子, i 2 \frac{i}{2} 2i是双亲
1.自我初始化
把一个现有数组自我初始化为堆:
原理:(以小顶堆为例)
- 叶子本身已经属于堆(因为没有孩子
- 从叶子的双亲开始,即从第二小的子树开始(最小的子树是叶子),判断左右是否右比双亲小的叶子,有则替换,得到小顶堆子树
- 根据得到的小顶堆子树,再往上维护小顶堆的性质,一层层直到根节点
实现:
- 索引值最大的第二小的子树是几?是 n 2 \frac{n}{2} 2n!(也就是 n n n 位置的双亲索引
- 注意,维护好一颗子树之后有可能影响子树的子树的小顶堆性质,需要往下继续维护,直到叶子
维护小顶堆示例:
上图中
- 5<25不需要调整
- 72>67需要调整,交换
- 17>5也需要调整,交换(交换后比较17和25,17<25不需要调整)
- 最后来到根节点,如下,属于 “影响子树的子树的小顶堆性质,需要往下继续维护” 的情况
代码(小顶堆
/*测试样例1
8
53 17 72 5 39 67 94 25
得到:
5 17 67 25 39 72 94 53
*/
/*测试样例2
9
53 17 72 5 39 94 67 25 1
得到:
1 5 67 17 39 94 72 25 53
*/
#include <iostream>
using namespace std;
int nums[105];
int n;
void adjustDown(int heap[],int i,int n)
{
int child_p = 2 * i; //i结点(在循环中是指对应调整的子树)的孩子的索引
int parent = heap[i]; //本子树的根结点的值
while (child_p<=n) //保证双亲是有孩子结点的,叶子结点本身就是排好的堆,不需要调整
{
if (child_p + 1 <= n && heap[child_p + 1] < heap[child_p]) child_p++;//选中左右孩子中更小的和双亲作比较
if (heap[child_p] < parent)
{
heap[child_p / 2] = heap[child_p];//将孩子的值赋给父亲
child_p *= 2;
}
else
{
break;
}
}
heap[child_p/2] = parent;//这一步容易忘记!!!!就是赋回i结点的值!当然如果每次比较完用tmp去做交换就可以不用这么麻烦,我就是不想用tmp交换,因为那样有三次赋值,而这样写只有一次
}
int main()
{
//数组初始化
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> nums[i];
}
//小顶堆初始化
for (int i = n/2; i >=1; i--)
{
adjustDown(nums, i, n);
}
//排序后展示
for (int i = 1; i <= n; i++)
{
cout << nums[i] << ' ';
}
cout << endl;
}
2.插入时维护
思路和自我初始化差不多,就是从下而上,从插入结点的双亲比较,往上找需要调整的每一个结点
/*测试样例1
8
53 17 72 5 39 67 94 25
得到:
5 17 67 25 39 72 94 53
*/
/*测试样例2
9
53 17 72 5 39 94 67 25 1
得到:
1 5 67 17 39 94 72 53 25(注意,这个和自我初始化的结果不一定一样)
*/
#include <iostream>
using namespace std;
int nums[105];
int n;
void heapInsertAdjust(int heap[], int i)//尾部插入后做的调整
{
int child = heap[i]; //最初插入的值,循环中做每次比较的孩子
int parent_p = i / 2; //循环中每次比较的双亲索引
int lastParent_p = i; //循环中最后一次赋给双亲的位置,默认不交换就是i原地(其实就是“上一个双亲”的位置,因为/2会默认向下取整,需要用这个标记
while (parent_p >= 1)
{
if (heap[parent_p] > child)
{
heap[lastParent_p] = heap[parent_p]; //将双亲的值赋给孩子,继续往上找
lastParent_p = parent_p; //更新赋值位置
parent_p /= 2;
}
else break;
}
heap[lastParent_p] = child;
}
int main()
{
//数组初始化
cin >> n;
for (int i = 1; i <= n; i++)
{
int tmp;
cin >> nums[i];
heapInsertAdjust(nums, i);//插入的时候调整
}
//排序后展示
for (int i = 1; i <= n; i++)
{
cout << nums[i] << ' ';
}
cout << endl;
}
时间复杂度
插入时维护:O(logn)
自我初始化:adjustDown部分为O(logn),有n/2趟,所以是O(nlogn)
应用
-
优先队列
-
给你 1 0 10 10^{10} 1010个数,选出最小的100个数,空间复杂度需要控制,如何操作?
这问题乍一看好像很奇怪,我们换一个问题,给你 1 0 10 10^{10} 1010个数,求最小的数,怎么操作?
这下看懂了! 不就是用一个 m i n min min 存第1个数,然后往后比较,遇到比 m i n min min 小的就替换掉min,直到结束吗,这样就可以用 O ( 1 ) O(1) O(1) 的空间复杂度,也就是 ‘ m i n min min’ 这一个 i n t int int 就可以完成任务了
那么上面这一个 i n t int int 对应到原题就是100个 i n t int int ,这100个 i n t int int我们就可以用 堆 存储,空间复杂度也就可以降到 O ( m ) , ( m = 100 ) O(m),(m=100) O(m),(m=100)了
所以只需要维护前100个为大顶堆,然后从101个开始,遇到比大顶堆的堆顶还小的,就纳入,并替换堆顶,进行维护,然后继续比较,遇到比堆顶还大的就可以跳过了(因为它绝不可能是最小的100个里的)