优先级队列(堆)详解

文章详细介绍了大根堆和小根堆的概念,以及如何使用Java语言来实现大根堆的基本操作,包括建堆、入队、出队和查看堆顶元素。同时,讲解了Java中的优先级队列PriorityQueue的使用,包括默认方法和如何自定义比较器将小根堆转换为大根堆。此外,还讨论了Java中的比较方式,如比较运算符、equals方法、Comparable接口和Comparator接口的实现。
摘要由CSDN通过智能技术生成

目录

1.概念(大根堆和小根堆)

2.使用Java语言来实现大根堆的基本操作

3.Java中自带的优先级队列的使用

3.1 默认的常用基本方法

3.2 将默认的小根堆改成大根堆

4.拓展Java中常涉及到的比较方式

4.1 比较运算符

4.2 equals

4.3 实现Comparable接口

4.4 实现自定义的比较器(实现Comparator接口)


1.概念(大根堆和小根堆)

     ● 在某些情况下,数据要根据优先级出入,就比如我们在看医生排队时,先挂号的人一般先进行诊治,但是突然来了个需要急救的病人,医生就需要先去诊治这个病情重的病人,所以使用普通队列是不合适的,我们需要去使用优先级队列,而优先级队列是用(顺序存储的完全二叉树)去实现,堆一般有两种。

     ①  小堆:堆中父节点值比两个孩子结点的值都小

     ②  大堆:堆中父节点值比两个孩子结点的值都大

注:假设父节点的下标为i,那么其左孩子的下标为2*i + 1,右孩子的下标为2*i + 2,反推过来,知道孩子结点的下标为j,那么父节点的下标就为(j-1) / 2。


2.使用Java语言来实现大根堆的基本操作

     1)建堆(假设将此数组(既不是大根堆也不是小根堆)建堆{55,66,10,77,88,25} )

     ● 以建大根堆为例,堆的构建有两种方式:

        ① 向下调整(O(N)) :在堆中,从最后一个非叶子结点开始调整,每个父结点通过比较可能会向下移动。此外,因为根据计算(最坏情况下为满二叉树除最后一层叶子结点外,其余每个结点都需要调整)其时间复杂度比向上调整建堆要快(满二叉树中最后一层的结点数差不多是整个满二叉树的一半,进行向上调整最坏情况下,除根节点之外的结点都要进行,所以时间复杂度会比向下调整建堆时间复杂度大),所以我们采用向下调整。

        ② 向上调整(O(N)):根据给出的数组,将一个个元素依次入堆(也就是堆中元素个数从0开始,不断往堆中插入元素,在插入前,堆已经是个大根堆或小根堆)。在往堆中添加元素时会用到。

注:根结点无需向上调整,直接入堆。

     ● 实现代码:

 //赋值
    public void initQueue(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            this.data[i] = arr[i];
        }
        this.usedSize = arr.length;
    }

    /**
     * 建堆的时间复杂度:O(N)
     *
     * @param arr 存放要存储到堆中的数据元素的数组
     */
    public void createHeap(int[] arr) {
        initQueue(arr);
        //从最后一个非叶子节点((usedSize - 1) / 2)开始
        for (int i = (usedSize - 1) / 2; i >= 0; i--) {
            //通过向下调整让每颗子树都变成大根堆
            //调整的区间从[i,usedSize)->[i-1,usedSize].....[0,usedSize)
            adjustDown(i, usedSize);
        }
    }

    /**
     * 向下调整:O(log2n(以2为底))
     *
     * @param parent   父节点下标(一般都是从每棵子树的根开始向下调整)
     * @param usedSize 每棵子树调整结束标志
     */
    private void adjustDown(int parent, int usedSize) {
        //由父节点下标得到孩子下标
        int child = parent * 2 + 1;//左孩子下标
        while (child < usedSize) {
            //左孩子和右孩子(在[2,usedSize)之间)比较得到较大结点的下标
            while (child + 1 < usedSize && data[child] < data[child + 1]) {
                child++;
            }

            //将两个孩子之间的较大值结点与父节点比较
            //如果父节点值大则不用继续调整,否则交换后继续调整
            if (data[child] < data[parent]) {
                break;
            } else {
                swap(data,child,parent);
//                int temp = data[child];
//                data[child] = data[parent];
//                data[parent] = temp;
                parent = child; //更新父节点
                child = 2 * parent + 1; //更新孩子结点
            }
        }
    }

    //交换数组中对应两个下标位置的值
    private void swap(int[] arr,int m, int n) {
        int temp = arr[m];
        arr[m] = arr[n];
        arr[n] = temp;
    }

     ● 过程展示:

        

     2)入队(已完成建大堆:88 77 25 55 66 10)

     ● 具体思路

        ① 将新的元素放到堆最后,堆中元素个数+1。

        ② 从最后一个叶子结点开始进行向上调整,直到child == 0 || parent < 0,结束调整 。

注:根结点无需向上调整。

     ● 实现代码:

/**
     * 入队
     * @param val
     */
    public void push(int val) {
        //将入的元素放队列最后
        data[usedSize++] = val;

        //向上调整让其整体成为大根堆(从最后一个叶子节点开始向上调整)
        //现在已经是局部大根堆,所以只需要跟父节点进行比较(父节点值一定大于两个孩子结点值)
        adjustUp(usedSize - 1);
    }

    /**
     * 向上调整
     * @param child 堆中最后一个孩子结点
     */
    private void adjustUp(int child) {
        //满了进行扩容
        if(isFull()) {
            this.data = Arrays.copyOf(data,2*defaultSize);
            return;
        }
        int parent = (child - 1) / 2;
        while(child > 0) {
            if(data[parent] > data[child]) {
                break;
            }else {
                swap(data,child,parent);
                child = parent;
                parent = (child - 1) / 2;
            }
        }
    }

    private boolean isFull() {
        return usedSize == defaultSize;
    }

     ● 过程展示:

     3)出队(已完成建大堆:88 77 25 55 66 10)

     ● 具体思路

        ① 将堆顶元素和放到最后一个元素交换,堆中元素个数-1。

        ② 将整个堆进行向下调整,调整区间为[0,size)。

     ● 实现代码:

/**
     * 出队:每次删除的都是优先级高的元素(删除后仍要保持是大根堆)
     */
    public void poll() {
        if(isEmpty()) {
            System.out.println("优先级队列为空,无法删除元素");
            return;
        }

        swap(data,0,usedSize - 1);
        usedSize--;
        adjustDown(0,usedSize);
    }

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

     ● 过程展示:

     4)取出堆顶元素(已完成建大堆:88 77 25 55 66 10)

     ● 具体思路

        0下标位置就是堆顶元素值

     ● 实现代码:

/**
     * 获取堆顶元素
     * @return
     */
    public int peek() {
        return data[0];
    }

3.Java中自带的优先级队列的使用

3.1 默认的常用基本方法

         所需要使用的类为PriorityQueue,其默认建小堆(也可以传比较器或者实现了Comparable接口的类):

        PriorityQueue<Integer> queue = new PriorityQueue<>();

注:堆中存的是自定义类型的对象时,自定义类型需要实现Comparable接口,按照需要的比较方式重写compareTo方法,或者单独重新实现一个比较器,重写compare方法,将比较器作为参数传给堆。这样在对堆进行操作时才方便比较。

        ● 常用操作:

           ① offer() : 入队(添加元素)

           ② poll() : 出队(删除元素)

           ③ peek() : 查看堆顶元素

3.2 将默认的小根堆改成大根堆

         如果要建大堆,需要自己实现一个比较器,重写compare方法:

        //Java自带优先级队列的使用
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        queue.offer(11);
        queue.offer(2);
        System.out.println(queue.peek());//默认小堆,这时候取堆顶为2
        //建立大根堆(实现一个匿名比较器重写其compare方法)
        PriorityQueue<Integer> queue2 = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //未传比较器,那么默认比较是返回o1 - o2(o1 > o2返回正数,相等返回0,o1 < o2返回负数)
                return o2-o1; //修改为o2 - o1(o2 > o1返回正数,相等返回0,o2 < o1返回负数)
            }
        });
        queue2.offer(11);
        queue2.offer(2);
        System.out.println(queue2.peek());//修改成大堆,这时候取堆顶为11

        输出:

注:如果堆中存储的数据的类型为自定义类型,也可以让自定义类型实现Comparable接口,重写compareTo方法,将原比较顺序(this.属性名 - o.属性名(整型)) 更改为o.属性名 - this.属性名,如:

class Student implements Comparable<Student>{
    int id;
    String name;

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

    @Override
    public int compareTo(Student o) {
        return o.id - this.id; //只能以id进行比较
    }
}
        PriorityQueue<Student> queue3 = new PriorityQueue<>();
        queue3.offer(new Student(11,"rose"));
        queue3.offer(new Student(2,"jack"));
        assert queue3.peek() != null;
        System.out.println(queue3.peek().id+ " " +queue3.peek().name);

这样也能将默认小堆修改为大堆,但是这样做有一定的局限性,Student类只能以id进行比较,不太灵活,所以我们如果想灵活一点,就可以另外实现多个比较器,当想以什么比较时,将对应的比较器传入堆中就好了。


4.拓展Java中常涉及到的比较方式

4.1 比较运算符

         >, < , = ,>=,<=,!= 等等,适用于基本类型比较。

4.2 equals

         主要比较两个字符串是否相等(包括字符串的长度和每个对应的字符),要实现两个相同自定义类型的对象的比较,需要让自定义类型重写其对应的equals方法。

4.3 实现Comparable接口

         implements Comparable<比较的类型>,并重写compareTo方法。

4.4 实现自定义的比较器(实现Comparator接口)

//实现一个自定义比较器
class IdComparator implements Comparator<Student> {

    @Override
    public int compare(Student o1, Student o2) {
        return o2.name.compareTo(o1.name); //以姓名比较
    }
}
        PriorityQueue<Student> queue4 = new PriorityQueue<>(new NameComparator()); //以姓名比较
        queue4.offer(new Student(11,"rose"));
        queue4.offer(new Student(2,"jack"));
        assert queue4.peek() != null;
        System.out.println(queue4.peek().id+ " " +queue4.peek().name);


分享完毕~ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值