❤️《画解数据结构》七张动图,画解单调队列❤️(建议收藏)

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️《画解数据结构》七张动图,画解单调队列❤️(建议收藏)相关的知识,希望对你有一定的参考价值。

本文已收录于专栏
💜《夜深人静写算法》💜
您可能感兴趣的专家文章推荐
画解数据结构
画解顺序表画解链表
画解栈画解队列画解双端队列
画解哈希表画解排序

直接跳到末尾 获取粉丝专属福利。


零、前言

  「 数据结构 」「 算法 」 是密不可分的,两者往往是「 相辅相成 」的存在,所以,在学习 「 数据结构 」 的过程中,不免会遇到各种「 算法 」
  数据结构 常用的操作一般为:「 增 」「 删 」「 改 」「 查 」。基本上所有的数据结构都是围绕这几个操作进行展开的。
  那么这篇文章,作者将用 「 十张动图 」 来阐述一种 「 一端插入 」「 两端删除 」 的数据结构

「 单调队列 」



   单调队列的操作浓缩为以下一张图:


  看不懂没有关系,我会把它拆开来一个一个讲,首先来看一下今天要学习的内容目录。

一、滑动窗口

1、引例

  【例题1】给定一个长度为 n ( n ≤ 1 0 5 ) n(n \\le 10^5) n(n105) 整数数组 a i a_i ai,有一个大小为 k ( k ≤ 1 0 5 ) k(k \\le 10^5) k(k105) 的滑动窗口从数组的最左侧移动到数组的最右侧。只能看到在滑动窗口内的 k k k 个数字。滑动窗口每次只向右移动一位。返回 每个滑动窗口 的最大值。

2、暴力求解

  看到这个问题,最简单的思路就是:枚举一个起点 s s s,然后记录区间 [ s , s + k − 1 ] [s, s + k-1] [s,s+k1] 内的最大值。枚举起点的时间复杂度为 O ( n ) O(n) O(n),记录区间最值的时间复杂度为 O ( k ) O(k) O(k),所以总的时间复杂度为 O ( n k ) O(nk) O(nk),对于这个问题的数据量,最大的数据量达到了 1 0 10 10^{10} 1010 量级,所以这个做法是行不通的。

3、区间最值

  这个问题是个经典的区间最值问题,可以通过 ST表 (Sparse Table, 稀疏表) 求解,时间复杂度为 O ( n l o g 2 k ) O(nlog_2k) O(nlog2k),除了 ST表,还可以采用 线段树 求解区间最值,时间复杂度也为 O ( n l o g 2 k ) O(nlog_2k) O(nlog2k)。当然,这两块内容都不是本文讨论的重点,如果对 ST表 感兴趣,可以参考以下文章:夜深人静写算法(六)- RMQ。对线段树感兴趣,可以参考以下文章:夜深人静写算法(四十一)- 线段树

4、容器抽象

  本文将介绍一种 O ( n ) O(n) O(n) 的算法。它将会用到一种数据结构 —— 单调队列。
  在这个问题中,两个相邻的滑动窗口,实际上只相差两个元素,如下图所示:

假设,我们提供了一种容器,这个容器能够支持三种操作:
  1)【询问】通过 O ( 1 ) O(1) O(1) 的时间,获取容器中元素的最大值。
  2)【删除】通过 O ( 1 ) O(1) O(1) 的时间,删除元素;
  3)【插入】通过 O ( 1 ) O(1) O(1) 的时间,插入元素;

  那么,我们只要不断的移动滑动窗口,每一次移动,删除一个元素,插入另一个元素,并且记录下最大值,那么,每一次滑动,只需要三步 O ( 1 ) O(1) O(1) 的操作。总共 n n n 次滑动,只需要 O ( n ) O(n) O(n) 的时间复杂度就能解决这个问题。
  这种容器存在吗?让我们首先简单了解一下 FIFO 队列双端队列,如果你对 以上两种数据结构 已经 了如指掌,则可以跳过相关内容,直接观看 👉🏻单调队列👈🏻 部分。

二、FIFO 队列

1、FIFO 队列的概念

1)队列的定义

  队列 是仅限在 一端 进行 插入另一端 进行 删除线性表
  队列 又被称为 先进先出 (First In First Out) 的线性表,简称 FIFO 队列。

2)队首

  允许进行元素删除的一端称为 队首。如下图所示:

3)队尾

  允许进行元素插入的一端称为 队尾。如下图所示:

2、FIFO 队列的接口

1)数据入队

  队列的插入操作,叫做 入队。它是将 数据元素队尾 进行插入的过程,如图所示,表示的是 插入 两个数据(绿色 和 蓝色)的过程:

2)数据出队

  队列的删除操作,叫做 出队。它是将 队首 元素进行删除的过程,如图所示,表示的是 依次 删除 两个数据(红色 和 橙色)的过程:

3)清空队列

  队列的清空操作,就是一直 出队,直到队列为空的过程,当 队首队尾 重合时,就代表队尾为空了,如图所示:

4)获取队首数据

  对于一个队列来说只能获取 队首 数据,一般不支持获取 其它数据。

5)获取队列元素个数

  队列元素个数一般用一个额外变量存储,入队 时加一,出队 时减一。这样获取队列元素的时候就不需要遍历整个队列。通过 O ( 1 ) O(1) O(1) 的时间复杂度获取队列元素个数。

6)队列的判空

  当队列元素个数为零时,就是一个 空队空队 不允许 出队 操作。

3、队列的实现

  队列的实现,可以参考以下这篇文章:❤️《画解数据结构》九张动图,画解队列❤️

三、双端队列

1、双端队列的概念

1)双端队列的定义

  双端队列 是一种具有 队列 的性质的数据结构,是我们常说的 dequedouble-ended queue),是一种限定 插入删除 操作在表的两端进行的线性表。这两端分别被称为 队首队尾

2)队首

  双端队列的一端被称为 队首,如下图所示:

3)队尾

  双端队列的另一端被称为 队尾,如下图所示:

2、双端队列的接口

1)队首入队

  队列的插入操作,叫做 入队
  队首入队 就是将 数据元素队首 进行插入的过程。如图所示,表示的是在队首 插入 一个蓝色数据的过程:

2)队尾入队

  队尾入队 就是将 数据元素队尾 进行插入的过程。如图所示,表示的是在队尾 插入 一个紫色数据的过程:

3)队首出队

  队列的删除操作,叫做 出队
  队首出队 是将 队首 元素进行删除的过程,如图所示,表示的是在队首 删除 一个蓝色数据的过程:

4)队尾出队

  队尾出队 是将 队尾 元素进行删除的过程,如图所示,表示的是在队尾 删除 一个紫色数据的过程:

5)清空队列

  队列的清空操作,就是一直 出队,直到队列为空的过程,当 队首队尾 正好错开一个位置时,就代表队尾为空了,如图所示,细心的读者会发现,队尾队首 错开了一个位置:

6)获取队列元素个数

  队列元素个数一般用一个额外变量存储,入队 时加一,出队 时减一。这样获取队列元素的时候就不需要遍历整个队列。通过 O ( 1 ) O(1) O(1) 的时间复杂度获取队列元素个数。

7)队列判空

  当队列元素个数为零时,就是一个 空队空队 不允许 出队 操作。

8)获取队首元素

   队首指针 指向的数据被称为 队首元素,可以通过 O ( 1 ) O(1) O(1) 的时间复杂度来获取。

9)获取队尾元素

   队尾指针 指向的数据被称为 队尾元素,可以通过 O ( 1 ) O(1) O(1) 的时间复杂度来获取。

3、双端队列的实现

  需要了解双端队列的实现,可以参考如下文章:❤️《画解数据结构》十张动图,画解双端队列❤️

四、单调队列

单调队列 就是能够完美支持下面三种操作的一种容器:
  1)【询问】通过 O ( 1 ) O(1) O(1) 的时间,获取容器中元素的最大值。
  2)【删除】通过 O ( 1 ) O(1) O(1) 的时间,删除元素;
  3)【插入】通过 O ( 1 ) O(1) O(1) 的时间,插入元素;

1、定义

  单调队列是一个限制只能 队尾插入,但是可以 两端删除双端队列单调队列 存储的元素值,是从 队首队尾 呈单调性的(要么单调递增,要么单调递减)。
  对于求解最大值的问题,则需要维护一个 单调递减 的队列。

  如图所示, 为原先的 队首元素,执行 队首删除(出队) 操作以后, 成为新的 队首元素;而在队尾执行插入这个元素的时候,为了保持单调性,需要将①②依次从队尾删除;当队尾执行插入这个元素的时候,满足单调性。

2、询问

  由于单调队列是单调递减的,所以队首元素 最大,直接 O ( 1 ) O(1) O(1) 获取队首元素。

  如图所示,head 指向 队首元素,直接获取,由于这是一个单调递减队列,所以得到的,就是最大值。

3、删除

  删除分为 队首删除队尾删除
  队首删除即直接队首元素出队, O ( 1 ) O(1) O(1) 即可完成操作。如图所示:

  队尾删除 一般是配合 队尾插入 进行的。我们接着往下看。

4、插入

  在进行 队尾插入 的时候,我们往往需要明白一个重要的点,就是需要保证它 单调递减 的性质,所以如果 队尾元素 ≤ \\le 插入元素 ,则当前的 队尾元素 是需要执行删除操作的(也就是上文提到的 队尾删除),直到满足 队尾元素 > \\gt > 插入元素,才能真正执行 插入 操作。
  这样才能保证,执行 队尾插入 后,单调队列仍然是 单调递减 的。插入过程,虽然伴随着元素的删除,但是每个元素至多被 插入一次删除一次,所以均摊时间复杂度还是 O ( 1 ) O(1) O(1) 的。

  如图所示,在队尾执行插入这个元素的时候,为了保持单调性,需要将①②依次从队尾删除;当队尾执行插入这个元素的时候,满足单调性,所以直接执行插入操作。

5、性质

1)保序性

  由于单调队列执行插入的时候,一定是从队尾进行插入,所以单调队列中的数据,从队首到队尾的顺序,一定是和原序列严格保序的;

2)下标存储

  为了让单调队列的数据足够干净,在单调队列中,一般存储 原序列的下标 即可,而不需要存储原序列的值,根据保序性,存储的下标一定是单调递增的;

3)单调性

  单调队列中的元素是 原序列的下标,对应到原序列时,根据求解问题的不同,当需要求最大值时,它是单调递减的;当需要求最小值时,它是单调递增的;

五、单调队列的应用

1、最值问题

1)问题描述

  继续回到上文提到的滑动窗口中的最大值问题。

  【例题1】给定一个长度为 n ( n ≤ 1 0 5 ) n(n \\le 10^5) n(n105) 整数数组 A i A_i Ai,有一个大小为 k ( k ≤ 1 0 5 ) k(k \\le 10^5) k(k105) 的滑动窗口从数组的最左侧移动到数组的最右侧。只能看到在滑动窗口内的 k k k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

2)思路分析

  我们要实现的,就是把原序列 A A A 中的元素逐个执行单调队列的 插入 操作。当 插入 的原序列的下标为 i i i 时,期望是单调队列中的元素从 队首队尾 都在原序列的区间 ( i − k , i ] (i-k, i] (ik,i] 范围内(也就是以 i i i 为右端点,长度为 k k k 的区间内),且对应到原序列的值单调递减,这样每次插入完毕,就可以在 O ( 1 ) O(1) O(1) 的时间内,从队首获取到最大值(即区间 ( i − k , i ] (i-k, i] (ik,i] 内的最大值)。
  为什么是单调递减?而不是单调递增?
  对于每个需要插入的下标 i i i,队尾的元素为原序列的下标 j j j,则根据保序性,一定能够满足 j < i j \\lt i j<i,如果对应到原序列中,满足 A j ≤ A i A_j \\le A_i AjAi,那么 A j A_j Aj 不会比 A i A_i Ai 更优,原因是:对于区间 ( i − k , i ] (i-k, i] (ik,i] 来说, A i A_i Ai 一定在区间内,而 A j A_j Aj 则未必,也就是说 下标 j j j 没必要存储到单调队列中。于是对于单调队列中的存储的元素 i 1 < i 2 < . . . < i n i_1 < i_2 < ... < i_n i1以上是关于❤️《画解数据结构》七张动图,画解单调队列❤️(建议收藏)的主要内容,如果未能解决你的问题,请参考以下文章

❤️《画解数据结构》两万字,十张动图,画解双端队列❤️(建议收藏)

❤️《画解数据结构》全网最全队列总结,九张动图搞懂队列 ❤️(文末有投票)

❤️《画解数据结构》全网最清晰哈希表入门,三张动图搞懂哈希 ❤️(建议收藏)

❤️《画解数据结构》之 顺序表八大算法总结❤️(建议收藏)

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)