数据结构与算法之美(十一)堆和堆排序
目录
- 堆(Heap)
- 堆的实现
- 堆的存储
- 堆的操作
- 1. 插入一个元素:自下而上的堆化
- 2. 删除堆顶元素:自上而下的堆化
- 时间复杂度
- 堆排序
- 步骤一:建堆
- 步骤二:排序
- 堆的三种应用
- 1. 优先级队列
- 应用一:合并多个有序小文件
- 应用二:高性能定时器
- 2. Top K
- 类型一:静态数据(数据集合不会变)
- 类型二:动态数据(数据集合会变)
- 3. 中位数、分位数
- 静态数组求中位数
- 动态数据求中位数
- 求分位数
堆(Heap)
堆是一种特殊的树,需要满足两点:
- 是一个完全二叉树(除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列);
- 每一个节点的值都大于等于(或小于等于)其子树中每个节点的值,叫大顶堆(小顶堆)
堆的实现
堆的存储
堆适合用数组来存储。因为堆是完全二叉树,用数组来存储完全二叉树是非常节省内存的。
存堆(或其他完全二叉树)的数组中:
- 下标为0的节点不存储信息
- 下标为i的节点的左子节点就是下标为2i的节点,右子节点就是下标为2i+1的节点
- 下标为i的节点的父节点就是下标为i/2的节点
堆的操作
1. 插入一个元素:自下而上的堆化
- 先把元素放到堆的最后一个元素,
- 然后从下往上堆化(heapify):也即向上(与父节点)对比、交换,直到与父节点满足大小关系(对于大顶堆来说父节点大于它,或对于小顶堆来说父节点小于它)、或无父节点。
// 向大顶堆中插入一个元素:自
void insertHeap(vector<int> & heap, int val, int count)
{// heap是一个大顶堆,用数组存储,目前已经存储了count个元素// val是要插入的元素if(count >= heap.size()) return; //堆已经满了++count; //要存进一个数,计数+1vector[count] = val; // 把要插入的数字放在最后int i = count; // i标记的是val的下标while(i / 2 >0 && vector[i] > vector[i/2]) // 自下而上堆化:如果val比父节点大,就要跟父节点交换,直到小于等于父节点或者没有父节点了{swap(vector[i], vector[i/2]);i = i / 2; // i标记的是val的下标,记得更新}
}
2. 删除堆顶元素:自上而下的堆化
- 先把最后一个元素放到堆顶
- 然后利用父子节点对比的方法,自上而下堆化:与子节点对比、交换,直到与子节点满足大小关系,或无子节点
// 删除堆顶元素:自上而下的堆化
void deleteHeapRoot(vector<int> & heap, int count)
{// heap是一个大顶堆,存了count个元素if(count == 0) return ;int n = heap.size() - 1; // heap一共可以存n个元素heap[1] = heap[count]; //把最后一个元素放到堆顶--count; //删掉了一个元素,计数-1int i = 1; // i标记栈顶元素值(其实是原来最后一个元素)所在的下标while(true){int j = i; // j标记要交换的值,是左子节点和右子节点中的最大值if(i*2 <= n && heap[i] < heap[i*2]) j = i*2;if(i*2+1 <= n && heap[j] < heap[i*2+1]) j = i*2+1;// 看是否要交换if(i == j) break;swap(heap[i], heap[j]);i = j;}}
时间复杂度
节点数为n,树的高度不会超过log2nlog_2nlog2n,堆化是顺着节点所在的路径走的,所以往堆中插入一个元素、删除堆顶元素的时间复杂度不会超过O(logn)O(logn)O(logn)。
堆排序
步骤一:建堆
原地建堆:不借助另一个数组,就在原数组上操作。
- 思路一:从下往上堆化。从前往后处理数组(将下标2向后到n的数据依次插入到堆中,因为下标为1是根节点,不需要向上堆化),插入时都是从下往上堆化。
- 思路二(复杂度更低):从上往下堆化。从后往前处理数组(从下标n/2向前到1的数据依次插入堆中,因为下标叶子节点不需要向下堆化),插入时都是从上往下堆化。
// 建大顶堆的C++实现(思路二)
void buildHead(vector<int> heap)
{if(heap.size() <= 1) return;int n = heap.size() - 1;for(int i = n / 2; i >= 1; --i){heapify(heap, n, i);}
}void heapify(vector<int> & heap, int n, int i)
{// 自上而下堆化: i为最上,下指的不只是左右子节点,而是这棵左子树和右子树,所以要whilewhile(true){int j = i; // j标记最大值所在的下标if(i*2 <=n && heap[i] < heap[i*2]) j = i*2;if(i*2+1 <=n && heap[j] < heap[i*2+1]) j = i*2+1;if(i == j) break;swap(heap[i], heap[j]);i = j;}
}
建堆的精确的时间复杂度是O(n)O(n)O(n),而不是$O(nlogn)O(nlogn)O(nlogn),推导如下:
步骤二:排序
堆排序的步骤:
- 第1步:先建堆
- 第2步:然后把数组中下标为1的元素(也即堆顶元素,最大的元素)与下标为n的元素交换,就把最大元素排好了(有点像删除堆顶元素的方法),将剩下的n-1个元素重新堆化
- 重复第2步,直到最后堆中只剩下下标为1的元素。
堆排序的代码:
void sortHeap(vector<int> heap)
{int n = heap.size() - 1;buildHeap(heap, n);int k = n;while(k > 1){swap(heap[1], heap[k]);--k; //heapify(heap, k, i);}
}
堆排序的分析:
- 空间复杂度:O(1)O(1)O(1),原地排序
- 时间复杂度:建堆$O(n) $ + n次节点堆化O(nlogn)O(nlogn)O(nlogn) = O(nlogn)O(nlogn)O(nlogn)
- 稳定性:不稳定排序,因为存在堆顶和最后一个节点的交换,就可以能改变相同数据的原始相对顺序。
在实际开发中,快排比堆排序性能好:
- 数据访问方式:快排是顺序访问的,堆排序是跳着访问的,所以堆CPU缓存不友好
- 交换次数:快排的交换次数不会比逆序多,堆排序里的第一步建堆回打乱数据原有的相对先后顺序,导致数据有序度降低,所以堆排序比快排的交换次数多
堆的三种应用
1. 优先级队列
优先级队列:出队顺序是优先级最高的最先出队。一个堆就可以看作一个优先级队列,往优先级队列中插入一个元素就相当于往堆中插入一个元素,从优先级队列中出队优先级最高的元素就相当于取出堆顶元素。
应用一:合并多个有序小文件
假设有100个小文件,每个文件的大小是100MB,每个文件中存储的都是有序字符串,失望将这些小文件合并成一个有序的大文件。
类似归并排序中的合并函数,只不过这里不是两个数字之间比较大小,而是100个数字(字符串)之间比较大小挑最小的,如果用数组来存和找就是O(100)的复杂度,如果用小顶堆来存和找就是O(log100)的复杂度,会比数组更高效。
应用二:高性能定时器
假设有一个定时器中维护了很多个定时任务,每个任务设定了一个要触发的时间点及动作。一般做法是定时轮巡这些任务,看是否有任务达到触发时间点,如果到达,就拿出来执行。
但这样每过一定的时间(如1s)就扫描一遍任务列表的做法比较低效:(1)离执行时间可能还比较久,很多次扫描是徒劳的;(2)每次都要扫描整个任务列表,如果任务列表很大,会有很多耗时。
用优先级队列来解决:按照触发时间点存储到优先级队列中,队首就是最先执行的任务,可以得到队首任务与当前时间点的差值T。等到T时就取队首任务执行,再计算新的队首任务与当前时间点的差值。这样就解决了上面的(1)(2)低效的问题。
2. Top K
类型一:静态数据(数据集合不会变)
如何在一个包含n个数据的数组中,查找**前K大(前K小)**数据呢?
维护一个大小为K的小顶堆(大顶堆)(结果数组),顺序遍历数组(输入数组),从数组中取出数据与堆顶元素进行比较。如果比堆顶元素大,就删除堆顶元素,将这个元素代替堆顶元素,自上而下堆化;如果比堆顶元素小,则不做处理,继续遍历数组。这样等数据都遍历完之后,堆中的数据就是前K大数据了。
// 自己写的,不保证正确
// 找前K大的数据,用小顶堆(把比堆顶元素更大的加进去);
// 找前K小的数据,用大顶堆(把比堆顶元素更小的加进去)
vector<int> TopkLarge(vector<int> nums, int k)
{// num是输入数组if(nums.size()== 0) return;// res是结果数组,是num里前k大的数据// 1. 先建一个大小为k的小顶堆vector<int> res;res.push_back(0); // 空一个值for(int i = 0; i < k; ++i) res.push_back(nums[i]);buildHead(res);// 2. 将num里剩下的元素与堆顶元素比较for(int i = k; i < nums.size(); ++i){if(nums[i] > res[1]){res[1] = nums[i];// 堆化heapify(heap, k, 1);}}
}
最坏复杂度:遍历数组O(n)O(n)O(n),一次堆化O(logK)O(logK)O(logK),所以是O(nlogK)O(nlogK)O(nlogK)
类型二:动态数据(数据集合会变)
动态数据求TopK举例:一个数据集合中有2种操作,添加数据、查询当前TopK大的数据。
对于查询TopK大的数据,对于动态数据如果每次当前查询都要重新计算的话,复杂度就是O(nlogK)O(nlogK)O(nlogK)。实际上可以在添加数据时,就将它去跟堆顶元素对比,如果比栈顶元素大,就把栈顶元素删掉,将这个元素代替栈顶元素,进行从上到下的堆化;如果比栈顶元素小,则不做处理,这样查询TopK大的数据时,可以立即返回。
3. 中位数、分位数
求中位数或分位数,用的是1个大顶堆+1个小顶堆,小顶堆里的数据都大于大顶堆中的数据。从数组的角度看,大顶堆的顶和小顶堆的顶就是一个数组里面较小的数和较大的数的分隔数。
静态数组求中位数
如果有n个数据,
- 如果n为偶数,前n/2个数据存储在大顶堆,后n/2个数据存储在小顶堆,这样大顶堆中的堆顶元素就是中位数;
- 如果n为奇数,前n/2+1个数据存储在大顶堆,后n/2个数据存储在小顶堆,同样的大顶堆的堆顶就是中位数。
具体实现方法是:遍历n个数字的数组,如果大顶堆和小顶堆都是空,将当前元素作为大顶堆堆顶;如果大顶堆非空,判断当前元素与大顶堆堆顶元素的关系,如果比大顶堆堆顶元素小,加入大顶堆,判断是否满足0<=大顶堆元素个数-小顶堆元素个数<=1,如果不满足把大顶堆堆顶元素删除,插入小顶堆;如果当前元素比大顶堆堆顶元素大,插入小顶堆,判断是否满足0<=大顶堆元素个数-小顶堆元素个数<=1,如果不满足把小顶堆堆顶元素删除,插入大顶堆。
动态数据求中位数
当新添加一个数据的时候,需要调整两个堆,让大顶堆中的堆顶元素继续是中位数,
- 如果新加入的数据<=大顶堆的堆顶元素,就将这个新数据插入到大顶堆;否则,将这个新数据插入到小顶堆;
- 这个时候可能出现两个堆中的数据个数不符合前面约定的情况(如果n是偶数,两个堆中的数据个数都是n/2;如果n是奇数,大顶堆中有n/2+1个数据,小顶堆中有n/2个数据),这个时候我们可以从一个堆中不停地将堆顶元素插入到另一个堆,来让两个堆中的数据满足约定。
每次插入会涉及几个数据的堆化,所以时间复杂度是O(logn)O(logn)O(logn)。查询时只需要返回堆顶数据,所以时间复杂度是O(1)O(1)O(1)。
例如:有一个包含10亿个搜索关键词的日志文件,如何快速获取Top 10最热门的搜索关键词?当限制为单机内存1GB时:
- 用散列表来记录关键词及其出现的次数,为满足内存限制,可以将关键词哈希分片到10个文件中;
- 用堆求TopK的方法,建立一个大小为10的小顶堆,遍历散列表,依次去除每个搜索词及其对应搜索次数,与堆顶的搜索关键词的次数对比,如果比堆顶的多,就删除堆顶关键词,将这个更多的词加入到堆中;
- 以此类推,当遍历完这个那个散列表中的关键词后,堆中的搜索关键词就是出现次数最多的Top10关键词了。
求分位数
问题:求分位数,例如99%响应时间,即如果有100个接口访问请求,每个接口请求的响应时间不同,把这些响应时间按照从小到大排序,排在第99的数据就是99%响应时间。
方法:维护两个堆,一个大顶堆,一个小顶堆,
- 对于静态数据:假设当前总数据的个数是n,大顶堆中保存n99%个数据,小顶堆中保存n1%个数据,大顶堆堆顶的数据就是99%响应时间。
- 对于动态数据:每次插入一个数据的时候,判断这个数据跟大顶堆和小顶堆堆顶数据的大小关系,如果比大顶堆堆顶数据小,就插入大顶堆;如果比小顶堆的堆顶数据大,就插入小顶堆。插入之后要重新移动两个堆的数据,直到满足99:1的个数比例。
数据结构与算法之美(十一)堆和堆排序相关推荐
- mysql索引用trie树_数据结构与算法之美【完整版】
资源目录: ├─01-开篇词 (1讲) │ ├─00丨开篇词丨从今天起,跨过"数据结构与算法"这道坎.html │ ├─00丨开篇词丨从今天起,跨过"数据结构与算法&qu ...
- 《数据结构与算法之美》目录
数据结构与算法之美_算法实战_算法面试 开篇词 (1讲) <数据结构与算法之美>学习指导手册 开篇词 | 从今天起,跨过"数据结构与算法"这道坎 入门篇 (4讲) 01 ...
- 数据结构与算法之美(一):概论
最近在极客时间上面学习王争老师的课程<数据结构与算法之美>,以前虽然学过一些皮毛,但是不够精,作为程序员的基本内功,还是要继续学习.至此通过总结的方式,把这门课的要点记录下来,供自己思考回 ...
- 极客时间 自我提升第二天 数据结构与算法之美 应该掌握 / 趣谈网络原理 / 深入浅出计算机组成原理 思维导图
菜鸟今天又来完成所说的诺言,也希望大家督促,在今天的学习中,菜鸟有了新的认知,我会将上一篇中理解不完善的一些地方进行补充,学习本就是不断打破自己的认知,如果思考都不做,何来的知识的积累 文章目录 数据 ...
- 王争数据结构与算法之美开篇问题整理
数据结构与算法之美笔记整理 为什么大多数编程语言中数组从 0 而不是从 1 开始编号? 从数组存储的内存模型上来看,"下标"最确切的定义应该是"偏移(offset)&qu ...
- 推荐学习-数据结构与算法之美
推荐一个学习资源:数据结构与算法之美.主要包括以下几个学习内容: 20个经典数据结构与算法 100个真实项目场景案例 文科生都能看懂的算法手绘图解 轻松搞定BAT的面试通关秘籍 作者:王争 前谷歌工程 ...
- 【Java数据结构与算法】第十一章 顺序存储二叉树、线索二叉树和堆
第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 文章目录 第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 一.顺序存储二叉树 1.介绍 2.代码实现 二.线索二叉树 1 ...
- 数据结构与算法之美笔记——基础篇(中):树,二叉树,二叉查找树,平衡二叉查找树,红黑树,递归树,堆
树: A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点.B.C.D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点.我们把没有父节点的节点叫作根节点,也就是图中的节点 E.我们 ...
- 数据结构与算法之美(三)
一,红黑树 平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于 1.最先被发明的平衡二叉查找树是AVL 树,它严格符合我刚讲到的平衡二叉查找树的定义,即任何节点的左右子树高 ...
- 数据结构与算法之美-目录
复杂度分析 数组 栈 队列 链表 递归 排序 二分查找 跳表 散列表 哈希算法 二叉树 红黑树 B+树 堆与堆排序 图的表示 深度广度优先搜索 拓扑排序 最短路径 A*算法 字符串匹配(BF RK) ...
最新文章
- 编辑PDF文档,Word 2013可以是您的选择
- linux驱动内核,Linux内核设备驱动之Linux内核基础笔记整理
- python 进度条程序_Python:显示程序运行进度条
- DOM中的onbeforeunload函数
- 区块链BaaS云服务(9)索尼 区块链通用数据库 BCDB
- LBaaS 实现机制 - 每天5分钟玩转 OpenStack(125)
- 【C#语言规范】从FxCop归纳出来的一些规范建议
- java 继承 冒号_java继承(extends)简单介绍
- 12项目综合变更设置
- Flash已死,有事烧纸!
- xampp 403 禁止访问 问题解决
- 全国企业税收调查数据(2007-2016)共10年数据,均未脱敏。可通过纳税人识别号,识别具体企业名称和地区信息等,可匹配中国工业企业数据库,中国出口海关统计数据、中国企业污染排放数据库、中国海关数据
- fenix3 hr 中文说明书_佳明 Fenix3 HR中、英文菜单对照 V4.0
- VNC远程控制软件,五大容易上手的VNC远程控制软件
- 加载mysql驱动失败_java mysql 驱动加载失败
- 淘宝内乱持续 QQ盛大京东“趁火打劫”
- 正则表达式的贪婪匹配和非贪婪匹配
- matlab显示hsi,matlab实现RGB与HSI的相互转换
- mysql设置可以存表情_Mysql实例使MySQL能够存储emoji表情字符的设置教程
- DEV pivotGridControl 单元格内容变色
热门文章
- 编程模拟飞船加速变轨过程-物理基础篇(5) 摄动方程
- 做SEO和SEM有什么区别?哪个推广效果会更好?
- 用C++编写桌面闹钟提醒程序,这个功能你不用就亏了!
- 过河问题(图论方法)
- java System.out.print();在控制台上修改输出颜色
- Winner 赢家 	(2A - Winner) map
- NSX edge命令行手册
- ue4 怎么修改骨骼动画_【UE4】神器!!!动画师必备!!!基于物理的动画制作软件 Cascadeur 使用指南!...
- Managing Non-Volatile Memory in Database Systems
- 程序在计算机内部是如何运行的