优先级队列(堆)

栈 队列: https://blog.csdn.net/2401_83837907/article/details/147976747?spm=1001.2014.3001.5501

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

==》层序遍历存储

小根堆:跟都比左右子树小

大根堆:根都比左右子树大

1.堆的存储方式

堆是⼀棵完全⼆叉树,因此可以层序 的规则采⽤顺序的⽅式来⾼效存储,

(注意:对于⾮完全⼆叉树,则不适合使⽤顺序⽅式进⾏存储,因为为了能够还原⼆叉树,空间中必须要存储空节点,就会导致空间利⽤率⽐较低。)

将元素存储到数组中后,可以对树进⾏还原。假设i为节点在数组中的下标,则有:

• 如果i为0,则i表⽰的节点为根节点,否则i节点的双亲节点为(i-1)/2

• 如果2*i+1⼩于节点个数,则节点i的左孩⼦下标为2*i+1,否则没有左孩⼦

• 如果2*i+2⼩于节点个数,则节点i的右孩⼦下标为2*i+2,否则没有右孩⼦

2.堆的创建

(大根堆)

依此再向下

问题:每棵子树结束的位置不固定==》usedSize(堆中当前元素的个数)

java 复制代码
public class MaxHeap {
    // 存储堆元素的数组
    private int[] elem;
    // 堆中当前元素的个数(有效长度)
    private int usedSize;

    // 构造函数:初始化堆数组
    public MaxHeap(int capacity) {
        //capacity 是堆底层数组的初始容量,用来提前分配数组的大小
        this.elem = new int[capacity];
        this.usedSize = 0;
    }

    // 建堆:把一个普通数组变成堆
    public void createHeap(int[] array) {
        // 把数组拷贝到堆里
        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
        }
        this.usedSize = array.length;
 
        // parent 的入口!!!从【最后一个非叶子节点】开始向下调整
        for (int i = (usedSize - 1 - 1) / 2; i >= 0; i--) {
        siftDown(i, usedSize);  
        }
    }

    /**
     * 堆的向下调整(大顶堆)
     * @param parent 要调整的父节点下标
     * @param usedSize 堆的有效长度(防止越界)
     */
    private void siftDown(int parent, int usedSize) {

        // 1. 先得到左孩子的下标(完全二叉树,左孩子一定是 2*parent+1)
        int child = 2 * parent + 1;

        // 循环条件:孩子下标在堆的有效范围内
        while (child < usedSize) {

            // 2. 左右孩子比较,让child指向值更大的那个孩子
            // 条件:右孩子存在(child+1 < usedSize),且左孩子值 < 右孩子值
            if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
                child++; // child指向右孩子(更大的那个)
            }

            // 3. 用最大的孩子和父节点比较
            if (elem[child] > elem[parent]) {
                // 孩子值 > 父节点值:交换两者
                swap(elem, child, parent);

                // !!!!父节点下沉到孩子位置,继续向下调整
                parent = child;
                child = 2 * parent + 1; // 计算新父节点的左孩子

            } else {
                // 孩子值 ≤ 父节点值:已经满足大顶堆性质,直接结束
                break;
            }
        }
    }

    //交换数组中两个下标的元素
    private void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

小根堆(逻辑一样 只改<,>符号)

java 复制代码
public class MinHeap {
    private int[] elem;
    private int usedSize;

    public MinHeap(int capacity) {
        this.elem = new int[capacity];
        this.usedSize = 0;
    }

    // 建堆
    public void createHeap(int[] array) {
        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
        }
        usedSize = array.length;

        // 入口 parent:最后一个非叶子节点
        for (int i = (usedSize - 2) / 2; i >= 0; i--) {
            siftDown(i, usedSize);
        }
    }

    // 小顶堆 向下调整
    private void siftDown(int parent, int len) {
        int child = 2 * parent + 1;

        while (child < len) {
            // 找 更小 的孩子
            if (child + 1 < len && elem[child] > elem[child + 1]) {
                child++;
            }

            // 孩子 < 父亲 → 交换
            if (elem[child] < elem[parent]) {
                swap(child, parent);
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

    private void swap(int i, int j) {
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }
}


建堆的时间复杂度

此处为了简化使⽤满⼆叉树来证明(时间复杂度看的就是近似值,多⼏个节点不影响最终结果):

3.堆的插入与删除

(1)堆的插入

java 复制代码
/**
     * 堆的向上调整(用于插入元素)
     * @param child 要调整的孩子节点下标
     */
    private void siftUp(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;
            }
        }
    }

    /**
     * 向大顶堆中插入元素
     * @param val 待插入的值
     * @return 插入成功返回true,满了返回false
     */
    public boolean offer(int val) {
        // 自动扩容!!!
        if (usedSize == elem.length) {
            // 新容量 = 原来的 2 倍
            int newCapacity = elem.length * 2;
            // 拷贝数组
            elem = Arrays.copyOf(elem, newCapacity);
        }

        // 放入最后位置
        elem[usedSize] = val;
        siftUp(usedSize);
        usedSize++;
        return true;
    }

(2)堆的删除

java 复制代码
    // ================== 删除堆顶 ==================
    public int poll() {
        if (usedSize == 0) {
            throw new RuntimeException("堆为空");
        }
        swap(elem, 0, usedSize - 1);
        usedSize--;
        siftDown(0, usedSize);
        return elem[usedSize];
    }


堆操作 调整方式 说明
建堆 向下调整(siftDown) 从最后一个非叶子节点往前,对非叶子节点执行
删除堆顶 向下调整(siftDown) 仅对堆顶执行一次
插入元素 向上调整(siftUp) 对新插入的末尾元素执行
堆排序 向下调整(siftDown) 每次取堆顶后,对堆顶执行一次

4.用堆模拟实现优先级队列

java 复制代码
import java.util.Arrays;

public class MaxHeap {
    // 存储堆元素的数组
    private int[] elem;
    // 堆中当前元素的个数(有效长度)
    private int usedSize;

    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 构造函数:使用默认容量
    public MaxHeap() {
        this.elem = new int[DEFAULT_CAPACITY];
        this.usedSize = 0;
    }

    // 构造函数:初始化堆数组
    public MaxHeap(int capacity) {
        if (capacity < 1) {
            capacity = DEFAULT_CAPACITY;
        }
        this.elem = new int[capacity];
        this.usedSize = 0;
    }

    /**
     * 堆的向下调整(大顶堆)
     * @param parent 要调整的父节点下标
     * @param usedSize 堆的有效长度(防止越界)
     */
    private void siftDown(int parent, int usedSize) {
        // 1. 先得到左孩子的下标
        int child = 2 * parent + 1;

        // 循环条件:孩子下标在堆的有效范围内
        while (child < usedSize) {
            // 2. 找更大的孩子
            if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
                child++;
            }

            // 3. 孩子 > 父亲,交换
            if (elem[child] > elem[parent]) {
                swap(elem, child, parent);
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

    /**
     * 交换数组中两个下标的元素
     */
    private void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // ================== 建堆 ==================
    public void createHeap(int[] array) {
        // 扩容到足够大
        if (array.length > elem.length) {
            elem = Arrays.copyOf(elem, array.length * 2);
        }

        System.arraycopy(array, 0, elem, 0, array.length);
        usedSize = array.length;

        // 从最后一个非叶子节点开始调整
        for (int i = (usedSize - 1 - 1) / 2; i >= 0; i--) {
            siftDown(i, usedSize);
        }
    }

    // ================== 向上调整(插入用) ==================
    private void siftUp(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;
            }
        }
    }

    // ================== ✔ 扩容 + 插入 ==================
    public boolean offer(int val) {
        // 自动扩容!!!
        if (usedSize == elem.length) {
            // 新容量 = 原来的 2 倍
            int newCapacity = elem.length * 2;
            // 拷贝数组
            elem = Arrays.copyOf(elem, newCapacity);
        }

        // 放入最后位置
        elem[usedSize] = val;
        siftUp(usedSize);
        usedSize++;
        return true;
    }

    // ================== 删除堆顶 ==================
    public int poll() {
        if (usedSize == 0) {
            throw new RuntimeException("堆为空");
        }
        swap(elem, 0, usedSize - 1);
        usedSize--;
        siftDown(0, usedSize);
        return elem[usedSize];
    }

    // ================== 获取堆顶 ==================
    public int peek() {
        if (usedSize == 0) {
            throw new RuntimeException("堆为空");
        }
        return elem[0];
    }

    // ================== 获取当前元素数量 ==================
    public int size() {
        return usedSize;
    }
}

二.PriorityQueue(默认小根堆)常用接口介绍

1.PriorityQueue中放置的元素必须要能够⽐较⼤⼩,不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出 ClassCastException异常

  1. 不能插⼊null对象,否则会抛出NullPointerException

  2. 没有容量限制,可以插⼊任意多个元素,其内部可以⾃动扩容

  3. 插⼊和删除元素的时间复杂度为O(log₂n)

  4. PriorityQueue底层使⽤了堆数据结构


1. 优先级队列的构造

注意:默认情况下,PriorityQueue队列是⼩堆,如果需要⼤堆需要⽤⼾提供⽐较器

java 复制代码
class IntCmp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;  // 👈 关键在这里
        //return o1 - o2 → 默认小顶堆(升序)
        //return o2 - o1 → 大顶堆(降序)
    }
}

public static void main(String[] args) {
    // 传入比较器 → 变成大顶堆
    PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
    
    p.offer(4);
    p.offer(3);
    p.offer(2);
    p.offer(1);
    p.offer(5);

    System.out.println(p.peek());//5
}

或者简写成:(不用写类)

java 复制代码
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);

2. 插⼊/删除/获取优先级最⾼的元素

优先级队列的扩容说明:

• 如果容量**⼩于64** 时,是按照oldCapacity的2倍⽅式扩容的

• 如果容量**⼤于等于64** ,是按照oldCapacity的1.5倍⽅式扩容的

• 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进⾏扩容

3.top-k问题:最⼩的K个数--利用堆

java 复制代码
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] res=new int[k];
        PriorityQueue<Integer> pq=new PriorityQueue<>();

        for(int i=0;i<arr.length;i++){
            pq.offer(arr[i]);
        }

        for(int i=0;i<k;i++){
            res[i]=pq.poll();
        }
        return res;
    }
}

对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了 (可能数据都不能⼀下⼦全部加载到内存中)。

最佳的⽅式就是⽤堆来解决,基本思路如下:

  1. ⽤数据集合中前K个元素来建堆

前k个最⼤的元素,则建⼩堆 ◦;前k个最⼩的元素,则建⼤堆

  1. ⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素 将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素
java 复制代码
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] res = new int[k];
        
        // 找最大 K 个 → 小顶堆
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        
        // 1. 前 k 个先入堆
        for (int i = 0; i < k; i++) {
            pq.offer(arr[i]);
        }
        
        // 2. 剩余元素依次和堆顶比较
        for (int i = k; i < arr.length; i++) {
            if (arr[i] > pq.peek()) { // 比堆顶大,就替换
                pq.poll();
                pq.offer(arr[i]);
            }
        }
        
        // 堆里就是最大 k 个
        for (int i = 0; i < k; i++) {
            res[i] = pq.poll();
        }
        return res;
    }
}
java 复制代码
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] res = new int[k];
        
        // 找最小 K 个 → 大顶堆
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);
        
        // 1. 前 k 个先入堆
        for (int i = 0; i < k; i++) {
            pq.offer(arr[i]);
        }
        
        // 2. 剩余元素依次比较
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < pq.peek()) { // 比堆顶小,替换
                pq.poll();
                pq.offer(arr[i]);
            }
        }
        
        for (int i = 0; i < k; i++) {
            res[i] = pq.poll();
        }
        return res;
    }
}
相关推荐
胡楚昊2 小时前
Polar PWN (4)
linux·运维·算法
今儿敲了吗2 小时前
51| 数独
算法·深度优先·图论
小樱花的樱花2 小时前
C++引用:高效编程的技巧
开发语言·数据结构·c++·算法
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:动态规划-最长子序列
c语言·c++·算法·动态规划
东离与糖宝2 小时前
成本砍半!Java 生产环境 INT4/INT8 模型量化 + 提示词缓存落地
java·人工智能
Lyyaoo.2 小时前
Spring中Bean的作用域与生命周期
java·后端·spring
遇见你...2 小时前
B03 SpringMVC拦截器
java·开发语言
星晨雪海2 小时前
缓存更新操作实例
java·spring·缓存
沉鱼.442 小时前
进制转换题
开发语言·c++·算法