13 位和字节

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) * 2floor(((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 字段的总和。

不会

你可能感兴趣的:(13 位和字节)