数据结构之探索“堆”的奥秘

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版)

目录

堆的概念 

堆的创建 

时间复杂度分析:

堆的插入与删除

优先级队列

PriorityQueue的特性

PriorityQueue源码分析 

PriorityQueue常用接口介绍

构造方法:

堆的应用 


堆的概念 

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储(从上到下、从左到右)在 一个一维数组 中,并满足:Ki <= K(2i+1) 且 Ki<=K(2i+2) (Ki >= K(2i+1) 且 Ki >= K(2i+2) ) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

注意:Ki <= K(2i+1) 且 Ki<=K(2i+2) 这个公式就是说明根结点的值小于等于左右孩子节点的值,即小根堆或者最小堆。与其相反就是根结点的值大于等于左右孩子节点,即大根堆或者最大堆。

堆的性质:

1、堆中某个节点的值总是不大于或不小于其父节点的值;

例如:小根堆就是根结点的值小于等于孩子节点的值,也就是说孩子节点的值大于等于根结点的值,也就对应了孩子节点不小于其父节点;反之,就是大根堆的性质了。

2、堆总是一棵完全二叉树。

因为堆是把数据按照完全二叉树的方式存储在一个一维数组中的。

3、堆的根结点总是这个一维数组中的最值,要么是最大值,要么是最小值。

如果是大根堆,按照 性质1 的推论就是:根结点的值大于等于孩子节点的值。这样一直递归下去,根结点肯定就是最大的。最坏情况就是所有结点的值全部相等。

4、堆的存储结构是一个一维数组,但是其逻辑结构是一个完全二叉树。

为什么不能是一个普通的二叉树呢?因为普通的二叉树会有空节点(空树),这样在数组中就会null元素的存在,导致了空间利用率比较低。

堆的创建 

现有一组数据 {0,1,2,3,4,5,6,7,8,9} 我们要把这组数据组织成大根堆。

public class Heap {int[] elem;int usedSize;public Heap(int k) {elem = new int[k];}public Heap() {elem = new int[10];}// 给堆初始化数据public void initHeap(int[] array) {for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}
}

思路:大根堆的特点是根结点的值大于左右孩子节点的值。这里采用的是一种向下调整的方法。

即从最后一棵树的根结点位置开始进行调整大根堆,一直调整到整棵树的根结点满足大根堆。

// 创建大根堆
public void createHeap() {// 从最后一棵子树的根结点位置开始for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {// 向下调整的方法:从要调整的位置开始,到整棵树结束siftDown(parent, usedSize);}
}private void siftDown(int parent, int usedSize) {int child = parent * 2 + 1;// 只有当孩子节点在有效数据之内时,才能调整while (child < usedSize) {// 先找到左右孩子节点的最大值if (child+1 < usedSize && elem[child] < elem[child+1]) { // 得确保右孩子存在child++;}// 比较孩子节点的最大值和根结点的值if (elem[parent] < elem[child]) {// 交换swap(elem, parent, child);// 交换完成只是本级满足了大根堆的条件,但是交换下去的值不一定满足当级的大根堆条件parent = child;child = parent * 2 + 1;} else {// 满足大根堆就不需要继续调整了break;}}
}private void swap(int[] elem, int i, int j) {int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;
}

 这里可能有几个小伙伴们疑惑的地方:

1、为什么交换完成之后还要再进行向下调整判断是否需要交换?

总而言之就是一句话:参与调整的,就得再次进行判断是否符合大根堆。

2、为什么本级满足大根堆的情况后,就不需要继续往下判断是否调整?

因为我们是从下面开始调整的,如果本级满足了大根堆,那么下面的就一定也满足大根堆。因此就无需继续判断了。

时间复杂度分析:

将上面的所有结果相加,就是最终的时间复杂度。

因此向下调整建堆的时间复杂度是:O(N)。

堆的插入与删除

堆的插入:

思路:因为堆在存储上是一个数组,那么我们肯定是按照插入数组元素的方法来进行插入,即尾插。尾插完之后,还得进行判断这个新的堆是否是大根堆。因为这个的判断方式是从插入的节点开始往上判断,因此这个判断是向上调整。

    public void offer(int val) {// 插入的元素放到最后,然后其所在的树进行向上调整// 判满,扩容if (isFull()) {elem = Arrays.copyOf(elem, elem.length*2);}elem[usedSize++] = val;siftUp(usedSize-1, 0);}private boolean isFull() {return usedSize == elem.length;}private void siftUp(int child, int end) {// 因为原来是满足大根堆的,因此我们只需要判断这个新插入的元素是否也满足int parent = (child-1) / 2;while (parent >= end) {if (elem[child] > elem[parent]) {// 交换swap(elem, child, parent);child = parent;parent = (child-1) / 2;} else {// 因为原来是满足大根堆的,如果这个也满足,那么就全部满足了break;}}}

有了插入方法,我们也就可以通过插入来创建堆了。

注意:我们手动创建堆的方法是采用向下调整,而插入元素采用的是向上调整。因此,两者创建出来的堆结果会不一样,但都是大根堆。

向上调整建堆的时间复杂度分析:

与向下调整相比,向上调整还要把最后一层的节点全部调整,因此,向上调整的时间复杂度肯定是大于向下调整的。

向上调整建堆的时间复杂度O(N+logN) 。

 堆的删除:

思路:堆的删除,我们采取的方式也和数组类似,是把堆顶元素与最后一个元素交换,再进行向下调整。

    public int poll() {// 判空,抛异常if (isEmpty()) {throw new HeapIsEmptyException("堆为空异常");}int val = elem[0];swap(elem, 0, usedSize-1);siftDown(0, usedSize-1);usedSize--;return val;}private boolean isEmpty() {return usedSize == 0;}

堆的删除的时间复杂度:O(logN)。

交换完,向下调整就只调整树的高度,也就是logN。

堆的插入的时间复杂度:O(logN)。

插在最后,然后进行向上调整,也是调整树的高度。

获取堆顶元素:

    public int peek() {if (isEmpty()) {throw new HeapIsEmptyException("堆为空异常");}return elem[0];}

看到这里,我们就应该可以猜出堆和队列是有关系的,否则,不会把队列的方法名给堆。堆这种数据结构可以实现优先级队列。 

优先级队列

通过堆的性质3,我们就可以推出一个结论:如果我们每次从堆中删除数据一定删除的是优先级最高的。如果是小根堆,那么就是删除最小值,如果是大根堆,那么删除的就是最大值。即优先级最高的先被删除。这就对应了队列中的一个特殊队列:优先级队列。实际上JavaAPI中优先级队列底层就是通过堆来实现的。

PriorityQueue的特性

1、使用时必须导入PriorityQueue所在的包,即:

import java.util.PriorityQueue;

2、PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常。

因为堆中的元素是需要可以比较大小。否则,无法判别优先级。

3、不能插入null对象,否则会抛出NullPointerException。

因为我们去比较的时候,是通过对象调用专属的比较方法,如果对象为null,就会发生空指针异常。

4、PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素。

5、其内部可以自动扩容,无需我们主动实现。

PriorityQueue源码分析 

PriorityQueue常用接口介绍

构造方法:

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(Collection c)用一个集合来创建优先级队列

使用:

public class Test {public static void main(String[] args) {// 创建一个优先级队列,默认容量11PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();// 创建一个优先级队列,容量是20PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(20);List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);list.add(5);// 创建一个优先级队列(容量根据list的大小来分配)PriorityQueue<Integer> priorityQueue3 = new PriorityQueue<>(list);// 长度System.out.println(priorityQueue3.size());// 小根堆System.out.println(priorityQueue3.poll());}
}

这里的“容量根据list的大小来分配”的意思是:本来的默认容量是11,如果list的长度大于11,那么就会按照2倍或者1.5倍去扩容。

插入/删除/获取优先级最高的元素 

函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度O(log2 N),注意:空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll ()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空返回true

堆的应用 

1、PriorityQueue的实现。

2、堆排序。

不同的顺序,建立不同的堆,但是一定是后面的元素先有序,再是前面的元素有序。

因此我们就可以知道:如果是从小到大排序,那么就要建大根堆;反之,则是建小根堆。

因为 如果是从小到大排序,且后面的元素先有序,那么后面的元素只能是最大的,因此建立大根堆的话,堆顶元素一定是最大的。这时,我们只需把堆顶元素和最后一个元素进行交换,然后再进行向下调整,直至调整到整棵树的根节点。

代码实现:

    public void heapSort() {int j = 0;for (int i = usedSize-1; i > 0; i--) {swap(elem,i,j);siftDown(0, i);}}

3、Top-k问题 

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,且K都比较小。

例如:全球前500强的企业。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

如果是要找前K个最小的元素,将前K个元素建成大根堆,然后再去遍历后N-K个元素,遇到小于堆顶元素的就交换,遍历完成后剩下的堆中元素就是前K个最小的。

练习:面试题 17.14.最小K的个数

题目: 

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例:

输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]

提示:

  • 0 <= len(arr) <= 100000
  • 0 <= k <= min(100000, len(arr))

思路一:直接排序,然后遍历前K个即可。

    public int[] smallestK(int[] arr, int k) {// 调用JavaAPI提供的方法才行,自己实现的方法会超出时间限制Arrays.sort(arr); // 默认是从小到大排序int[] ret = new int[k];for (int i = 0; i < k; i++) {ret[i] = arr[i];}return ret;}

思路二:将N个元素建成小根堆,然后每次取堆顶元素,取K次即可。

    public int[] smallestK(int[] arr, int k) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();for (int i = 0; i < arr.length; i++) {priorityQueue.offer(arr[i]);}// 上面是建成的小根堆int[] ret = new int[k];for (int i = 0; i < k; i++) {ret[i] = priorityQueue.poll();}return ret;}

思路三:取前K个元素建成大根堆,然后再遍历剩下的元素,如果小于堆顶元素,则交换。

class Solution {public int[] smallestK(int[] arr, int k) {int[] ret = new int[k];if (k == 0 || arr == null) {return ret;}PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k, new Incompare());for (int i = 0; i < k; i++) {priorityQueue.offer(arr[i]);}for (int i = k; i < arr.length; i++) {if (priorityQueue.peek() > arr[i]) {priorityQueue.poll();priorityQueue.offer(arr[i]);}}for (int i = 0; i < k; i++) {ret[i] = priorityQueue.poll();}return ret;}
}// 创建新的比较器
class Incompare implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);}
}

好啦!本期 数据结构之探索“堆”的奥秘 的学习之旅就到此结束啦!我们下一期再一起学习吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3270270.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

学习大数据DAY23 Linux基本指令4与ngnix安装以及Shell,python编写环境配置

目录 其他扩展类 echo 输出字符串 date 显示当前日期 (用于日期转字符串) date -d 日期解析&#xff08;用于字符串转日期&#xff09; date 设置日期 linux 网络对时 cal 查看日历 wget 命令 seq 命令 Linux 定时执行计划 特殊符号说明 linux 添加硬盘分区挂载 上…

【QT】QT 系统相关(事件、文件、多线程、网络、音视频)

一、Qt 事件 1、事件介绍 事件是应用程序内部或者外部产生的事情或者动作的统称。在 Qt 中使用一个对象来表示一个事件。所有的 Qt 事件均继承于抽象类 QEvent。事件是由系统或者 Qt 平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘&#xff0c;或者是窗口需要重新绘制…

初阶数据结构完结 图解所有初阶数据结构 顺序表

1数据结构 1.线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使 ⽤的 数据结构&#xff0c;常⻅的线性表&#xff1a;顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构&#xff0c;也就说是连…

Centos7_Minimal安装Cannot find a valid baseurl for repo: base/7/x86_6

问题 运行yum报此问题 就是没网 解决方法 修改网络信息配置文件&#xff0c;打开配置文件&#xff0c;输入命令&#xff1a; vi /etc/sysconfig/network-scripts/ifcfg-网卡名字把ONBOOTno&#xff0c;改为ONBOOTyes 重启网卡 /etc/init.d/network restart 网路通了

SSRF中伪协议学习

SSRF常用的伪协议 file:// 从文件系统中获取文件内容,如file:///etc/passwd dict:// 字典服务协议,访问字典资源,如 dict:///ip:6739/info: ftp:// 可用于网络端口扫描 sftp:// SSH文件传输协议或安全文件传输协议 ldap://轻量级目录访问协议 tftp:// 简单文件传输协议 gopher…

Python | TypeError: ‘float’ object is not subscriptable

Python | TypeError: ‘float’ object is not subscriptable 在Python编程中&#xff0c;遇到“TypeError: ‘float’ object is not subscriptable”这一错误通常意味着你尝试对浮点数&#xff08;float&#xff09;使用了下标访问&#xff08;如数组或列表那样的访问方式&a…

Typecho仿百度响应式主题Xaink源码

新闻类型博客主题&#xff0c;简洁好看&#xff0c;适合资讯类、快讯类、新闻类博客建站&#xff0c;响应式设计&#xff0c;支持明亮和黑暗模式 直接下载 zip 源码->解压后移动到 Typecho 主题目录->改名为xaink->启用。 源码下载&#xff1a;https://download.csdn…

【秋招笔试题】小Q的树

解析&#xff1a;分析易得走过的路中至多存在一个分叉&#xff0c;则维护每个结点接下来的路的最大值与次大值然后相加即可。 #include <iostream> #include <vector> #include <algorithm> using namespace std; #define int long long const int MAXN 1…

09 算术运算符

① 运算符除了用于算数加法以外&#xff0c;还可以用于列表、元组、字符串的连接&#xff0c;但不支持不同类型的对象之间的相加或连接。 print([1, 2, 3] [4, 5, 6]) # 连接两个列表 print((1, 2, 3) (4,)) # 连接两个元组 print(hello 123) # 连接字符串 print(Fa…

c语言第四天笔记

关于 混合操作&#xff0c;不同计算结果推理 第一种编译结果&#xff1a; int i 5; int sum (i) (i) 6 7 13 第二种编译结果&#xff1a; int i 5; int sum (i) (i) 6 7 7 7 前面的7是因为后面i的变化被影响后&#xff0c;重新赋值 14 第一种编译结果&#xff…

【Linux网络】应用层协议:HTTP 与 HTTPS

本篇博客整理了 TCP/IP 分层模型中应用层的 HTTP 协议和 HTTPS协议&#xff0c;旨在让读者更加深入理解网络协议栈的设计和网络编程。 目录 一、协议是什么 1&#xff09;结构化数据的传输 2&#xff09;序列化和反序列化 补&#xff09;网络版计算器 .1- 协议定制 .2- …

OpenAI推出SearchGPT:革新搜索体验的新工具

引言 原文链接 在信息爆炸的时代&#xff0c;搜索引擎已经成为人们日常生活中不可或缺的工具。然而&#xff0c;传统的搜索引擎在理解复杂查询和提供准确答案方面仍有许多不足。为了解决这一问题&#xff0c;OpenAI与20240725推出了SearchGPT原型&#xff0c;将生成式AI与传统…

【Golang 面试基础题】每日 5 题(九)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

【Android】Fragment与Activity间通信知识总结

文章目录 一、Activity向Fragment通信1.1 通过方法1.1.1 构造方法1.1.1 普通public方法 1.2 通过setArguments方法1.3 通过接口 二、Fragment向Activity通信2.1 通过getActivity2.2 通过接口 三、Fragment之间传递数据通过Activity中转 一、Activity向Fragment通信 1.1 通过方…

聊聊基于Alink库的主成分分析(PCA)

概述 主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09;是一种常用的数据降维和特征提取技术&#xff0c;用于将高维数据转换为低维的特征空间。其目标是通过线性变换将原始特征转化为一组新的互相无关的变量&#xff0c;这些新变量称为主成分&…

关于链表、顺序表、栈和队列的一些总结

关于链表、顺序表、栈和堆的一些总结 1.顺序表2.链表2.1 单向链表2.1 带哨兵位双向循环链表 3.栈4.队列 1.顺序表 2.链表 2.1 单向链表 2.1 带哨兵位双向循环链表 3.栈 4.队列

【Matlab】绘图时使用字母控制线型和颜色(内含多图对比示例)

概要 测试了英文字母a-z不同输入下线条的颜色和线型&#xff0c;供参考选择。 语法 plot(x, y, 颜色); 如 plot(x, y, b); 测试 以下测试设置线宽为1.5&#xff0c;代码 x 0: 0.01: 2*pi; y sin(x); plot(x, y, b, LineWidth, 1.5);修改时把 b 改成不同字母即可 ‘a’…

基于关联规则的分类算法(CBA) | 项集、频繁项集、关联规则 | arulesCBA库

基于关联规则的分类算法 目前使用较多且较为简洁的关联规则分类算法是基于关联规则的分类算法&#xff08;Classification Based on Association, CBA&#xff09;&#xff0c;下面将从该算法的相关概念开始介绍。 这部分笔记参考论文&#xff1a;孙菡悦.基于多因素交互效应的…

Linux第五节课(权限02)

1、Linux下的用户分类 root&#xff1a;超级用户普通用户&#xff1a;通过root新建的用户&#xff0c;adduser root不受权限约束&#xff1b;普通用户受权限约束&#xff1b; Linux系统中&#xff0c;所有用户都需要有密码&#xff0c;无论是root还是其他&#xff0c;即便是…

MySQL内如何改变编码格式

查找数据库的编码格式&#xff1a; show variables like character%;具体内容时这些 在创建表时设定编码格式&#xff1a; create database <要创建的数据库的名字> charset utf8; 修改数据库默认编码&#xff1a; set character_set_databaseutf8mb4; character_…