堆(二叉堆)-优先队列-数据结构和算法(Java)

news2024/11/15 23:49:52

文章目录

    • 1 概述
      • 1.1 定义
      • 1.2 二叉堆表示法
    • 2 API
    • 3 堆相关算法
      • 3.1 上浮(由下至上的堆有序化)
      • 3.2 下沉(由上至下的堆有序化)
      • 3.3 插入元素
      • 3.4 删除最大元素
    • 4 实现
    • 5 性能和分析
      • 5.1 调整数组的大小
      • 5.2 元素的不可变性
    • 6 简单测试
    • 6 后记

1 概述

1.1 定义

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

堆是非线性数据结构,相当于一维数组,有两个直接后继。

堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。

k i ≤ k 2 i 且 k i ≤ k 2 i + 1 或 者 k i ≥ k 2 i 且 k i ≥ k 2 i + 1 , i = [ 1 , n 2 ] k_i\le k_{2i}且k_i\le k_{2i+1}或者k_i\ge k_{2i}且k_i\ge k_{2i+1},i=[1,\frac{n}{2}] kik2ikik2i+1kik2ikik2i+1,i=[1,2n]

若将和此次序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。

1.2 二叉堆表示法

一个堆有序的完全二叉堆,如下图1.2-1所示:在这里插入图片描述

它通常用数组实现,用数组表示与堆的对应关系,如下图1.2-2所示:在这里插入图片描述

在一个堆中,位置 k k k的结点的父结点的位置为 k 2 \frac{k}{2} 2k,而它的左子结点和右子结点位置分别为 2 k 和 2 k + 1 2k和2k+1 2k2k+1。这样在不使用指针的情况下我们可以通过计算数组的索引在树中移动:从 a [ k ] a[k] a[k]向上一层令 k = k 2 k=\frac{k}{2} k=2k,向下一层令 k = 2 k 或 者 2 k + 1 k=2k或者2k+1 k=2k2k+1

命题P:一棵大小为N的完全二叉树的高度为 ⌊ lg ⁡ N ⌋ \lfloor \lg N\rfloor lgN

证明:通过归纳法很容易证明,切当N为2的幂时,高度+1。

2 API

public  class HeapPriorityQueue<E extends Comparable<E>> implements Queue<E>, Serializable

继续沿用了队列接口API,类新增方法和成员变量如下:

  • 成员变量

    访问修饰和类型名称描述
    private final E[]table存放元素容器
    private intsize元素个数
    private final Comparator<E>comparator比较器
  • 方法

    访问修饰和返回类型名称描述
    publicHeapPriorityQueue(int)构造器
    publicHeapPriorityQueue(int, Comparator<E>)构造器
    private voidexch(int,int)交换元素位置
    private booleanless(int, int)比较指定位置2个元素的大小
    private voidswim(int k)指定位置的元素上浮
    private voidsink(int)指定位置的元素下沉

3 堆相关算法

我们用size+1的私有数组table表示一个大小为size的堆,不使用table[0],堆元素存放在[1,size]中。在排序算法中,使用less()和exch()来访问数组元素,通过传递索引而不是数组作为参数。堆的某些操作可能打破堆的状态,我们需要按照堆的性质将堆恢复,这个过程称为堆的有序化。

在有序化的过程中可能遇到两种情况。当某个结点的优先级上升(或者堆底加入一个新元素)时,我们需要由下至上恢复堆的顺序。当某个结点的优先级下降(或者根结点替换为一个较小的元素)时,我们需要由上至下恢复堆的顺序。

3.1 上浮(由下至上的堆有序化)

如果堆的有序状态因为某个结点变得比它的父结点大而打破,我们需要通过交换它和它的父结点的位置来修复堆。交换后,这个结点比它的两个子结点都大(一个是曾经的父结点,一个是曾经的父结点的子节点)。一遍遍重复这个操作,直至该结点不再大于它的父结点或者到堆顶。代码如下:

/**
 * 上浮
 * @param k 上浮元素索引
 */
private   void swim(int k) {
    while (k > 1 && less(k / 2, k)) {
        exch(k / 2, k);
        k = k / 2;
    }
}

动态颜色图如下3.1-1所示:在这里插入图片描述

3.2 下沉(由上至下的堆有序化)

如果堆的有序状态因为某个结点变得比它的任一子结点小而打破,我们需要通过交换它和它的较大的结点的位置来修复堆。交换后,这个结点的父结点比它的两个子结点都大。一遍遍重复这个操作,直至该结点的子结点都比它小或者到底堆底。代码如下:

/**
 * 下沉
 * @param k 下沉元素索引
 */
private  void sink(int k) {
    while (2 * k <= size) {
        int j = 2 * k;
        if (j < size && less(j, j + 1)) j++;
        if (!less(k, j)) break;
        exch(k, j);
        k = j;
    }
}

动态图如下3.2-1所示:在这里插入图片描述

3.3 插入元素

我们把新元素添加到数组末尾,增加堆的大小并让它上浮到合适位置。

3.4 删除最大元素

我们从数组顶端删去最大元素并将数组最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适位置。

4 实现

完整代码如下4-1所示:

import java.io.Serializable;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * 堆
 *  根据传入比较器,判断是最大堆还是最小堆
 *      默认为最大堆
 *      (o1,o2) -> {o2.compareTo(o1)} 最小堆
 *
 * @author Administrator
 * @date 2022-12-03 20:41
 */
public  class HeapPriorityQueue<E extends Comparable<E>> implements Queue<E>, Serializable {

    /**
     * 存放元素容器
     */
    private final E[] table;

    /**
     * 元素个数
     */
    private int size;

    /**
     * 比较器
     */
    private final Comparator<E> comparator;

    /**
     * 构造器
     * @param initialCapacity   初始容量
     */
    public HeapPriorityQueue(int initialCapacity) {
        if (initialCapacity <= 0) {
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
        table = (E[]) new Comparable[initialCapacity + 1];
        comparator = Comparable::compareTo;
    }

    /**
     * 构造器
     * @param initialCapacity   初始容量
     * @param comparator        比较器
     */
    public HeapPriorityQueue(int initialCapacity, Comparator<E> comparator) {
        if (initialCapacity <= 0) {
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
        table = (E[]) new Comparable[initialCapacity + 1];
        this.comparator = comparator == null ? Comparable::compareTo: comparator;
    }

    /**
     * 插入元素
     * @param e 元素
     */
    @Override
    public void offer(E e) {
        if (size + 1 > table.length - 1) {
            throw new IndexOutOfBoundsException();
        }
        table[++size] = e;
        swim(size);
    }

    /**
     * 返回堆顶元素
     * @return  堆顶元素
     */
    @Override
    public E peek() {
        return table[1];
    }

    /**
     * 弹出堆顶元素
     * @return 堆顶元素
     */
    @Override
    public E poll() {
        if (size <= 0) {
            throw new NoSuchElementException("堆为空");
        }
        E e = table[1];
        table[1] = table[size--];
        table[size + 1] = null;
        sink(1);
        return e;
    }

    /**
     * 判断堆是否为空
     * @return
     */
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 获取堆中元素个数
     * @return  堆中元素个数
     */
    @Override
    public int size() {
        return size;
    }

    /**
     * 暂时不做实现
     * @return
     */
    @Override
    public Iterator<E> iterator() {
        return null;
    }

    /**
     * 比较所有i,j出元素大小
     * @param i 所有i
     * @param j 索引j
     * @return 所有i处元素小于索引j处元素返回true;否则返回false
     */
    private boolean less(int i, int j) {
        return comparator.compare(table[i], table[j]) < 0;
    }

    /**
     * 交换元素
     * @param i 索引i
     * @param j 索引j
     */
    private void exch(int i, int j) {
        E t = table[i];
        table[i] = table[j];
        table[j] = t;
    }

    /**
     * 上浮
     * @param k 上浮元素索引
     */
    private   void swim(int k) {
        while (k > 1 && less(k / 2, k)) {
            exch(k / 2, k);
            k = k / 2;
        }
    }

    /**
     * 下沉
     * @param k 下沉元素索引
     */
    private  void sink(int k) {
        while (2 * k <= size) {
            int j = 2 * k;
            if (j < size && less(j, j + 1)) j++;
            if (!less(k, j)) break;
            exch(k, j);
            k = j;
        }
    }

}

5 性能和分析

命题Q:对于含有N个元素的基于堆的优先队列,插入元素只需要不超过 ( lg ⁡ N + 1 ) (\lg N +1) (lgN+1)次比较,删除最大元素的操作需要不超过 2 lg ⁡ N 2\lg N 2lgN次比较。

证明:有命题P可知,两种操作都需要在堆顶和堆顶之间移动元素,而路径的长度不超过 lg ⁡ N \lg N lgN。对于路径上的每个结点,删除最大元素需要两次比较(除了堆底元素),一次用来找比较大的子结点,一次用来确定该子结点是否需要上浮。

5.1 调整数组的大小

我们可以在offer()中判断如果堆大小达到数组容量上限,根据一定的策略增大数组的容量;在poll()删除堆顶元素时,通过判断小于一定的值来缩减数组的容量。这样我们无需关注大小限制。同样低,命题Q指出的对数时间复杂度上限只是针对一般性的队列长度N而言了。

5.2 元素的不可变性

堆存储了用例创建的对象,但同时加锁用例代码不会修改它们。我们可以将这个假设转化为强制条件,但程序员一般不会这么做,因为增加代码的复杂性会降低性能。

6 简单测试

代码如下:

/**
 * @author Administrator
 * @date 2022-12-04 20:10
 */
public class HeapPQTest {
    public static void main(String[] args) {
        // 最大堆
//        HeapPriorityQueue<Integer> hpq = new HeapPriorityQueue<>(10); 
        // 最小堆
        HeapPriorityQueue<Integer> hpq = new HeapPriorityQueue<>(10, (o1, o2) -> o2.compareTo(o1)); 
        hpq.offer(3);
//        System.out.println(hpq.isEmpty());
//        System.out.println(hpq.size());
//        System.out.println(hpq.peek());
        hpq.offer(4);
        hpq.offer(1);
        hpq.offer(5);
        hpq.offer(8);
        hpq.offer(6);
        hpq.offer(9);
        int size = hpq.size();
        System.out.println(size);
//        System.out.println(hpq.peek());

        for (int i = 0; i < size; i++) {
            System.out.println(hpq.poll());
        }
    }
}

可以自己根据需要随意测试代码,如果发现bug最好😄。

6 后记

​ 如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm

[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10

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

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

相关文章

2006-2020年全国31省人口老龄化水平

2006-2020年全国31省人口老龄化 1、时间为2006-2020年 2、来源&#xff1a;人口与就业年鉴 3、数据缺失情况说明&#xff1a; 其中2010年存在缺失&#xff0c;采用线性插值法进行填补&#xff0c;内含原始数据、线性插值 4、计算说明&#xff1a;以城镇地区老年抚养比衡量…

uImage的制作过程详解

1、uImage镜像介绍 参考博客&#xff1a;《vmlinuz/vmlinux、Image、zImage与uImage的区别》&#xff1b; 2、uImage镜像的制作 2.1、mkimage工具介绍 参考博客&#xff1a;《uImage的制作工具mkimage详解(源码编译、使用方法、添加的头解析、uImage的制作)》&#xff1b; 2.2…

软路由搭建:工控机(3865U)安装esxi并在esxi上创建iStoreOS做主路由(网卡直通)

一、硬件介绍 1、工控机&#xff08;3865U&#xff09; CPU&#xff1a;3865U 内存&#xff1a;8G 硬盘&#xff1a;120G 网卡&#xff1a;六口网卡 2、无线路由器&#xff08;荣耀路由器pro2&#xff09; 3、主机 下载资料、制作启动盘、系统设置 4、U盘 至少8G以上 …

ConcurrentHashMap 1.7与1.8的区别

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于&#xff1a;put和 get 两次Hash到达指定的HashEntry&#xff0c;第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表 从1.7到1.8版本&#xff0c;由于HashEntry从链表 变成了红黑树所以 concurr…

Python Gui之tkinter(下)

6.Radiobutton单按按钮 Radiobutton控件用于选择同一组单选按钮中的一个。Radiobutton可以显示文本&#xff0c;也可以显示图像。 7.Checkbutton复选按钮 Checkbutton控件用于选择多个按钮的情况。Checkbutton可以显示文本&#xff0c;也可以显示图像。 经典的Gui类的写法&a…

关于liunx 宝塔运行php项目

文章目录前言一、申请liunx服务器安装宝塔环境二、安装php看你自己安装需要的版本三.php文件创建四.数据库创建五.访问项目就可以了前言 自己研究学习&#xff0c;大佬勿喷 一、申请liunx服务器安装宝塔环境 我是线上安装的都一样看个人习惯爱好吧 等待安装完成提示地址和账…

Java基础—重新抛出异常

重新抛出异常 在catch块内处理完后&#xff0c;可以重新抛出异常&#xff0c;异常可以是原来的&#xff0c;也可以是新建的&#xff0c;如下所示&#xff1a; try{ //可能触发异常的代码 }catch(NumberFormatException e){ System.out.println("not valid numbe…

电子印章结构以及规范讲解

前言 为了确保电子印章的完整性、不可伪造性&#xff0c;以及合法用户才能使用&#xff0c;需要定义一个安全的电子印章数据格式&#xff0c;通过数字签名&#xff0c;将印章图像数据与签章者等印章属性进行安全绑定&#xff0c;形成安全电子印章 电子印章&#xff1a;一种由…

MVVM与Vue响应式的实现

Vue的响应式实现原理 MVVM M&#xff1a;模型 》data中的数据 V&#xff1a;视图 》模板 VM&#xff1a;视图模型 》Vue实例对象 ViewModel是一个中间的桥梁将视图View与模型Model连接起来&#xff0c;ViewModel内部通过数据绑定&#xff0c;实现数据变化&#xff0c;视图发…

链接装载(一)虚拟地址与物理地址

文章目录一、基本概念二、一个基本问题三、程序的执行四、从堆中分配的数据的逻辑地址一、基本概念 当我们写出一个程序&#xff0c;即便是最基本的 Hello World&#xff0c;都需要经过 预处理、编译、汇编、链接才能生成最终的可执行文件。 预处理&#xff1a; 预处理过程主…

spring ioc的循环依赖问题

spring ioc的循环依赖问题什么是循环依赖spring中循环依赖的场景通过构造函数注入时的循环依赖通过setter或Autowired注入时的循环依赖循环依赖的处理机制原型bean循环依赖单例bean通过构造函数注入循环依赖单例bean通过setter或者Autowired注入的循环依赖三级缓存对象的创建分…

Metasploit 操作及内网 Pivot图文教程

目录 一、metasploit 简介 二、 基本使用 三、 使用 encoders 四、pivot 技术 一、metasploit 简介 Metasploit 是一款开源的安全漏洞检测工具&#xff0c;集成了丰富的渗透测试工具&#xff0c;深受安 全工作者喜爱。官方网站&#xff1a;www.metasploit.com 本案例将以图…

OS-调度

调度 多个程序在并发的情况下执行&#xff0c;最大化CPU利用率&#xff0c;同时要保证一定的公平性 调度的时机 五种情况&#xff1a; Running -> Waiting&#xff1a;例如等待I/ORunning -> Ready: interupt&#xff0c;计时器到时间了Running -> TerminatedWait…

我把Idea给改了,看看有没有你常用的功能,没有,你告诉我,我来改

改造的目标 时隔2个多月的研发&#xff0c;11月25日&#xff0c;终于把Idea插件BG-BOOM的1.1.0版本搞上线了&#xff0c;本次更新勇哥也是百忙之中挤时间&#xff0c;加班加点开发为粉丝&#xff0c;目的也主要是帮助大家提升开发效率&#xff0c;有更多摸鱼和内卷时间&#x…

[附源码]Python计算机毕业设计SSM晋中学院教室管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

QT中怎么设置定时器/周期任务/定时触发任务

Qt中定时器的使用有两种方法&#xff0c;一种是使用QObject类提供的定时器&#xff0c;还有一种就是使用QTimer类。 其精确度一般依赖于操作系统和硬件&#xff0c;但一般支持20ms。下面将分别介绍两种方法来使用定时器。 QObject类提供的定时器 QObject中的定时器的使用&am…

Makefile 详解

文章目录1.什么是Makefile2.Makefile文件命名规则3.编写Makefile4.Makefile 的工作原理5.Makefile中的变量6.模式匹配7.函数1.什么是Makefile 一个工程中的源文件不计其数&#xff0c;按期类型、功能、模块分别放在若干个文件中&#xff0c;MakeFile文件定义了一系列的规则来制…

HIN应用调研总结

文章目录1. 代码安全iDev: enhancing social coding security by cross-platform user identification between GitHub and stack overflow【A】2. API推荐Group preference based API recommendation via heterogeneous information network【A】3.Andorid恶意软件检测Out-of-…

SSM甜品店系统计算机毕业论文java毕业设计选题源代码

&#x1f496;&#x1f496;更多项目资源&#xff0c;最下方联系我们✨✨✨✨✨✨ 目录 Java项目介绍 资料获取 Java项目介绍 计算机毕业设计java毕设之SSM甜品店系统-IT实战营_哔哩哔哩_bilibili项目资料网址: http://itzygogogo.com软件下载地址:http://itzygogogo.com/i…

中英双语多语言外贸企业网站源码系统 - HanCMS - 安装部署教程

随着跨境独立站的流行&#xff0c;中英双语的公司官网越来越受到重视。 此项目是基于开源CMS开发出的中英文双语外贸企业网站内容管理系统&#xff0c;命名HanCMS HanCMS 汉CMS中英双语多语种外贸网站系统&#xff0c;是一个轻量级的网站系统&#xff0c;访问速度极快&#xff…