美文网首页
位运算运用的技巧

位运算运用的技巧

作者: Robin92 | 来源:发表于2020-05-07 23:56 被阅读0次

    Go 中的位运算符

    • & 按位与
    • ^ 一元操作为非,二元操作为异或
    • | 按位或
    • &^ 清零操作,为二元操作,a&^b 等同于 a&(^b)
    • << 左移位操作
    • >> 右移位操作

    注意,go 中是没有 >>> 操作的,这个操作在 java 中为带符号移位操作。

    移位操作的特点

    • 速度快
    • 安全,防溢出

    安全的特点:比如在算 a = b * 2 时,若 b 很大,可能导致溢出报错,但用 a = b << 1 的方式的话,则不会报错。但你可能会发现这个计算结果不正确,并不是真正的 2 * b,这个点需要注意。

    取反操作的妙用

    计算类型中的最大最小值

        // 求无符号数的最大值
        max_uint := ^uint(0)     // 18446744073709551615
        max_uint8 := ^uint8(0)   // 255
        max_uint16 := ^uint16(0) // 65535
        max_uint32 := ^uint32(0) // 4294967295
        max_uint64 := ^uint64(0) // 18446744073709551615
        fmt.Println(max_uint, max_uint8, max_uint16, max_uint32, max_uint64)
        // 有符号数取反后被机器认为是补码,所以 ^0 皆为 -1
        f := ^int(0) // -1,int8, int16, int32, int64 皆一样
        fmt.Println(f)
        // 有符号数的最大值可以这样取(移位是带符号的)
        max_int := int(^uint(0) >> 1)       // 9223372036854775807
        max_int8 := int8(^uint8(0) >> 1)    // 127
        max_int16 := int16(^uint16(0) >> 1) // 32767
        max_int32 := int32(^uint32(0) >> 1) // 2147483647
        max_int64 := int64(^uint64(0) >> 1) // 9223372036854775807
        fmt.Println(max_int, max_int8, max_int16, max_int32, max_int64)
        // 有符号数的最小值可以这样算(利用溢出)
        min_int := max_int + 1     // -9223372036854775808
        min_int8 := max_int8 + 1   // -128
        min_int16 := max_int16 + 1 // -32768
        min_int32 := max_int32 + 1 // -2147483648
        min_int64 := max_int64 + 1 // -9223372036854775808
        fmt.Println(min_int, min_int8, min_int16, min_int32, min_int64)
        // 但注意,不可以这样算
        // wrong_example := int(^uint(0)>>1) + 1
        // 报错:constant 9223372036854775808 overflows int
        // 差别是没有用中间变量进行运算,而是直接使用的常数值进行的计算
    
        // 最小值也可以这样算,但注意这也是在常数上的操作,右移位数超出范围了也会报上方错误
        min_int_2 := int(-1) << 63 // -9223372036854775808
        fmt.Println(min_int_2)
    

    求一个基本数值类型的最小值,也可以从二进制码入手,我们知道一个有符号数值的最小值为 “符号位为 1,其余位皆为 0”(补码形式),所以可以 用一个全 1 的数整体向右移一位,首位补 0,再整体取反,来得到这个二进制码。由于 Go 中移位是带符号的,所以前面需要是不带符号类型,后面需要是带符号的类型。如

        // 另一种方式算得有符号数的最小值
        a := ^(^uint(0) >> 1) // -9223372036854775808
        fmt.Println(int(a))
    

    提取一个无符号数的二进制形式的最右侧的 1

    比如,一个数的二进制数 a 为 11011000,即要得出 00001000 这个数。

    这个算法题可以先将 a 取反,则为 00100111,然后加 1,得到 00101000,我们对比一下:

    11011000 // a
    00100111 // ^a
    00101000 // ^a+1
    00001000 // a & (^a+1)
    

    此时可以明显得看到 a&(^a+1) 就是我们所要的结果。

    异或的妙用

    首先来说说异或运算的性质

    • 0^N == N
    • N^N == 0
    • 异或满足交换律、结合律

    由以上三个性质,我们来介绍下异或的几个算法题:

    • 找到一组数据中唯一出现奇数次的数
    • 不用额外的变量实现两数交换
    • 找到一组数据中有且仅有的两个出现奇数次的数

    找到一组数据中唯一出现奇数次的数

    由于异或有上述三条性质,所以可以有这样的推导公式:

    a^b^c^b^a == (a^a)^(b^b)^c == 0^0^c == c
    

    显然,在一组数据中有且仅有一个奇数次数据时,可以用它进行运算找出。

    不用额外的变量实现两数交换

    另外,异或上述的三条性质可以实现 不需要额外的变量即可实现两数交换,以下是证明过程:

        a := 345
        b := 564
        a = a ^ b         // a' => a^b
        b = a ^ b         // b' => a'^b == (a^b)^b == a^(b^b) == a
        a = b ^ a         // a'' => b'^a' == a^a^b == b
        fmt.Println(a, b) // 564 345
    

    找到一组数据中有且仅有的两个出现奇数次的数

    上面可以完成找出一组数据中有且仅有一个奇数次的数,但如何找出两个呢?

    我们上面介绍了,可以通过数据全部异或,得到一个结果 A,其中出现偶数次的数相互异或后结果为 0,所以可以得出结论,这个结果 A 的某一个 bit 位是 1 的原因肯定是两个奇数次数值的 bit 位不一致引起的。

    因为所有数它们的某一个 bit 位非 1 即 0,所以找结果 A 的任意一个为 1 的 bit 位,可以将整组数据分为两组:一组数据此 bit 位值为 1;另一组数据此 bit 位为 0。还可以得到结论:此 bit 位为 1 的数中,有且仅有一个数出现了奇数次;此 bit 位为 0 的数中,有且仅有一个数出现了奇数次。

    于是,这这问题就回到了 有且仅有一个元素出现了奇数次 的问题上了。

    所以此答案是这样的:

    // 找出一组数据中有且仅有两个出现奇数次的数
    func getTwoOddValue(arr []uint64) (uint64, uint64) {
        // 1 得到全部异或的结果
        xor := uint64(0)
        for _, e := range arr {
            xor ^= e
        }
        // 2 按 xor 的某一个 bit 位的值为 1 的位找出此元素
        // 2.1 找出此 bit 位为 1 的一个值
        singleBitVal := xor & (^xor + 1)
        // 2.2 找出此 bit 位为 1 的那个奇数次元素
        elem_1 := uint64(0)
        for _, e := range arr {
            if singleBitVal&e > 0 {
                elem_1 ^= e
            }
        }
        // 3 找出另一个元素
        elem_2 := elem_1 ^ xor
        return elem_1, elem_2
    }
    

    总结,这里介绍了位操作的一些用法。位操作是性能相当快的操作,而且它还有一些其他的妙用,这些在算法题中常常遇到。本文还介绍了几个简单的算法题:

    • 找类型(无/有符号)的最大/小值
    • 提取一个无符号数二进制形式的最右侧的 1
    • 不用额外的空间实现两数交换
    • 找出一组数据中唯一出现奇数次的一个数
    • 找出一组数据中有且仅有两个出现奇数次的两个数

    相关文章

      网友评论

          本文标题:位运算运用的技巧

          本文链接:https://www.haomeiwen.com/subject/wuuzghtx.html