2024年二叉搜索树与优先级队列及其堆的应用_二叉排序树存储优先队列(1),2024年最新实战篇

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

8141b5c8189748d69a4a0978fe9ad7fa.png

那为什么优先级又体现在何处呢?请继续往下看~


堆的性质:

上面我们已经学会了堆的存储方式,假设现在已经建立了一颗完全二叉树,那么该二叉树就必须满足以下性质,才能被称为是优先级队列:

  • 堆是一颗完全二叉树;

1dc72abdb2a7412b980c1a0ecf20061b.png

由于每个根节点的值是与数组对应的,可想而知,如果这棵树不是完全二叉树,那必然会导致后面的新增或删除元素是,导致数组中存放空值,使内存被浪费。

  • 堆中的某个节点的值总数不大于或不小于其父节点的值;
    无论是整棵树还是没一颗子树,它们都是大根堆或小根堆(具体介绍在下面)。
  • 对于每棵树而言,其左子树的值与右子树的值之间没有必然的大小关系;
    只强调根节点要大于或小于所有子树节点的值,而左子树和右子树的值大小关系不必在意;

小根堆:

父节点的值小于子树节点的值:

827ee5f1f8474ce3bb0ba1b91e4d9793.png

大根堆:

父节点的值大于子树节点的值:

ab297150aaf0411fb1ecc7753a576d54.png

数组还原为二叉树:

借助数组的索引 i ,我们可以用以下总结用来与树中的节点来对应:

  1. 如果 i = 0,则 i 表示的节点为整棵树的根节点;
  2. 如果 i 不为 0,则 i 节点的双亲节点为 (i - 1) / 2;
  3. 如果 2 * i + 1 小于节点个数,则节点 i 的的左孩子下标为 2 * i + 1,否则没有左孩子;
  4. 如果 2 * i + 2 小于节点个数,则节点 i 的右孩子下标为 2 * i + 2,否则没有右孩子;

以上结论结合图解才能更加直观的理解。


堆的模拟实现

上面我们已经通过图解学会了堆的存储,下面我们将模拟实现堆的基本功能。

创建堆:

思路:假设要创建一个大根堆

1.堆的数值与数组的值对应,就需要一个数组elem;其次需要一个计数器usedSize,保存目前树中节点个数,以便后续插入或删除找到所需节点;

2.实例化出一个堆对象时,需要将往外面需要的数值赋值给elem,那么再来一个initElem方法;

public class TestHeap {
    public int[] elem;
    public int usedSize;

    //给数组一个默认容量
    private static final int DEFAULT_SIZE = 10;
    public TestHeap() {
        elem = new int[DEFAULT_SIZE];
    }

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

}

3.创建队就是将已知数组的元素插入到树的节点中的过程,但堆中的值比数组的要求更苛刻,要满足大根堆的要求,那我们就会想到,先直接将数组的所有元素直接按照顺序方式来创建成一颗完全二叉树,再向下调整每一棵子树的节点,直到每颗子树都满足大根堆,最终整棵树就是一个大根堆了;

4.向下调整:(最关键的细节)

先对最后一颗子树进行操作 ——> 拿到其父节点和子树节点中值最大的那一个 ——> 比较,如果不符合大根堆,就交换 ——> 继续向后比遍历这棵树,重复上述比较过程,直到每颗子树走到结束位置 ——> 当每颗子树都被调整成大根堆以后,整棵树就是大根堆了。

最详细的调整过程需要结合代码和下图进行推理:

    /**
     * 创建大根堆
     * 复杂度:O(N)
     */
    public void createHeap() {
        for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
            shiftDown(parent,usedSize);
        }

    }
    private void shiftDown(int parent,int len) {
        int child = 2 * parent + 1;
        while(child < len) {
            //开始准备比较
            //1.chile 是左子树节点,还要看它和右子树节点谁大,用大的来和parent比较
            //所有要先看是否有有节点,没有的话就直接拿左子树节点去比较
            if(child+1 < len && elem[child] < elem[child+1]) {
                child++;
            }
            //此时的child一定是当前子树上最大的节点
            //现在才可以开始比较
            if(elem[child] > elem[parent]) {
                int temp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = temp;
                //一次换完后还要看后面的节点是否也需要比较
                parent = child;
                child = 2 * parent + 1;
            }
            // 如果本来就是大根堆,那么这支树上就不需要交换
            else {
                break;
            }
        }
    }

c9469d9c269245ea9463c70983add983.png

创建堆的复杂度:O(n)

推导过程:以满二叉树为例,假设每个节点都需要调整,这样就是最复杂的情况。

60e2c674bd604f4f92cba3cb1bba808b.png

堆的插入:

对于堆的数组而言,插入元素即简单的尾插,但对于堆的二叉树而言,要时刻保持树是大根堆,就需要每次新增元素后向上调整根的大小;

思路:依然是大根堆

1.先检查数组是否需要扩容,插入新元素;

2.拿到最后一颗子树的最后一个节点,再拿到其父节点,两个节点值比较,不符合大根堆就交换,再向上检查,直到整棵树的根节点结束;

c1a88d204f864e82ac2b702dfc31a772.png

    /**
     * 插入元素 复杂度:O(logN)
     * 对于自带的数组:肯定是尾插
     * 对于树:插入后要保证依然是大根堆,因此要向上调整
     */
    public void offer(int val) {
        //满了要扩容
        if(isFull()) {
            elem = Arrays.copyOf(this.elem,2 * this.elem.length);
        }
        //未满 - 直接插入
        elem[usedSize] = val;
        //插入后要向上调整
        shiftUp(usedSize);
        //调整完
        usedSize++;

    }
    private void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (parent >= 0) {
            if(elem[child] > elem[parent]) {
                int temp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = temp;
                //交换完后 - 向上走
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;
            }
        }

    }
    private boolean isFull() {
        return usedSize == elem.length;
    }


堆的删除:

思路:堆的删除,即弹出堆顶第一个元素;

1.先将树中下标为0的节点的值与树最后一个节点的值交换;

2.交换完从整棵树的根节点开始向下调整,同上,依然要保证树是大根堆;

/**
     * 删除元素
     * 先让下标为0的根节点数值与整棵树最后一个节点的数组交换
     * 交换后 - 向下调整
     */
    public int pop() {
        //判断是否为空
        if(isEmpty()) {
            return -1;
        }
        //先交换
        int temp = elem[0];
        elem[0] = elem[usedSize - 1];
        elem[usedSize - 1] = temp;
        //交换完 - 向下调整
        usedSize--;
        shiftDown(0,usedSize);
        return temp;

    }
    private boolean isEmpty() {
        return usedSize == 0;
    }

7ed25fa08c9046008fec96785d6f2b0f.png

易错点:

usedSize–只有一次,因为下次插入元素时会直接将80覆盖掉。

6b2bafcaa1da4871995d3322848c633c.png

易错题:

已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()
A: 1         B: 2         C: 3         D: 4

31f0f00a42e548569fe9ce527f764b67.png


PriorityQueue集合类源码剖析

cbdf9c8f01474129818628ad0d9e9212.png

  • PriorityQueue即优先级队列,其实现了Queue接口,所以具有Queue索具有的方法;
  • 优先级队列有PriorityQueue和PriorityBlockingQueue之分,前者是线程不安全的,后者是线程安全的;

构造方法:

PriorityQueue类的构造方法有七种:

9f4553e738ab478cb7a428297bf8ed2b.png

以下面这段代码为例展开源码剖析:

00c45169ca6e41749630bb3d51770810.png

**PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();**

1.第一次调用构造方法:

908375618a534b5e8d8e5b3d475382dc.png

priorityQueue.offer(10);

2.第一次offer元素:

3c3ba35e0f174d74882cc6d7bebcf17c.png

priorityQueue.offer(30);

3.第二次offer元素:

855a8a1d5c6b4706bab548e80687f006.png

4.除了上述方法,我们还可以自己设置数组的默认长度,调用第②种构造方法:

8244a882ff6b4e4c92db4e14f241249c.png

  1. 如果实例化时传入我们的比较器,就会调用第③种构造方法:

()比较器的写法在下面的“特性”中有介绍)

0317dff6d2ee47de9069a651be226f44.png

7.给堆传入Collection接口下的结构:

8be4c8e6cc304791b6605b45c476c68c.png

对应的构造方法:

21753c31037e45eeaabb67264baba7c2.png

8.关于其他构造方法的时候,大家感兴趣可以自行阅读源码学习,如果这里的源码剖析有不明白的地方,可以学习完下面的“特性”后再回过来就一定可以完全理解。

总结:

1.第一次调用构造方法实例化出一个PriorityQueue对象时,相当于new一个长度为默认值的数组;

2.如果调用构造方法时,传入一个数值,就会调用初始化数组的构造方法;

如果调用构造方法时,传入一个比较器,就会调用实例化比较器的构造方法;

3.插入元素:

如果是第一次插入:直接放在0下标的位置;

如果不是第一次插入,并且没有传入比较器,那么首先要保证传入的对象是可比较的,其次offer方法会自动new一个可比较的key对象,再根据向上调整,最后选择合适位置放入元素;

如果不是第一次插入,并且传入了比较器,那么说明插入的数据是可比较的,因此offer方法会直接根据向上调整选择合适的位置放入元素;

4.优先使用比较器来比较;

PriorityQueue特性:

  • PriorityQueue 本身实现的是小根堆:

f145ebea71774d9fbaf20a3ffacc7d8d.png

  • PriorityQueue中放置的元素必须是能够比较大小的,否则会抛ClassCastExcepyion异常;

我们可以自己创建一个Student来,这个类是我们自己写的,并不具备比较功能,因此春给堆后就会抛异常;

3607c75eba874013868012194ca30bca.png

  • 可以通过实现Comparable接口,重写CompareTo方法,传入我们的自定义类型:

2740b44de6704fda99da3fc1e742dc56.png

如果要变成大根堆,只需要交换CompareTo方法的比较规则;

  • 如果传入对象是语法中现有的类型,如Integer类、String类等,我们不能改变其源码中的比较规则,那怎么才能让它们变成大根堆呢?
    方法:实现比较器

629cf9bd4b2b467999fe6c230db2b433.png

三种做题时的写法:

1.匿名内部类:

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如果要变成大根堆,只需要交换CompareTo方法的比较规则;

  • 如果传入对象是语法中现有的类型,如Integer类、String类等,我们不能改变其源码中的比较规则,那怎么才能让它们变成大根堆呢?
    方法:实现比较器

629cf9bd4b2b467999fe6c230db2b433.png

三种做题时的写法:

1.匿名内部类:

[外链图片转存中…(img-cVgwNoDm-1715592937100)]
[外链图片转存中…(img-CvzO0uFG-1715592937100)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值