优先队列的实现:基于最小堆的 Java 实现

优先队列是一种重要的数据结构,与普通队列不同,它每次从队列中取出的是具有最高优先级的元素。本文将介绍如何使用最小堆来实现优先队列,并提供详细的 Java 代码示例和解释。

什么是优先队列?

优先队列是一种抽象数据类型,其中每个元素都有一个与之相关的优先级。在删除操作中,总是删除具有最高优先级的元素(对于最小堆来说是最小值)。优先队列的典型应用包括任务调度、图算法(如 Dijkstra 算法)等。

为什么选择最小堆?

最小堆是一种完全二叉树,能够高效地进行插入和删除操作:

  • 插入操作:时间复杂度为 (O(\log n))。
  • 删除最小元素操作:时间复杂度为 (O(\log n))。

最小堆的这种高效性使其成为实现优先队列的理想选择。

构造树的选择:完全二叉树的数组表示

在实现最小堆时,我们有多种选择来构造树结构,但完全二叉树的数组表示方法是最为高效和适用的。以下是一些常见的树结构及其优缺点对比,说明我们为什么选择完全二叉树的数组表示。

实现方式 1:显式引用的树结构

Tree1A

public class Tree1A<Key> {
  Key k;
  Tree1A left;
  Tree1A middle;
  Tree1A right;
  ...
}

优点:

  • 结构清晰,容易理解。
  • 方便直接操作每个节点和子节点。

缺点:

  • 每个节点都需要存储多个引用(left, middle, right),占用额外内存。
  • 遍历和查找操作的复杂度较高,尤其是对于非二叉树的情况。

Tree1B

public class Tree1B<Key> {
  Key k;
  Tree1B[] children;
  ...
}

优点:

  • 结构较为灵活,可以处理任意数量的子节点。

缺点:

  • 需要为每个节点存储一个子节点数组,内存开销较大。
  • 访问子节点时需要遍历数组,效率较低。

Tree1C

public class Tree1C<Key> {
  Key k;
  Tree1C favoredChild;
  Tree1C sibling;
  ...
}

优点:

  • 适用于偏斜树(如二叉堆),可以优化特定树结构的存储。

缺点:

  • 代码复杂度较高,尤其是在处理兄弟节点关系时。
  • 内存开销较大,需要存储额外的指针。
实现方式 2:父数组和键数组
public class Tree2<Key> {
  Key[] keys;
  int[] parents;
  ...
}

优点:

  • 只需两个数组,存储效率高。
  • 可以直接通过数组索引访问父节点和子节点,访问效率高。

缺点:

  • 需要额外的父数组来存储父节点索引。
  • 对于完全二叉树,这种表示有些冗余,因为完全二叉树可以直接通过索引计算父子关系。
实现方式 3:完全二叉树的数组表示
public class TreeC<Key> {
  Key[] keys;
  ...
}

优点:

  • 存储效率最高,只需要一个数组,没有额外的指针或索引开销。
  • 通过简单的索引计算(例如,对于节点i,其左子节点为2i+1,右子节点为2i+2)可以高效访问节点。

缺点:

  • 适用于完全二叉树,对于不完全二叉树,需要处理“间隙”问题。
  • 不适合表示非完全二叉树或其他非规则树结构。

结论

对于堆这种完全二叉树结构,用于优先队列的实现时,**实现方式 3(完全二叉树的数组表示)**是最优选择。

理由如下:

  • 存储效率:只需要一个数组来存储节点,无需额外存储指针或索引。
  • 访问效率:通过索引计算可以高效访问父节点和子节点。
  • 代码简单性:实现和维护简单,不需要复杂的指针操作。

在实现优先队列时,由于堆的性质决定了其是一个完全二叉树,完全二叉树的数组表示方式最为高效和简单,因此是最佳选择。

最小堆优先队列的实现

我们将用数组实现一个最小堆,以下是关键方法的详细解释和代码。

1. 数据结构

首先,我们定义了一个泛型类 MinHeapPriorityQueue,用来存储我们的最小堆:

import java.util.NoSuchElementException;

public class MinHeapPriorityQueue<Key extends Comparable<Key>> {
    private Key[] keys; // 用于存储堆元素的数组
    private int size; // 当前堆中的元素数量

    // 构造函数,初始化优先队列,指定初始容量
    public MinHeapPriorityQueue(int capacity) {
        keys = (Key[]) new Comparable[capacity];
        size = 0;
    }

    // 检查堆是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    // 返回堆中的元素数量
    public int size() {
        return size;
    }

    // 返回节点 k 的父节点索引
    private int parent(int k) {
        return (k - 1) / 2;
    }

    // 返回节点 k 的左子节点索引
    private int leftChild(int k) {
        return 2 * k + 1;
    }

    // 返回节点 k 的右子节点索引
    private int rightChild(int k) {
        return 2 * k + 2;
    }

    // 交换索引 i 和 j 处的元素
    private void swap(int i, int j) {
        Key temp = keys[i];
        keys[i] = keys[j];
        keys[j] = temp;
    }
2. 核心操作:swim 和 sink

swim 方法用于在插入元素时维护堆的性质,通过将新插入的元素上浮到适当位置:

    // 上浮操作,用于维持堆的性质
    private void swim(int k) {
        while (k > 0 && keys[parent(k)].compareTo(keys[k]) > 0) {
            swap(k, parent(k));
            k = parent(k);
        }
    }

sink 方法用于在删除最小元素时维护堆的性质,通过将替换到根位置的元素下沉到适当位置:

    // 下沉操作,用于维持堆的性质
    private void sink(int k) {
        while (leftChild(k) < size) {
            int j = leftChild(k);
            if (j < size - 1 && keys[j].compareTo(keys[j + 1]) > 0) {
                j++;
            }
            if (keys[k].compareTo(keys[j]) <= 0) {
                break;
            }
            swap(k, j);
            k = j;
        }
    }
3. 添加和删除操作

add 方法用于向优先队列添加新元素:

    // 向堆中添加新元素
    public void add(Key key) {
        if (size == keys.length) {
            resize(2 * keys.length);
        }
        keys[size] = key;
        swim(size);
        size++;
    }

getSmallest 方法用于获取堆顶元素,即最小元素:

    // 获取堆顶元素(最小元素)
    public Key getSmallest() {
        if (isEmpty()) {
            throw new NoSuchElementException("Priority queue underflow");
        }
        return keys[0];
    }

removeSmallest 方法用于删除并返回堆顶元素:

    // 删除并返回堆顶元素(最小元素)
    public Key removeSmallest() {
        if (isEmpty()) {
            throw new NoSuchElementException("Priority queue underflow");
        }
        Key min = keys[0];
        swap(0, size - 1);
        size--;
        sink(0);
        keys[size] = null; // 避免对象游离
        if (size > 0 && size == keys.length / 4) {
            resize(keys.length / 2);
        }
        return min;
    }
4. 扩容和缩容

为了确保数组具有足够的空间存储新元素,当数组已满时需要扩容,当数组元素较少时进行缩容:

    // 调整数组

容量
    private void resize(int capacity) {
        Key[] temp = (Key[]) new Comparable[capacity];
        for (int i = 0; i < size; i++) {
            temp[i] = keys[i];
        }
        keys = temp;
    }
}

结论

通过上述实现,我们完成了一个基于最小堆的优先队列。此实现不仅高效,而且简单易懂,适用于大多数需要优先队列的数据结构和算法应用场景。无论是插入还是删除操作,时间复杂度均为 (O(\log n)),这使得最小堆成为实现优先队列的理想选择。

通过学习和实现最小堆优先队列,我们不仅掌握了优先队列的基本原理,还深入理解了堆这种数据结构的高效性和适用性。希望本文对您有所帮助,能够更好地理解和应用优先队列。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

poison_Program

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值