LeetCode Lect7 堆及其应用
概述
堆是一颗完全二叉树。分为大根堆(父节点>=所有的子节点)和小根堆(父节点<=所有的子节点)。
插入、删除堆顶都是O(logN),查询最值是O(1)。
完全二叉树(Complete Binary Tree)
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
完全二叉树是由满二叉树而引出来的。对于深度为K的,有N个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
若一棵二叉树至多只有最下面的两层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,则此二叉树成为完全二叉树。
完全二叉树的特性:对于结点t,t/2为它的父节点,2*t、2*t+1为它的子节点。所以可以直接用一个线性的数组存放堆。
堆的基本操作:(以小根堆为例)
1.堆元素上移:和自己的父节点比较,若满足条件则交换。再次比较。
void up(int x)
{
int p,q;
p=x;
while (p/>=)
{
q=p/;
if (a[q]>a[p])
swap(&a[p],&a[q]);
else
break;
p=q;
}
}
2.堆元素下移:和自己的子节点中更满足条件(更大or更小)的一个比较,若满足条件则交换。再次比较。
void down(int x)
{
int p,q;
p=x;
while (p*<=n)
{
q=*p;
if ((a[q+]<a[q])&&(q+<=n)) q++;
if (a[q]<a[p])
swap(&a[q],&a[p]);
else
break;
p=q;
}
}
3.建堆:
cin>>n;
for (i=;i<=n;i++)
cin>>a[i];
for (i=n/;i>=;i--) //
down(i);
注:对1处语句的解释:
. a[(n/)+]----a[n]的元素都是堆上的叶子节点,无需down(i)操作。
. 倒序循环是因为,对于一个节点i,只有等它的子节点都完成了down操作之后,才可以对它进行操作。即保证循环到i时,i+、i+、…..、n都是一个最大\最小堆的根。
4.删除元素(删除堆顶的最值元素)
用堆最末端(二叉树的最右下角)的元素替换堆顶元素,n--,然后对其进行down(i)操作
5.添加元素
将元素添加到堆的最末端(二叉树的最右下角),n++,然后对其进行up(i)操作
6. 删除任意一个点(事先要保证堆中没有重复元素)
首先在堆中找到该元素的位置(可以提前用hashmap记录)
用堆最末端(二叉树的最右下角)的元素替换要删除的元素,n--。然后对其进行up(i)或者down(i)操作,根据元素的大小而定。
注意:一个已从小到大排好序的数组是一个小根堆,但一个小根堆数组里面的元素不一定排好序 (source:算法导论P153 Exercise6.1-5、6.1-6)
题目
https://www.jiuzhang.com/solution/heapify/
基于 Siftup 的版本 O(NlogN)
public class Solution {
/**
* @param A: Given an integer array
* @return: void
*/
private void siftup(int[] A, int k) {
while (k != 0) {
int father = (k - 1) / 2;
if (A[k] > A[father]) {
break;
}
int temp = A[k];
A[k] = A[father];
A[father] = temp; k = father;
}
} public void heapify(int[] A) {
for (int i = 0; i < A.length; i++) {
siftup(A, i);
}
}
}
算法思路:
对于每个元素A[i],比较A[i]和它的父亲结点的大小,如果小于父亲结点,则与父亲结点交换。
交换后再和新的父亲比较,重复上述操作,直至该点的值大于父亲。
时间复杂度分析
对于每个元素都要遍历一遍,这部分是 O(n)。
每处理一个元素时,最多需要向根部方向交换 logn 次。
因此总的时间复杂度是 O(nlogn)
基于 Siftdown 的版本 O(N)
public class Solution {
/**
* @param A: Given an integer array
* @return: void
*/
private void siftdown(int[] A, int k) {
while (k * 2 + 1 < A.length) {
int son = k * 2 + 1; // A[i] 的左儿子下标。
if (k * 2 + 2 < A.length && A[son] > A[k * 2 + 2])
son = k * 2 + 2; // 选择两个儿子中较小的。
if (A[son] >= A[k])
break; int temp = A[son];
A[son] = A[k];
A[k] = temp;
k = son;
}
} public void heapify(int[] A) {
for (int i = (A.length - 1) / 2; i >= 0; i--) {
siftdown(A, i);
}
}
}
算法思路:
初始选择最接近叶子的一个父结点,与其两个儿子中较小的一个比较,若大于儿子,则与儿子交换。
交换后再与新的儿子比较并交换,直至没有儿子。
再选择较浅深度的父亲结点,重复上述步骤。
时间复杂度分析
这个版本的算法,乍一看也是 O(nlogn), 但是我们仔细分析一下,算法从第 n/2 个数开始,倒过来进行 siftdown。也就是说,相当于从 heap 的倒数第二层开始进行 siftdown 操作,倒数第二层的节点大约有 n/4 个, 这 n/4 个数,最多 siftdown 1次就到底了,所以这一层的时间复杂度耗费是 O(n/4),然后倒数第三层差不多 n/8 个点,最多 siftdown 2次就到底了。所以这里的耗费是 O(n/8 * 2), 倒数第4层是 O(n/16 * 3),倒数第5层是 O(n/32 * 4) ... 因此累加所有的时间复杂度耗费为:
T(n) = O(n/4) + O(n/8 * 2) + O(n/16 * 3) ...
然后我们用 2T - T 得到:
2 * T(n) = O(n/2) + O(n/4 * 2) + O(n/8 * 3) + O(n/16 * 4) ...
T(n) = O(n/4) + O(n/8 * 2) + O(n/16 * 3) ...
2 * T(n) - T(n) = O(n/2) +O (n/4) + O(n/8) + ...
= O(n/2 + n/4 + n/8 + ... )
= O(n)
因此得到 T(n) = 2 * T(n) - T(n) = O(n)
堆的应用:
- 堆排序
原理:对输入数据建堆,然后每次输出堆顶元素,然后删除堆顶元素。循环n次即可输出完排序好的n个数。
从小到大排序选小根堆,从大到小排序选大根堆。
#include <iostream>
using namespace std;
int a[];
int n,i,tx; void swap(int *a,int *b)
{
int tmp;
tmp=*a;
*a=*b;
*b=tmp;
} void down(int x)
{
int p,q;
p=x;
while (p*<=n)
{
q=*p;
if ((a[q+]<a[q])&&(q+<=n)) q++;
if (a[q]<a[p])
swap(&a[q],&a[p]);
else
break;
p=q;
}
} int main()
{
cin>>n;
for (i=;i<=n;i++)
cin>>a[i]; for (i=n/;i>=;i--)
down(i); tx=n;
for (i=;i<=tx;i++)
{
cout<<a[]<<" ";
a[]=a[n];
n--;
down();
}
cout<<endl;
}
补充:STL里的堆操作
首先,需要#include <algorithm>
STL里支持4种堆操作:
void make_heap(start_pointer,end_pointer,comp)
在指定范围的数组上建堆
void pop_heap(start_pointer,end_pointer,comp)
删除堆顶元素,然后重建堆
void push_heap(start_pointer,end_pointer,comp)
假设数组区间a[start]……a[end-1]已经是一个堆,然后将a[end]作为要入堆的新元素加进去,使得a[start]……a[end]是一个堆
void sort_heap(start_pointer,end_pointer,comp)
假设数组区间a[start]……a[end-1]已经是一个堆,然后对其中的序列进行排序(排序后就不是一个有效堆了>_<)
注释:start_pointer、end_pointer分别表示起始位置、终止位置的指针
(调用方法示例:make_heap(&number[0],&number[12],cmp); )
cmp为比较函数(若不指定,默认建大根堆)
Sample from MSDN_1996:
Program Output is:(大根堆)
Original array:
Numbers { 4 10 70 10 30 69 96 100 }
After calling make_heap
Numbers { 100 30 96 10 4 69 70 10 }
After calling sort_heap
Numbers { 4 10 10 30 69 70 96 100 }
After calling push_heap and make_heap: (事先加入了一个新元素7)
Numbers { 100 69 96 30 4 70 10 10 7 }
After calling pop_heap
Numbers { 96 69 70 30 4 7 10 10 100 } (此时100已不再属于堆)
#include <iostream>
#include <algorithm>
using namespace std; bool comp(int a,int b) //用于建小根堆的比较函数
{
return a>b;
} int main()
{
int i;
int a[]={,,,,,,,,};
//a[1..8]
for (i=;i<=;i++)
cout<<a[i]<<" ";
cout<<endl;
//输出原数组:30 96 10 4 69 70 10 100
make_heap(&a[],&a[]); //注意:必须多留一个空,我也不知道为什么… >_<
for (i=;i<=;i++) //比如说,对a[1…8]操作就得写成(&a[1],&a[9])
cout<<a[i]<<" "; //我知道为什么啦:^_^
cout<<endl; //在C语言中,数组a[1..8]这一区段的开头是a[1],但结尾是a[9]
//而STL中要求调用的就是数组开头和结尾,所以要多一位。
//比如说有long a[1000],那么数组区段其实是a[0..999]
//(因为C是从0开始计),而数组结尾就是a[1000],尽管a[1000]实际上无意义。
//输出大根堆[1..8]:100 96 70 30 69 10 10 4
make_heap(&a[],&a[],comp);
for (i=;i<=;i++)
cout<<a[i]<<" ";
cout<<endl;
//输出小根堆[1..8]:4 30 10 96 9 10 70 100
a[]=;
push_heap(&a[],&a[],comp);
for (i=;i<=;i++)
cout<<a[i]<<" ";
cout<<endl;
//a[1..9]
//插入一个元素8,然后输出小根堆[1..9]:4 8 10 30 69 10 70 100 96
pop_heap(&a[],&a[],comp);
for (i=;i<=;i++)
cout<<a[i]<<" ";
cout<<endl;
//a[1..8]
//删除堆顶元素4,然后输出小根堆[1..8]:8 30 10 96 69 10 70 100 4
//(4在堆外,已经不是堆里面的元素了)
}
总结:堆相关题目的核心算法
Solution:根据要求建堆,然后取堆顶作为答案输出,然后由堆顶下一步扩展出新元素再次入堆
题目:
POJ2442
采用以下算法都可以实现在m个数中取前n小的数:
快速排序:最慢 手写堆:快 用STL堆:目测还能再快一点
另外注意一个问题:过大的数组也会影响运行速度
渣渣表示这种水题竟然写了三种版本才过….. >_<
Ver1.0:用快排,最好想的算法
本来算法就不快,空间复杂度O(n*n)又太大,也影响速度。
设目前已经处理到了第i列,那么将第i列中的元素与第i+1列中的元素分别相加,得到了n*n个元素。将这些元素记录到一个数组中,快排取前n小的n个数,并记录到数组heap[1..n]中。再用heap数组中的元素与第i+2列操作,……直到处理完m列输出heap数组即可。
这么渣渣的算法当然TLE啦……→_→
Ver2.0:用手动堆
参考http://blog.csdn.net/c0de4fun/article/details/7312831
算法快了不少,空间复杂度也降到了O(n)
不过我写的貌似有问题……WA了
Ver3.0:直接用STL堆操作
STL支持四种堆操作:新元素入堆、弹出堆顶、建堆、排序。(见 堆及其应用.docx)
首先还和刚才一样,把第一行排序后记录到a[1..n]数组中,第二行排序后记录到b[1..n]数组中。
然后,为了节省空间,我们不能再简单的将a[1..n]、b[1..n]分别相加了,而是这样:
因为b[1]一定是b[1..n]里最小的,所以先将a[1..n]+b[1]得到的这n个元素加入一个大根堆heap。然后再a[1..n]+b[2..n]分别相加,而对于这n(n-1)个元素,判断如果a[k]*b[j]小于堆顶元素heap[1],则弹出堆顶,然后将a[k]*b[j]加入堆。处理完这一行之后将heap中的n个元素从小到大记录到数组a中,再将下一行读入b[1..n]中。以此类推,直到m行处理完。
AC!^_^
POJ2051
一次AC! ^_^
题真心不难。
首先将所有的进程读入,记录period[i]为Q_num为i的进程的周期,tm[i]为Q_num为i的进程已经运行过的次数(初始都为1)。
构造一个小根堆记录进程heap[i]=period[j]*tm[j]。一开始将所有的进程都入堆。然后取堆顶元素输出,并将堆顶元素的tm[j]++,之后再将period[j]*tm[j]再次入堆并调整。这样输出k次即可。
注意:如果在堆的调整中需要比较多个关键字(比如本题中Q_num和发生时间都是关键字),那么STL就不能用了,只能手写T^T……方法类比双关键字快速排序即可
---恢复内容结束---
LeetCode Lect7 堆及其应用的更多相关文章
- 【LeetCode】堆 heap(共31题)
链接:https://leetcode.com/tag/heap/ [23] Merge k Sorted Lists [215] Kth Largest Element in an Array (无 ...
- LeetCode:堆专题
堆专题 参考了力扣加加对与堆专题的讲解,刷了些 leetcode 题,在此做一些记录,不然没几天就忘光光了 力扣加加-堆专题(上) 力扣加加-堆专题(下) 总结 优先队列 // 1.java中有优先队 ...
- Leetcode Lect7 哈希表
传统的哈希表 对于长度为n的哈希表,它的存储过程如下: 根据 key 计算出它的哈希值 h=hash(key) 假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中 如果该箱子中已 ...
- leetcode上题目的分类
leetcode链表部分题目 https://zhuanlan.zhihu.com/p/29800285 <[Leetcode][链表]相关题目汇总/分析/总结> leetcode堆部分题 ...
- LeetCode刷题总结-栈、链表、堆和队列篇
本文介绍LeetCode上有关栈.链表.堆和队列相关的算法题的考点,推荐刷题20道.具体考点分类如下图: 一.栈 1.数学问题 题号:85. 最大矩形,难度困难 题号:224. 基本计算器,难度困难 ...
- 【Leetcode 堆、快速选择、Top-K问题 BFPRT】有序矩阵中第K小的元素(378)
题目 给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素. 请注意,它是排序后的第k小元素,而不是第k个元素. 示例: matrix = [ [ 1, 5, 9], [ ...
- [LeetCode]347. 前 K 个高频元素(堆)
题目 给定一个非空的整数数组,返回其中出现频率前 k 高的元素. 示例 1: 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2] 示例 2: 输入: nums = [1 ...
- [LeetCode]215. 数组中的第K个最大元素(堆)
题目 在未排序的数组中找到第 k 个最大的元素.请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素. 示例 1: 输入: [3,2,1,5,6,4] 和 k = 2 输出 ...
- Leetcode 155 Min Stack 小顶堆+栈,优先队列实现 难度:0
https://leetcode.com/problems/min-stack/ #include <vector> #include <queue> #include < ...
随机推荐
- QLCDNumber
继承于 QFrame 展示LCD样式的数字,它可以显示几乎任何大小的数字,它可以显示十进制,十六进制,八进制或二进制数 能够展示的字符: 0/O, 1, 2, 3, 4, 5/S, 6, 7, 8, ...
- 【bzoj4551】【NOIP2016模拟7.11】树
题目 在2016年,佳媛姐姐刚刚学习了树,非常开心.现在他想解决这样一个问题:给定一颗有根树(根为1),有以下 两种操作:1. 标记操作:对某个结点打上标记(在最开始,只有结点1有标记,其他结点均无标 ...
- rk3328设备树学习
一.用到的rk3328好像使用了设备树 设备树我知道的有三种文件类型,dtbs是通过指令make dtbs编译的二进制文件,供内核使用. 基于同样的软件分层设计的思想,由于一个SoC可能对应多个mac ...
- Bugku web 计算器
计算器 打开网页,想输入正确的计算结果发现只输进去一位数??? 遇事不决先F12看一眼源码,发现flag
- 解决:父类中的@NotNull无效以及@Notnull 验证list对象无效
解决方法如图: controller层 vo.param层 父类验证注解要使用@NotEmpty 不能使用 @NotNull,否则验证无效的,反正笔者是没有成功过
- 移动端续讲及zepto移动端插件外加touch插件介绍
媒体查询:针对不同设备,显示不同的样式. 设备像素比:dpr device-piexl-ratio 在he开发中,要一个3陪高清图片: 1080>=320*3 (主要是为了解决图片的失真问题) ...
- centos 6.x 安装配置 node.js 环境
下载 可以在本地下载node.js最新版,然后通过ftp工具上传到服务器,或者直接在服务器终端使用wget命令下载(我当时下载的是node-v6.11.3-linux-x64版本,其他版本请查看上面链 ...
- 设置bios加快系统的启动
设置bios加快系统的启动 bois的设置错误, 还会引起系统的工作不正常 bios设置正确合理, 关系到系统的高效运行: 一般, 应该将bios中, 系统的各个cache 缓存开启,如cpu缓存, ...
- Node - 模块加载与 lerna 提升
从node_modules 加载模块的过程 如果要加载的模块非核心模块,并且路径不是'/'. '../'和'./'开头,这个模块就会从当前文件夹递归向上在node_modules文件夹中寻找这个模块. ...
- 如何把一些字符串用dict组织成json格式?(小算法)
说明: 1. 数据库中的一条记录取出来是这样的(直接复制):'value1','value2' ,'value3' 2. 我希望使用的数据格式是:{key1:'value1',key2:'value2 ...