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

Lua最小堆:上浮下沉的完全二叉树

2026-10-02 01:41 作者:996 技术组 996引擎Lua教程传奇脚本进阶实战游戏功能996引擎

【语法算法】

local p = math.floor((i - 1) / 2)——先拆这一行。堆的第 i 个成员,它的父亲就在第 i 除以二再取整的位置——一行取整算出亲缘,数组就这样当树使。最小堆的全部机关就这一行加两条规矩:父永远小于子,出账永远从堆顶走。这篇把最小堆整套写法拆开:配置表、入堆上浮、出堆下沉、看顶、定时器、调试按钮,一段一段照抄能跑。

一、效果演示:上浮下沉的完全二叉树

演示场左半是树、右半是数组——同一份账的两种长相,树给人看亲缘,数组给人看存储,换位的时候两边一起动。点「入堆」:一个随机数从末位进堆,一步步跟自己的父亲比:比父亲小就上浮一格,浮到父亲不再比它大为止,绿光标就是它此刻的位置。点「出堆」:堆顶的最小值离场,末位的数补到顶上,再一路跟子换位沉到底。树在换位,数组在同步换位——两种长相一个账本。演示里试两笔账:连入五个数再连出五次,出堆的次序永远从小到大——堆不管你进多乱,出的时候一准有序,这就是它吃这碗饭的本钱。最小堆教的是规矩:账乱进,账平出——进的时候不挑,出的时候有序,这份从容全靠那两条家规撑着。

mermaid
flowchart TD
A[新数从末位入堆] --> B{比父亲小}
B -- 是 --> C[与父亲换位 继续上浮]
B -- 否 --> D[停 浮到位]
C --> B
E[出堆 堆顶离场] --> F[末位补到顶]
F --> G{比小的子大}
G -- 是 --> H[与较小的子换位 继续下沉]
G -- 否 --> I[停 沉到位]
H --> G
demo
 fx-minheap

二、底层原理:一次上浮加一次下沉

模块的机关是一对搭档。入堆上浮:新数从末位进账,一路跟父亲比——比父亲小就换位继续浮,浮到顶或者父亲不再比自己大为止,上浮保住"父小于子"的家规——家规不破,堆顶永远是全场最小,这就是家规的回报,也是这个数据结构全部的骄傲。出堆下沉:堆顶的最小值离场,账不能空——末位的数补到顶,再一路跟自己较小的子换位沉到底,下沉同样保家规。和无序数组分野在"堆顶永远是最小":无序数组取最小要扫全表,堆取最小只看顶——进账出账频繁、每次只要最值的玩法,堆就是为它长的——每次只要最值的账,排序是杀鸡用牛刀还费柴火。

为什么出堆要末位补顶?顶空了堆就散了架——末位补顶保住了完全二叉的形状,形状一散,下标算亲缘的账就全乱了,形状在,下标算亲缘的那一行才成立;补顶后的下沉是给新顶补课,两步合起来才是一次数的交接——交接不清,家规就破在一处。

三、核心代码:完整模块(上·骨架)

lua
-- @file MinHeap.lua
-- 最小堆 —— 上浮下沉的完全二叉树

local MinHeap = {}

local CONST = {
    HEAP_CAP      = 64,

    AUTOINC_BASE  = 1143000,
}

local _heap      = {}
local _count     = 0
local _compare   = nil

local function ShowTip(msg)
    if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end

local function ParentOf(i)
    return math.floor((i - 1) / 2)
end

local function LeftOf(i)
    return 2 * i + 1
end

local function RightOf(i)
    return 2 * i + 2
end

local function Less(a, b)
    if _compare then
        return _compare(_heap[a], _heap[b])
    end
    return _heap[a] < _heap[b]
end

function MinHeap.Count()
    return _count
end

function MinHeap.Peek()
    return _heap[0]
end

四、核心代码:完整模块(下·上浮与下沉)

lua
-- 上浮:新数从末位一路跟父亲比
local function Swim(i)
    while i > 0 do
        local p = ParentOf(i)
        if Less(i, p) then
            _heap[i], _heap[p] = _heap[p], _heap[i]
            i = p
        else
            break
        end
    end
end

-- 下沉:补顶的数一路跟较小的子比
local function Sink(i)
    while true do
        local l = LeftOf(i)
        local r = RightOf(i)
        local m = i
        if l < _count and Less(l, m) then m = l end
        if r < _count and Less(r, m) then m = r end
        if m == i then break end
        _heap[i], _heap[m] = _heap[m], _heap[i]
        i = m
    end
end

function MinHeap.Push(value)
    if _count >= CONST.HEAP_CAP then
        ShowTip("堆满了——出几个再入")
        return false
    end
    _heap[_count] = value
    Swim(_count)
    _count = _count + 1
    return true
end

function MinHeap.Pop()
    if _count == 0 then return nil end
    local top = _heap[0]
    _count = _count - 1
    _heap[0] = _heap[_count]
    _heap[_count] = nil
    Sink(0)
    return top
end

function MinHeap.BindCompare(func)
    _compare = func
end

SL:ScheduleOnce(function()
    SL:BindDebugButton("演示入堆", function()
        MinHeap.Push(math.random(10, 99))
    end)
    SL:BindDebugButton("演示出堆", function()
        MinHeap.Pop()
    end)
    ShowTip("技能已加载: 最小堆 (调试按钮触发)")
end, 1.0)

function MinHeap.Unload()
    _heap = {}
    _count = 0
    _compare = nil
end

return MinHeap

五、机制问答

问:为什么数组能当树使?
答:下标算亲缘——第 i 个的父亲是 i 除二取整,左右子是二 i 加一减一,形状是完全二叉,下标永远算得回亲缘。

问:上浮什么时候停?
答:两道停——浮到根没有父亲可比,或者父亲不再比自己大,家规没破就到家了。

问:出堆为什么末位补顶而不是挪整棵子树?
答:保形状——末位补顶只动一条路径,挪子树要动半棵树,补顶下沉是最省的交接。

问:堆和直接排序的区别是什么?
答:动态与静态——排序管一批一次性排好,堆管进进出出随时要最值,账本活着就用堆。

问:堆里的次序稳定吗?
答:不稳定——同值的两个数谁在上谁在下不看先后,在乎先后就往成员里塞一个自增的次序号再比。

六、调参与实战怎么用

第四笔是比较器的挂载:默认比数值、挂了比较器比任意字段——大顶堆就是换一行比较器的事,一套上浮下沉通吃大小顶。第三笔是容量的封顶:六十四格的顶是内存的闸——堆满了入堆返回假不硬塞,调用方自己决定挤出还是丢弃。第二笔是下沉的选子:跟较小的子换位是下沉的铁律——跟大子换位家规当场就破,这一行写错堆就不是堆。第一笔是取顶的零耗:看顶不出账是频繁查最值的福利——出堆再入堆是浪费,看一眼的事就走顶上一格。常见坑三个:下选举子时漏了右子导致家规单边破;容量闸只拦入堆不拦账内换位;比较器换了方向之后忘了同步看顶的语义,顶上站的反而是最大。

实战里最小堆是"随时要最值"的通用答案:技能冷却队列谁先转好、定时器谁先到点、寻路的开放列表谁离目标近,全是堆顶那一格的事——堆顶换人,优先级就换了人。组里的约定是:凡是要"反复取最值"的账,一律上堆不排序——排序是死账,堆是活账。这篇的模块照抄能跑,改的就是 CONST 那张表加比较器。

写完留一句给做优先级系的同学:最小堆卖的是"最值永远在顶上"——那一次一次的上浮下沉和堆顶雷打不动的最小值,是把乱账变成有序出账的一棵完全二叉树。

作者履历与出处

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

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

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

LATEST ARTICLES

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

进阶实战游戏功能

Lua自动炮台:架一个自动索敌开火

【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua元素转换:火伤转冰伤附带减速

【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua连锁引燃:一个着火引燃下一个

【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua弹射地垫:踩上去弹飞到空中

【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua时间减缓:流速减半的局部时差

【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua引力球:丢出去把怪吸过来

【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…

2026-10-02 04:50 996 技术组