优先级队列(堆)

优先级队列(堆)

PriorityQueue 是 Java 集合框架中的一个重要类,它底层基于**堆(Heap)**这种数据结构实现,能够保证每次取出的元素都是队列中优先级最高的(默认是最小元素,即小顶堆)。

堆的模拟实现

PriorityQueue 的底层核心是堆,我们先通过代码手动实现一个大顶堆(每个父结点的值都大于等于其子结点的值),理解堆的初始化、插入、删除、调整等核心操作。

完整模拟实现代码

import java.util.Arrays;

/**
 * 模拟实现大顶堆(对应 PriorityQueue 底层结构)
 * 底层用数组存储数据,实现堆的初始化、插入、删除、排序等核心操作
 */
public class MyHeap {

    // 存储堆元素的数组
    public int[] elem;
    // 记录堆中有效元素的个数(区别于数组容量)
    public int usedSize;

    // 构造方法:初始化数组容量为10
    public MyHeap() {
        this.elem = new int[10];
    }

    /**
     * 初始化堆的元素数组
     * @param arr 待初始化的原始数组
     */
    public void initArray(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            this.elem[i] = arr[i];
            this.usedSized++;
        }
    }

    /**
     * 构建大顶堆(核心:从最后一个非叶子结点开始向下调整)
     */
    public void createHeap() {
        // 最后一个非叶子结点的索引:(usedSized-1-1) = (最后一个元素索引 - 1) / 2
        for (int parent = (usedSized - 2) / 2; parent >= 0; parent--) {
            // 对每个非叶子结点进行向下调整,构建大顶堆
            siftDown(parent, usedSized);
        }
    }

    /**
     * 堆的向下调整(核心方法:用于构建堆、删除堆顶元素后调整堆)
     * @param parent 待调整的父结点索引
     * @param usedSized 堆的有效元素个数(调整范围边界)
     */
    private void siftDown(int parent, int usedSized) {
        // 先获取左孩子结点索引(堆的数组存储特性:左孩子 = 2*parent + 1)
        int child = parent * 2 + 1;

        // 循环条件:孩子结点索引在有效范围内(说明存在孩子结点)
        while (child < usedSized) {
            // 1. 找到左右孩子中值较大的那个孩子(右孩子存在且右孩子值 > 左孩子值)
            if (child + 1 < usedSized && elem[child] < elem[child + 1]) {
                child++; // 此时 child 指向右孩子(较大值的孩子)
            }

            // 2. 比较较大孩子与父结点的值,若孩子值 > 父结点值,交换两者
            if (elem[child] > elem[parent]) {
                // 交换父结点和孩子结点的值
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;

                // 3. 向下继续调整:将父结点更新为当前孩子结点,孩子结点更新为新父结点的左孩子
                parent = child;
                child = 2 * parent + 1;
            } else {
                // 若孩子值 <= 父结点值,说明当前堆结构已满足大顶堆要求,直接退出
                break;
            }
        }
    }

    /**
     * 向堆中插入元素(对应 PriorityQueue 的 offer 方法)
     * @param val 待插入的元素值
     */
    public void offer(int val) {
        // 1. 判断堆是否已满,若已满则扩容(扩容为原容量的2倍)
        if (isFull()) {
            this.elem = Arrays.copyOf(elem, 2 * elem.length);
        }

        // 2. 将新元素插入到堆的末尾(有效元素个数的位置)
        elem[usedSized] = val;

        // 3. 向上调整堆,恢复大顶堆的特性
        siftUp(usedSized);

        // 4. 有效元素个数加1
        usedSized++;
    }

    /**
     * 堆的向上调整(核心方法:用于插入元素后调整堆)
     * @param child 待调整的孩子结点索引(刚插入的元素索引)
     */
    private void siftUp(int child) {
        // 1. 获取当前孩子结点的父结点索引(堆的数组存储特性:父结点 = (child - 1) / 2)
        int parent = (child - 1) / 2;

        // 2. 循环条件:父结点索引 >= 0(说明存在父结点)
        while (parent >= 0) {
            // 3. 比较孩子结点与父结点的值,若孩子值 > 父结点值,交换两者
            if (elem[child] > elem[parent]) {
                // 交换父结点和孩子结点的值
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;

                // 4. 向上继续调整:将孩子结点更新为当前父结点,父结点更新为新孩子结点的父结点
                child = parent;
                parent = (child - 1) / 2;
            } else {
                // 若孩子值 <= 父结点值,说明当前堆结构已满足大顶堆要求,直接退出
                break;
            }
        }
    }

    /**
     * 判断堆是否已满
     * @return 已满返回 true,否则返回 false
     */
    public boolean isFull() {
        return usedSized == this.elem.length;
    }

    /**
     * 删除堆顶元素(对应 PriorityQueue 的 poll 方法,大顶堆删除的是最大值)
     * @return 被删除的堆顶元素值,堆为空时返回 -1
     */
    public int poll() {
        // 1. 判断堆是否为空,若为空返回 -1
        if (isEmpty()) {
            return -1;
        }

        // 2. 保存堆顶元素值(用于返回)
        int val = elem[0];

        // 3. 交换堆顶元素和最后一个有效元素
        swap(0, usedSized - 1);

        // 4. 有效元素个数减1(逻辑上删除最后一个元素,即原堆顶元素)
        usedSized--;

        // 5. 向下调整堆,恢复大顶堆的特性
        siftDown(0, usedSized);

        // 6. 返回被删除的堆顶元素
        return val;
    }

    /**
     * 交换数组中两个索引位置的元素
     * @param a 第一个元素索引
     * @param b 第二个元素索引
     */
    private void swap(int a, int b) {
        int tmp = elem[a];
        elem[a] = elem[b];
        elem[b] = tmp;
    }

    /**
     * 判断堆是否为空
     * @return 为空返回 true,否则返回 false
     */
    public boolean isEmpty() {
        return usedSized == 0;
    }

    /**
     * 获取堆顶元素(对应 PriorityQueue 的 peek 方法)
     * @return 堆顶元素值,堆为空时返回 -1
     */
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return elem[0];
    }

    /**
     * 堆排序(基于大顶堆实现升序排序)
     */
    public void heapSort() {
        // 1. 记录最后一个有效元素的索引
        int end = usedSized - 1;

        // 2. 循环交换堆顶元素和最后一个有效元素,并调整堆
        while (end > 0) {
            // 交换堆顶(最大值)和当前最后一个有效元素
            swap(0, end);

            // 向下调整堆(调整范围为 [0, end),不包含 end)
            siftDown(0, end);

            // 最后一个有效元素索引向前移动一位
            end--;
        }
    }
}

核心方法说明

  1. createHeap() 构建堆:从最后一个非叶子结点开始,依次对每个结点执行向下调整,最终将普通数组转换为大顶堆,时间复杂度为 O(n)。
  2. siftDown() 向下调整:堆构建、堆顶元素删除后的核心调整方法,通过比较父结点与孩子结点,将较小的父结点“下沉”到合适位置。
  3. offer() + siftUp() 插入元素:先将元素插入到数组末尾,再通过向上调整,将较大的新元素“上浮”到合适位置,保证堆的特性不被破坏。
  4. poll() 删除堆顶:通过交换堆顶与最后一个元素,再执行向下调整,高效删除堆顶元素,时间复杂度为 O(logn)。

堆的实际使用

理解了堆的底层实现后,我们来学习 Java 中 PriorityQueue 的实际使用,它默认实现了小顶堆,也可以通过自定义比较器实现大顶堆。

堆的构造方法

PriorityQueue 提供了多种构造方法,常用的有以下三种,满足不同场景的初始化需求:

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

public class PriorityQueueTest {
    public static void main(String[] args) {
        // 1. 无参构造:创建一个空的 PriorityQueue,默认容量为11,默认是小顶堆
        PriorityQueue<Integer> queue1 = new PriorityQueue<>();

        // 2. 指定容量构造:创建一个指定初始容量的 PriorityQueue,默认是小顶堆
        PriorityQueue<Integer> queue2 = new PriorityQueue<>(100);

        // 3. 集合构造:通过已有的 Collection 集合创建 PriorityQueue,默认是小顶堆
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        PriorityQueue<Integer> queue3 = new PriorityQueue<>(list);
    }
}

大顶堆的构造(自定义比较器)

PriorityQueue 默认是小顶堆,若需要大顶堆,需要通过自定义比较器实现,常用两种方式:实现 Comparator 接口、使用 Lambda 表达式(简洁高效)。

方式1:实现 Comparator 接口

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

/**
 * 实现 Comparator 接口,自定义比较规则构建大顶堆
 */
public class MaxHeapTest implements Comparator<Integer> {
    public static void main(String[] args) {
        // 传入自定义比较器实例,构建大顶堆
        PriorityQueue<Integer> queue = new PriorityQueue<>(new MaxHeapTest());

        // 向队列中插入元素
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);

        // 输出堆顶元素(预期为 5,大顶堆堆顶是最大值)
        System.out.println("堆顶元素:" + queue.peek());
    }

    /**
     * 重写 compare 方法,定义比较规则
     * @param o1 第一个待比较元素
     * @param o2 第二个待比较元素
     * @return 返回值 > 0:o1 > o2;返回值 = 0:o1 = o2;返回值 < 0:o1 < o2
     * 此处返回 o2 - o1,实现大顶堆(优先保留较大值)
     */
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}

方式2:Lambda 表达式(推荐,更简洁)

import java.util.PriorityQueue;

public class MaxHeapLambdaTest {
    public static void main(String[] args) {
        // 使用 Lambda 表达式直接传入比较规则,构建大顶堆
        PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2 - o1);

        // 向队列中插入元素
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);

        // 输出堆顶元素(预期为 5)
        System.out.println("堆顶元素:" + queue.peek());
    }
}

堆的常用方法

PriorityQueue 提供了一套简洁的方法用于操作队列,核心方法与我们模拟实现的堆方法一一对应,常用方法如下:

import java.util.PriorityQueue;

public class PriorityQueueCommonMethods {
    public static void main(String[] args) {
        // 创建一个默认的小顶堆
        PriorityQueue<Integer> queue = new PriorityQueue<>();

        // 1. offer(E e):向队列中插入元素,插入失败抛出异常(实际开发中常用,比 add 更安全)
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        System.out.println("插入元素后队列:" + queue);

        // 2. peek():获取堆顶元素(不删除),队列为空时返回 null
        Integer top = queue.peek();
        System.out.println("堆顶元素:" + top);

        // 3. poll():删除并返回堆顶元素,队列为空时返回 null
        Integer removedTop = queue.poll();
        System.out.println("被删除的堆顶元素:" + removedTop);
        System.out.println("删除堆顶后队列:" + queue);

        // 4. size():返回队列中有效元素的个数
        int size = queue.size();
        System.out.println("队列元素个数:" + size);

        // 5. clear():清空队列中的所有元素
        queue.clear();
        System.out.println("清空后队列:" + queue);

        // 6. isEmpty():判断队列是否为空,为空返回 true,否则返回 false
        boolean isEmpty = queue.isEmpty();
        System.out.println("队列是否为空:" + isEmpty);
    }
}

运行结果参考

插入元素后队列:[1, 2, 3]
堆顶元素:1
被删除的堆顶元素:1
删除堆顶后队列:[2, 3]
队列元素个数:2
清空后队列:[]
队列是否为空:true
二叉树 2025-08-19
Map和Set 2025-09-15

评论区