JAVA版数据结构-----优先级队列(堆)

概念

1.定义:

队列是一种 先进先出(FIFO) 的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。

2.特性:

Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
    ClassCastException异常。
  2. 不能插入null对象,否则会抛出NullPointerException。
  3. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。
  4. 插入和删除元素的时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)
  5. PriorityQueue底层使用了堆数据结构。

注意:
堆(Heap),首先我们需要搞清楚的是,此处我们所讲的堆和JAVA中的堆(JVM持有内存中的一个内存区域)以及操作系统中的堆(每个进程所持有的一定内存空间里面的特定内存区域)是不相同的!!!

3.优先级队列的构造
在这里插入图片描述

static void TestPriorityQueue(){
        // 创建一个空的优先级队列,底层默认容量是11
        PriorityQueue<Integer> q1 = new PriorityQueue<>();
        // 创建一个空的优先级队列,底层的容量为initialCapacity
        PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
        ArrayList<Integer> list = new ArrayList<>();
        list.add(4);
        list.add(3);
        list.add(2);
        list.add(1);
        // 用ArrayList对象来构造一个优先级队列的对象
        // q3中已经包含了三个元素
        PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
        System.out.println(q3.size());
        System.out.println(q3.peek());
   }

注意: 默认情况下,PriorityQueue队列底层默认容量是11。

4.优先级是如何判定的?

public class Test {
    public static void main(String[] args) {
        //定义一个优先队列,并且打印输出!
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        int[] arr = {9,5,2,7,3,6,8};
        for (int x: arr){
            queue.offer(x);
        }
        System.out.print("优先队列输出结果:");
        while (!queue.isEmpty()){
            Integer x = queue.poll();
            System.out.print(x+" ");
        }

运行结果:在这里插入图片描述
那这里就有一个疑问了:——>优先级队列中的元素的 “大小关系” 该如何制定???

再看一则代码示例:

import java.util.Comparator;
import java.util.PriorityQueue;

class Boy implements Comparable<Boy> {
    public String name;
    public int age;
    public int money; // 有钱
    public int face;  // 有颜

    public Boy(String name, int age, int money, int face) {
        this.name = name;
        this.age = age;
        this.money = money;
        this.face = face;
    }

    @Override
    public int compareTo(Boy o) {
        return o.money - this.money;
    }
}

class BoyComparator implements Comparator<Boy> {
    @Override
    public int compare(Boy o1, Boy o2) {
        return o1.money - o2.money;
    }
}

public class Test {
    public static void main(String[] args) {
        Boy[] arr1 = {
                new Boy("张三", 20, 10, 100),
                new Boy("李四", 40, 100, 10),
                new Boy("王五", 30, 50, 50),
                new Boy("赵六", 50, 120, 5),
        };

        PriorityQueue<Boy> queue1 = new PriorityQueue<>();
        for (Boy boy : arr1) {
            queue1.offer(boy);
        }
        while (!queue1.isEmpty()) {
            Boy cur = queue1.poll();
            System.out.println(cur.name);
        }
    }
}

运行结果:在这里插入图片描述

这里需要讲一下java.lang.Comparable这个包,它其实是标准库内置的一个接口,里面只有一个抽象方法compareTo(Object other),通过这个方法来指定对象自身和另一个对象之间的大小关系!用来定义"比较规则",除此之外,还有一个Comparator,也可以起到类似作用!两者不同之处在于:comparable接口,它是哪个类需要比较,就需要让这个类实现接口!Comparator接口,定义一个新类,实现该接口,里面compare方法的参数是两个,对应到要比较的类。

使用Comparable已经能解决大部分情况~少数情况下使用Comparator,如需要指定多重比较规则的时候!

5. 插入/删除/获取优先级最高的元素
在这里插入图片描述
代码实现:

static void TestPriorityQueue2(){
   int[] arr = {4,1,9,2,8,0,7,3,6,5};
        // 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
        // 否则在插入时需要不多的扩容
        // 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        for (int e: arr) {
            q.offer(e);
        }
        System.out.println("\n"+"打印优先级队列中有效元素个数:"+q.size());
        System.out.println("\n"+"获取优先级最高的元素:"+q.peek());
        System.out.println("\n"+"从优先级队列中删除两个元素之和,再次获取优先级最高的元素!!!");
        q.poll();
        q.poll();
        System.out.println("\n"+"打印优先级队列中有效元素个数:"+q.size());
        System.out.println("\n"+"获取优先级最高的元素:"+q.peek());
        System.out.println("\n"+"入队一个元素“0”!!!");
        q.offer(0);
        System.out.println("\n"+"获取优先级最高的元素:"+q.peek());
        System.out.println("\n"+"将优先级队列中的有效元素删除掉,检测其是否为空!!!");
        q.clear();
        if(q.isEmpty()){
            System.out.println("优先级队列已经为空!!!");
        }else{
            System.out.println("优先级队列不为空");
        }
    }
}

运行结果:在这里插入图片描述
6.优先级队列的应用

在当前阶段,主要是有两个用途:
1.用来排序(堆排序)(下一篇博客涉及);
2.用来解决topK问题。

假设有1亿个数据(int),大约400M大小,想从其中找出前一千个最大的数字!(t这是一个topK问题)

方案一:直接针对这1亿个数据的数组进行建造大堆,接下来循环1000次,进行取堆顶元素/删除堆顶元素操作~这个方案得到的前一千个数据本身也是有序的!!!

方案二:创建一个大小为1000的小堆,遍历这1亿个数据,依次往堆里进行插入,如果堆没满,就直接插入,如果堆满了,此时小堆的堆顶就是这个堆里的最小值,就拿当前值和这个堆顶元素进行比较,如果当前值比这个堆顶元素还小,那就直接pass;如果当前值比堆顶元素大,就删除原来的堆顶元素,把新的这个元素插入堆中。最终遍历完1亿个元素后此时堆里剩下的元素就一定是前1000个元素~这个方案比较节省时间和空间消耗!!!

7.优先级队列的模拟实现

堆的定义:

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

图一:最大堆; 图二:最小堆在这里插入图片描述
堆的性质:

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

堆的存储方式:

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储!
在这里插入图片描述
注意: 对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

性质:
如果 i 为 0,则 i 表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2;
如果2 * i + 1 小于节点个数,则节点 i 的左孩子下标为2 * i + 1,否则没有左孩子;
如果2 * i + 2 小于节点个数,则节点 i 的右孩子下标为2 * i + 2,否则没有右孩子。

堆的基本操作:

堆的创建:给定一个数组,整理成堆这样的结构(转成完全二叉树并且使用数组形式来存储,满足下标关系和堆的关系)。

堆的创建可以使用向上调整实现,也可以使用向下调整实现。

向下调整实现代码:

 /*
     向下调整是创建堆的一个核心操作
     前提条件, 要求当前被调整节点的左右子树都已经是堆了!
     方法参数给出了一个 size 表示当前数组的有效元素大小.
     虽然可以通过 arr.length 取到大小, 这个大小是数组的总的大小
     index 表示从这个位置开始进行向下调整,
     还是按照大堆的方式来实现.
     时间复杂度 O(logN)
     如果数据是依次 (* 2 )(/ 2) 的方式变化的时候, 时间复杂度基本都和 logN 相关
    */

    public static void shiftDown(int[] arr, int size, int index) {
        int parent = index;
        int child = 2 * parent + 1;//此处得到的是左子树
        while (child < size) {
            // 需要找到左右子树中较大的值
            // 左右子树下标差 1,child+1得到的是右子树
            if (child + 1 < size && arr[child + 1] > arr[child]) {
                child = child + 1;
            }
            // 当上述条件执行完后, 就能保证 child 指向左右子树较大的元素。
            // 拿父节点和刚才找出的这个较大的节点再去比较, 看是否符合大堆的要求!
            if (arr[parent] < arr[child]) {
                // 不满足大堆的要求, 交换两个元素。
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
            } else {
                // 调整完毕了, 已经把父元素放到了合适的位置上
                break;
            }
            // 更新 parent 和 child 以备下次循环
            parent = child;
            child = 2 * parent + 1;
        }
    }

向上调整代码实现:

 /*
    向上调整只需要保证该节点比父节点大即可!!!
     */

    public static void shiftUp(int[] arr, int size, int index) {
        int child = index;
        int parent = (child - 1) / 2;
        // 如果 child 为 0, 说明已经调整到最上面了
        while (child > 0) {
            if (arr[parent] < arr[child]) {
                // 不符合大堆的要求
                // 交换两个元素
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
            } else {
                break;
            }
            child = parent;
            parent = (child - 1) / 2;
        }
    }

堆的创建代码实现:

// 建堆操作
    public static void createHeap(int[] array) {
        /*
         基于向下调整的建堆:
         从后往前遍历数组, 针对每个下标都去进行向下调整即可~~
         此处的循环不必从 length - 1 开始.从叶子节点往下进行查找是不合适的~~
         从第一个非叶子节点开始进行向下调整,
         实际向下调整的时候不一定非得从最后一个元素的下标开始.
         可以从最后一个非叶子节点开始即可.就可以通过最后一个节点再找到父节点即可~~
         直观上看, 建堆的时间复杂度 O(NlogN)
         但是实际上, 不是, 实际上是 O(N) (数学推导出来的)
         */
        for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
            shiftDown(array, array.length, i);
        }
    }

向对中插入元素代码实现:

// 往堆中插入元素
    // 这就表示当前存储堆的数组
    private int[] arr = new int[100];
    private int size = 0;
    public void offer(int val) {
        if (size >= arr.length) {
            // 插入失败, 已经满了.
            // 也可以实现扩容逻辑.
            return;
        }
        // 先是把这个元素给尾插到数组末尾
        arr[size] = val;
        size++;
        // 把最后的这个元素进行向上调整
        shiftUp(arr, size, size - 1);
    }

获取堆顶元素代码实现:

 // 获取堆顶元素
    public Integer peek() {
        if (size == 0) {
            return null;
        }
        return arr[0];
    }

删除堆顶元素代码实现:

// 删除操作(一定是删除堆顶的元素)
    public Integer poll() {
        if (size == 0) {
            return null;
        }
        int result = arr[0];

        // 交换 0 号元素和 size - 1 号元素
        int tmp = arr[0];
        arr[0] = arr[size - 1];
        arr[size - 1] = tmp;

        // size--, 把最后的元素干掉
        size--;

        // 从 0 号元素开始, 往下进行向下调整
        shiftDown(arr, size, 0);
        return result;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值