热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:面试必知必会|理解堆和堆排序

本文将阐述堆和堆排序的基本原理,通过本文将了解到以下内容:堆数据结构的定义堆的数组表示

本文将阐述堆和堆排序的基本原理,通过本文将了解到以下内容:


  1. 堆数据结构的定义

  2. 堆的数组表示

  3. 堆的调整函数

  4. 堆排序实践

1.堆的简介

堆是计算机科学中的一种特别的树状数据结构。
若是满足以下特性,即可称为堆:给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于C的值。若母节点的值恒小于等于子节点的值,此堆称为最小堆;反之称为最大堆。
堆始于J. W. J. Williams在1964年发表的堆排序,当时他提出了二叉堆树作为此算法的数据结构,堆在戴克斯特拉算法和带优先级队列中亦为重要的关键。

数据结构中的堆区别于内存分配的堆,我们说的用于排序的堆是一种表示元素集合的结构,堆是一种二叉树。

堆有两个决定性特性:元素顺序和树的形状


  • 元素顺序
    在堆中任何结点与其子结点的大小都遵守数值大小关系。
    A. 如果结点大于等于其所有子结点,也就是堆的根是所有元素中最大的,这种堆称为大根堆(大顶堆、最大堆);
    B. 如果结点小于等于其所有子结点,也就是堆的根是所有元素中最小的,这种堆称为小根堆(小顶堆、最小堆);
    C. 大根堆/小根堆只是约定了父结点和子结点的大小关系,但是并不约束子结点的相对大小和顺序;
    如图为小根堆结构:

技术图片


  • 树的形状

堆这种二叉树最多在两层具有叶子结点,并且最底层的叶子结点靠左分布,该树种不存在空闲位置,也就是堆是个完全二叉树。上述的两种性质可以保证快捷找到最值,并且在插入和删除新元素时可以实现重新组织再次满足堆的性质。

2.堆的数组表示

堆中没有空闲位置并且数组是连续的,但是数组的下标是从0开始,为了统一,我们统一从1开始,也就是root结点的数组index=1,那么可以通过数组的index可以通过父结点找到左右子结点,也可以通过子结点找到父结点。数组的元素遍历就是堆的层次遍历的结果,因此数组存储的堆具备以下性质:

//数组下标范围
i<=n && i>=1
//根结点下标为1
root_index = 1
//层次遍历第i个结点的值等于数组第i个元素
value(i) = array[i]
//堆中第i个元素的左孩子下标i*2
left_child_index(i) = i*2
//堆中第i个元素的右孩子下标i*2+1
right_child_index(i) = i*2+1
//堆中第i个元素的父结点下标i/2
parent(i) = i/2

堆和数组的对应关系如图:

技术图片

3.堆的调整函数

堆调整的过程非常像数学归纳法的递推过程,看一下就知道。

敲黑板!以下两个函数对于掌握堆非常重要。


  • siftup函数的原理

以小根堆为例,之前a[1...n-1]满足堆的特性,在数组a[n]插入新元素之后,就产生了两种情况:

A. 如果a[n]大于父结点那么a[1...n]仍然满足堆的特性,不需要调整;

B. 如果a[n]比它的父结点要小无法保证堆的特性,就需要进行调整;

循环过程自底向上的调整过程就是新加入元素不断向上比较置换的过程,直到新结点的值大于其父结点,或者新结点成为根结点为止。

停止条件:siftup是一个不断向上循环比较置换的过程,理解循环的关键是循环停止的条件,从伪码中可以清晰地看到,siftup的伪码:

//siftup运行的前置条件
heap(1,n-1) == True
void siftup(n)
i
= n
loop:
// 循环停止条件一
// 已经是根结点
if i == 1:
break;
p
= i/2
// 循环停止条件二
// 调整结点大于等于在此位置的父结点
if a[p] <= a[i]
break;
swap(a[p],a[i])
// 继续向上循环
i = p

siftup调整过程演示

在尾部插入元素16的调整过程如图:

 

技术图片


  • siftdn函数的原理

以小根堆为例,之前a[1...n]满足堆的特性,在数组a[1]更新元素之后,就产生了两种情况:

A. 如果a[1]小于等于子结点仍然满足堆的特性,不需要调整;

B. 如果a[1]大于子结点无法保证堆的特性,就需要进行调整;

循环过程自顶向下的调整过程就是新加入元素不断向下比较置换的过程,直到新结点的值小于等于其子结点,或者新结点成为叶结点为止。

停止条件:siftdn是一个不断向下循环比较置换的过程,理解循环的关键是循环停止的条件,从伪码中可以清晰地看到siftdn的伪码:

heap(2,n) == True
void siftdn(n)
i
= 1
loop:
// 获取理论上的左孩子下标
c = 2*i
// 如果左孩子下标已经越界
// 说明当前已经是叶子结点
if c > n:
break;
//如果存在右孩子
// 则获取左右孩子中更小的一个
// 和父结点比较
if c+1 <= n:
if a[c] > a[c+1]
c
++
// 父结点小于等于左右孩子结点则停止
if a[i] <= a[c]
break;
// 父结点比左右孩子结点大
// 则与其中较小的孩子结点交换
// 也就是让原来的孩子结点成为父结点
swap(a[i],a[c])
// 继续向下循环
i = c

siftdn调整过程演示

在头部元素更新为21的调整过程如图:

技术图片

 

4.堆排序

堆排序的场景:

假如有200w数据,要找最大的前10个数,那么就需要先建立大小为10个元素的小顶堆,然后再逐渐把其他所有元素依次渗透进来比较或入堆淘汰老数据或跳过,直至所有数据渗透完成,最后小根堆的10个元素就是最大的10个数了。

最大TopN使用小根堆的原因:选择最大的TopN个数据使用小根堆,因为堆顶就是最小的数据,每次进来的新数据只需要和堆顶比较即可,如果小于堆顶则跳过,如果大于堆顶则替换掉堆顶进行siftdn调整,来找到新进元素的正确位置,以及产生新的堆顶。

建堆过程:可以自顶向下自底向上均可,以下采用自底向上思路分析。可以将数组的叶子节点,是单个结点满足二叉堆的定义,于是从底层叶子结点的父结点从左到右,逐个向上构建二叉堆,直到第一个节点时整个数组就是一个二叉堆,这个过程是siftup和siftdn的混合,宏观上来看是自底向上,微观上每个父结点是自顶向下。

渗透排序过程:完成堆化之后,开处理N之后的元素,从N+1~200w,遇到比当前堆顶大的则与堆顶元素交换,进入堆触发siftdn调整,直至生产新的小根堆。

实例代码(验证AC):

题目:leetCode 第215题 数组中的第K个最大元素,这道题可以用堆排序来完成,建立小根堆取堆顶元素即可。

//leetcode 215th the Kth Num
//Source Code:C++
class Solution {
public:
//调整以当前节点为根的子树为小顶堆
int heapadjust(vector<int> &nums,int curindex,int len){
int curvalue = nums[curindex];
int child = curindex*2+1;
while(child<len){
//左右孩子中较小的那个
if(child+1 nums[child+1]){
child
++;
}
//当前父节点比左右孩子其中一个大
if(curvalue > nums[child]){
nums[curindex]
=nums[child];
curindex
= child;
child
= curindex*2+1;
}
else{
break;
}
}
nums[curindex]
=curvalue;
return 0;
}
int findKthLargest(vector<int>& nums, int k) {
//边界条件
if(nums.size()<k)
return -1;
//建立元素只有K个的小顶堆
//截取数组的前k个元素
vector<int> subnums(nums.begin(),nums.begin()+k);
int len = nums.size();
int sublen = subnums.size();
//将数组的前k个元素建立小顶堆
for(int i=sublen/2-1;i>=0;i--){
heapadjust(subnums,i,sublen);
}
//建立好小顶堆之后 开始逐渐吸收剩余的数组元素
//动态与堆顶元素比较 替换
for(int j=k;j){
if(nums[j]<=subnums[0])
continue;
subnums[
0] = nums[j];
heapadjust(subnums,
0,sublen);
}
return subnums[0];
}
};

5.总结

网上有很多堆排序过程的图解,本文因此并没有过多重复这个过程,从实践来看,重点是初始化堆和调整堆两个过程,然而这两个过程都离不开siftup和siftdn两个函数,因此掌握这两个函数,基本上就掌握了堆。

由于堆是二叉树,因此在实际使用中需要结合树的遍历和循环来实现堆调整。掌握堆调整过程和二叉树遍历过程,拿下堆,指日可待。

6.参考资料


  1. 《编程珠玑》 第14章 堆


推荐阅读
  • 生产环境下JVM调优参数的设置实例
     正文前先来一波福利推荐: 福利一:百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。福利二 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • JVM:33 如何查看JVM的Full GC日志
    1.示例代码packagecom.webcode;publicclassDemo4{publicstaticvoidmain(String[]args){byte[]arr ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • PriorityQueue源码分析
     publicbooleanhasNext(){returncursor&amp;amp;lt;size||(forgetMeNot!null&amp;amp;am ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了如何通过维持两个堆来获取一个数据流中的中位数。通过使用最大堆和最小堆,分别保存数据流中较小的一半和较大的一半数值,可以保证两个堆的大小差距为1或0。如果数据流中的数量为奇数,则中位数为较大堆的最大值;如果数量为偶数,则中位数为较大堆的最大值和较小堆的最小值的平均值。可以使用优先队列来实现堆的功能。本文还提供了相应的Java代码实现。 ... [详细]
  • Python中程序员的面试题有哪些
    小编给大家分享一下Python中程序员的面试题有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有 ... [详细]
author-avatar
手机用户2502914373
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有