数据结构选型:为什么Python原生list不适合高频订单簿?

大家好,我是你们的讲师。今天我们来聊一个很基础、但又很关键的问题——数据结构选型。

你可能觉得,不就是个列表吗?Python的list用起来多顺手啊。但我要告诉你,在高频交易场景下,用list来维护订单簿,基本等于自杀。这话不是我危言耸听,是我在实盘环境里踩过的坑。

先说说订单簿到底长什么样

订单簿,说白了就是买卖双方的排队队列。买盘从高到低排,卖盘从低到高排。每一档价格上,挂着多少手单子,一目了然。

举个例子:

卖五:10.05  200手
卖四:10.04  150手
卖三:10.03  300手
卖二:10.02  100手
卖一:10.01  250手
-------------------
买一:10.00  180手
买二:9.99   220手
买三:9.98   160手
买四:9.97   90手
买五:9.96   300手

嗯,这就是最经典的订单簿结构。每一笔新的报单进来,要么插入到合适的位置,要么修改已有的档位,要么删除某个档位。你想想看,这些操作在Python list里做,会是什么效果?

Python list的致命伤

Python的list,底层是动态数组。动态数组有个特点:随机访问快,但插入和删除慢

为什么会这样?

因为数组在内存里是连续存储的。你要在中间插入一个元素,后面的所有元素都得往后挪一位。删除也是同理,后面的元素要往前补位。这个操作的时间复杂度是O(n)。

我举个例子你就明白了:

# 假设订单簿有1000档价格
order_book = [9.90, 9.91, 9.92, ..., 10.90]

# 现在要插入一个9.95的价格
# 需要找到插入位置,然后把后面的所有元素往后移
index = bisect_left(order_book, 9.95)  # 二分查找O(log n)
order_book.insert(index, 9.95)  # 插入操作O(n)

你看,虽然查找用了二分法,时间复杂度是O(log n),但插入这一步直接把整体拖成了O(n)。1000档还好说,要是10000档呢?要是每秒有几千笔报单呢?

我曾经在回测系统里用list维护订单簿,数据量一上来,回测速度直接崩了。后来一分析,90%的时间都花在list的插入和删除上。那叫一个惨。

那数组呢?C++里的vector也不行吗?

如果你用C++,可能会想到std::vector。它和Python list本质一样,都是动态数组。同样的问题——中间插入删除是O(n)。

但C++有个优势:你可以用环形缓冲区或者预分配大数组来优化。比如,你知道订单簿最多只有100档价格,那就直接开一个固定大小的数组,用索引来模拟插入删除。这样能避免内存搬移。

不过,这属于"取巧"的做法。真正的低延迟场景,我们一般不这么干。

链表:插入快,但查找慢

链表呢?插入和删除确实是O(1),只要你已经找到了位置。但问题在于——查找位置本身是O(n)

订单簿里,你每次操作都需要先找到正确的价格档位。链表不支持随机访问,只能从头遍历。1000档价格,平均要遍历500次才能找到目标位置。

你说,那用哈希表存位置映射?嗯,这倒是个思路。但哈希表本身有开销,而且订单簿的价格是连续区间,哈希冲突也会影响性能。

我个人习惯是:链表只适合写多读少的场景。订单簿是读写都频繁,链表扛不住。

跳表:折中的王者

那有没有一种数据结构,既能快速查找,又能快速插入删除?

有,跳表(Skip List)。

跳表的本质,是在链表的基础上,增加了多层索引。每一层索引都跳过一部分节点,让查找时能"跳着走",而不是一个个遍历。

你看这张图就明白了:

跳表结构示意图 9.90 9.92 9.95 9.98 10.01 10.03 9.92 9.98 10.03 9.98 查找 9.95 的路径: 第0层(完整数据) 第1层索引 第2层索引

查找的时候,从最高层开始,往右走。如果下一个节点比目标大,就往下跳一层。这样,查找的时间复杂度降到了O(log n)。

插入和删除呢?也是O(log n)。因为你先用O(log n)找到位置,然后执行O(1)的指针操作。

核心结论:跳表在查找、插入、删除三个操作上,都做到了O(log n)的时间复杂度。对于订单簿这种读写均衡的场景,跳表是最优解之一。

三种数据结构的对比

操作 Python list 链表 跳表
查找 O(1) 随机访问
O(log n) 二分查找
O(n) O(log n)
插入 O(n) 内存搬移 O(1) 已知位置 O(log n)
删除 O(n) 内存搬移 O(1) 已知位置 O(log n)
内存开销 中(指针开销) 较高(多层索引)
适用场景 只读、小数据量 写多读少 读写均衡

避坑指南:我曾经在早期版本里用Python list + bisect模块做订单簿,觉得二分查找很快。结果实盘一跑,延迟直接飙到毫秒级。后来换成跳表,延迟降到了微秒级。差距就是这么大。

那Python里怎么用跳表?

Python标准库没有跳表实现。但你可以自己写一个,或者用第三方库。我个人建议自己写,因为订单簿的跳表有一些特殊需求:

  • 价格是浮点数,需要处理精度问题
  • 同一价格可能有多个订单,需要支持FIFO队列
  • 需要快速获取最优买卖价,即跳表的首尾节点

一个简单的跳表节点定义:

class SkipNode:
    __slots__ = ('price', 'volume', 'forward', 'height')
    
    def __init__(self, price, volume, height):
        self.price = price
        self.volume = volume
        self.forward = [None] * (height + 1)
        self.height = height

注意我用了__slots__,这是Python性能优化的一个小技巧。它能减少每个对象的内存开销,提升属性访问速度。

嗯,这里要注意:跳表虽然好,但不是银弹。如果你的订单簿只有几十档价格,用list反而更快。因为跳表的索引维护有额外开销。我一般建议,超过100档价格,才考虑用跳表。

总结一下

Python原生list不适合高频订单簿,根本原因在于插入和删除的O(n)时间复杂度。在高频场景下,每秒几千笔报单,O(n)的操作就是灾难。

链表虽然插入快,但查找慢,不适合订单簿这种需要频繁定位的场景。

跳表通过多层索引,把查找、插入、删除都优化到了O(log n),是目前订单簿实现的主流选择。

当然,还有更极致的数据结构,比如红黑树、B树,甚至是一些定制的内存结构。这些我们后面章节再聊。

记住一句话:选对数据结构,比优化代码逻辑更重要。数据结构选错了,后面再怎么优化都是白费力气。


专注资料整理