CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
高级技巧语法算法996引擎

Lua双指针队列:弹出零搬移的O(1)实现

2026-09-28 00:04 作者:996 技术组 996引擎Lua教程传奇脚本高级技巧语法算法996引擎

【语法算法】
一、一行代码的代价

table.remove(t, 1)——弹出队头最顺手的一行。Lua 5.1 里它的实现是数组段整体前移:第 2 到第 n 个元素逐格左移,末位置 nil。队里一千个元素,弹一次搬九百九十九次;战斗飘字、伤害队列、拾物事件每帧都在弹头,等于每帧做一次微型搬家。问题的根子不在 table.remove 写得差,而在数组段的物理形态:连续、有序、下标即身份,物理弹头等于整队平移。

二、底层原理:两根指针夹出活动区间

零搬移的思路是"不删元素,只动边界"。队列维护两根指针:_index 是头指针,_border 是尾指针。push 把元素写进 _data[_border],_border 加一;pop 取出 _data[_index] 并置 nil,_index 加一。任意时刻活动元素都夹在两指针之间,size() 是一次减法,push 与 pop 双向 O(1)。数组前段会留下已消费的空洞,这是设计内的代价——队列耗尽时一次性 reset 重建表壳,把带洞的旧表整体丢给 GC。

mermaid
flowchart LR
    subgraph data["_data 数组段"]
        d0["[0] nil"] --- d1["[1] nil"] --- d2["[2] 弹出中"] --- d3["[3] 活动"] --- d4["[4] 活动"]
    end
    I["头指针 _index = 2"] -.-> d2
    B["尾指针 _border = 5"] -.-> d4
    P["push:写 _data[_border],_border+1"] --> d3
    O["pop:取 _data[_index] 置 nil,_index+1"] --> d2

三、正确写法(可直接落地)

lua
local queue = class('queue')

function queue:ctor()
    self:reset()
end

function queue:push(d)
    self._data[self._border] = d
    self._border = self._border + 1
end

function queue:pop()
    local ret = nil
    if self._index < self._border then
        ret = self._data[self._index]
        self._data[self._index] = nil     -- 断引用,交给 GC
        self._index = self._index + 1
    else
        self:reset()                      -- 队列耗尽,重建表壳
    end
    return ret
end

function queue:front()
    if self._index < self._border then
        return self._data[self._index]
    end
    return nil
end

function queue:size()
    return self._border - self._index
end

function queue:empty()
    return self:size() == 0
end

function queue:reset()
    self._index  = 0
    self._border = 0
    self._data   = {}
end

四、三个必须钉死的细节

  1. pop 置 nil 不是洁癖而是纪律。数组段中间留洞,# 的边界语义会失控——它只承诺返回某个边界位置;保持 [0, _index) 区间全 nil,_data 从 _index 起仍是完美数组段,#、ipairs 都不踩坑。更重要的是引用断裂:弹出的节点或消息当场失去来自队列的引用,GC 三色标记下一轮即可回收,队列里不养"逻辑已删、物理还挂"的僵尸对象。
  1. reset 放在队空时机。_index 追上 _border 才重建,一次重建摊给 N 次操作,均摊 O(1);若每次 pop 都重建表壳,反而退化成 O(n) 分配,前功尽弃。
  1. 哈希段也能实现同样语义(t[head] = nil; head = head + 1,键永远递增不冲突),代价是放弃数组段的 ipairs 顺序语义与紧凑内存布局。对象池回收通道、网络消息队列、技能事件队列这类高频结构,数组段双指针是最稳的底座。

五、老手再看一眼

热路径的最后一步:把 queue 实例与常用方法提为局部变量,table.insert 提成 upvalue,循环里省掉全局查找与点号寻址。对象池内部拿这条队列做回收通道,每帧进出几百个节点也不产生一次搬移。四十行代码换来帧循环里确定性的 O(1)——数据结构选型在游戏前端最直接的回报,就长这样。

作者履历与出处

本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。

← 返回文章地图返回研学路径

幂尔框架 · 实战干货 · 接口调用

LATEST ARTICLES

全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

Lua+next遍历:next的底层遍历本质

【语法算法】 next 的遍历本质就这一行: 传入上一个键,返回下一个键值对——不传上一个键就从第一个开始——next 是 …

2026-09-29 15:16 996 技术组
进阶实战游戏功能

Lua+多重返回值:函数返回值的截取与传递本质

【语法算法】 返回值的截取本质就这一行: 函数返回三个值,左侧三个变量各接一个——多余的返回值被丢弃,不足的补 nil——多…

2026-09-29 15:16 996 技术组
进阶实战游戏功能

Lua+collectgarbage:GC的手动触发与分步本质

【语法算法】 collectgarbage 的分步回收就这一行: "step" 模式让 GC 执行一步增量回收——参数 20…

2026-09-29 15:16 996 技术组
进阶实战游戏功能

Lua+string.gmatch:迭代匹配的遍历本质

【语法算法】 gmatch 的迭代本质就这一行: gmatch 返回一个迭代函数——每次调用返回下一个匹配——遍历完返回 n…

2026-09-29 15:16 996 技术组
进阶实战游戏功能

Lua+xpcall:错误处理函数的传入本质

【语法算法】 xpcall 的错误处理函数就这一行: xpcall 和 pcall 的本质区别就一个:xpcall 可以传入…

2026-09-29 15:16 996 技术组
进阶实战游戏功能

Lua+元方法__index为函数:代理转发与懒加载的本质

【语法算法】 元方法 __index 设为函数的本质就这一行: 读取表中不存在的键时,Lua 调用 __index 函数——…

2026-09-29 15:16 996 技术组