---
主题列表: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:
理论基础
二进制的原码、反码和补码
对于一个数字,计算机需要使用一定的编码方式将它存储下来。而原码、反码和补码就是计算机表示数字的不同编码方式。
-
模的概念
生活中有很多四则运算的例子,十进制逢十进一、时钟逢12进位,这里的10和12就是模。对应到计算机,计算机的运算零件和寄存器都有字长限制,比方字长是8记满 256 后就会溢出。可以对溢出的数字进1位余数从零开始计数。
有两种方式将时钟的10拨到5
- 时针逆时针回拨5 (10 - 5)
- 时针顺时针增加10 + (12 - 5),将减法转换成加法运算。减去一个整数相当于加上对应的负数,这样计算机只保留累加计数器即可
-
原码
原码是符号位+真值的绝对值(|真值|)比如8位二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
第一位是符号位,0是正数1是负数。其余各位代表真实值。所以8位二进制的取值范围是:
1111 1111 ~ 0111 1111 即 [-127 ~ 127]
-
反码
正数的反码是其原码本身。负数的反码是在原码基础上,符号位不变其余各位取反。
[+1] = [0000 0001]原 = [0000 0001]反
[-1] = [1000 0001]原 = [1111 1110]反
-
补码
正数的补码 = 反码 = 原码。负数的反码是在其反码基础上加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… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
- n 无符号右移。你可以将输入的整数 n 和 1 做与运算,判断二进制右起第一位是否是1。然后将二进制数无符号右移,直到归零。
- n & (n-1) 消去末位1。一个数字减一即是该数末位1变为0,它右边的所有0变为1。n &(n-1) 即是消去末位1,循环直至 n 归零。
- 标记位 flag 1 依次左移。将 1 顺序左移判断二进制当前位是否为1,直到 flag 大于 n。~~ 处理不了负数。边界测试用例 11111111111111111111111111111101
代码实现
-
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) 额外空间
-
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;
}
```