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

news2024/12/24 2:36:15

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

个人主页:我要学编程(ಥ_ಥ)-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) {
        // 创建一个优先级队列,默认容量11
        PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();

        // 创建一个优先级队列,容量是20
        PriorityQueue<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> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}

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

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

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

相关文章

nodejs - express 学习笔记

express 是一个基于 Node.js 平台的极简、灵活的 WEB 应用开发框架&#xff0c;官方网址&#xff1a;https://www.expressjs. com.cn/ 简单来说&#xff0c;express 是一个封装好的工具包&#xff0c;封装了很多功能&#xff0c;便于我们开发 WEB 应用&#xff08;HTTP 服务&am…

TinyALSA 简介

TinyALSA 是一个轻量级的 ALSA&#xff08;Advanced Linux Sound Architecture&#xff0c;高级 Linux 音频架构&#xff09;实现&#xff0c;用于与 Linux 内核中的 ALSA&#xff08;高级 Linux 声音架构&#xff09;进行交互&#xff0c;旨在为嵌入式系统和资源受限的设备提供…

提取视频中的文字如何提取?分享4种简单提取方法

在短视频时代&#xff0c;视频已成为信息传播的重要载体。然而&#xff0c;面对海量的视频资源&#xff0c;如何高效提取其中的文字信息&#xff0c;成为许多人关注的焦点&#xff0c;因为快速提取出视频中的文字可以帮助我们整理、编辑文本信息&#xff0c;下面给大家分享4种简…

Linux 基础开发工具 : Vim编辑器

Vim 是 Linux 和其他类 Unix 系统上广泛使用的文本编辑器之一。它基于更早的 vi 编辑器&#xff0c;但添加了许多增强功能和扩展。Vim 是“Vi IMproved”的缩写&#xff0c;意为“改进的 Vi”&#xff0c;我们常使用Vim编辑器编写c/c代码。 ps&#xff1a;该篇介绍均为最基础介…

【LeetCode:3106. 满足距离约束且字典序最小的字符串 + 贪心】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

谷粒商城实战-58-商品服务-API-三级分类-删除-批量删除小结

文章目录 一&#xff0c;增加一个批量删除的按钮并绑定事件二&#xff0c;全栈工程师三&#xff0c;逆向工程在全栈开发中的应用提升效率的方式&#xff1a;使用案例&#xff1a; 这一节的主要内容是开发批量删除分类的功能。 一&#xff0c;增加一个批量删除的按钮并绑定事件 …

数论第一节:整数的可除性

[TOC ] 1、整除的概念 整除&#xff1a; 设a&#xff0c;b∈Z&#xff0c;若存在整数c∈Z&#xff0c;s.t. a bc&#xff0c;则称b整除a&#xff0c;记为 b ∣ a b|a b∣a否则称b不整除a。 带余除法&#xff1a; 设 a &#xff0c; b ∈ z a&#xff0c;b∈z a&#xff0c;b∈…

Jupyter notebook无法自动关联打开浏览器,怎么办?

最近为了方便进行数据分析和处理&#xff0c;在电脑上安装了Anaconda。 Anaconda 是一个功能强大的数据科学平台&#xff0c;它包含了‌conda、Python以及180多个科学包及其依赖项。 Anaconda 常常用于科学计算、数据分析和人工智能等领域&#xff0c;并且得到了广大开发者的…

【中项】系统集成项目管理工程师-第5章 软件工程-5.5部署交付

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

界面控件Telerik UI for WPF 2024 Q2亮点 - 全新的AIPrompt组件

Telerik UI for WPF拥有超过100个控件来创建美观、高性能的桌面应用程序&#xff0c;同时还能快速构建企业级办公WPF应用程序。UI for WPF支持MVVM、触摸等&#xff0c;创建的应用程序可靠且结构良好&#xff0c;非常容易维护&#xff0c;其直观的API将无缝地集成Visual Studio…

PostgreSQL成为最受欢迎的数据库; TiDB马拉松大赛开启, Serverless和Vector为比赛焦点

重要更新 1. TiDB Hackathon大赛报名开启&#xff0c;总奖金达21万&#xff0c;主题是基于 TiDB Serverless 内置的向量搜索功能&#xff08;Vector Search&#xff09;构建 AI 创新应用&#xff0c;感兴趣的可以报名参加。( [1] ) 2. Stack Overflow 2024 开发者调研结果公布…

Java面试还看传统八股文?快来看看这个场景题合集吧【附PDF】

以下就是这份面试场景文档↓ 这里有什么&#xff1f; ↓↓ 1.针对 2024 年面试行情的变化设计的面试场景题以及回答思路 2. 如何快速通过面试的详细攻略 3. 简历优化技巧 1.知己知彼才能百战百胜&#xff0c;如何做好面试前的准备工作 场景题答案以及更多场景题八股文一线大…

注意力机制(三)- 空间注意力机制

空间注意力的核心思想是让模型能够聚焦于输入数据中最重要的空间区域 关注区域&#xff1a; 空间注意力机制使模型能够识别并关注输入图像或文本中的关键区域。 权重分配&#xff1a; 通过学习一个权重分布&#xff0c;模型可以为输入数据的每个空间位置分配不同的重要性。这些…

刷题了:150. 逆波兰表达式求值 |239. 滑动窗口最大值 |347.前 K 个高频元素

150. 逆波兰表达式求值 题目链接:https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/ 文章讲解:https://programmercarl.com/0150.%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC.html 视频讲解:https://www.bilibili.…

食品管理系统

目录 第1章 系统概述 第2章 可行性研究 2.1 项目背景及意义 2.2 可行性研究 第3章 需求分析 3.1 功能性需求 第4章 总体设计 4.1 系统的功能模块图。 4.2各功能模块简介 4.3主要业务流程 第5章 详细设计 5.1 食品管理系统页面 5.2个人中心页面 5.3 系统管理页面 …

Linux编程:使用python或者shell获取系统信息

0. 概要 在日常的系统管理和性能监控中&#xff0c;获取系统信息是一个非常常见的需求。 本文将介绍如何使用Python和Shell脚本来获取这些系统信息。 1. 使用Python获取系统信息 使用psutil库来获取系统的CPU、内存、磁盘和网络信息。 1.1 安装psutil库 首先&#xff0c;我…

论文阅读——Wirelessly-Powered Wireless Sensor Platform

文章目录 摘要一、整流天线二、仿真结果三、系统测试总结 论文来源&#xff1a;https://ieeexplore.ieee.org/document/4403991 摘要 文章介绍了一种低功耗&#xff08;约10μW&#xff09;的2.45 GHz无线传感器平台&#xff0c;该平台包括一个三轴加速度计、温度计和皮肤电导…

开源大模型的格式转成GGUF,并量化后使用ollama推理

https://github.com/ggerganov/llama.cpphttps://github.com/ggerganov/llama.cpp使用到的工具: llama.cpp ollama 步骤 1、下载llama.cpp,并使用make编译 2、新建conda环境,安装llama.cpp里所需的库(requirements.txt) 3、下载需要量化的模型

15.C语言形式参数和实际参数的介绍及函数总结

形参和实参的介绍及函数总结 1.形式参数和实际参数2.获取两个最大的数3.关于函数的一些总结 1.形式参数和实际参数 实际参数可以是常量、变量、表达式 y get(1);//常量 y get(x);//变量 y get(x1);//表达式形参和实参数值相同&#xff0c;地址不同(传递参数是数值的传递)。 …

全能数据分析工具:Tableau Desktop 2019 for Mac 中文激活版

Tableau Desktop 2019 一款专业的全能数据分析工具&#xff0c;可以让用户将海量数据导入并记性汇总&#xff0c;并且支持多种数据类型&#xff0c;比如像是编程常用的键值对、哈希MAP、JSON类型数据等&#xff0c;因此用户可以将很多常用数据库文件直接导入Tableau Desktop&am…