挑战408——数据结构(17)——优先队列与二叉堆

优先队列

我们回顾之前我们学过的队列,队列中的元素按照特定的顺序进行储存,并只能先进先出。然而,在现实生活中,我们却想把元素按照一定的优先级储存起来。举个现实中的例子:

  • 我们平时坐高铁,会有所谓的头等舱,二等舱,普通舱。
  • 在银行排队,总会有vip客户提前办理业务

所谓优先队列(priority queue),就是把元素按照一定的优先级储存起来,而不是根据特定的顺序。因此,它与我们之前接触的基于位置的数据结构有着本质的区别。因为这里并没有所谓的特定位置(类似于数组的下标)这个概念。
假设这里存在一个优先队列P,那么优先队列P应该有下面三个基本操作:
在这里插入图片描述
优先级队列也有一些较少的的基本函数:
在这里插入图片描述
可以看到,优先队列比我们传统的队列简单多了,因为我们不用考虑元素的位置问题,也就是说我们不用去实现复杂的insert,add功能。我们要做的只是,enqueue跟dequeue操作。原理如下
在这里插入图片描述

二叉堆(binary heap)

那么怎么去实现这个优先级队列呢?可能第一的反应就是使用我们的链表吧,一个存储优先级一个存储我们的值,但是作为扩展,我们介绍一个更方便的结构——二叉堆.。
堆,是一种基于树的结构,它具有以下属性: 父母的优先权要高于他们的孩子。
而一般的,我们有以下两种形式的堆:小根堆(左),大根堆(右)
在这里插入图片描述
注意:小根堆的根一定是堆中最小的元素,而且他们的父亲一定是要小于他们的任一个儿子。大根堆中的根一定是堆中的最大的元素。且他们的父亲一定是要大于他们的任一个儿子。如下图,显然左边不是小堆右边才是(因为左边的12比11大):
在这里插入图片描述
当然,如果我们再仔细观察,我们可以发现,每一个堆除了最底层,都是充满的。因此,堆是一种完全二叉树(complete binary trees)。那么什么又是完全二叉树?我们从它的命名来解释:

  • 完全(complete):除了最后一层,其他层都是满的。
  • 二叉(binary):每个父亲节点都只有两个儿子。

下图是一个堆,也是一个完全二叉树:
在这里插入图片描述

二叉堆的存储

那么应该如何来存储我们的二叉堆。假设我们有下面的这个二叉堆:

我们看到这个,也许我们第一反应是用一个基于节点的方式储存(因为这样确实很像)在这里插入图片描述
我们看到这个,也许我们第一反应是用一个基于节点的方式储存(因为确实长得很像!):
在这里插入图片描述
是的,这就是一个很正常也是最容易想到的方式。然而,科学家却发现使用基于数组的方式能更好的存储我们的二叉堆。我们把根(root),放在下标为1的位置(而不是0),这是为了我们能从数学的角度更好的理解这个原理。 如下图所示:
在这里插入图片描述
在这里插入图片描述
为什么是这样存放我们的数组呢?因为用数组表示二叉堆,使得我们确定父亲与孩子之间的问题就变成了简单的算术问题:
对于每一个位于下标为 i 的元素来说

  • 它的左孩子一定在 2i 的位置
  • 右孩子一定在 2i + 1 的位置
  • 父亲一定在[i / 2]的位置(向上取整)

我们举例子,上面的元素12,下标为4,所以它的左孩子的下标就是 2 x 4 = 8.也就是上图的22元素。它的右孩子同理。它的父亲为 4 / 2 = 2的位置,也就是元素10。

二叉堆的基本操作

回到二叉堆的基本功能上:
现在,我们可以从堆的角度去解决这个问题:

peek()

只需返回堆中的第一个元素即可。 显然树根的位置是固定的,算法复杂度为O(1)。

return heap[1];
enqueue(k)

在堆中插入元素。在这里假设我们要在堆中插入一个元素为9.我们应该要经过下面的一些步骤:

  1. 我们先把元素插入到旧数组元素的尾部,也就是heap[heap.size() + 1]的位置。
  2. 执行“冒泡”(bubble up)或“上堆”(up-heap)操作:比较添加的元素与其父亲, 如果按正确的顺序,停止操作,如果不是,交换并重复步骤2。(这一步实际上就是把插入的元素正确的放入堆中的位置)。

下面,假设在下列的堆中插入一个元素9
在这里插入图片描述在这里插入图片描述
首先,们把新元素插入到旧数组的尾部,也就是堆中的空白位置(注意,这不是随意放入哪个空白的位置,因为我们完全可以通过下标计算父亲是谁,比如9放在数组的尾部,就是位置10,所以他应该在位置为5的节点下面。并且是左节点),也就是heap[heap.size() + 1]的位置,在这个例子中就是我们的下标10:
在这里插入图片描述
然后,根据堆的属性,我们找到下标为10的元素的父亲位于哪个位置。也就是计算10 / 2 = 5,也就是位于5的元素就是新插入元素的父亲,那么它符不符合小堆的要求?11 > 9 显然是不符合,所以我们把他们进行交换位置。如下面两幅图所示:
在这里插入图片描述
在这里插入图片描述
这个时候我们再对比5号位置的值与它的父亲位置的值,这个时候我们计算 5 / 2 = 2.5,可是这里并没有下标为2.5的元素,我们采取的是向下取整(其实设置为int型就会自动转换的),所以下标为2的元素10,仍然大于9,于是我们再交换,(这一步实际上是上一步的重复):
在这里插入图片描述
重复上述判断,发现此时位置正确。

对于算法复杂度,我们通常都讨论的是最坏情况下的算法复杂度。而插入元素的操作,显然是个递归的过程,(这与我们之前讨论的合并排序一样)。非递归操作并不需要循环执行,我们只需要关心交换节点的操作要做多少次。显然,最差情况下,我们只需要看这颗树最多可以有多少层即可。所以,算法复杂度为O(log N)。平均复杂度为O(1).

dequeue()

这个操作是要移除堆中的堆顶(也就是树根),那么移除之后,需要整个堆都需要重组(因为移除了堆顶后就破坏了堆的结构,需要找一个新的节点来做这个堆顶)。具体操作如下:

  1. 当我们删除根的时候,我们仍然需要保留一个完全树:于是我们用最后一个元素替换根。
  2. 使用下堆(down-heap)操作,寻找新符合条件的新的根:将根与它的孩子们进行比较,如果他们的顺序正确,操作停止。否则将根与两个孩子中最小的那个进行交换,然后继续重复步骤2.
  3. 要时刻注意该节点是否存在孩子,如果没有孩子或者只有一个,那么就不用执行比较操作。那么如何判断呢?可以这样想,如果有该节点有右孩子,那么它的左孩子也必定存在!!(在实现的时候,可以作为递归的simple case)。

假设我们的初始堆如下:
在这里插入图片描述
那么移除了堆顶后,情况是这样的:
在这里插入图片描述
此时将最后一个元素(此时它的位置当然是在 heap[heap.size()] 处)移动到堆顶处,准备我们的down-heap操作。(注意,移动了堆顶以后,不要忘记了堆的大小是要减一的)
在这里插入图片描述
递归地将与它的较小的节点进行比较,如果符合要求就交换(这里指的是13和8)
在这里插入图片描述
如果有必要,那么我们继续保持交换,直到它再也没有更多的孩子跟它比较了,我们也就完成工作了!
在这里插入图片描述
至此,简单的二叉堆的基本操作就完成了,但是这有个问题。这些操作都是建立在已有堆的操作上的。但是如果给的是一堆无关的数字呢?显然我们要学会怎么建立一个二叉堆!

下次再说,如何建立二叉堆与如何完成堆排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值