数据结构-优先队列

优先队列特点

这里的特点,主要是和普通队列比较:

  • 普通队列: 先进先出,后进后出
  • 优先队列:出队和入队顺序无关,和优先级有关。

思路

  1. 优先队列,也是队列,因此需要实现队列接口

    public interface Queue<E> {
        int getSize();
        default boolean isEmpty() {
            return getSize() == 0;
        }
        void enqueue(E e);
        E dequeue();
        E getFront();
    }
    
  2. 既然前面已经实现了二分搜索树,而二分搜索树也有元素具备可比较性,因此保证出队操作dequeue()处找到最大节点并删除就行了:

        @Override
        public E dequeue() {
            return bst.removeMax();
        }
    
  3. 在二分搜索树中增加findMax(),作为复写getFront()的具体实现:

        public E findMax() {
            Node maximum = maximum(root);
            return maximum == null ? null : maximum.e;
        }
    

缺点

上例中,虽然使用BST实现了优先队列,但是感觉有些僵。

要知道,我们这里的dequeue操作直接指定删除最大节点而缺乏灵活性,是否可以传入一个比较器Comparator来提升灵活性呢?

其次,考虑二分搜索树本身的特点,是有可能退化为链表的,因此dequeue操作的最差时间复杂度是O(n)。有一种数据结构可以将最差时间复杂度控制在O(logn),叫(二叉)堆。

二叉堆

基础

  1. 二叉堆是一棵完全二叉树,意味着可以使用线性结构(如数组)表示
  2. 按层级放置元素,意味着不会退化为链表
  3. 最大堆,堆中每个节点的值总是不大于其父节点的值,最小堆,反之

实现

为了专注于堆的特性,而不考虑其他问题,如数组的扩容和缩容等,这里直接使用已实现的动态数组来代替。

索引关系

已知当前索引为i,那么可以推算出它的父节点((i-1)/2)及孩子节点(2*i+1,2*i+2)的索引值:

    private int parent_index(int i) {
        return Math.max(0, (i - 1) >> 1);
    }

    private int left_index(int i) {
        return Math.min(array.getSize() - 1, (i << 1) + 1);
    }

    private int right_index(int i) {
        return Math.min(array.getSize() - 1, (left_index(i)) + 1);
    }
节点的增删

增删节点可能引起堆特性的破坏,比如删除了第一个元素后,其余的元素应该如何重新排序呢?假设有一个层高h的元素,在进行增删操作后层高发生了变化,一般根据变化情况,即层级的升降,将其分别称为上浮(siftUp)或下沉(siftDown)操作。

上浮

上浮的思路很简单,在给定索引的前提下,比较其与父节点的优先级,如果比父节点优先级高,则进行元素交换。直到当前索引为0为止。

    private void sift_up(int i) {
        if (i == 0) {
            return;
        }
        int parent_index = parent_index(i);
        E parent = array.get(parent_index);
        E current = array.get(i);
        if (comparator.compare(current, parent) > 0) swap(i, parent_index);
        sift_up(parent_index);
    }
下沉

下沉比起上浮稍微复杂一些,因为有俩孩子,因此需要先找到优先级比较高的孩子,然后对比,如果当前节点比优先级较高的孩子节点优先级低,则与其交换,直到叶子节点。

    private void sift_down(int i) {
        if (array.isEmpty() || i == array.getSize() - 1) return;
        int priority_child_index = find_priority_child_index(i);
        E max_child = array.get(priority_child_index);
        E current = array.get(i);
        if (comparator.compare(current, max_child) < 0)
            swap(i, priority_child_index);
        sift_down(priority_child_index);
    }
增加

在数组末尾放置元素,然后进行上浮操作

    public E put(E e) {
        if (null == e) return null;
        array.addLast(e);
        sift_up(array.getSize() - 1);
        return e;
    }
删除

这里的删除,指的是优先级最高元素(也就是数组的第一个元素)出队。

首先交换第一个和最后一个元素的位置,再把最后(优先级最高)的元素删除,然后从头开始下沉。

    public E pop() {
        E e = array.get(0);
        swap(0, array.getSize() - 1);
        array.removeLast();
        sift_down(0);
        return e;
    }
构造器

上面提到了应该增加比较器来增加灵活性,同时支持数组等线性结构表达,那么可以直接将其设置为构造器参数。但需要注意传入的数组可能不具备堆的特性,因此在传入后将其堆化,也就是所谓的heapify,思路也比较简单,遍历数组,然后逐个下沉:

    public BiHeap(Comparator<? super E> comparator, E... es) {
        this(comparator);
        if (es != null) {
            array = new Array<>(es).removeIf(Objects::isNull);
        }
        for (int i = 0; i < array.getSize(); i++) {
            sift_down(i);
        }
    }
查看优先级最高元素

这里的堆是为优先队列服务的,故实现对应的getFront方法:

	public E peek() {
        return array.isEmpty() ? null : array.get(0);
    }

二叉堆实现的优先队列

实现了二叉堆,基本的优先队列就很简单了,直接调用相应方法即可。

public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
    private BiHeap<E> biHeap;
    public PriorityQueue() {
        biHeap = new BiHeap<>();
    }

    public PriorityQueue(Comparator<E> comparator) {
        super();
        biHeap = new BiHeap<>(comparator);
    }

    @Override
    public int getSize() {
        return biHeap.size();
    }

    @Override
    public void enqueue(E e) {
        biHeap.put(e);
    }

    @Override
    public E dequeue() {
        return biHeap.pop();
    }

    @Override
    public E getFront() {
        return biHeap.peek();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值