13.1 位运算
Lua语言从 5.3 版本开始提供了针对数值类型的一组标准位运算符。与算术运算符不同的是,位运算符只能用于整型数。位运算符包括,&
按位与、|
按位或、~
按位异或、>>
逻辑右移、<<
逻辑左移,~
一元运算符按位取反。(注意,^
在 Lua 语言中代表幂运算)。
print(string.format("%x", 0xff & 0xabcd)) --> cd
print(string.format("%x", 0xff | 0xabcd)) --> abff
print(string.format("%x", 0xaaaa ~ -1)) --> ffffffffffff5555
print(string.format("%x", ~0)) --> ffffffffffffffff
移位数是负数表示向相反的方向以为,即 a >> n 与 a << -n 等价:
print(string.format("%x", 0xff << 12)) --> ff000
print(string.format("%x", 0xff >> -12)) --> ff000
如果移位数等于或大于整型表示的位数,由于所有的位都从结果中移出了,所以结果是 0。
13.2 无符号整型数
加法减法和乘法操作对于有符号整型数和无符号整型数是一样的,关系运算符对于有符号整型数和无符号整型数是不一样的,当比较具有不同符号位的整型数时就会出现问题。对于有符号整型数而言,符号位被置位的整数更小,因为它代表的是负数:
print(0x7fffffffffffffff < 0x8000000000000000) --> false
如果把两个整型数都当作无符号的,那么结果显然不对。因此,我们需要使用一种不同的操作来比较无符号整型数。Lua 5.3 提供了函数 math.ult(unsigned less than)来完成这个需求。
print(math.ult(0x7fffffffffffffff, 0x8000000000000000)) --> true
另一种方式是再比较之前先去除符号位:
local mask = 0x8000000000000000
print((0x7fffffffffffffff ~ mask) < (0x8000000000000000 ~mask)) --> true
无符号数和有符号数的除法也不一样,示例 13.1 给出了一种无符号除法的算法:
示例 13.1 无符号除法
---@return number
---@param n number
---@param d number
function udiv(n, d)
if d < 0 then
if math.ult(n, d)
then
return 0 --被除数小于除数,结果肯定为 0
else
return 1 --如果 n 比 d 大那么最多也不可能商到 2 ,因为再商 2 就多一位丢失了
end
end
local q = ((n >> 1) // d) << 1 --先右移算完再左移
local r = n - q * d --商乘以除数 与被除数做差
if not math.ult(r, d)
then
q = q + 1 --如果 r 算出来的差值 比除数还要大,说明还可以再商 1
end
return q
end
第一个比较(d<0)等价于比较 d 是否大于 263。如果大于,那么商只能是 1 或者 0(取决于被除数 n 比 d 大还是小)。否则,我们使被除数右移一位,然后除以除数,再把结果乘以2。右移之后左移还原了原来的除法。
总体上说,floor(floor(n / 2) / d) * 2
与floor(((n / 2) / d) * 2)
并不等价。不过,要证明它们之间最多相差 1 并不困难。因此,算法计算了余数,然后判断余数是否比除数大,如果余数比除数大则纠正商 +1 即可。
无符号整型数和浮点型数之间的转换需要进行一些调整。要把一个无符号整型数转换为浮点型数,可以先将其转换成有符号整型数,然后通过取模运算纠正结果。
local u = 11529215046068469760
local f = (u + 0.0) % 2 ^ 64
print(string.format("%f", f)) -->11529215046068469760.000000
要把一个浮点型书转换为无符号整型数,可以使用如下的代码:
local f = 0xA000000000000000.0
local u = math.tointeger(((f + 2 ^ 63) % 2 ^ 64) - 2 ^ 63)
print(string.format("%x", u)) --> a000000000000000
加法把一个大于 263 的数转换为一个大于 264 的数,取模运算把这个数限制到 [0,263) 范围内,然后通过减法把结果变成一个“负值”。对于小于 263 的值,加法结果小于 264,所以取模运算没有任何效果,之后的减法则把它恢复到了之前的值。
13.5 练习
- 练习 13.1:请编写一个函数,该函数用于进行无符号整型数的取模运算。
这一题我试着做了一下发现极其复杂,我查到一个计算公式:
return y<=x and x<0 and x-y or y<0 and x or ((x>>1)%y*2+(x&1)-y)%y
,这个相当于使用了短路原理来对结果进行条件输出,适当增加括号:return (y<=x and x<0 and x-y) or (y<0 and x) or (((x>>1)%y*2+(x&1)-y)%y)
首先我们要认清一点是,Lua 默认是有符号数,现在我们将其视为无符号数来进行处理,那么任何负数值都意味着它是超出 263 - 1 的大数值。
第一个括号内的意思是,首先除数比被除数小,然后被除数小于 0,也就是说两个参数都是大数值,但无论怎样也不会商到1,因为达到2倍或以上时数据已经溢出了,因此直接作差就可以得到模。
第二个括号的意思是,如果除数是大数值而被除数不是,那么取模结果就是被除数本身。
第三个括号的意思是,在除数不是大数值时,无论被除数是不是大数值均可以进行通用的计算。
---@return number
---@param n number number
---@param m number mod
function UnsignedMod(n, m)
return m<=n and n<0 and n-m or m<0 and n or ((n >> 1) % m * 2 + (n & 1) - m ) % m
end
local x = 0x7fffffffffffffff
local y = 0x8000000000000000
--local x = 6
--local y = 15
print(y % x) --> 9223372036854775806
print(UnsignedMod(y, x)) --> 1
- 练习 13.2:请实现计算 Lua 语言中整型数所占位数的不同方法。
--- 通过反复传入和传出外部变量 size 的值,最终确定什么时候会报错,当报错时就知道整型数的大小了
---@return number
---@param size number
function SizeOfInteger(size)
local x = string.pack("i", 1 << size)
print(size)
return size + 1
end
local size = 0
while true do
size = assert(SizeOfInteger(size))
end
在打印完 30 之后,程序报错,说明参数为 i 的时候 size最大为 30 位,但是当参数换成 j 以后,程序会进入死循环,这说明 lua_integer 类型的大小至少是 64 位,由于 64 位系统最多容纳 64 位数据,所以数据 1 已经被推到有效位之外了,得到 0,所以永远不会报错。
- 练习 13.3:如何判断一个指定整数是不是 2 的整数次幂?
2 的整数次幂二进制的特点为,除了最高位以外均为0(1 特殊 只有一个 1),因此只需要对数值 -1 然后进行与,看结果是否为 0 即可。
---@return boolean
---@param n number
function IsAPowerOfTwo(n)
n = math.tointeger(n)
if n <= 0 then
return false
end
local temp = n - 1
return (temp & n) == 0
end
print(IsAPowerOfTwo(-1)) --> false
print(IsAPowerOfTwo(0)) --> false
print(IsAPowerOfTwo(1)) --> true
print(IsAPowerOfTwo(2)) --> true
print(IsAPowerOfTwo(3)) --> false
print(IsAPowerOfTwo(4)) --> true
- 练习13.4:请编写一个函数,该函数用于计算指定整数的汉明权重(一个数的汉明权重是其二进制表示中 1 的个数)。
---@return number
---@param n number
function HammingWeight(n)
n = math.tointeger(n)
local count = 0
while n > 0 do
n = n & (n - 1)
count = count + 1
end
return count
end
for i = 0, 5 do
print(HammingWeight(i))
end
- 练习 13.5:请编写一个函数,该函数用于判断指定整数的二进制表示是否为回文数。
require("utils.ToBinaryString")
---@return boolean
---@param n number
function IsAPalindrome(n)
local str
local t = {}
while n > 0 do
table.insert(t, n & 1)
n = n >> 1
end
str = table.concat(t)
return str == string.reverse(str)
end
for i = 0, 8 do
print(i, ToBinaryString(i), IsAPalindrome(i))
end
--------------------
0 true
1 1 true
2 10 false
3 11 true
4 100 false
5 101 true
6 110 false
7 111 true
8 1000 false
附上我自定义的工具类
--- 用于把给定的整形数转换成二进制形式的字符串
---@return string
---@param n number
function ToBinaryString(n)
local t = {}
while n > 0 do
table.insert(t, n & 1)
n = n >> 1
end
return string.reverse(table.concat(t))
end
- 练习 13.6:请在 Lua 语言中实现一个比特数组,该数组应支持如下的操作:
newBitArray(n) (创建一个具有 n 个比特的数组)。
setBit(a, n, v)(将布尔值 v 赋给数组 a 的第 n 位)
testBit(a, n)(将第 n 位的值作为布尔值返回)。
BitArray = {
array = {},
setBit = function(self, n, v)
self.array[n] = v
end,
testBit = function(self, n)
return self.array[n]
end
}
function BitArray:new(n)
local o = {array = {}}
for i = 1, n do
table.insert(o.array, false)
end
self.__index = self
setmetatable(o, self)
return o
end
local ba = BitArray:new(3)
print(ba.testBit(ba,1)) --> false
print(ba.testBit(ba,2)) --> false
print(ba.testBit(ba,3)) --> false
print(ba.testBit(ba,4)) --> nil
ba.setBit(ba,1,true)
print(ba.testBit(ba,1)) --> true
- 练习 13.7:假设有一个以一系列记录组成的二进制文件,其中的每一个记录的格式为:
struct Record{
int x;
char[3] code;
float value;
};
请编写一个程序,该程序读取这个文件,然后输出 value 字段的总和。
不会