大顶堆、小顶堆 原理、代码、经典应用

如有误欢迎指出

基本概念

大顶堆是一颗完全二叉树,同时保证所有双亲都比自己的孩子大(可以相等
小顶堆反之

堆的维护

使用数组存储,长度为 n+1,从 1 索引开始存储,对 i i i 结点, 2 i 2i 2i 2 i + 1 2i+1 2i+1 是孩子, i 2 \frac{i}{2} 2i是双亲

1.自我初始化

把一个现有数组自我初始化为堆:
 原理:(以小顶堆为例)

  1. 叶子本身已经属于堆(因为没有孩子
  2. 从叶子的双亲开始,即从第二小的子树开始(最小的子树是叶子),判断左右是否右比双亲小的叶子,有则替换,得到小顶堆子树
  3. 根据得到的小顶堆子树,再往上维护小顶堆的性质,一层层直到根节点

 实现:

  1. 索引值最大的第二小的子树是几?是 n 2 \frac{n}{2} 2n!(也就是 n n n 位置的双亲索引
  2. 注意,维护好一颗子树之后有可能影响子树的子树的小顶堆性质,需要往下继续维护,直到叶子

维护小顶堆示例:
在这里插入图片描述
上图中

  1. 5<25不需要调整
  2. 72>67需要调整,交换
  3. 17>5也需要调整,交换(交换后比较17和25,17<25不需要调整)
  4. 最后来到根节点,如下,属于 “影响子树的子树的小顶堆性质,需要往下继续维护” 的情况
    在这里插入图片描述

代码(小顶堆

/*测试样例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. 优先队列

  2. 给你 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个里的)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值