• 【数据结构】优先级队列 - 堆


    1.优先级队列

    1.1概念

    之前我们学习过队列是一种先进先出的数据结构,但在某些场景下,操作的数据具有优先级,在出队列时,需要优先级高的数据先出队列。比如在打游戏时来电话了,那么操作系统应该先处理电话;还有在游戏英雄联盟中泰坦的Q的优先级比机器人高;

    诸如这些情况下,我们需要数据结构提供两种最基本的操作,首先添加新的对象,然后返回最高优先级对象,这种数据结构就是优先级队列(Priority Queue).

    2.堆的模拟实现

    在JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆通常是一个可以被看做一棵完全二叉树的数组对象。

    2.1 堆的概念

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

    堆满足以下性质:

    • 堆中某个节点的值总是不大于或不小于其父节点的值。
    • 堆总是一棵完全二叉树。
      在这里插入图片描述

    2.2 堆的存储方式

    因为堆是一颗完全二叉树,所以我们可以利用层序遍历的方式来存储元素
    在这里插入图片描述
    注:对应非完全二叉树则不适用于顺序方式来存储,因为为了通过顺序表来还原二叉树,则空间中就需要存储空结点,这样会导致空间利用率低。

    当元素存储到数组中后,我们如何还原二叉树呢?假设i为节点在数组中的下标,

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

    用代码来表示就是:

    	parent = i / 2;
    	leftChild = 2 * i;
    	rightChild = 2 * i + 1;
    	//或者已知父节点为root
    	parent = root;
    	leftChild = (2 * parent) + 1;
    	rightChild = (2 * parent)  + 1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.3 堆的创建

    2.3.1 堆的向下调整(shiftDown)

    对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如何将其创建成堆呢?
    在这里插入图片描述
    通过观察可知:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。

    建小堆为例:

    1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
    2. 如果parent的左孩子存在,即:child < len, 进行以下操作,直到parent的左孩子不存在或者已经满足堆的性质。
      2.1 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
      2.2 将parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束;否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后重复2 。
      在这里插入图片描述
    /**
     * 向下调整 -
     * 时间复杂度 0(logN)
     * @param arr 二叉树中存放的元素
     * @param root 根节点的下标
     * @param len 数组长度
     */
    private static void shiftDown(int[] arr, int root, int len) {
        int parent = root;
        int child = (2 * parent) + 1;//左孩子
        //孩子节点索引不会大于len
        while (child < len) {
            //使child索引位置保存子节点的最大值
            if (child + 1< len && arr[child] < arr[child + 1]) {
                child++;
            }
            if (arr[child] > arr[parent]) {
                swap(arr, child, parent);
                parent = child;
                child = (2 * parent) + 1;
            } else {
                break;
            }
        }
    }
    
    private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    2.3.2 堆的创建

    对于一个普通序列,即根的左右子树都不满足堆的特性,那么该如何调整呢?

    在这里插入图片描述

    /**
     * 建堆
     * 时间复杂度 0(n)
     * @param arr
     */
    private static void createHeap(int[] arr) {
    	// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
        for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) {
            shiftDown(arr, p, arr.length);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3.3 建堆的时间复杂度

    堆是完全二叉树,这里我们简化用满二叉树来证明
    在这里插入图片描述
    所以,建堆的时间复杂度为0(N)

    2.4堆的插入和删除

    2.4.1 堆的插入

    插入步骤:

    1. 先把元素放到堆最后(即数组最后),如果空间不足,则需要扩容。
    2. 将插入的元素向上调整,直到满足堆的性质
      在这里插入图片描述
    	
    /**
     * 向堆中插入元素
     * @param val
     */
    public void push(int val) {
        //判满
        if (ifFull()) {
            this.elem = Arrays.copyOf(elem, elem.length * 2);
        }
        //插入
        this.elem[usedSize] = val;
        //调整
        shiftUp(usedSize);
        //有效数据调整
        usedSize++;
    }
    
    private boolean ifFull() {
        return elem.length == usedSize;
    }
    
    /**
     * 向上调整 - 向堆中添加元素 - 只能添加到末尾然后向上调整
     */
    private  void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (child > 0) {
            //这里本来就是小根堆,根节点的值一定比原来的左右子结点小,所以不需要比较左右节点的大小
            if (elem[child] > elem[parent]) {
                swap(elem, child, parent);
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    2.4.3 堆的删除

    删除步骤:
    注:堆的删除一定是删除优先级高的元素(即堆顶元素)

    1. 将堆顶元素和堆中最后的元素交换
    2. 有效数据减一
    3. 从堆顶元素开始,向下调整
      在这里插入图片描述
    /**
     * 出堆顶元素
     */
    public void pollHeap() {
        if (isEmpty()) {
            System.out.println("队列为空");
            return;
        }
        //把堆顶元素和堆尾元素交换
        swap(elem, 0, usedSize - 1);
        //调整大小
        usedSize--;
        //堆的结构改变,所以要向下调整
        shiftDown(elem, 0, usedSize);
    }
    /**
     * 向下调整 -
     * 时间复杂度 0(logN)
     * @param arr 二叉树中存放的元素
     * @param root 根节点的下标
     * @param len 数组长度
     */
    private static void shiftDown(int[] arr, int root, int len) {
        int parent = root;
        int child = (2 * parent) + 1;//左孩子
        //孩子节点索引不会大于len
        while (child < len) {
            //使child索引位置保存子节点的最大值
            if (child + 1< len && arr[child] < arr[child + 1]) {
                child++;
            }
            if (arr[child] > arr[parent]) {
                swap(arr, child, parent);
                parent = child;
                child = (2 * parent) + 1;
            } else {
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    3.Java中的优先级队列

    3.1 PriorityQueue

    Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,我们主要介绍PriorityQueue。

    使用注意事项:

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

    3.2 PriorityQueue常用接口介绍

    1. 构造方法

    它的构造方法在JDK1.8中一共有7种,下面我们介绍几种常见的构造方法

    构造器功能介绍
    PriorityQueue()创建一个空的优先级队列,默认容量是11
    PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常
    PriorityQueue(Collection<? extends E> c)用一个集合来创建优先级队列
    PriorityQueue(Comparator<? super E> comparator)设置自己的比较器
        public static void main(String[] args) {
            // 创建一个空的优先级队列,底层默认容量是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<Integer> p4 = new PriorityQueue<>(new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o2 - o1;
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    注:在默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户自定义比较器

    1. 自定义比较器
    class IntCmp implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 使用匿名内部类
            PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o2 - o1;
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 自定义类中实现Comparable接口
    class Student implements Comparable<Student> {
        public int age;
    
        public Student(int age) {
            this.age = age;
        }
    
        @Override
        public int compareTo(Student o) {
            return o.age - this.age;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "age=" + age +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.常用方法

    函数名功能介绍
    boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度0(logN) ,注意:空间不够时候会进行扩容
    E peek()获取优先级最高的元素,如果优先级队列为空,返回null
    E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
    int size()获取有效元素的个数
    void clear()清空队列
    boolean isEmpty()检测优先级队列是否为空,空返回true

    3. 扩容方式

    JDK1.8 中PriorityQueue的扩容方式及其源码

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    扩容介绍:

    • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
    • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
    • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

    4. 堆的应用

    4.1 堆排序

    堆排序即利用堆的思想来进行排序,总共分为两个步骤

    1. 建堆
    • 升序:建大堆
    • 降序:建小堆
    1. 利用堆删除思想来进行排序
      在这里插入图片描述
    /**
         * 时间复杂度:0(N*logN)
         * 空间复杂度:0(1)
         * 稳定性:不稳定
         * @param arr
         */
        public static void heapSort(int[] arr) {
            //建堆
            createHeap(arr);
            int end = arr.length - 1;
            while (end >= 0) {
                swap(arr, 0, end);
                shiftDown(arr, 0, end);
                end--;
            }
        }
    
        /**
         * 向下调整 -
         * 时间复杂度 0(logN)
         * @param arr 二叉树中存放的元素
         * @param root 根节点的下标
         * @param len 数组长度
         */
        private static void shiftDown(int[] arr, int root, int len) {
            int parent = root;
            int child = (2 * parent) + 1;//左孩子
            //孩子节点索引不会大于len
            while (child < len) {
                //使child索引位置保存子节点的最大值
                if (child + 1< len && arr[child] < arr[child + 1]) {
                    child++;
                }
                if (arr[child] > arr[parent]) {
                    swap(arr, child, parent);
                    parent = child;
                    child = (2 * parent) + 1;
                } else {
                    break;
                }
            }
        }
    
        /**
         * 建堆
         * 时间复杂度 0(n)
         * @param arr
         */
        private static void createHeap(int[] arr) {
            for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) {
                shiftDown(arr, p, arr.length);
            }
    
        }
        private static void swap(int[] arr, int i, int j) {
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    4.2 Top-k问题

    TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,所以对于一般排序是不可取的,最佳的方式是堆排序,基本思想如下:

    1. 用数据集合中前K个元素来建堆
      1.1 前k个最大的元素,则建小堆
      1.2 前k个最小的元素,则建大堆
    2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

  • 相关阅读:
    MySQL和Java程序建立连接的底层原理(JDBC),一个SQL语句是如何执行的呢?
    经历了源码的痛苦,掌握DRF的核心序列化器
    Alpine镜像安装telnet
    路由算法简介
    【Elasticsearch索引】索引基础操作
    JavaScript中错误处理
    R语言空间分析、模拟预测与可视化
    Scala系列-4、scala中特质、柯里化、闭包等
    程序员都看不懂的代码
    爬虫代理ip池创建【使用redis TTL实现】
  • 原文地址:https://blog.csdn.net/weixin_61543874/article/details/125503103