模拟实现优先级队列

目录

前言:

大小堆概念

建堆(大堆)

 代码实现

堆中插入数据

代码实现

堆中删除数据

代码实现

获取堆顶元素

代码实现

TopK问题

题目

解析

代码实现

小结:


前言:

🎉优先级队列可以很快找出一组数据中最小或者最大的数据,只需改正它建小堆或者大堆。它底层是用数组存储的(顺序存储),所以它是一颗完全二叉树,这样才不会浪费数组空间。这种数据结构对于一些数据的操作带来了很大的便利。

大小堆概念

 😉这是一个大堆,父亲节点值是要大于它左右孩子值,对于每一个子树都要符合此条件。小堆就是父亲节点值是要小于它左右孩子值,同样的对于每一棵子树也要符合此条件。注意它左右孩子大小值是随机的。

建堆(大堆)

🎈首先我们认为一个数据是有序的,所以从18开始往前依次进行建大堆,由于数组中是顺序存储这些值的,当我们从18开始往前遍历完数组时,那么每一个子树都是大堆的结构。

🎈在左右孩子中找最大的数据,然后和父亲节点值进行比较。如果父亲节点值比左右孩子中最大的值都小,那么就交换这两个节点的值,这样最大的值就在堆顶。这样的方法叫做向下调整。从18开始往前遍历完数组,全部采取向下调整,每棵子树都是大堆的结构,那么整棵树的大堆就建成了。

🎈采取向下调整时,调整完成后,它孩子这颗子树就不一定是大堆的结构,那么就需要一直往下调,直到求出的孩子越界,说明这棵树调整完成了。

🎈已知父亲下标求左右孩子下标:childLeft = parent * 2 + 1      childRight = parent * 2 + 2

🎈已知孩子下标求父亲下标:parent = (child - 1) / 2

🎈注意:如果要建小堆,只需改变比较的判定即可。

 代码实现

注意:这里是成员方法,操作的是elem数组,usedSize是有效数据个数。

/**
     * 向下调整,建大堆
     * 时间复杂度:log(n)
     * @param parent
     * @param len
     */
    private void shiftDown(int parent, int len) {
        int child = parent * 2 + 1;
        while(child < len) {
            //确保有右孩子的存在,防止越界
            if(child + 1 < len && elem[child] < elem[child + 1]) {
                child++;
            }
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                //确保整棵树都是符合大堆要求
                parent = child;
                child = parent * 2 + 1;
            }else {
                break;
            }
        }
    }
    /**
     * 建堆,将每个元素向下调整
     * 时间复杂度 o(n)
     */
    public void createHeap() {
        //由下往上调整
        for(int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
            shiftDown(parent, usedSize);
        }
    }

堆中插入数据

🪖往最后面插入数据,然后和父亲节点值比较,如果比它大,就交换两个节点的值,这样大的数据就在堆顶。但是第一次交换完成后,只是这颗子树是大堆的结构,这样的方法叫做向上调整,所以依次向上调整,直到堆顶元素比它大,当80遍历完成后这颗树就是大堆的结构。 

代码实现

/**
     * 向上调整,调孩子
     * @param child
     */
    private void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while(child > 0) {
            if(elem[parent] < elem[child]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }
    /**
     * 堆中插入,插入到最后面,向上调整,保持大堆
     * @param val
     */
    public void offer(int val) {
        //判断空间,不够则扩容
        if(isFull()) {
            this.elem = Arrays.copyOf(elem, elem.length * 2);
        }
        this.elem[usedSize++] = val;

        shiftUp(usedSize - 1);
    }

堆中删除数据

🎄堆中删除数据只能删除堆顶元素,将堆顶元素和最后一个元素交换,然后控制usedSize将最后一个元素删除。由于原来就是大堆的结构,交换完成后,只有堆顶元素这颗树不符合大堆结构,只需要将堆顶数据进行向下调整,整棵树就保持大堆结构。

代码实现

public int pop() {
        if(isEmpty()) {
            throw new NullArrayExpection("空数组");
        }
        int tmp = this.elem[0];
        this.elem[0] = this.elem[usedSize - 1];
        this.elem[usedSize - 1] = tmp;
        this.usedSize--;
        shiftDown(0, usedSize);
        return tmp;
    }

获取堆顶元素

😊如果堆不为空,直接访问第一个数据即可。注意只获取不删除。

代码实现

 public int peek() {
        if(isEmpty()) {
            throw new NullArrayExpection("空数组");
        }
        return this.elem[0];
    }

TopK问题

题目

    设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例

    输入: arr = [1,3,5,7,2,4,6,8], k = 4

    输出: [1,2,3,4]

解析

🧢首先将前k个数据建大堆,即堆顶元素就是这k个数据中最大的。然后其他数据依次和堆顶元素比较,如果比它小就弹出堆顶数据,插入这个数据。当数组遍历完成后,这个堆中存储的就是前k个最小的数据。

代码实现

class Solution {
    class IntCmp implements Comparator<Integer> {
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    }
    public int[] smallestK(int[] arr, int k) {
        if(k == 0) {
            return new int[0];
        }
        int[] arr2 = new int[k];
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());
        for(int i = 0; i < k; i++) {
            priorityQueue.offer(arr[i]);
        }
        for(int i = k; i < arr.length; i++) {
            int tmp = priorityQueue.peek();
            if(arr[i] < tmp) {
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        for(int i = 0; i < k; i++) {
            arr2[i] = priorityQueue.poll();
        }
        return arr2;

    }
}

注意:

🤨PriorityQueue中如果传入比较器,会优先调用比较器。如果没有传比较器,那么这个数据必须具有可比较性,意味需要实现Comparable接口,重写CompareTo方法。源码中会向上转型为Comparable调用CompareTo方法。

🤨由于int的包装类型是Integer,如果不传递比较器,会调用Integer中的CompareTo方法,默认是小堆。那么要建大堆就需要传入比较器,改变比较的判定。

小结:

🐵在学习集合类型时,我们可以去看一些源码,明白底层的原理。这样当我们使用它时就会得心应手。

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小太空人w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值