堆是一棵树,其每个节点都有一个键值,且每个节点的键值都大于等于/小于等于其父亲的键值。以大根堆为例,每个节点的键值都小于等于其父亲的键值,所以堆顶就是最大值。堆还支持插入操作,删除堆顶操作。

操作时间复杂度
取最值 O ( 1 ) O(1) O(1)
插入 O ( l o g 2 n ) O(log_2n) O(log2n)
删除堆顶 O ( l o g 2 n ) O(log_2n) O(log2n)
堆的结构

最常见的堆的结构就是二叉堆,二叉堆就是完全二叉树。完全二叉树,从根往下数,除了最下层外都是全满(都有两个子节点),而最下层所有叶结点都向左边靠拢填满。构造一颗完全二叉树就是从上到下,从左往右的放置节点。

下图就是一个完全二叉树,每个节点旁边的数字就是这个节点的编号,内部就是节点的数据。

image-20201212102420844image-20201212103311547

观察节点的编号,发现可以很好的用数组进行模拟,假设某个节点的编号为 x, 那么他的两个孩子的编号就是 2*x, 2*x+1.

堆的插入

现在有一个集合S={2, 5, 8, 9, 10, 13},对这个集合建大根堆,然后再插入一个数字 16.

image-20201212141957098

在插入之前堆的结构是这个样子的,一共有6个节点,根节点的编号是1。可以看到每个节点孩子的值都小于等于当前节点的值。所以根节点就是最大值。

image-20201212142631445

当我们插入 16 的时候,新增一个编号为 7 的节点, 也就是 3 号节点的右儿子。16 就是 7 号节点的值。

把新增的数放在了最后的位置,不一定满足大根堆的性质,所以需要调整结构,既然儿子的值大于其父亲的值,那么让儿子造反就行了, 把儿子和父亲换一下,这样儿子的值就会小于父亲的值。也就是把3号节点的值和7号节点的值换一下。如果调整一次还不满足大根堆的性质,那么就接着调整,直到满足大根堆的性质或者该节点变成了根节点。

image-20201212143551018 image-20201212143648535

最终的结构如上图所示,一共调整了两次,就满足了大根堆的性质,堆顶就是最大值的位置。

由于完全二叉树的性质,树的高度最多就是 l o g 2 n log_2n log2n 层,所以调整的次数最多也就是 l o g 2 n log_2n log2n 次。

总结一下:

构建大根堆的过程就是把每一个要加入的数放在最后的位置,然后调整堆的结构,直到满足大根堆的性质。构建小根堆也是一样的过程,只是改一下大于小于的符号。

堆的删除

删除堆顶的步骤:

1、把堆顶和堆的最后一个节点交换,堆的最后一个节点变成了堆顶,堆顶在最后一个节点上,然后删除最后一个节点,就相当于删除了堆顶。

2、从堆顶开始,如果堆顶小于堆顶的两个儿子中最大的那一个,那么堆顶就和大的儿子交换。一直交换到满足大根堆的性质。

删除堆顶也就是重复的操作,和插入一样,最多交换 l o g 2 n log_2n log2n 次。

下面画图来演示一下。一开始堆是这个样子的。

image-20201212151318991

经过步骤 1 变成了下图。

image-20201212151409019

很明显堆顶小于他的右儿子,所以节点1,3要交换. 交换之后3号节点还是小于6号节点,所以节点 3,6还要交换。

image-20201212151618555 image-20201212151633084

最终的结构如上图所示,堆顶的值变成了13.

/*
head 为模拟大根堆的数组
sz   为堆里面元素的个数。
*/
void push(int x){ // 插入一个数 x
	heap[++sz] = x; // 把新增加的数放到堆的最后。
	int now = sz;   // 当前节点为最后一个节点。
	while(now > 1 && heap[now] > heap[now / 2]){  // 然后不断的和 其父亲毕竟,如果大于父亲的值,就要交换。
		swap(heap[now], heap[now / 2]);
		now /= 2;    // 交换之后,当前节点变为父亲的编号。
	} 
}

void erase(){
	swap(heap[1], heap[sz]);  // 交换 堆顶 和 堆最后节点的值。
	sz--;    // 堆元素个数减少
	int now = 1;   // 当前节点为堆顶。
	while(now * 2 <= sz){  // 判断当前节点是不是还有儿子。
		int tmp = now * 2;
		if (tmp + 1 <= n && heap[tmp+1] > heap[tmp]) tmp++;  // tmp 是两个儿子中最大值的编号。
		if (heap[tmp] <= heap[now]) break;   // 如果满足了大根堆的性质,那么就break 退出。
		swap(heap[now], heap[tmp]);   // 交换 当前节点 和 儿子节点。
		now = tmp;    // 当前节点变成儿子节点的编号。
	}
}

堆的两种操作已经介绍完了,最重要的是了解堆的思想。如果想要使用堆排序,那么就需要把所有的元素都加入到堆里面,然后每次取堆顶,删除堆顶。

平常我们不用写堆的代码,就像我们也不写快排的代码一样。堆和快排一样,C++ 和 Java 都自己实现了,我们只需要调用接口会用就可以了。我们要知其然也要知其所以然,了解底层到底是什么样的一个结构。了解底层结构之后,也方便自己长期记忆。

C++ 堆的使用方法

priority_queue<int> que1;   // 定义一个大根堆
for (int i = 0; i <= 10; ++i)
    que1.push(i);            // 向大根堆里面添加元素
printf("%d \n",que1.top());  // 输出来大根堆的堆顶。
que1.pop();                  // 删除大根堆的堆顶。

priority_queue<int, vector<int>, greater<int>>que2;  // 定义一个小根堆
for (int i = 0; i <= 10; ++i)   
    que2.push(i);            // 向小根堆里面添加元素
printf("%d \n",que2.top());  // 输出来小根堆的堆顶。
que2.pop();                  // 删除小根堆的堆顶。
例题

剑指 Offer 40. 最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

这是一个简单题,把前 k 小的输出来,那么我们直接排序一遍,然后把答案输出来就可以了。

今天的专题是堆, 所以要用堆来做。

题目要求前 k 小, 所以我们要建一个小根堆, 然后把所有的数都放到堆里面。这个时候堆顶就是最小的。剩下的操作就是取堆顶,然巴删除堆顶,操作k次就好了。

c++ 和 Java 都有接口,我们学会使用接口就行,具体看c++的实现代码。

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int>ans;  // 答案
        priority_queue<int, vector<int>, greater<int>>que; // 声名一个小根堆。
        for (auto it: arr)
            que.push(it);  // 把所有的数都放到小根堆里面。
        for (int i = 0; i < k; ++i){  // k 次遍历, 去堆顶, 删除堆顶。
            ans.push_back(que.top());
            que.pop();
        }
        return ans;
    }
};

练习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值