Skip to content

双端队列

2026 大纲 三(六)栈、队列和数组的应用(另见《括号匹配》《表达式求值》《栈在递归中的应用》)。

松开限制,能产生的序列就变多

栈和队列的基本概念那一篇讲过一条主线:限制越多,能产生的输出序列越少。双端队列走的是反方向——它前端和后端都允许插入和删除,限制最松,所以序列集合最大。

在它和栈之间,还有两种"半松"的形式:

类型前端 front后端 rear
双端队列可插入、可删除可插入、可删除
输入受限双端队列只能删除可插入、可删除
输出受限双端队列可插入、可删除只能插入

🔴 受限的是"动作",不是"端"。 输入受限 = 插入只能在一端(所以插入只能在 rear、删除两端都行);输出受限 = 删除只能在一端(所以删除只能在 front、插入两端都行)。自检办法:受限的那种动作,在权限表里只应出现一次。

栈和队列都是它的特例:只在一端插删就是栈,一端只插、另一端只删就是队列。

先看一眼

加载可视化中...

试着从前端插入一个元素——这个"插队"动作是队列做不到的,也正是双端队列比队列强的全部来源。

输出序列判定:两条现场方法

408 关心的不是 deque 怎么实现(实现就是循环队列的两端版),而是"某个序列能不能由某种受限双端队列产生"。方法与出栈序列判定同源,仍是模拟,只是每种受限形式的可用动作集不同。

不过这两种受限形式各有一条能省力的性质:

输入受限:队内元素永远按编号递增。 因为只能从 rear 插入,元素按编号顺序进入,所以 front 端最小、rear 端最大,而还没入队的元素都比队内所有元素大。既然删除只能在两端发生,每一步能输出的就只有队内的最小值或最大值。拿这条扫一遍目标序列,找那个"既不是当时的最小、也不是当时的最大"的元素,就是失败点。

输出受限:没有这条捷径,因为可以从前端插队,队内不再有序。 但有另一个办法——倒着剥端点:把目标序列从后往前看,检查每一步"最后插入的那个元素是否位于当时的某一端"。插入只能发生在两端,所以一旦发现某个元素必须待在中间,就矛盾。

三个用来定性的序列(输入均为 1234):

序列输入受限输出受限
4 1 3 2
4 2 1 3
3 1 2 4
三个判别序列的逐步模拟与失败点(第一次学、或想手动走一遍时展开)

输入受限 · 4 1 3 2 —— 可以产生

动作队列(front→rear)输出
11、2、3、4 依次从 rear 入队1 2 3 4
2从 rear 删除1 2 34
3从 front 删除2 31
4从 rear 删除23
5从任一端删除2

注意这个序列栈是产生不了的(栈弹出 4 之后栈顶是 3,取不到 1)。

输出受限 · 3 1 2 4 —— 可以产生(技巧:要先输出谁,就把谁安排到 front):

动作队列(front→rear)输出
11 从 rear 入1
22 从 rear 入1 2
33 从 front 入(插队)3 1 2
4从 front 删 3 次3, 1, 2
54 入队后删除4

输出受限 · 4 2 1 3 —— 可以产生(对照上面输入受限的失败):

动作队列(front→rear)输出
11 从 rear 入1
22 从 front2 1
33 从 rear 入2 1 3
44 从 front4 2 1 3
5从 front 连删 4 次4, 2, 1, 3

两个失败的例子更值得单独看,因为它们正是两条现场方法各自的用法。

输入受限产生不了 4 2 1 3 要先输出 4,就得让 1、2、3、4 全部入队,队内是 1 2 3 4;从 rear 删掉 4 之后队内剩 1 2 3。这时需要输出 2,可队内最小是 1、最大是 3,2 夹在中间,两端都够不着——递增规则一句话判死。

输出受限产生不了 4 1 3 2 要先输出 4,说明 4 已经入队并位于 front,此时 1、2、3 也都已入队,队内必须恰好是 4 1 3 2。倒着剥端点:4 是最后插入的、位于 front 端,合法;剥掉它剩下 1 3 2,而其中最后插入的是 3,它却位于中间——插入只能发生在两端,不可能把 3 放到中间,矛盾。

把这两条对照着看就很清楚:同一个序列 4132,输入受限做得到、输出受限做不到;而 4213 恰好反过来。 这就是"互不包含"的具体样子。

前两行说明两个集合各自有对方没有的元素,谁也不包含谁——"输入受限比输出受限强"或反过来的说法都是错的。第三行说明两者都严格强于栈(3124 命中了栈的禁用模式"大、小、中")。

于是四个集合的关系是:

S队列SS输入受限, S输出受限Sdeque

其中中间两项之间无包含关系。三条依据分别是:栈只用"可插可删"的那一端就能被两种受限 deque 模拟出来(所以是包含),上表前两行给出了严格更大的证据(所以是真包含),而队列只能产生恒等序列 12n 一种,最小。

这条关系能省掉一半工作量:栈能产生的序列,两种受限 deque 一定也能产生,不必再逐个模拟。反过来则不成立。

六种基本操作与结构示意(想看清哪些操作被禁用就展开)
text
  前端 front                      后端 rear
    ↕                               ↕
 ┌──────┬──────┬──────┬──────┬──────┐
 │  a₁  │  a₂  │  a₃  │  a₄  │  a₅  │
 └──────┴──────┴──────┴──────┴──────┘
  可插入/删除                    可插入/删除

双端队列的六种基本操作:初始内容为 1、4、2;EnQueueHead(3) 在前端插入 3;EnQueueTail(5) 在后端插入 5;DeQueueHead() 从前端删除并返回 3;DeQueueTail() 从后端删除并返回 5;getHead() 读到 1,getTail() 读到 2,读操作不改变内容

图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图3.27 双端队列的基本操作,p127

图里六个操作两两成对:EnQueueHead / EnQueueTail 是两端插入, DeQueueHead / DeQueueTail 是两端删除,getHead / getTail 是两端读取(不改变队列)。 把其中若干个操作"禁用",就得到本篇讨论的各种受限形式。

实现:就是循环队列的两端版

两种受限 deque 都用循环数组实现,指针移动完全沿用循环队列的取模口径:front 指向队头元素,rear 指向队尾元素的下一个位置,判空 front == rear,判满 (rear + 1) % MaxSize == front(牺牲一个单元)。所以这一节没有新东西要学,唯一的新问题是"多出来的那两个端点操作,指针该怎么动"。

两种受限 deque 的完整代码(写代码题时展开)

输入受限双端队列:只允许从 rear 插入,两端都可删除。

c
#define MaxSize 50
typedef struct {
    int data[MaxSize];
    int front, rear;   // front 指向队头元素,rear 指向队尾元素的下一个位置
} InputRestrictedDeque;

void InitDeque(InputRestrictedDeque *dq) { dq->front = dq->rear = 0; }

// 唯一的插入方式:从 rear 端插入
bool InsertRear(InputRestrictedDeque *dq, int x) {
    if ((dq->rear + 1) % MaxSize == dq->front) return false;  // 队满
    dq->data[dq->rear] = x;                    // rear 指的是空位,先写入
    dq->rear = (dq->rear + 1) % MaxSize;       // 再前进
    return true;
}

// 从 front 端删除
bool DeleteFront(InputRestrictedDeque *dq, int *x) {
    if (dq->front == dq->rear) return false;   // 队空
    *x = dq->data[dq->front];
    dq->front = (dq->front + 1) % MaxSize;
    return true;
}

// 从 rear 端删除:rear 要先后退一格才指到有效元素
bool DeleteRear(InputRestrictedDeque *dq, int *x) {
    if (dq->front == dq->rear) return false;   // 队空
    dq->rear = (dq->rear - 1 + MaxSize) % MaxSize;  // 先退,+MaxSize 防负数
    *x = dq->data[dq->rear];                   // 再取值
    return true;
}

输出受限双端队列:两端都能插入,只能从 front 删除。

c
typedef struct {
    int data[MaxSize];
    int front, rear;
} OutputRestrictedDeque;

// 从 rear 端插入
bool InsertRear2(OutputRestrictedDeque *dq, int x) {
    if ((dq->rear + 1) % MaxSize == dq->front) return false;
    dq->data[dq->rear] = x;
    dq->rear = (dq->rear + 1) % MaxSize;
    return true;
}

// 从 front 端插入:front 指向队头元素,要先退一格腾出位置
bool InsertFront(OutputRestrictedDeque *dq, int x) {
    if ((dq->rear + 1) % MaxSize == dq->front) return false;
    dq->front = (dq->front - 1 + MaxSize) % MaxSize;  // 先退,再写
    dq->data[dq->front] = x;
    return true;
}

// 唯一的删除方式:从 front 端删除
bool DeleteFront2(OutputRestrictedDeque *dq, int *x) {
    if (dq->front == dq->rear) return false;
    *x = dq->data[dq->front];
    dq->front = (dq->front + 1) % MaxSize;
    return true;
}

四个端点操作、判空判满全是 O(1);数组固定占 O(MaxSize),与实际元素个数无关,单次操作的辅助空间 O(1)。采用"牺牲一个单元"的判满方案时,可用容量是 MaxSize - 1

上面代码里四个端点操作的指针方向是镜像的:front 端插入让 front 减小、删除让它增大rear 端插入让 rear 增大、删除让它减小。至于"先移指针还是先读写",判据仍是那条贯穿本章的老规矩——指针指着有效元素就"先腾位再写",指着空位就"先写再移"。所以 front 端插入要先退一格再写,rear 端删除要先退一格再取。

⚠️ 指针减小时的取模必须写成 (x - 1 + MaxSize) % MaxSizerear 为 0 时 (0-1) % MaxSize 在 C 中得到 1,直接当下标就越界了。

考点速记

三条会被反复调用的结论:

  1. 栈的序列集合真包含于两种受限双端队列各自的集合,而两种受限形式互不包含
  2. 输入受限的队内元素始终递增,于是每步只能输出队内的最小值或最大值——这条把判定变成一眼可查;输出受限没有这条捷径,要倒着剥端点。
  3. 指针指有效元素就"先腾位",指空位就"先写入"——这条规则贯穿顺序栈、循环队列和本篇。

这一节在真题里被考过的形式(下方「真题练习」逐题对应):给一种受限方式,问哪个出队序列得不到。题面通常不出现"双端队列"四个字,而是用一句话描述权限,所以第一步永远是把那句话翻译成权限表

  • "允许在两端入队,仅允许在一端出队"——插入两端、删除一端,是输出受限
  • "一端仅能入队,另一端既能入队又能出队"——插入仍是两端、删除仍是一端,同样是输出受限,只是换了个说法。
  • 翻译完再套方法:输出受限用倒着剥端点,输入受限用递增规则

易错把"输入受限"和"输出受限"认反。 记住受限的是动作:输入受限 = 插入受限,输出受限 = 删除受限。看到"两端都能入队"就该立刻判定为输出受限。

易错以为两种受限形式有强弱之分。 它们互不包含,各有对方产生不了的序列(41324213 正是这一对反例)。

易错用栈的禁用模式去判 deque。 栈的"大、小、中"禁用模式对双端队列不成立——3124 栈产生不了,两种受限 deque 都能产生。

教材出处
  • 队列的抽象数据类型定义(本篇 deque 的操作集是它的推广): 严蔚敏《数据结构(C 语言版)》(第 2 版)p69「3.5.1 队列的类型定义」
  • 循环队列的入队、出队与取模指针移动(本篇代码沿用其口径):同书 p71–p72
  • 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版) 图3.27 双端队列的基本操作,p127

说明:严蔚敏《数据结构(C 语言版)》(第 2 版)正文未单列双端队列一节, 本篇的定义与操作权限表以殷人昆书的双端队列操作集为依据(见上图), 输出序列的判定与包含关系则由本篇自行推导并给出了可复核的具体序列,不附会页码。

相关知识

栈和队列的基本概念(合法出栈序列的模拟法与禁用模式,本篇判定方法由此推广)| 顺序栈(只用一端插删就退化为它)| 循环队列(本篇代码的取模写法全部来自该篇)| 链式队列(双端队列同样可以用双链表实现,两端插删都是 O(1)

真题练习

相关真题(2题)