0、

堆(Heap)

堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法。

引入:快速排序平均情况下,时间复杂度为O(nlogn),甚至堆排序比快速排序的时间复杂度还要稳定,但在实际中,快排性能要比堆排序好,为什么???

一、堆的概述

堆——是一种特殊的树

  • 堆是一个完全二叉树(除了最后一层,其他层的节点个数都是满的,最后一层的节点都是靠左排列)
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值(也就是说,堆中每个节点的值都大于等于(或小于等于)其左右子节点的值)

大顶堆:对于每个节点的值都大于等于子树中每个节点的堆。

小顶堆:对于每个节点的值都小于等于子树中每个节点的堆。

注意:对于同一组数据,可以构建多种不同形态的堆(大顶堆可有不同形态,小顶堆同理)

二、堆的操作

ps:以大顶堆为例

1、插入操作

  • 把新插入的元素放在堆的最后;
  • 堆化(heapify):对堆进行调整,使其重新满足堆的特性
    • 两种堆化方法:

      • 思路:顺着节点所在的路径,向上或向下进行对比,然后交换
      • (1)从下往上(如下图所示)
      • (2)从上往下


==》code

public class Heap {private int[] a;     // 数组,从下标1开始存储堆中的数据private int n;       // 堆可以存储的最大数据个数private int count;   // 堆中已经存储的数据个数// 初始化public Heap(int capacity) {a = new int[capacity + 1];n = capacity;count = 0;}public void insert(int data) {if(count >= n)return;  // 堆满了count++;a[count] = data;int i = count;while(i/2 > 0 && a[i] > a[i/2]) {// 自下往上堆化// swap()函数用于交换下标为 i 和 i/2 的两个元素swap(a, i, i/2);i = i/2;}}
}

2、删除堆顶元素

栈顶元素存储的就是堆中数据的最大值或最小值

以大顶堆为例,堆顶元素就是最大元素。当删除堆顶元素之后,则需要把第二大的元素放到堆顶,第二大元素必为于左右子节点中。然后迭代地删除第二大节点,以此类推,直到叶子节点被删除。
==》会出现数组空洞,也就是堆化出来的堆并不满足完全二叉树的特性。

解决方法

  • 将最后一个元素放在堆顶;
  • 然后利用同样的父子节点对比方法:若不满足父子节点大小关系,则交换两个节点并重复该过程,直到父子节点之间满足大小关系为止。==》从上往下的堆化方法

实现代码

public void removeMax() {// 堆中没有数据if (count == 0)       return -1;a[1] = a[count];--count;heapify(a, count, 1);
}private void heapify(int[] a, int n, int i) {// 自上往下堆化while(true) {int maxPos = i;// 寻找最大值的位置if(i*2 <= n && a[i] < a[i/2])maxPos = i * 2;if(i*2+1 <= n && a[maxPos] < a[i*2+1])maxPos = i * 2 + 1;if(maxPos == i)break;swap(a, i, maxPos);i = maxPos;}
}

3、时间复杂度分析

① 一个包含 n 个节点的完全二叉树,树的高度不会超过 log2n;
② 堆化过程是顺着节点所在路径比较交换的
==》堆化时间复杂度与树高成正比,也就是O(logn)
==》插入数据和删除堆顶元素主要逻辑是堆化
==》时间复杂度O(logn)

三、堆的存储

完全二叉树比较适合用数组来存储,非常节省存储空间。

  • 原因:不需要存储左右节点的指针,可单纯地通过数组的下标,来找到节点的左右子节点和父节点。(根节点存储在数组下标为1的位置,数组下标为i的节点的左子结点的下标为2*i,右子节点的下标为2*i+1

四、堆排序的实现

  • 时间复杂度为O(n2):冒泡排序、插入排序、选择排序
  • 时间复杂度为O(nlogn):归并排序、快速排序、线性排序

1、堆排序

堆排序:基于堆这种数据结构实现的排序算法。

堆排序时间复杂度非常稳定的原地排序算法——O(nlogn)

堆排序的过程大致分解为:建堆和排序

(1)建堆

目标:将数组原地建成一个堆。 原地就是不借助另一个数组,就在原数组上进行操作。

思路一:将元素依次插入堆中,数组下标从1开始。该思路从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化。

思路二

  • 与思路一相反,从后往前处理数组,并且每个数据都是从上往下堆化。

  • 思路二代码实现:

private static void buildHeap(int[] a, int n) {for(int i = n/2; i >= 1; --i){heapify(a, n, i);    // 自上往下堆化}
}private void heapify(int[] a, int n, int i){// 自上往下堆化while(true){int maxPos = i;if(i*2 <= n && a[i] < a[i*2])maxPos = i*2;if(i*2+1 <= n && a[maxPos] < a[i*2+1])maxPos = i*2 + 1;if(maxPos == i)break;swap(a, i, maxPos);i = maxPos;}
}
  • 分析:代码仅对下标从 n/2 开始到 1 的数据进行堆化,下标从 n/2+1 到 n 的节点是叶子节点,不需要堆化

  • 时间复杂度

    • 节点堆化的时间复杂度:O(logn)
    • n/2 个节点堆化的总时间复杂度:O(nlogn) ==》不够精确
    • 堆排序的建堆过程的时间复杂度:O(n)
  • 推导过程:
    ① 由于叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始;
    ② 每个节点堆化的过程中,需比较和交换的节点个数,与该节点的高度 k (从节点到叶节点的路径长度)成正比

    ③ 将每个非叶子节点的高度求和,公式如下:


    ④ 由于 h = log2n,代入公式S,可得 S = O(n)
    ⑤ 时间复杂度——O(n)

(2)排序——大顶堆《==》小顶堆

  • 建堆后,数组中的数据已经是按照大顶堆的特性组织的;

    • 将数组中的第一个元素(堆顶)与最后一个元素交换,则最大元素就放在下标为 n 的位置;
    • 类似“删除堆顶元素”的操作,然后通过堆化方法将剩下的 n-1 个元素重新构建为堆;
    • 重复上述过程,直到最后堆只剩一个下标为1的一个元素

  • 代码

// n 表示数据的个数,数组 a 中的数据从下标 1 到 n 的位置。
public static void sort(int[] a, int n) {buildHeap(a, n);int k = n;while(k > 1){swap(a, 1, k);--k;heapify(a, k, 1);}
}
  • 分析

    • 原地排序算法:整个堆排序过程,都只需要极个别临时存储空间。
    • 时间复杂度:建堆O(n)+排序O(nlogn)
      • ==》整体时间复杂度: O(nlogn)
    • 不稳定排序算法:在排序过程中,存在将堆的最后一个节点跟堆顶点互换的操作,可以改变值相同数据的原始相当顺序。

注意:若堆中数据从数组下标0开始存储,则节点下标为 i 时,其左子节点下标为 2i+1,右子节点的下标为2i+2,其父节点的下标为 (i-1)/2

五、在实际开发中,为什么快排要比堆排序性能好?

  • 堆排序数据访问的方式没有快速排序友好。

    • 快排数据——顺序访问
    • 堆排序数据——跳着访问==》对CPU缓存不友好
  • 对于同样的数据,在排序过程中,,堆排序算法的数据交换次数要多于快速排序。
    • 快排数据交换的次数不会比逆序度多;
    • 堆排序的建堆过程会打乱数据原有的相对前后顺序,导致原数据的有序度降低;

六、堆应用

场景:假设现有一个包含10亿个搜索关键词的日志文件,如何快速获取热门榜 Top 10 的搜索关键词?

思路

  • 通过哈希算法求取对应的哈希值,然后对哈希值同 10 取模,得到的结果就是这个搜索关键词应被分到的文件编码。
  • 然后利用散列表和堆,分别求取 Top 10,将10个Top 10放在一起,取Top 10。

1、优先级队列

  • 优先级队列:数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队。
  • 堆可以看作为一个优先级队列——往优先级队列插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出最高的元素,就相当于取出堆顶元素。
  • 应用:赫夫曼编码、图的最短路径、最小生成树算法等等。
  • 实现:JAVA 中 PriorityQueue,C++的priority_queue等。

(1)合并有序小文件

目标:假设存在100个小文件,每个文件大小为100M,每个文件中存储都是有序的字符串。==》合并成一个有序的大文件。

思路1:类似归并排序中的合并函数。分别从100个文件中,各取第一个字符串,放入数组,然后比较,将最小的字符串放入合并后的大文件中,并从数组中删除;然后从最小字符串文件中取下一个字符串,并放入数组,重复上述过程,直到所有的文件中的数据都放入到大文件为止。

思路2:利用优先级队列,也就是利用堆。从小文件中取出字符串放入小顶堆中,那堆顶的元素就是优先级队列队首的元素,即最小的字符串;将该字符串放入大文件中,并将其从堆中删除;然后再从小文件中取出下一个字符串,放入到堆中,重复上述过程,直到可以将100个小文件中的数据依次放入大文件中。
==》删除堆顶数据和往堆中插入数据的时间复杂度:O(logn),n表示堆中的数据个数,这里是100

(2)高性能定时器

目标:有个定时器,定时器中维护很多定时任务,每个任务都设定了一个要触发执行的时间点。定时器每过一个很小的单位时间就要扫描一次任务,看是否有任务达到设定的执行时间,若达到,则执行。

思路:利用优先级队列按照设定的执行时间,将这些任务存储到优先级队列中,队列首部(堆顶)存储的就是最先执行的任务。
==》只需取队首任务的执行时间点,与当前时间相减,就得到一个时间间隔 T。
==》该时间间隔 T 就是从当前时间开始到第一个任务需要被执行的所需时间;从当前时间点到(T-1)这段时间内,定时器不需要做任何事情,当T时间过后,定时器取优先级队列中队首的任务执行。然后再计算新的队首任务的执行时间点与当前时间点的差值。

2、求 Top K

场景:将求 Top K 的问题抽象成两类。

  • 针对静态数据集合——数据集合事先确定,不再改变;
  • 针对动态数据集合——数据集合事先不确定,有数据动态地插入到集合中。

(1)静态数据

在包含n个数据的数组中,查找前 K 大数据
==》维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。若比堆顶元素大,则删除堆顶元素,将这个元素插入到堆中;若比堆顶元素小,则继续遍历数组。
==》 遍历数组的时间复杂度:O(n)
==》一次堆化操作的时间复杂度:O(logK)
==》最坏情况下,n个元素都入堆一次,时间复杂度:O(nlogK)

(2)动态数据

针对动态数据求得 Top K 就是实时 Top K。

一个数据集合中有两个操作:

  • 添加数据:
  • 询问当前的前 K 大数据:
    • 维护一个大小为 K 的小顶堆,当有数据被添加到集合中时,将其与堆顶元素比较。若比堆顶元素大,则删除堆顶元素,将这个元素插入到堆中;若比堆顶元素小,则不做处理。

3、利用堆求中位数

目标:求动态数据集合中的中位数。

中位数

  • 若数据的个数是奇数,把数据从小往大排,第 n/2+1 个数据就是中位数;
  • 若数据的个数是偶数,把数据从小往大排,第 n/2 个数据和第 n/2+1 个数据的单独一个或均值或…就是中位数(此处为第 n/2 个数据);

(1)利用堆高效地实现求中位数

通过维护两个堆:一个大顶堆,一个小顶堆。 大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中数据。

静态数据

如果有 n 个数据,n 是偶数,我们从小到大排序,那前 n/2 个数据存储在大顶堆中,后 n/2 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 n/2+1 个数据,小顶堆就存储 n/2 个数据。

动态数据

若新加入的数据小于等于大顶堆的堆顶元素,则将这个新数据插入到大顶堆;否则若新加入的数据大于等于小顶堆的堆顶元素 ,则将这个新数据插入到小顶堆。

若两个堆中的数据个数不符合前面约定的情况:

  • 若 n 是偶数,则两个堆中数据个数都是 n/2;
  • 若 n 为奇数,则大顶堆有 n/2+1 个数据,小顶堆有 n/2 个数据;
  • 若两个堆数据个数不满足约定,则从一个堆中不停地将堆顶元素移动到另一个堆,通过这样的调整,让两个堆的数据满足上面约定。

    ==》插入数据需要堆化操作,所以时间复杂度:O(logn)
    ==》中位数只需返回大顶堆的堆顶元素,所以时间复杂度:O(1)

(2)利用堆求百分位的数据

目标:快速求接口的 99% 响应时间?
注:99 百分位数的概念可以类比中位数,如果将一组数据从
小到大排列,这个 99 百分位数就是大于前面 99% 数据。

如果有 n 个数据,将数据从小到大排列之后,99 百分位数大
约就是第 n*99% 个数据,同类,80 百分位数大约就是第 n*80% 个数据。

为了保持大顶堆中的数据占 99%,小顶堆中的数据占 1%,
在每次新插入数据之后,我们都要重新计算,这个时候大顶堆和小顶堆中的数据个数,是否还符合 99:1 这个比例。如果不符合,我们就将一个堆中的数据移动到另一个堆,直到满足这个比例。

十四、堆(Heap)相关推荐

  1. JVM上篇:内存与垃圾回收篇十四--垃圾回收器

    JVM上篇:内存与垃圾回收篇十四–垃圾回收器 1. GC分类与新能指标 1.1 垃圾回收器概述 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商.不同版本的JVM来实现. 由于JDK的版本处于高 ...

  2. 第十四课 k8s源码学习和二次开发原理篇-调度器原理

    第十四课 k8s源码学习和二次开发原理篇-调度器原理 tags: k8s 源码学习 categories: 源码学习 二次开发 文章目录 第十四课 k8s源码学习和二次开发原理篇-调度器原理 第一节 ...

  3. Frank Luna DirectX12阅读笔记:绘制进阶(第八章-第十四章)

    目录 第八章 光照 8.1 光和材质的交互 8.2 法向 8.3 光照中其他重要的向量 8.4 Lambert余弦定律 8.5 散射光(diffuse lighting) 8.6 环境光(ambien ...

  4. 一文搞懂栈(stack)、堆(heap)、单片机裸机内存管理malloc

    大家好,我是无际. 有一周没水文了,俗话说夜路走多了难免遇到鬼. 最近就被一个热心网友喷了. 说我的文章没啥营养,所以今天来一篇烧脑的. 哈哈,开个玩笑,不要脸就没人能把我绑架. 主要是最近研发第二代 ...

  5. 山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将...

    山海演武传·黄道·第一卷 雏龙惊蛰 第二十二 ~ 二十四章 真龙之剑·星墟列将 "我是第一次--请你,请你温柔一点--"少女一边娇喘着,一边将稚嫩的红唇紧贴在男子耳边,樱桃小嘴盈溢 ...

  6. [Python从零到壹] 十四.机器学习之分类算法五万字总结全网首发(决策树、KNN、SVM、分类对比实验)

    欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...

  7. 二叉树类图_数据结构(十四)——二叉树

    数据结构(十四)--二叉树 一.二叉树简介 1.二叉树简介 二叉树是由n(n>=0)个结点组成的有序集合,集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的.互不相交的二叉树组成. ...

  8. 第十四章 - 垃圾回收概述

    第十四章 - 垃圾回收概述 文章目录 第十四章 - 垃圾回收概述 1.什么是垃圾 1.1 **大厂面试题** 1.2 什么是垃圾? 2.为什么需要GC 3.早期垃圾回收 4.Java垃圾回收机制 担忧 ...

  9. 数据结构之堆(Heap)及其用途

    本文采用图文码结合的方式介绍堆来实现优先队列 什么是优先队列? 队列是一种先进先出(FIFO)的数据结构.虽然,优先队列中含有队列两个字,但是,他一点也不像队列了.个人感觉,应该叫他优先群.怎么说那, ...

  10. 第十四届蓝桥杯大赛软件赛省赛JavaB组解析

    目录 说在前面 试题 A: 阶乘求和 代码: 题目分析: 试题 B: 幸运数字 代码: 题目分析: 试题 D: 矩形总面积 代码: 题目分析: 试题 G: 买二赠一 代码: 题目分析: 试题 H: 合 ...

最新文章

  1. 在项目中常用到的几个注解@JsonInclude、@JsonFormat、@DateTimeFormat
  2. 计算机组装与维护 授课计划,计算机课程教学计划
  3. HDU 1879 继续畅通工程 最小生成树
  4. linux 多线程并行计算,浅谈.NET下的多线程和并行计算(五)线程池基础上
  5. Intellij IDEA 2017 如何导入 GitHub 中的项目
  6. 正经程序员是怎么完美度过元旦假期的?
  7. 我的Linux生涯之开机自动挂载
  8. 区块链的本质是什么?写给区块链的未来十年
  9. 【渝粤教育】电大中专成本会计作业 题库
  10. log4j使用和配置详解
  11. git本地项目推动到gitlab远端服务器
  12. 英特尔®以太网700系列的动态设备个性化
  13. 图片,PDF转换成文字
  14. 实现计算机系统的资源共享,实现多操作系统计算机的资源共享
  15. 使用R语言进行单(双)因素方差分析
  16. 两个日期区间跨度是否超过一年,开始日期距当前日期是否超过一年——js实现
  17. Unity - 人物对象的 LOD 管理
  18. 高德地图的circle圈
  19. Windows添加route
  20. c++由动态库dll文件生成lib文件的方法

热门文章

  1. rocketmq基本安装与使用(一)
  2. java oom dump_Java OOM 内存溢出分析
  3. 幼儿园计算机知识培训内容,幼儿园教师计算机培训计划
  4. vue 计算属性和data_Vue计算属性原理和使用场景
  5. python随机抽取人名_用Python打造一个CRM系统(五)
  6. ps cs6 磨皮插件_PS后期磨皮插件美颜润肤如此简单,效果比DR3要好
  7. java怎么获取固定的日期,如何获取一个指定时间的java.util.Date对象
  8. 人月神话贯彻执行_上古神话知识梳理,精华帖
  9. 使用网站模板快速建站_建站工具使用教程看了就懂网站建设
  10. 自学计算机二级office用什么书,暑假里想要自学计算机二级office有哪些什么好的建议...