lua 工具函数,版本Lua 5.3+
--[[
util.lua —— 数值 / 位运算 / 采样 工具集
依赖:
* Lua 5.3+(使用了原生位运算符 & 、>> 、<<)
* 若在 Lua 5.1 / 5.2 使用,请引入 bit 库并替换位运算,
或只使用其中不依赖位运算的函数(Round / RoundXDecimal / Log2 / SampleArray)。
约定:
* 所有位相关函数仅对「正整数」有意义,非法输入返回 nil。
* FindSingleBitPosition / Log2Exact 仅接受 2 的幂,否则返回 nil。
* Round 系列对正负数的舍入方向统一为「远离零」。
* SampleArray 返回新数组,不修改入参数组。
--]]
local util = util or {}
util = util
local LN2 = math.log(2)
--------------------------------------------------------------------------------
-- 舍入
--------------------------------------------------------------------------------
--- 四舍五入到整数(正负数都正确,舍入方向为「远离零」)
--- @param num number 待舍入的数
--- @return integer
function util.Round(num)
if num >= 0 then
return math.floor(num + 0.5)
else
return math.ceil(num - 0.5)
end
end
--- 四舍五入到指定小数位
--- @param n number 待舍入的数
--- @param decimals integer? 小数位数,默认 0(即舍入到整数)
--- @return number
function util.RoundXDecimal(n, decimals)
local mult = 10 ^ (decimals or 0)
return util.Round(n * mult) / mult
end
--- 四舍五入到 1 位小数
--- @param n number
--- @return number
function util.Round1Decimal(n)
return util.RoundXDecimal(n, 1)
end
--------------------------------------------------------------------------------
-- 对数
--------------------------------------------------------------------------------
--- 计算任意正数的以 2 为底的对数(浮点结果)
--- 说明:使用 math.log 直接换算;x <= 0 时返回 nil。
--- 若需要「最高位 bit 位置」请用 Log2Exact 或另行添加 Log2Floor。
--- @param x number 必须 > 0
--- @return number|nil
function util.Log2(x)
if x <= 0 then
return nil
end
return math.log(x) / LN2
end
--- 计算以 2 为底的对数,仅当 x 是 2 的幂时返回精确整数
--- 例:Log2Exact(8) --> 3,Log2Exact(12) --> nil
--- 实现:先校验 2 的幂,再用二分位掩码定位,无浮点误差,覆盖 64 位整数。
--- @param x integer 必须为正整数且是 2 的幂
--- @return integer|nil
function util.Log2Exact(x)
-- 必须为正整数
if x <= 0 or x % 1 ~= 0 then
return nil
end
-- 必须是 2 的幂(二进制只有一个 1)
if (x & (x - 1)) ~= 0 then
return nil
end
local n = 0
if x & 0xFFFFFFFF00000000 ~= 0 then n = n + 32; x = x >> 32 end
if x & 0x00000000FFFF0000 ~= 0 then n = n + 16; x = x >> 16 end
if x & 0x000000000000FF00 ~= 0 then n = n + 8; x = x >> 8 end
if x & 0x00000000000000F0 ~= 0 then n = n + 4; x = x >> 4 end
if x & 0x000000000000000C ~= 0 then n = n + 2; x = x >> 2 end
if x & 0x0000000000000002 ~= 0 then n = n + 1 end
return n
end
--------------------------------------------------------------------------------
-- 位操作
--------------------------------------------------------------------------------
--- 找出一个数中唯一那个二进制位的位置(从 1 开始计数)
--- 仅接受 2 的幂,否则返回 nil。
--- 例:
--- FindSingleBitPosition(1) --> 1
--- FindSingleBitPosition(2) --> 2
--- FindSingleBitPosition(4) --> 3
--- FindSingleBitPosition(8) --> 4
--- FindSingleBitPosition(1024) --> 11
--- FindSingleBitPosition(12) --> nil (不是 2 的幂)
--- FindSingleBitPosition(0) --> nil
--- FindSingleBitPosition(-4) --> nil
--- @param num integer 必须为正整数且是 2 的幂
--- @return integer|nil
function util.FindSingleBitPosition(num)
if num <= 0 or num % 1 ~= 0 then
return nil
end
if (num & (num - 1)) ~= 0 then
return nil
end
-- 直接复用 Log2Exact,零误差且覆盖 64 位
return util.Log2Exact(num) + 1
end
--------------------------------------------------------------------------------
-- 采样 / 洗牌
--------------------------------------------------------------------------------
--- Fisher-Yates 洗牌采样。洗的是索引,不改动入参数组
--- (入参常是配置表缓存,原地洗牌会污染缓存)
--- 说明:
--- * 从 n 个元素中等概率抽取 sampleNum 个(无放回)。
--- * 只洗前 sampleNum 个位置,复杂度 O(sampleNum) 而非 O(n)。
--- * 返回新数组,不修改入参。
--- * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[] 输入数组
--- @param num integer 希望抽取的数量
--- @return any[] 抽取结果(新数组)
function util.SampleArray(array, num)
if array == nil then
return {}
end
local n = #array
if num <= 0 or n == 0 then
return {}
end
local sampleNum = math.min(math.floor(num), n)
-- 索引表
local indexes = {}
for i = 1, n do
indexes[i] = i
end
-- 只洗前 sampleNum 个位置:j ∈ [i, n]
for i = 1, sampleNum do
local j = math.random(i, n)
indexes[i], indexes[j] = indexes[j], indexes[i]
end
-- 取前 sampleNum 个
local result = {}
for i = 1, sampleNum do
result[i] = array[indexes[i]]
end
return result
end
--- Fisher-Yates 原地采样(会修改入参数组!)
--- 说明:
--- * 直接把前 sampleNum 个位置洗成随机样本,省掉索引表。
--- * 入参数组的前 sampleNum 个元素会被打乱,请勿用于共享/缓存数组。
--- * 返回的是新数组(从入参拷贝出的样本),不是入参本身。
--- * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[] 输入数组(会被修改)
--- @param num integer 希望抽取的数量
--- @return any[] 抽取结果(新数组)
function util.SampleArrayInPlace(array, num)
if array == nil then
return {}
end
local n = #array
if num <= 0 or n == 0 then
return {}
end
local sampleNum = math.min(math.floor(num), n)
-- 原地 Fisher-Yates:只洗前 sampleNum 个位置
for i = 1, sampleNum do
local j = math.random(i, n)
array[i], array[j] = array[j], array[i]
end
-- 拷贝前 sampleNum 个作为结果
local result = {}
for i = 1, sampleNum do
result[i] = array[i]
end
return result
end
--- Fisher-Yates 极致省内存版(会修改入参数组!)
--- 说明:
--- * 只洗牌,不拷贝样本,零额外分配(除入参本身)。
--- * 洗完后样本位于 array[1 .. 返回的 sampleNum]。
--- * 入参数组前 sampleNum 个元素会被打乱,请勿用于共享/缓存数组。
--- * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[] 输入数组(会被修改)
--- @param num integer 希望抽取的数量
--- @return integer 实际样本数量 sampleNum,样本在 array[1..sampleNum]
function util.ShuffleInPlace(array, num)
if array == nil then
return 0
end
local n = #array
if num <= 0 or n == 0 then
return 0
end
local sampleNum = math.min(math.floor(num), n)
for i = 1, sampleNum do
local j = math.random(i, n)
array[i], array[j] = array[j], array[i]
end
return sampleNum
end
--------------------------------------------------------------------------------
-- 便捷导出(可选)
--------------------------------------------------------------------------------
return util