Skip to content

模型Model

队列

Queue · FIFO queue

在队尾加入、从队首取出,并保持尚未离队元素的到达次序的抽象数据类型;循环数组和链表给出不同表示。

形式陈述 ​

状态与操作 ​

令抽象状态为有限序列 Q=[a1,…,ak],左边是队首,右边是队尾。常见接口规定:

操作 返回值 新状态
enqueue(x) 无 [a1,…,ak,x]
front(),要求 k>0 a1 原状态
dequeue(),要求 k>0 a1 [a2,…,ak]
isEmpty() k=0 的真假 原状态

从空队列依次加入 A、B,取出一次,再加入 C,状态由 [A,B] 变成 [B],再变成 [B,C]。下一次必须取出 B,因为 B 比 C 更早加入,且还没有离队。

空队列上的 front、dequeue 需要明确失败行为。有界队列还要说明满队列时是拒绝、等待还是覆盖旧项;这三种策略具有不同的可观察语义。尤其是“覆盖最旧元素”的环形日志,不能在没有声明的情况下充当不丢元素的普通队列。

网络FIFO可在硬容量之外另做主动拥塞通知。RED在到达时更新平均队长,以条件概率提前选择丢弃或标记;CoDel在出队时观察驻留时间,并用移除候选后的剩余字节保护短队列。二者仍须交代每个包究竟入队、丢弃还是交付;提前通知不取消本页的满队列合同。

直觉

让最早等待的元素先离开 ​

队列(queue)按照先进先出(FIFO)的规则处理元素:从队尾加入,从队首取出。可以把它想成一列有编号的待处理任务;后来加入的任务排在后面,不能越过仍在等待的旧任务。

这是一个抽象数据类型的行为规则,而不是特定内存形状。链表可以连接队首到队尾,数组也可以借助循环下标复用槽位。与栈相比,差别在于从哪一端删除,而不在于有没有使用指针。[1]

队列尾入头出示意图
例子与边界

数组不必每次把元素往前搬 ​

若总让队首占据数组下标 0,出队时就要移动整个后缀,一次可能花费 Θ(k)。循环数组不搬后缀,只移动“队首在哪里”的下标。

取容量 C>0,维护队首下标 h 和当前元素数 s,不变量为 0≤h<C、0≤s≤C。第 j 个逻辑元素位于

A[(h+j)modC],0≤j<s.

非满时,入队写入 A[(h+s)modC],再令 s←s+1。非空时,出队读出 A[h],再令 h←(h+1)modC、s←s−1。不再属于队列的槽位可以清除引用,避免无意保留对象。

容量为 4 的一次完整绕回过程如下,— 表示不属于逻辑队列的槽位:

操作完成后 下标 0 下标 1 下标 2 下标 3 h s 逻辑队列
加入 A、B、C A B C — 0 3 A、B、C
取出 A、B — — C — 2 1 C
加入 D、E E — C D 2 3 C、D、E
加入 F E F C D 2 4 C、D、E、F

最后一行数组中的物理次序是 E,F,C,D,逻辑次序却仍是 C,D,E,F。循环下标让两者对应,不要求它们看起来一致。

若只存首尾两个模 C 下标,则两者相等既可能表示空,也可能表示满。保存元素数、额外保存满标志,或牺牲一个槽位,都是消除这种歧义的不同约定。本例保存 s,因此四个槽位都可使用。[2]

三种常见实现的时间界 ​

固定容量循环数组在非满、非空等相应条件下,可以用最坏 O(1) 的局部操作完成入队、出队和查看队首。若允许几何扩容,复制旧元素的某一次操作会变慢,通常只能对更新声称摊还 O(1);扩容时应按逻辑次序复制,再重新设置队首。

链表实现同时保存头、尾引用。入队接到尾部,出队删除头部,都只修改常数个链接。删除最后一个节点时必须同时把头、尾置空,否则尾引用会指向已经离队的节点。在常数大小节点、局部分配释放按单位成本的模型下,这些操作为最坏 O(1)。[1]

队列还可以用两个栈实现:新元素压入输入栈;需要出队且输出栈为空时,把输入栈逐个弹出并压入输出栈,反转后的最旧元素恰好位于输出栈顶。每个元素至多经历一次这样的转移,所以从空结构开始的一串操作总成本为线性,单次出队却可能搬动很多元素。这是摊还常数时间,不是最坏常数时间。

推论与应用

FIFO 在算法里提供了什么 ​

广度优先搜索从起点入队开始,每次取出队首,再把尚未发现的邻居加入队尾。只在第一次发现时入队,队列就按距离层次推进:距离为 d 的点扩展时,新发现的点距离为 d+1,会排在当前已发现的点之后。这支撑无权图的最短路径结论。

“访问过”的标记通常在入队时设置。若等到出队才标记,多个前驱可能把同一个顶点重复加入,破坏每个顶点只入队一次的时间记账。

任务系统中的FIFO要明确约束的是哪个事件。按FIFO分发给多个工作线程,只保证取任务的次序,不保证任务完成次序;较晚取出的短任务可能先结束。并发实现如Michael–Scott队列进一步用链接CAS与Head推进定义入队/出队的生效时刻;Tail暂时落后可由别人帮助修复,并不改变抽象FIFO内容。

亏额轮转分组调度同时使用每流FIFO和活跃流FIFO:一次服务只扣本流头包,未用字节预算保留到下轮,非空流再加入活跃队尾。这样可以在包长不等时讨论字节份额,而不是把“每流一次”误当相等的已发送字节数。

优先队列按优先级选择下一项,双端队列允许两端操作,二者提供的都不是纯FIFO接口。

循环缓冲区也可结合动态数组的扩容策略,但扩容时必须按逻辑队列顺序搬移元素。

参考资料

[1] Robert Sedgewick、Kevin Wayne,Algorithms, 4th ed., Addison-Wesley, 2011,§1.3: Bags, Queues, and Stacks:队列接口与带头尾引用的链式实现。

[2] Pat Morin,Open Data Structures: An Introduction,Athabasca University Press,2013,§2.3 ArrayQueue 与第 3 章:循环下标、容量管理及摊还成本。

关系图谱26 个相邻概念 · 5 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
分类位置

上位 / 更一般

下位 / 直接特例

暂未标注直接特例。

类型化关系