3. 优先队列基础与订单簿映射

订单簿在数据结构层面并不是“两个排好序的数组”这么简单。它要同时满足两个看似矛盾的需求:一是按价格严格有序,二是同一价格上要按时间先后排队。把这两个需求统一起来的核心抽象,就是优先队列。本节从优先队列的抽象定义出发,说明最大堆与最小堆的堆序性质,再把它映射到买、卖两个队列上,最后给出订单优先级规则的完整判定逻辑。

一、优先队列抽象:只关心“下一个该处理谁”

优先队列(Priority Queue)是一种容器抽象,它不保证先进先出,而是保证每次取出的是当前优先级最高(或最低)的元素。它对外暴露的操作通常只有四个:

  • 插入(push / insert):放入一个带优先级的元素;
  • 查看队首(peek / top):读取当前优先级最高的元素,但不移除;
  • 弹出(pop / extract):移除并返回当前优先级最高的元素;
  • 规模(size / empty):判断容器状态。

关键点在于:优先队列不承诺整体有序,只承诺“队首一定是当前最优”。这一点对订单簿极其重要——撮合引擎在绝大多数时刻只需要知道“最优买价是多少、最优卖价是多少、该价位上最早的是哪一笔”,而不需要把全部订单完整排序。用堆实现优先队列,插入和弹出都是 O(log n),查看队首是 O(1),比每次全量排序的 O(n log n) 高效得多。

二、最大堆与最小堆:堆序性质

堆是一棵完全二叉树,通常用数组存储:下标 i 的节点,其左孩子在 2i+1,右孩子在 2i+2,父节点在 ⌊(i−1)/2⌋。堆只维护一条约束,称为堆序性质:

  • 最大堆:任意节点的值 ≥ 其所有孩子的值,因此根节点是全局最大值;
  • 最小堆:任意节点的值 ≤ 其所有孩子的值,因此根节点是全局最小值。

注意堆序性质只约束“父子”,不约束“兄弟”,所以堆内数组并不是有序序列。以最大堆为例,数组 [9, 7, 8, 3, 6, 5, 2] 满足堆序:9 的孩子是 7 和 8,7 的孩子是 3 和 6,8 的孩子是 5 和 2,逐对比较均成立;但 7 与 8 的先后、3 与 6 的先后并无要求。插入时采用“上浮”(sift-up):新元素放到数组末尾,与父节点比较,若违反堆序就交换,直到满足为止。弹出时采用“下沉”(sift-down):把末尾元素移到根,再与较大(最大堆)或较小(最小堆)的孩子交换,逐层下沉。

三、映射到订单簿:买用最大堆,卖用最小堆

订单簿的撮合规则是“价格优先、时间优先”。价格优先意味着:买方希望以最高买价成交,卖方希望以最低卖价成交。因此天然形成一对镜像结构:

  • 买队列(Bid)用最大堆:堆顶是最高买价,即最优买价;
  • 卖队列(Ask)用最小堆:堆顶是最低卖价,即最优卖价。

但订单簿还有第二层结构:同一价格上往往有多笔订单,它们必须按到达时间先后成交。若把每笔订单直接塞进一个堆,堆只能保证价格最优,无法保证同价位内的时间顺序。工程上的标准做法是两级结构:外层是“价格档位”的堆,每个堆元素是一个价格档;每个价格档内部再挂一条按时间排序的链表(或 FIFO 队列)。这样,堆负责价格优先,链表负责时间优先,职责清晰、互不干扰。

下面这张图展示了订单簿与两级优先队列的映射关系:左侧买堆为最大堆,右侧卖堆为最小堆,堆顶分别指向最优买价与最优卖价;每个价格档内部挂一条时间链表,箭头方向表示成交时从队首取单。

订单簿的两级优先队列映射 买队列 Bid · 最大堆(价格优先) 100.5 堆顶 = 最优买价 100.3 100.1 99.8 99.5 100.5 档位内部:时间链表(FIFO) #A 09:00:01 #B 09:00:03 #C 09:00:07 成交时从队首 #A 开始取单 卖队列 Ask · 最小堆(价格优先) 100.8 堆顶 = 最优卖价 101.0 101.2 101.5 101.8 100.8 档位内部:时间链表(FIFO) #X 09:00:02 #Y 09:00:05 #Z 09:00:09 成交时从队首 #X 开始取单 撮合判断 最优买价 ≥ 最优卖价? 100.5 < 100.8 → 不成交 堆顶比较 O(1) 插入/弹出 O(log n)

四、订单优先级规则:价格优先、时间优先

把堆序性质与档位内链表结合后,订单优先级规则可以精确表述为一条两级比较链:

  1. 第一级比较价格:买单价格越高越优先,卖单价格越低越优先。这由最大堆/最小堆的堆顶保证。
  2. 第二级比较时间:价格相同时,到达时间越早越优先。这由档位内 FIFO 链表保证。

撮合时只需比较两个堆顶:若最优买价 ≥ 最优卖价,则存在可成交价格区间,从买堆顶档位和卖堆顶档位各取队首订单,按“卖方要价、买方出价、先到者价格”的规则确定成交价(常见规则是以先进入订单簿的一方价格成交),成交后从链表头部移除;若某档位链表空了,则对该档位执行堆的弹出操作,让新的堆顶浮现。若最优买价 < 最优卖价,则买卖价差存在,撮合暂停,等待新订单插入。

举一个示例(数据为教学假设):买堆顶为 100.5,档位内依次是 #A(09:00:01)、#B(09:00:03);卖堆顶为 100.8,档位内依次是 #X(09:00:02)、#Y(09:00:05)。此时 100.5 < 100.8,不成交。若新来一笔卖单 #W 以 100.4 挂出,它进入卖堆后成为新的最小堆顶(100.4 < 100.8),此时 100.5 ≥ 100.4,触发撮合:#A 与 #W 成交,成交价取先到者 #A 的买价 100.5。若 #A 全部成交,则买堆顶档位链表头变为 #B,继续与卖侧比较。

实现要点:堆中存储的应是“价格档位对象”而非单笔订单,档位对象需携带价格键与指向内部链表的指针;比较函数只比较价格键,时间顺序完全交给链表维护。这样插入新订单时,若档位已存在则只做链表尾插 O(1),否则才做一次堆插入 O(log n)。
常见误区:把每笔订单直接放进堆,再用“价格+时间”复合键比较。这样做虽然也能得到正确顺序,但每次插入都要 O(log n),且同价位订单在堆中分散,撤单时定位困难。两级结构把高频操作(同价位追加、队首移除)降到 O(1),是撮合引擎的标准取舍。

理解这套映射后,后续讨论价格档位聚合、深度快照生成、撤单定位时,都可以回到同一个心智模型:堆管价格方向,链表管时间先后,堆顶比较决定是否成交。