浅析二进制

193 阅读7分钟

---

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black

贡献主题:github.com/xitu/juejin…

theme: juejin highlight:

理论基础

二进制的原码、反码和补码

对于一个数字,计算机需要使用一定的编码方式将它存储下来。而原码、反码和补码就是计算机表示数字的不同编码方式。

  1. 模的概念

    生活中有很多四则运算的例子,十进制逢十进一、时钟逢12进位,这里的10和12就是模。对应到计算机,计算机的运算零件和寄存器都有字长限制,比方字长是8记满 256 后就会溢出。可以对溢出的数字进1位余数从零开始计数。

    有两种方式将时钟的10拨到5

    • 时针逆时针回拨5 (10 - 5)
    • 时针顺时针增加10 + (12 - 5),将减法转换成加法运算。减去一个整数相当于加上对应的负数,这样计算机只保留累加计数器即可
  2. 原码

    原码是符号位+真值的绝对值(|真值|)比如8位二进制:

    [+1]原 = 0000 0001

    [-1]原 = 1000 0001

    第一位是符号位,0是正数1是负数。其余各位代表真实值。所以8位二进制的取值范围是:

    1111 1111 ~ 0111 1111 即 [-127 ~ 127]

  3. 反码

    正数的反码是其原码本身。负数的反码是在原码基础上,符号位不变其余各位取反。

    [+1] = [0000 0001]原 = [0000 0001]反

    [-1] = [1000 0001]原 = [1111 1110]反

  4. 补码

    正数的补码 = 反码 = 原码。负数的反码是在其反码基础上加1。即符号位不变其余各位取反,然后加1。

    [+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补

    [-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补

与运算 &

只有两个对应位都是1才为1

或运算 |

两个对应位任意一位是1就是1

异或运算 ^

两个对应位不同时才是1

取反 ~

~ 是把 num 的补码中的0和1全部取反(0变成1,1变成0)。有符号整数的符号位也会在~运算中取反。

[5] = [0000 0101]原 = [0000 0101]反 = [0000 0101]补

~[5] = ~[0000 0101]补 = [1111 1010]补 = [1111 1001]反 = [1000 0110] = [-6]

[-5] = [1000 0101]原 = [1111 1010]反 = [1111 1011]补

~[-5] = ~[1111 1011]补 = [0000 0100]补 = [0000 0100]反 = [0000 0100]原 = [4]

左移 右移 >> <<

num << i 表示将 num 的二进制向左移动n位,即乘以 Math.power(2, i);

num >> i 表示将 num 的二进制向右移动n位,即除以 Math.power(2, i);

[11] = [0000 1011]原

[11 << 3] = [0101 1000] = [64 + 16 + 8] = [88]

[11 >> 2] = [0000 0010] = [2]

巧用实战

二十六进制和十进制相互转换

Excel 中用 A, B, C... 表示第1, 2, 3... 列,编写一个函数传入字母表示的列编码,输出十进制列号。

public static int convertFrom26(String s) {
        int ans = 0;
        for (int i = 0; i < s.length(); i++) {
            int weight = 1 + s.charAt(i) - 'A';
            ans += weight * Math.pow(26, s.length() -i -1);
        }
        return ans;
    }

反向十进制转二十六进制

public static String convertTo26(int n) {
        StringBuilder buffer = new StringBuilder();
        while (n > 0) {
            // 当余数是零时,代表该位应转为Z
            int m = (n % 26 == 0 ? 26 : n % 26);
            // 'A' ASCII码表里是65。这里m的取值范围是[1, 26],所以从+64
            buffer.insert(0, (char)(m + 64));
            // 计算去除余数后降级的n
            n = (n - m) / 26;
        }
        return buffer.toString();
    }

剑指Offer | 二进制中1的个数

题目

请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1:

输入:00000000000000000000000000001011 输出:3 解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/er… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

  1. n 无符号右移。你可以将输入的整数 n 和 1 做与运算,判断二进制右起第一位是否是1。然后将二进制数无符号右移,直到归零。
  2. n & (n-1) 消去末位1。一个数字减一即是该数末位1变为0,它右边的所有0变为1。n &(n-1) 即是消去末位1,循环直至 n 归零。
  3. 标记位 flag 1 依次左移。将 1 顺序左移判断二进制当前位是否为1,直到 flag 大于 n。~~ 处理不了负数。边界测试用例 11111111111111111111111111111101

代码实现

  1. n 无符号右移

    public int helper1(int n) {
            int count = 0;
            while (n != 0) {
                count += (n & 1);
                n >>>= 1;
            }
            return count;
    }
    

注意循环终止条件时 n != 0 而不是 n > 0; 你需要考虑 n 是负数的情况。

复杂度分析:

  • 时间复杂度 O(log2n) 算法循环内部仅有与、加、移位运算占用 O(1)。逐位判断需要循环 log2n 次,log2n 代表数字 n 最高位1所在的位数。log2^16 = 4
  • 空间复杂度O(1) 变量 count 占用 O(1) 额外空间
  1. n & (n-1) 消去末位1

    public int helper3(int n) {
            int count = 0;
            // n != 0 存在负数的可能 11111111111111111111111111111101
            while (n != 0) {
                count++;
                n = n & (n-1);
            }
            return count;
        }
    

复杂度分析:

  • 时间复杂度 O(M) 。M 代表二进制 数字 n 的1的个数。每循环一次就消去1位1
  • 空间复杂度 O(1) 。变量 count 占用O(1) 的额外空间

判断一个整数是否是2的幂次方

public static boolean isBinaryPower(int n) {
    return n > 0 && (n & (n-1)) == 0;
}

2 的整数次方要求数字 n 的二进制只能有1位1。你可以使用 n & (n-1) 消去末位1 判断是否是零,注意边界条件负数和零。

计算从数字m到n需要改变的位数

例如数字 1100 到 1011 需要改动3位才能从 1100 到 1011。你可以分两步得到要改动的位数,先求 m和n的异或再计算异或结果中1的位数。

public static int changeBits(int m, int n) {
        int t = (m ^n);
        int ret = 0;
        while (t != 0) {
            ret++;
            t = t & (t - 1);
        }
        return ret;
    }

只出现一次的数字

题目

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1] 输出: 1 示例 2:

输入: [4,1,2,1,2] 输出: 4

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/si… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

你可以使用异或运算做到不使用额外空间的。一个数字和它本身做异或结果是零,任意数字和零做异或结果是它本身。题意描述数组中只有一个出现一次的元素,其他元素都是成对出现。

成对的元素偶数次异或会消成零,落单的元素和零做异或仍是它本身。最终的异或结果即是只出现了一次的元素。

代码实现

public int singleNumber(int[] nums) {
        int ret = 0;
        for (int num : nums) {
            ret ^= num;
        }
        return ret;
    }
```![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a83a93a8ba024902b158c629aff7b0ff~tplv-k3u1fbpfcp-watermark.image)