数据结构之堆的详解

news2024/9/20 1:13:59

数据结构之堆

  • 一.堆的概念
    • 1.1 堆的基本概念
    • 1.2 堆的存储方式
  • 二.堆的操作和实现
    • 基本框架
    • 建堆
    • 插入
    • 删除
  • 三.堆的应用
    • 优先队列
    • top-k问题:最小的K个数或者最大k个数
    • 堆排序

一.堆的概念

1.1 堆的基本概念

  • 堆是一种特殊的完全二叉树
    在这里插入图片描述

  • 堆分为小根堆和大根堆,大根堆的根节点值最大,小根堆的根节点值最小
    最小堆
    在这里插入图片描述

大根堆
在这里插入图片描述

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

1.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储.
在这里插入图片描述

二.堆的操作和实现

基本框架

我们采用数组的方式实现一课完全二叉树,下述就是基本描述代码.

public class TestHeap {

    public int[] elem;
    public int usedSize;

    public TestHeap() {
        this.elem = new int[10];
    }

    public void initElem(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }
    }

建堆

当然,在我们构建了完全插树之后,我们要进行一系列的操作,接下来的第一个操作就是建堆操作.
我这里给出一个建堆操作的过程,我们以建立最大堆为例子.
在这里插入图片描述

以下方法的思路如下:
大家可以参考一下.
shiftDown 方法的思路如下:

  1. 首先找到当前节点的左子节点,作为当前比较的子节点。
  2. 如果有右子节点,并且右子节点的值大于左子节点,则将右子节点作为比较的子节点。
  3. 如果比较的子节点的值大于父节点的值,则交换父节点和子节点的值,并继续比较交换后的子节点。
  4. 重复步骤 2-3,直到父节点的值大于子节点的值,则结束下沉。

createHeap 方法的思路:

  1. 从最后一个非叶子节点开始,逐层下沉。
  2. 对每个非叶子节点调用 shiftDown 方法进行下沉操作。
  3. 重复步骤 1-2 直到根节点,则整个数组形成一个大顶堆。
  public void createHeap() {
        for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
            shiftDown(parent,usedSize);
        }
    }

    /**
     * 父亲下标
     * 每
     * 棵树的结束下标
     * @param parent
     * @param len
     */
    private void shiftDown(int parent,int len) {
        int child = 2*parent + 1;
        //最起码 要有左孩子
        while (child < ![len](https://img-blog.csdnimg.cn/e8c15d3b3ab04739bd816ca98f40b28b.bmp)
) {
            //一定是有右孩子的情况下
            if(child+1 < len && elem[child] < elem[child+1]) {
                child++;
            }
            //child下标 一定是左右孩子 最大值的下标
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }


下面是时间复杂度的分析过程.
在这里插入图片描述

插入

数据的插入
向上调整的具体过程
在这里插入图片描述
下面的代码思路如下:
shiftUp 方法的思路如下:

  1. 首先找到当前新增元素的父节点。
  2. 如果当前元素的值大于父节点的值,则交换两者的值。
  3. 交换后,当前元素的父节点变为原先的祖父节点。
  4. 重复步骤 2-3,直到当前元素的值不大于父节点的值,上浮结束。

offer 方法的思路:

  1. 首先判断堆是否已满,如果满了则扩容。
  2. 将新增元素添加到数组尾部。
  3. 调用 shiftUp 方法,对新增元素进行上浮操作。
  4. 重复上浮,直到上浮结束,则新增元素添加完成。
    该实现的时间复杂度是 O(logn),空间复杂度是 O(n)。
  //向上调整建堆的时间复杂度:N*logN
    public void offer(int val) {
        if(isFull()) {
            //扩容
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize++] = val;//11
        //向上调整
        shiftUp(usedSize-1);//10
    }
      private void shiftUp(int child) {
        int parent = (child-1)/2;
        while (child > 0) {
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }
    }

具体的向上调整时间复杂度分析
在这里插入图片描述

删除

删除过程如下:
在这里插入图片描述

具体思路如下:
pop 方法的思路:

  1. 首先判断堆是否为空,如果为空则返回。
  2. 交换根节点和最后一个元素。
  3. 减小 usedSize,表示元素数量减一。
  4. 调用 shiftDown 方法,对根节点进行下沉操作。
  5. 重复下沉,直到下沉结束,则弹出根节点元素完成。
  6. 该实现的时间复杂度是 O(logn),空间复杂度是 O(1)。
public void pop() {
        if(isEmpty()) {
            return;
        }
        swap(elem,0,usedSize-1);
        usedSize--;
        shiftDown(0,usedSize);
    }
       private void shiftDown(int parent,int len) {
        int child = 2*parent + 1;
        //最起码 要有左孩子
        while (child < len) {
            //一定是有右孩子的情况下
            if(child+1 < len && elem[child] < elem[child+1]) {
                child++;
            }
            //child下标 一定是左右孩子 最大值的下标
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

三.堆的应用

优先队列

再开始堆的拓展之前,我们先来看一个概念,就是java里面优先队列api的概念
概念
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列
数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)
PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,这里主要是介绍的是PriorityQueue.
PriorityQueue的构造方法解释
第一种
构造方法没有比较器,直接传入参数.
在这里插入图片描述
第二种
在这里插入图片描述
指定初始化容量
第三种
这一种就是指定比较器,也是我们实现最大堆的关键因素.
在这里插入图片描述

看了上面的构造方法,我们,我们发现这个优先队列是可以指定比较器的,这样我们就可以实现堆的大小,可以定义大根堆,还可以定义小根堆.

具体先来看看,为什么一开始,我们说优先队列构建的是小根堆.
看代码构建.

在这里插入图片描述

接下来我会一一解释这些方法,你尽量代入我们上面实现的思想,你就能一下子明白了.
grow方法的解释
在这里插入图片描述
siftUpComparable和siftUpUsingComparator
在这里插入图片描述

在这里插入图片描述

整个offer的逻辑:

  1. 检查要插入的元素e是否为null,如果是则抛出NullPointerException
  2. 修改modCount,这个字段主要用于并发修改检测
  3. 计算优先队列当前大小i,如果超过容量则调用grow方法扩容
  4. size增加1,表示插入1个新元素
  5. 如果优先队列是空的(i==0),直接将新元素插入根节点
  6. 否则调用siftUp方法,将新元素插入尾部,然后执行向上筛选操作,使新元素上浮到正确位置
  7. 返回true表示插入成功

看了上面的例子之后,我们发现在我们没有指定具体的比较器或者说实现compareto接口的时候,他调用自己的比较器,进行比较,所以默认是小根堆,这里我们可以利用比较器的知识,
我用下面的例子来说明一下,怎么构建大根堆,代码如下:

class Student implements Comparable<Student>{
    public int age;
    public String name;

    public Student(String name,int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
        //return o.age - this.age;//大根堆了
    }
}
public class Test {
    public static void main(String[] args) {
     Queue<Student> priorityQueue2 = new PriorityQueue<>();
        priorityQueue2.offer(new Student("zhangsan",27));
        priorityQueue2.offer(new Student("lisi",15));
        System.out.println(2111);
    }
    }

在这里插入图片描述

当然我们还有另外一种方式,就是传入比较器

class Student{
        public int age;
        public String name;

    public Student(String name,int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }


class AgeComparator implements Comparator<Student> {

    @Override
    public int compare(Student o1, Student o2) {
        return o2.age-o1.age;
    }
}
}
 public static void main(String[] args) {
        AgeComparator ageComparator = new AgeComparator();
        Queue<Student> priorityQueue2 = new PriorityQueue<>(ageComparator);
        priorityQueue2.offer(new Student("zhangsan",27));
        priorityQueue2.offer(new Student("lisi",15));

具体图示如下:
在这里插入图片描述

使用的注意事项
1.使用时必须导入PriorityQueue所在的包,即:
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. 插入和删除元素的时间复杂度为
6. PriorityQueue底层使用了堆数据结构
7. PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素

top-k问题:最小的K个数或者最大k个数

我这里以最大的k个元素为例子

第一种思路:
通过维持一个大小为N的最小堆,并从中弹出K个元素,从而找到数组中最大的K个元素。
代码如下:

 public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr == null || k == 0) {
            return ret;
        }
        //O(N*logN)
        Queue<Integer> minHeap = new PriorityQueue<>(arr.length);
        for (int x: arr) {
            minHeap.offer(x);
        }
        //K * LOGN
        for (int i = 0; i < k; i++) {
            ret[i] = minHeap.poll();
        }
        return ret;
    }

第二种思路:

  1. 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆 ,前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素\

具体过程
在这里插入图片描述

代码如下:

 /**
     * 前K个最大的元素
     * N*logK
     */
    public static int[] maxK(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr == null || k == 0) {
            return ret;
        }
        Queue<Integer> minHeap = new PriorityQueue<>(k);
        //1、遍历数组的前K个 放到堆当中 K * logK
        for (int i = 0; i < k; i++) {
            minHeap.offer(arr[i]);
        }
        //2、遍历剩下的K-1个,每次和堆顶元素进行比较
        // 堆顶元素 小的时候,就出堆。  (N-K) * Logk
        for (int i = k; i < arr.length; i++) {
            int val = minHeap.peek();
            if(val < arr[i]) {
                minHeap.poll();
                minHeap.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = minHeap.poll();
        }
        return ret;
    }

堆排序

具体步骤
分为两步:

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
 private void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    /**
     * 时间复杂度:n*logn
     * 空间复杂度:O(1)
     */
    public void heapSort() {
        int end = usedSize-1;//9
        while (end > 0) {
            swap(elem,0,end);
            shiftDown(0,end);
            end--;
        }
    }
  1. 首先调用shiftDown方法构建一个最大堆或最小堆。这个步骤时间复杂度为O(n)。
  2. 初始化end为最后一个元素的索引。
  3. 然后从end到0进行循环,在每次循环中:
  • 交换堆顶元素(索引0)和end位置的元素
  • 调用shiftDown方法对索引0的位置进行堆化(向下调整),使得索引0的位置上有最大值或最小值
  • end减1,用于下一轮循环
  1. 循环结束后,排序完成。

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

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

相关文章

Notes/Domino 14 Drop1

大家好&#xff0c;才是真的好。 2023年5月31号&#xff0c;Notes/Domino 14 Drop1如约而至。在晚上照理检查了一下Notes相关博客时&#xff0c;就发现该版本现在可以下载。一诺千金&#xff0c;信若尾生&#xff0c;这是我对14版本的第一个评价。 很多人关心Notes/Domino 14…

【redis-初级】redis安装

文章目录 1.非关系型数据库&#xff08;NoSQL&#xff09;2.在Linux上安装redis2.1 安装前准备2.2 安装2.3 启动2.4 关闭 3. redis客户端3.1 命令客户端3.2redis远程客户端3.3 redis编程客户端 1.非关系型数据库&#xff08;NoSQL&#xff09; 2.在Linux上安装redis 2.1 安装前…

提升网络安全的关键利器:EventLog Analyzer

导语&#xff1a; 随着网络攻击和数据泄露事件的不断增加&#xff0c;企业对于网络安全的关注度也日益提高。在这样的背景下&#xff0c;安全信息与事件管理系统&#xff08;SIEM&#xff09;成为了提升网络安全的关键利器之一。本文将重点介绍一款强大的SIEM工具——EventLog…

Spark大数据处理学习笔记1.4 掌握Scala运算符

文章目录 一、学习目标二、运算符等价于方法&#xff08;一&#xff09;运算符即方法&#xff08;二&#xff09;方法即运算符1、单参方法2、多参方法3、无参方法 三、Scala运算符&#xff08;一&#xff09;运算符分类表&#xff08;二&#xff09;Scala与Java运算符比较 四、…

mac docker 安装 ES

一. docker 安装 ES 1. 下载镜像 docker pull elastcisearch:8.7.1 2. 启动镜像 docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" -e ES_JAVA_OPTS"-Xms256m -Xmx256m" elasticsearch:8.7.1 参数说明…

AntDB 存储技术——Hash分片技术

单台机器很难处理海量的数据或者很高的并发查询&#xff0c;需要把数据拆分到多个节点上&#xff0c;在多个节点上进行存储和处理&#xff0c;这种技术叫作数据分区&#xff0c;也称为数据分片。数据分片的主要目的是提高可扩展性&#xff0c;使数据分散到多个节点上&#xff0…

【Java】冒泡排序

文章目录 一、什么是冒泡排序定义冒泡思想代码实现 二、冒泡排序的优化第一次优化第二次优化 三、鸡尾酒排序 一、什么是冒泡排序 定义 冒泡排序(bubble sort)是最基础的排序算法&#xff0c;它是一种基础的交换排序。它的原理就像汽水一样&#xff0c;汽水中常常有许多小气泡…

Vue第八篇Vue3

一 Vue3的变化 1.性能的提升 打包大小减少41% 初次渲染快55%, 更新渲染快133% 内存减少54% 2.源码的升级 使用Proxy代替defineProperty实现响应式 重写虚拟DOM的实现和Tree-Shaking 3.拥抱TypeScript Vue3可以更好的支持TypeScript 4.新的特性 Composition API&#…

Leetcode---349周赛

题目列表 2733. 既不是最小值也不是最大值 2734. 执行子串操作后的字典序最小字符串 2735. 收集巧克力 2736. 最大和查询&#xff08;这题难度较大&#xff0c;等以后有时间再和大家分享&#xff09; 一、2733、既不是最小值也不是最大值 关键是看到题目中说数组中的元素不…

一文快速了解软件技术基础

前言 数据结构和算法是计算机科学的基石&#xff0c;它们为我们提供了处理和组织数据的方法和工具。通过学习数据结构&#xff0c;您将能够理解如何存储和操作不同类型的数据&#xff0c;如何优化内存使用和访问效率&#xff0c;以及如何设计高效的算法来解决各种计算问题。掌…

iOS -- isa指针

isa指针&#xff1a;isa指针是一个指向对象所属类或元类的指针。它决定了对象可以调用的方法和属性。isa指针在对象的结构中存在&#xff0c;并且在运行时会被自动设置。isa 指针&#xff0c;表示这个对象是一个什么类。而 Class 类型&#xff0c; 也就是 struct objc_class * …

XML 的命名空间及其在OPCUA中的应用

为了防止不同XML 使用的名称冲突&#xff0c;XML 规范中规定的命名空间&#xff0c;平时编写单一命名空间的XML 文档&#xff0c;没有过多地注意XML 命名空间的细节。当开发OPCUA 信息模型时&#xff0c;被命名空间搞得云里雾里。为此&#xff0c;做了一个笔记。 URI(Uniform R…

Vue3_ElementPlus_简单增删改查(2023)

Vue3&#xff0c;Element Plus简单增删改查 代码&#xff1a;https://github.com/xiaoming12318/Vue3_ElementPlus_CRUD.git 环境&#xff1a; Visual Studio Code Node.js 16.0或更高版本&#xff0c;https://nodejs.org/en axios 快速上手&#xff1a; 如果已经有16.0及…

Vue单文件组件

单文件组件 单文件组件是在开发中用的比较多的&#xff0c;它的后缀都是.vue结尾的 既然是.vue结尾&#xff0c;那么直接给浏览器是不能运行的&#xff0c;.vue文件是vue团队打造的特殊文件&#xff0c;想让.vue文件让浏览器识别并且运行&#xff0c;需要对它进行处理加工成纯…

5款大厂设计师都在用的网页设计工具

本文收集了5款大厂设计师使用的几种流行易用的网页设计工具&#xff0c;其中一个可以AI自动生成设计稿&#xff0c;非常的方便&#xff0c;相信一定有一个适合你的网页设计工具。 1.即时设计 即时设计是一款免费的在线 UI 设计工具&#xff0c;无系统限制&#xff0c;浏览器打…

三个原则判定电源环路稳定性

1 环路稳定性评价指标 衡量开关电源稳定性的指标是相位裕度和增益裕度。同时穿越频率&#xff0c;也应作为一个参考指标。 (1) 相位裕度是指&#xff1a;增益降到0dB时所对应的相位。 (2) 增益裕度是指&#xff1a;相位为0deg时所对应的增益大小(实际是衰减)。 (3) 穿越频率…

DVWA下载、安装及使用教程,网络安全小白必看!

DVWA是一款基于PHP和mysql开发的web靶场练习平台&#xff0c;集成了常见的Web漏洞。旨在为安全人员测试自己的专业技能和工具提供合法的环境&#xff0c;帮助web开发者更好的理解web应用安全防范的过程&#xff1b; 一、DVMA介绍 DVWA官网 (opens new window)DVWA Github (ope…

springboot配置文件中的巨坑!!

问题 application.yml test:password1: 030030password2: 030030使用此配置的bean Component public class Test {Value("${test.password1}")private String password1;Value("${test.password2}")private String password2;PostConstructpublic void …

企业微信建设代开发应用的基本流程及实现

一、企业微信代开发应用的基本流程 企业微信中除了系统提供的应用以及上架应用商店的第三方应用外&#xff0c;也可以使用自己开发的应用&#xff0c;不过多数企业都不具备开发能力&#xff0c;只能采用代开发的形式进行定制。就给大家分享一下企业微信代开发应用的基本流程&a…

java八股面试文(带答案,万字总结,精心打磨,建议收藏)堪称2023最强

前言 2023秋招即将来临&#xff0c;很多同学会问Java面试八股文有必要背吗&#xff1f; 我的回答是&#xff1a;很有必要。你可以讨厌这种模式&#xff0c;但你一定要去背&#xff0c;因为不背你就进不了大厂。 国内的互联网面试&#xff0c;恐怕是现存的、最接近科举考试的…