二叉堆实现的优先队列

本文深入探讨了优先队列的概念及其与二叉堆的紧密关系。优先队列在插入和删除操作时自动维护元素顺序,常通过最大堆或最小堆实现。二叉堆是一种以数组形式存储完全二叉树的数据结构,允许高效地获取最大值或最小值。文中还介绍了关键方法如插入、删除、上浮和下沉,以及如何通过Java泛型实现简化版的优先级队列。
摘要由CSDN通过智能技术生成

什么是优先队列?

优先队列这个数据结构的特点就是在我们执行插入或者删除元素的操作时候,优先队列自己会来维护队列中元素的顺序,不用我们自己去重新对队列中的数据进行排序,所以优先队列中就有了两个主要的 API,分别是 insert 插入一个元素和 delMax 删除最大元素(如果底层用最小堆,那么就是 delMin
优先队列底层的实现原理就是对二叉堆这个数据结构的使用,因为优先队列底层使用的是二叉堆,所以我们可以用O(1)的时间复杂度去获取优先队列中的最大值(这里我们以最大堆来说,所以获取的是最大值)

好,到这里我们对优先队列建立起了大概的认知:

  1. 优先队列在我们插入或者删除元素的时候会帮我们自动排序
  2. 优先队列底层使用的就是对二叉堆
  3. 我们可以很高效的获取到优先队列中的最值

什么是二叉堆?

现在我们再回过头来说,什么是二叉堆。
二叉堆其实就是以数组的方式存储完全二叉树并且满足每个父节点都大于他的两个子节点,然后这个数组就叫做二叉堆。二叉堆在逻辑上就是一个完全二叉树,只不过存放在了数组当中,我们在操作平衡二叉树的时候通过链表来操作,而在这里我们操作二叉堆则是通过索引的方式来操作二叉堆。
先来看一个二叉堆的图,加深印象:
在这里插入图片描述
索引为0的位置用*占位,并没有什么实际意义,我们可以直接按照顺序从索引为0的位置开始存储

相信大家一定能看明白完全二叉树和二叉堆之间的对应关系

二叉堆还分为最大堆最小堆。最大堆的性质是:每个节点都大于等于它的两个子节点。类似的,最小堆的性质是:每个节点都小于等于它的子节点。
所以显然我们想要获取数组中最大的值非常的高效可以直接通过索引1就获取到了

好,到这里我们也建立起了二叉堆的基本认知了:

  1. 二叉堆分为最大堆最小堆
  2. 二叉堆在逻辑上就是一个完全二叉树
  3. 二叉堆获取最大值(或者说是最值)非常高效

所以说优先队列和二叉堆的关系是什么?

注意!!!这里是直接复制的labuladong的代码,仅用于学习。
原文地址:https://labuladong.gitee.io/algo/2/21/62/
下面我们实现一个简化的优先级队列,先看下代码框架:

这里用到 Java 的泛型,Key 可以是任何一种可比较大小的数据类型,比如 Integer 等类型。

public class MaxPQ
    <Key extends Comparable<Key>> {
    // 存储元素的数组
    private Key[] pq;
    // 当前 Priority Queue 中的元素个数
    private int size = 0;

    public MaxPQ(int cap) {
        // 索引 0 不用,所以多分配一个空间
        pq = (Key[]) new Comparable[cap + 1];
    }

    /* 返回当前队列中最大元素 */
    public Key max() {
        return pq[1];
    }

    /* 插入元素 e */
    public void insert(Key e) {...}

    /* 删除并返回当前队列中最大元素 */
    public Key delMax() {...}

    /* 上浮第 x 个元素,以维护最大堆性质 */
    private void swim(int x) {...}

    /* 下沉第 x 个元素,以维护最大堆性质 */
    private void sink(int x) {...}

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

    /* pq[i] 是否比 pq[j] 小? */
    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    /* 还有 left, right, parent 三个方法 */
}

可以看到我们可以非常高效的获取到优先队列中的最值,就是因为优先队列的底层使用的是二叉堆,我们可以直接使用max()方法就可以以O(1)的时间复杂度获取到这个最值

列出重要的方法:

  1. 插入元素insert(Key e)
  2. 删除并返回当前队列中最大元素delMax()
  3. 上浮第x个元素,以维护最大堆的性质swim(int x)
  4. 下沉第x个元素,以维护最大堆的性质sink(int x)

swim方法和sink方法

为什么要有这两个方法呢?
因为我们在插入元素到优先队列中的时候是插入到数组的末尾,这就会出现一种情况就是这样会破坏优先队列中的一个顺序,此时我们就需要swim方法和sink方法进行维护这个队列的一个顺序。
对于最大堆,会破坏堆性质的有两种情况:

# 如下内容参考labuladong算法小抄
1、如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行下沉。

2、如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮。

当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个 while 循环。

上面这段内容什么意思呢?
说白就是为了维护最大堆的性质,需要将破坏顺序的元素,下沉或者上浮到一定的位置,使数组又满足最大堆的一个性质。

swim上浮

举个例子解释上浮:

      	   10
    	/      \
	  8          12   (注意这个12就破坏了最大最的特性了,此时如果从下向上的顺序看的话,就应该将他上浮 )
    /   \      /    \
  7     4     5      6

			||
			||     将10和12交换位置
			\/
			
      	   12
    	/      \
	  8         10  
    /   \      /    \
  7     4     5      6		

那代码应该怎么写呢?

// 代码逻辑
private void swim(int x) {
    while (如果x没有到堆顶 && x大于它的父亲节点) {
        交换x和它的父亲节点
        此时将x的父亲节点再作为x继续while循环
    }
}

// 伪代码参考labuladong算法小抄
private void swim(int x) {
    // 如果浮到堆顶,就不能再上浮了
    while (node > 1 && less(parent(x), x)) {
        // 如果第 x 个元素比上层大
        // 将 x 换上去
        exch(parent(x), x);
        x = parent(x);
    }
}

sink下沉

下沉于上浮是有区别的,因为上浮的话,父节点只有一个,而下沉子节点有两个,需要都进行比较,如果待下沉的节点比任意一个子节点小,那么就进行下沉,如果比子节点都大,那么就没必要进行下沉了

      	   10 (注意这个10就破坏了最大最的特性了,此时如果从上向下的顺序看的话,就应该将它进行下沉 )
    	/      \
	  8          12   
    /   \      /    \
  7     4     5      6

			||
			||     下沉的过程比较做子节点8,发现比8大,然后比较12发现比它小,进行然后进行交换
			\/
			
      	   12
    	/      \
	  8         10  
    /   \      /    \
  7     4     5      6		

代码如下:

// 代码逻辑
private void sink(int x) {
    while (当前x是否到最底下) {
        获取两个子节点中较大的一个设为tmp
        如果当前x大于tmp,说明不用继续找了,break
        
        反之需要将当前的x跟tmp进行交换
		然后将tmp作为x继续下沉,继续执行while循环
    }
}
// 代码实现参考labuladong
private void sink(int x) {
    // 如果沉到堆底,就沉不下去了
    while (left(x) <= size) {
        // 先假设左边节点较大
        int older = left(x);
        // 如果右边节点存在,比一下大小
        if (right(x) <= N && less(older, right(x)))
            older = right(x);
        // 结点 x 比俩孩子都大,就不必下沉了
        if (less(older, x)) break;
        // 否则,不符合最大堆的结构,下沉 x 结点
        exch(x, older);
        x = older;
    }
}

然后再来看delMax 和 insert方法就简单了

这两个方法就是建立在 swim 和 sink 上的
insert代码如下:

public void insert(Key e) {
    N++;
    // 先把新元素加到最后
    pq[N] = e;
    // 然后让它上浮到正确的位置
    swim(N);
}

delMax 方法先把堆顶元素 A 和堆底最后的元素 B 对调,然后删除 A,最后让 B 下沉到正确位置。

public Key delMax() {
    // 最大堆的堆顶就是最大元素
    Key max = pq[1];
    // 把这个最大元素换到最后,删除之
    exch(1, N);
    pq[N] = null;
    N--;
    // 让 pq[1] 下沉到正确位置
    sink(1);
    return max;
}

此时一个最大堆的核心方法就都说明完了,我们就获取到了一个插入和删除时间复杂度为O(logk)的一个数据结构了,K 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 sink 或者 swim 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。


最后

强烈推荐labuladong的算法小抄:
地址如下:https://labuladong.gitee.io/algo/
没收广告钱,真心推荐!!!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值