数据结构(三)-- 优先队列

什么是优先队列?

我们在前几篇文章中学习过了“队列”这种数据结构。那么优先队列和普通队列有什么区别的呢?普通队列的特点是“先进先出”,优先队列则不一样,它的入队顺序没有变化,但是出队的顺序是根据优先级的高低来决定的。优先级高的优先出队。

本文首发于心安-XinAnzzZ 的个人博客,转载请注明出处~

举个生活中的小栗子。病人去医院看病,正常情况下都是需要取号排队,这是普通队列。但是特殊情况下,有一个情况紧急的病人突然来了,这个病人就拥有优先权,他就优先出队,这就是优先队列。

再比如说,玩过LOL的同学都知道,游戏里面防御塔都有一个自动攻击功能,小兵排着队进入防御塔的攻击范围,这时候大炮车的优先级更高(因为系统判定大炮车对于防御塔的威胁更大),所以防御塔会优先攻击大炮车。而当大炮车阵亡,剩下的全部都是普通小兵,这时候离得近的优先级越高,防御塔优先攻击距离更近的小兵。

希望通过这两个小栗子,各位同学能够明白优先队列是怎么一回事,从而更好的理解后面的内容。

  • 各种不同的底层数据结构所对应的时间复杂度
入队 出队(取出最大元素)
普通线性结构 O(1) O(n)
顺序线性结构 O(n) O(1)
O(logn) O(logn)

我们假设有一个优先队列,它的优先级是元素越大优先级越高,也就是每次出队的是队列中值最大的元素。那么如果我们使用普通的线性结构(比如数组和链表)来实现,那么入队的操作,直接插入即可,时间复杂度是O(1),出队我们需要进行遍历整个队列找到最大元素,然后出队,时间复杂度是O(n)。而如果使用顺序的线性结构(顺序线性结构指的是数据已经有了顺序,比如二分搜索树,中序遍历就是有序的),入队的时候需要进行遍历整个队列来找到合适的位置来插入这个元素,时间复杂度是O(n),出队的时候由于有序,所以直接取出,时间复杂度是O(1)。我们知道,O(n)的时间复杂度是效率比较低的,所以我们接下来介绍一种入队和出队都是O(logn)时间复杂度的数据结构--堆。

  • 完全二叉树

有一定基础的同学对这个概念应该不会陌生,这里还是贴上完全二叉树的百度百科。概念性的东西我不解释太多,大家自己去体会和理解。这里我只说自己的理解,也是我认为最简单的理解。

一棵树,元素从左往右、从上到下依次排列,中间不能有空缺,就是一个完全二叉树。这里要注意区分完全二叉树和满二叉树的区别。如下图:这棵树拥有10个元素,这10个元素从上到下、从左到右依次排列,这就是一个完全二叉树。假如说6、7、8、9这四个位置任意一个位置元素为空,那么就不是完全二叉树。
image
  • 二叉堆

二叉堆是特殊的完全二叉树。它满足“堆中任意一个节点的值都不大于其父亲节点的值”这一特征。当然,这个“不大于”是由我们来定义的,我们也可以定义为“不小于”。如果是“不大于”,那么形成的就是“最大堆”,反之就是“最小堆”。下面就是一个最小堆和一个最大堆。注意一个误区,在最大堆当中,堆中任意一个节点的值都不大于父亲节点的值,是否意味着层次越低的节点值越大呢?这是不一定的。

image
image
  • 二叉堆的数组实现

上面我们说了,二叉堆是一个完全二叉树,就是一个一个元素从左到右、从上到下依次排列出来的,那也就可以使用数组的方式来表示二叉堆。用数组来表示一个二叉堆要解决的问题就是,我们如何找到一个节点的左右孩子。如果我们使用的是树的形式来表示的话,我们是可以通过一个“指针”来表示它的左右孩子,但是数组是没有“指针”的,应该如何实现呢?仔细观察一下下图,我对这个二叉堆从上到下、从左到右进行了编号,编号就代表了元素在数组中的索引,我们可以发现,节点编号满足以下规律:

parent(i) = (i - 1) / 2 left child(i) = 2 * i + 1 right child(i) = (i + 1) * 2

image
  1. 下面我们新建一个工程,创建名为MaxHeap(最大堆)的类在里面加入以下代码,这里我们底层依赖ArrayList。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n49" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 30px 10px 0px; border: 1px solid; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> public class MaxHeap<E extends Comparable<E>> {

private ArrayList<E> data;

/*** 根据孩子节点索引获取父节点的索引 /
private int getParentIndex(int childIndex) {
if (childIndex == 0) {
throw new IllegalArgumentException("the root node doesn't have parent node !");
}
return (childIndex - 1) / 2;
}

/
** 根据父节点索引获取左孩子索引 /
private int getLeftChildIndex(int parentIndex) {
if (parentIndex * 2 + 1 > size() - 1) {
throw new IllegalArgumentException("the parent node doesn't have left child node !");
}
return parentIndex * 2 + 1;
}

/
** 根据父节点索引获取右孩子索引 */
private int getRightChildIndex(int parentIndex) {
if ((parentIndex + 1) * 2 > size() - 1) {
throw new IllegalArgumentException("the parent node doesn't have right child node !");
}
return (parentIndex + 1) * 2;
}
}</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 30px 10px 0px; border: 1px solid; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> public void add(E e) {
data.add(e);
int index = data.size() - 1;
// 如果父节点的值小于新节点的值,进行位置互换
while (index > 0 && data.get(getParentIndex(index)).compareTo(e) < 0) {
data.set(index, data.get(getParentIndex(index)));
data.set(getParentIndex(index), e);
index = getParentIndex(index);
}
}</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n66" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 30px 10px 0px; border: 1px solid; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> /*** 提取最大值 */
public E extractMax() {
if (data.isEmpty()) {
throw new IllegalArgumentException("The heap is empty !");
}
E result = data.get(0);
E remove = data.remove(size() - 1);

if (size() == 0) {
return result;
}
data.set(0, remove);

int index = 0;
// 如果说左孩子的索引值小于 size 说明左孩子存在,当左孩子不存在的时候 循环终止
while (getLeftChildIndex(index) < size()) {
// 假设左右孩子中左孩子的值较大
int maxIndex = getLeftChildIndex(index);

// 如果右孩子存在且有孩子的值大于左孩子,则最大值的索引等于右孩子
if (getRightChildIndex(index) < size()
&& data.get(maxIndex).compareTo(data.get(getRightChildIndex(index))) < 0) {
maxIndex = getRightChildIndex(index);
}
// 如果当前节点值小于左右孩子节点中较大的那个值,则进行位置互换,否则跳出循环
if (data.get(index).compareTo(data.get(maxIndex)) < 0) {
// 互换位置
E e = data.get(index);
data.set(index, data.get(maxIndex));
data.set(maxIndex, e);

index = maxIndex;
} else {
break;
}
}
return result;
}</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n72" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 30px 10px 0px; border: 1px solid; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> public interface Queue<E> {

/**

  • inserts the element into the queue
  • @param e the element to add
    /
    void enqueue(E e);

    /
    *
  • remove the head of this queue
  • @return the has been removed element
    /
    E dequeue();

    /
    *
  • get the head of this queue
  • @return the element at front of the queue
    /
    E getFront();

    /
    *
  • get the queue's size
  • @return the queue's size
    /
    int size();

    /
    *
  • whether is empty or not
  • @return {@code true} if the queue is empty
    */
    boolean isEmpty();
    }</pre>

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n76" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 30px 10px 0px; border: 1px solid; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;"> public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {

private MaxHeap<E> maxHeap;

public PriorityQueue() {
this.maxHeap = new MaxHeap<>();
}

@Override
public void enqueue(E e) {
maxHeap.add(e);
}

@Override
public E dequeue() {
return maxHeap.extractMax();
}

@Override
public E getFront() {
return maxHeap.getMax();
}

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

@Override
public boolean isEmpty() {
return maxHeap.isEmpty();
}
}</pre>

示例代码Github

好了,本文就到此为止了。后续的博文会为大家带来LeetCode上面的一道题目,就是使用本文的最大堆来解决的,到时候可以看一下最大堆的实际应用。 谢谢观看~~

  1. 具体实现

  2. 定义队列接口

以最大堆为底层实现优先队列

以上就是使用数组来实现一个最大堆的核心代码,下面我们以这个最大堆为基础实现一个优先队列。

移除最大元素代码如下:

image

由于堆的特殊性,所以我们取出元素只会取出堆中的最大元素,毕竟人家名字就叫“最大堆”。由于堆的性质,最大元素其实就是根元素,如果移除根元素,那么就不再是一个树了,解决办法是移除根元素之后将堆中最后一个元素放到根元素的位置,但是这样就不再满足堆中元素不大于父节点元素这一性质了。 所以之后一步的操作是用新的根元素和左右孩子中较大的元素进行比较,然后和较大的元素进行互换,直到新的根元素不小于左右孩子为止,这样删除最大节点之后的树就还能满足堆的性质了。这个过程我们称为“Sift down”,即下沉。 下面使用图解和代码来演示如何从取出堆中最大元素。

  1. 从堆中移除最大元素

然后我们就可以编写添加元素的代码了:

image

一般来讲,如果新添加的元素大于其父亲节点,那么将这个元素和其父亲节点进行位置互换,如果仍然大于其父亲节点,继续互换,直到不大于父节点为止。 在数据结构中,我们将这个过程称之为“Sift up”,翻译过来就是上浮。下图展示了“上浮”的过程。

通过上面的的概念可以知道,向堆中添加一个元素只需要在最后一个元素后面添加一个元素,这个在数组的操作中是非常简单的。但是如果添加的元素比父亲节点大,那么就不再满足二叉堆的性质,这个问题应该如何解决呢?

  1. 向堆中添加一个元素。

首先,我们的类中的元素需要具有可比性,所以我们的元素需要是Comparable接口的子类。成员变量data用来装载堆中的数据,它使用的是ArrayList。然后根据上面我们得到的公式,写出了“根据子节点得到父节点索引”以及“根据父节点得到子节点”的方法,这三个方法在后面的增删等操作中尤为重要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 223,426评论 6 521
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,567评论 3 401
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 170,342评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,420评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,424评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,964评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,365评论 3 426
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,330评论 0 278
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,862评论 1 322
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,921评论 3 343
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,063评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,717评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,393评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,880评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,002评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,540评论 3 380
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,084评论 2 361

推荐阅读更多精彩内容