Java数据结构:堆详解

文章目录

  • [1. 什么是堆](#1. 什么是堆)
  • 2.为什么需要堆
  • 3.堆的存储方式
  • 4.堆的创建
    • [4.1 向下调整](#4.1 向下调整)
    • [4.2 向上调整](#4.2 向上调整)
    • [4.3 时间复杂度](#4.3 时间复杂度)
  • [5.Java中的 PriorityQueue](#5.Java中的 PriorityQueue)
    • [5.1Comparable 和 Comparator](#5.1Comparable 和 Comparator)

1. 什么是堆

堆是一种特殊的完全二叉树。

它除了满足完全二叉树的结构要求之外,还需要满足一定的大小关系。

堆又分为两种:小根堆和大根堆

小根堆: 父节点 <= 子节点,最小值永远在堆顶

大根堆: 父节点 >= 子节点,最大值永远在堆顶

2.为什么需要堆

如果现在有很多数据,我想快速找到其中的最小值怎么办?

如果每次都遍历,时间复杂度是O(N),而如果我们维护一个小根堆,最小元素永远在堆顶,时间复杂度只有O(1)

堆非常适合处理"不断加入数据,同时需要快速获取最大值/最小值"的场景。

3.堆的存储方式

堆是一棵完全二叉树 ,因此可以层序的规则采用顺序的方式来高效存储

对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

假设i为节点在数组中的下标

父节点: (i - 1) / 2

左孩子: 2 * i + 1,否则没有左孩子

右孩子: 2 * i - 1,否则没有右孩子

4.堆的创建

4.1 向下调整

我们要删除节点1,如果直接删除数组结构就不好维护

我们可以交换1节点和尾节点,然后 将堆中有效数据个数减少一个,对堆顶元素进行向下调整

  1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
  2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在
    • parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
    • 将parent与较小的孩子child比较,如果:parent小于较小的孩子child,调整结束,否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2
java 复制代码
private void shiftDown(int parent){
        int child = 2*parent +1;
        while(child<this.size){
             // 如果有右孩子
            // 找左右孩子中较小的
            if(child+1<size&&arr[child+1]<arr[child]){
                child++;
            }
            // 如果父节点 <= 最小的孩子
            // 已经满足小根堆
            if(arr[parent]<arr[child]){
                break;
            }
            int temp = arr[parent];
            arr[parent] = arr[child];
            arr[child] = temp;
            // 继续向下
            parent =child;
            child = 2*parent +1;
        }
    }

4.2 向上调整

我们想要插入一个元素2,新元素首先放到数组最后:1, 3, 5, 7, 9, 2,2 < 5破坏了小根堆的性质,所以需要让 2 不断向上调整。

java 复制代码
private void shiftUp(int child){
        int parent = (child-1)/2;
        while(child>0){
            if(arr[parent]>arr[child]){
                int temp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = temp;
                child = parent;
                parent = (child-1)/2;
            }
            else break;
        }
    }

了解了向上调整和向下调整,对于根节点的左右子树已经完全满足堆的性质的序列只需将根节点向下调整好即可得到堆,那么我们思考该如何将普通序列转化成堆呢?
对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性中的数据,如何将其创建成堆呢?图示将其转化成大根堆

由图,我们从最后一个非叶子节点开始 每一个非叶子节点向下调整,一直到根节点

最后一个非叶子节点

↓

shiftDown

↓

前一个非叶子节点

↓

shiftDown

↓

...

↓

根节点

4.3 时间复杂度

注意: 对于上述自底向上建堆建堆方式时间复杂度为O(N),而对于一个一个插入的建堆方式时间复杂度为O(N log N)

1

↓

插入第二个

↓

插入第三个

↓

插入第四个

↓

...

每次插入O(log N)一共N次,时间复杂度为O(N log N)

最后一个非叶子节点

↓

shiftDown

↓

向前

↓

shiftDown

↓

...

↓

根节点

虽然单次 shiftDown 最坏是:O(log N)但是大部分节点距离叶子很近,根本没有 log N 那么长的调整路径,时间复杂度为O(N)

下面是完整证明:

5.Java中的 PriorityQueue

在实际开发中,我们很多时候不需要自己实现堆,Java已经提供了基于堆实现的优先级队列 PriorityQueue,默认创建的是小根堆

java 复制代码
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);   // 入队
pq.offer(2);
pq.offer(8);

pq.peek();     // 查看堆顶
pq.poll();     // 删除并返回堆顶
pq.size();     // 元素个数
pq.isEmpty();  // 是否为空

如果我想要创建一个大根堆该怎么办呢

我们发现PriorityQueue自带一个可以传比较器的构造方法

public PriorityQueue(Comparator<? super E> comparator)

也就是说我们可以自己实现一个比较方法而不用其默认的

java 复制代码
//自己写一个大根堆比较器,直接实现Comparator接口,然后重写该接口中的compare方法即可
class MyComparator implements Comparator<Integer> {

    @Override
    public int compare(Integer a, Integer b) {
        return a - b;
    }
}
PriorityQueue<Integer> pq = new PriorityQueue<>(new MyComparator());

注意:

  • PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
  • 不能插入null对象,否则会抛出NullPointerException
  • 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 插入和删除元素的时间复杂度为O(log₂N)
  • PriorityQueue底层使用了堆数据结构

5.1Comparable 和 Comparator

如果我们存的对象Integer类型那么他们的大小很好比较,那如果我们存的是自定义对象呢

java 复制代码
class Student {
    String name;
    int age;
}
PriorityQueue<Student> pq = new PriorityQueue<>();

这里Java 会产生一个问题:

两个 Student 到底应该按照什么比较?我们就可以通过Comparable 和 Comparator这两个接口来解决这个问题

Comparable实现

java 复制代码
import java.util.PriorityQueue;

class Student implements Comparable<Student> {

    String name;
    int age;

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

    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}

public class Test {

    public static void main(String[] args) {

        PriorityQueue<Student> pq =
                new PriorityQueue<>();

        pq.offer(new Student("张三", 20));
        pq.offer(new Student("李四", 18));
        pq.offer(new Student("王五", 22));

        Student student = pq.poll();

        System.out.println(student.name);
        System.out.println(student.age);
    }
}

Comparator实现

java 复制代码
import java.util.Comparator;

class Student {
    String name;
    int age;

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

class StudentComparator//将这个比较器传给PriorityQueue的构造方法
        implements Comparator<Student> {

    @Override
    public int compare(Student a, Student b) {
        return a.age - b.age;
    }
}
相关推荐
重生之小比特1 小时前
【C++进阶】map和set
java·开发语言·c++
lie..2 小时前
30天从零开始学AI应用开发(Day 12):像产品一样稳:流式输出、报错重试、花钱控制
开发语言·人工智能·php
傻啦嘿哟2 小时前
Python的默认参数把我坑惨了,原来写[]和写None的区别这么大
开发语言·python·机器学习
需要8262 小时前
分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑
java·spring boot·分布式·spring·spring cloud
晨小安2 小时前
ThreadLocal及其内存泄漏问题深度解析
java·jvm·tomcat·哈希表
DeepAgent2 小时前
AI Agent 工程实践(49):一次真实优化——从 Agent v1 到 v2
开发语言·人工智能·agent
知识分享小能手2 小时前
C++ 学习教程,从入门到精通,C++对象和类 — 详细知识点总结(10)
开发语言·c++·学习
m0_587383002 小时前
深圳24小时自助健身房解决方案实战:从系统架构到部署指南
java·人工智能·spring boot·spring·系统架构·需求分析
郑州光合科技余经理3 小时前
同城电商系统:库存变更怎么同步到订单
java·开发语言·前端·后端·uni-app·php·ai编程