用二进制实现各种基础代码逻辑

基础:

二进制中的原码、反码和补码描述如下。
原码:就是数字本身。比如 1110 的原码就是 1110。

反码:将原码除符号位按位取反就是反码。比如 1110 的反码是 1001。

补码:将反码加 1 就是补码。比如 1110 的反码是 1001,补码就是 1010。
正数的原码、反码、补码都相同,都等于原码;负数的反码等于除符号位按位取反,负数的补码等于反码末位加1。
比如0110,是以0开头的,就是正数,它的原码、反码、补码都是0110;再比如上述的1110,它是以1开头的,就是负数,所以就用上述方式计算它的反码和补码。
那为什么要有补码呢?
为了简化减法运算!使用补码可以将减法变成加法。
十进制的补码等于用 9 减去各个位置的数字后末位再加 1,比如 1 的补码是 9,2 的补码是 8……以此类推。
先来看个问题:比如:9-4,4 的补码是 6,我们就变成 9+6,结果是 15,我们再去掉最高位的 1,结果是 5,所以 9-4=5。换句话说就是:数 A 减去数 B 等于数 A 加上数 B 的补码,然后砍掉最高位。
再比如:81-21=81+(78+1)=81+79=160,去掉最高位的 1,就是 60,结果也是成立的。
有人会问,这计算补码的时候,要用 9 减去各个位子的数字,这本身就做了一次减法啊。是!但是这是 10 进制,二进制就不需要了,二进制只需要按位取反,末位再加 1,就行了。
比如:二进制的减法:0110−00010110−0001,-0001的原码是1001,那反码就是 1110,补码就是 1111。换成补码计算就是 0110+1111,结果是:10101,去掉最高位就是 0101,也就是 5,答案明显是对的。
那就是说:0110-0001=0110+1111,而我们又知道:0110-0001=0110+(-0001),那是不是意味着 -0001 就等于 1111 呢?是的。
如果用补码表示二进制,那么高位就表示符号位置:如果高位是 1,就表示负数;如果高位是 0,就表示正数。
所以 1111 就表示负数,它对应的原码等于对它再次求补码,1111 的反码是 1000,补码就是 1001,高位是 1,表示负数,所以就是 -0001,所以就有:1111=(-0001)。这就是二进制的神奇之处。
所以,为什么要用补码呢?
因为可以避免负数和减法,利用高位溢出,将减法变成加法。这样,计算机就不用设计减法器了,直接用加法器就可以做减法了。
计算机只有加法器,没有减法器。
那么,只有负数使用补码表示吗?
不是,在计算机中,所有数字都使用补码表示,只不过正数的补码等于原码,负数的补码等于对应的正数按位取反后末位加1,所以,我们一般的计算中,只需要注意负数就行了。
现在我们知道了二进制的加法和减法,那么二进制的乘除怎么计算呢?
那就是位运算了。

位运算

按位与(&)

按位与运算就是:将各个数位的数字进行逻辑与,都是 1 才为 1,否则为 0。
比如:110&011=010。因为只有第二位都是 1,那么其他位都为 0,所以就是 010。
再比如:1111&111=0111,由于 111 是三位数,所以高位补个 0,那么结果就是 0111。
你说这些我都知道了,所以这有什么用?
使用与运算可以消除指定位置的数字。也可以判断是否具有某个标记。
比如:我要取一个 8 位数的高 4 位,那么我就让它和 1111 0000 进行按位与运算,这样,它的低 4 位一定都是 0,高 4 位原来是 1 还是 1,原来是 0 还是 0,这样也就等于:消除了它的低 4 位,只取它的高 4 位。
现在来模拟某奇艺的会员制度:

int vip = 0000 0001 // 表示vip 能看收费剧了
int svip = 0000 0011 // 表示svip 能看提前播放的剧了
int vipp = 0000 0111 // 表示vip中p // 能跳过vip广告了
int svipp = 0000 1111 // 表示svip中p // 能跳过svip广告了
int tvip = 0001 1111 // 表示电视vip,能投屏了。

// 是否是vip
boolean isVip(int flag) {
    return vip&flag == vip; 
}

// 是否是svip
boolean isSvip(int flag) {
    return svip&flag == svip;
}

// 是否是tvip
boolean isTvip(int flag) {
    return tvip&flag == tvip;
}

...

现在假设你的 vip 标记和我的 vip 标记如下:

int uFlag = 0000 0011 // 你的vip标记

int mFlag = 1111 1111 // 我的vip标记

好,现在拿着你的 vip 标记uFlag去试试,是不是 vip 呢?计算了下是,而且也是 svip。但是也仅仅是 svip 了。拿着我的去呢?直接挑最大的 tvip 试试,发现是的,那么也是其他的 vip 了。
这里你可以看到,一个 int 型的 flag 值,就可以表示这么多 vip 了,这对优化程序相当高。

按位或(|)

按位或运算就是:将各个数位的数字进行逻辑或,都是 0 才为 0,否则为 1。
理解了上述的按位与运算,这个就很好理解了。
比如还是上述的两个数:110∣011=111,1111∣111=1111,011∣001=011。
但是有什么用呢
使用或运算可以给指定位置添加标记。 说白了就是,可以给指定位置设置为 1。
比如:0011 1001 这个数字,我要将它的低 4 位全部设置成 1,而高 4 位不变。那么就让它跟 0000 1111 进行或运算:00111001∥00001111=00111111,因为跟 0 进行或运算,结果是本身,跟 1 进行或运算,结果是 1,所以答案就不解释了。
但是有什么用呢
还是上述的 vip,比如你现在充了 10086 块钱,开了个视频 vip,那么我怎么给你开通呢?很简单:

...
int tvip = 0001 1111 // 表示电视vip,能投屏了。

// 开通tvip
int openTvip(int flag){
    return tvip|flag;
}

假设你啥都不是,只要进行了openTvip()操作,就直接是 tvip 了,这就是或运算的添加标记功能。
现在,我们已经能用与运算和或运算来实现:添加、删除、判断,这些操作了。

异或(^)

异或运算就是:将各个位置数字进行异或,相同为 0,不同为 1。
比如:1111 ^ 1111 = 0000,0000 ^ 1111 = 1111。
说白了就是:将相同的除去。
一个数和 0 异或等于它本身。
如果你原来是 1,那么就和 0 不同,不同的两个数异或等于 1,就是你本身;如果是 0,就和 0 相同,相同的两个数异或等于 0,还是你本身,所以:一个数和 0 异或等于它本身。
一个数和自身异或等于 0。
这个就不解释了,因为相同,异或肯定是 0。
那这有啥用呢?除去相同的。
我们先来看一个操作:交换两个变量的值。
一般的写法:

int temp = a;
a = b;
b = temp;

二般的写法:

a = a + b;
b = a - b;
a = a - b;

三般的写法:

a = a ^ b;
b = a ^ b;
a = a ^ b;

二般的写法就不说了,虽然是对的,但是有溢出的风险。而且也容易看懂。
三般的这里解释下:
a = a ^ b; 此时 a 就是 a^b;
b = a ^ b; 也就是 b = (ab)b = abb = a(bb);而相同的数字异或等于 0,那就是 a^0,也就是 a,此时 b=a 了。
a = a ^ b; 此时经过第一步运算 a=a^b,经过第二步运算 b=a,所以 a = (ab)a = aab = 0^b,也就是 b。
我们再来看实际的应用,比如:我们开发了个打卡机,每天要进行签到签退,每次签到签退都会记录员工号,那么既然如此,一个人的工号肯定出现偶数次,如果是奇数次,就肯定有缺卡记录,那么怎么找出来呢?
可以使用异或啊。
我们直接遍历所有的员工号,挨个进行异或,那么最后得出的那个数字,就是缺卡的那个员工。因为其他的都经过两次异或变为 0 了。
代码如下:

int findNumber(int[] nums) {
   int result = nums[0];
   for (int i = 1; i < nums.Length; i++){
       result ^= nums[i];
   }
   return result;
}

非(!)

按位非就是:将所有位置的数字取反,原来是 0 就变为 1,原来是 1 就变为 0。
那么它有啥用途呢?
首先我们能想到的就是:求反码。
再比如:开关操作。
考察如下代码:

if(a = false) {
    a = true;
} else {
    a = false;
}

这看着真别扭,直接改成:
a = !a;
简单明了。

左移(<<)

左移运算就是:将所有数字向左边移动 n 位,右边补 0。
比如我们先看 10 进制的左移,100,向左边移动 2 位,100__ ,然后右边补 0:10000,等于变为原来的 100 倍了。
同样的二进制也是这样,比如 0011,左移 3 位就变成 11000,原来是 3,现在是 24,变为原来的 8 倍了,所以可以得出结论,在 2 进制中,左移动 n 位,就等于乘以 2的n次方 倍。
这个结论可以用于乘法运算,比如,要计算:a×15,考虑到 15=16-1,16 是 2的4次方,所以就可以计算为:a<<4−a,这样就不用计算乘法了(计算机计算乘法的效率比较低)。
左移运算还有个用途,就是可以丢掉高位,比如一个 8 位数:1000 1111,左移 4 位就变成:1111 0000,原来的高 4 位,被移出去了,只剩下低 4 位了。
左移运算在不溢出的情况下等于乘法,在溢出的情况下等于裁掉高位。

右移(>>)

右移运算就是:将所有数字向右边移动 n 位,左边补符号位,正数就补 0,负数就补 1。
这个也很好理解,比如:0111>>1=0011,因为是正数,所以左边补 0。
切记,对于负数的右移要转为补码来计算。
比如:要计算-5右移1位,
先计算-5的补码,也就是1011
执行位运算: 1011>>1=1101,因为1011是负数,所以高位补1
再把1101求一次补码,得到原码: 1011,因为1101是负数,所以就是-3。
这就有问题了,-5右移1位不应该也是除以2吗,那就应该是-2啊,怎么是-3呢? 没错,它就是不等于,原因就在于高位补符号位置了,这个细节不必纠结。
那么,负数的右移不等于除法,那要怎么快速计算呢?很简单,先使用正数做右移,完了再转成负数就行了。
比如,还是-5,我们想除以2,我们可以先计算5的右移:
0101>>1=0010,也就是2,也就是0010,我们把最高位符号位改为1,就是1010,也就是-2的原码,这样我们就计算出了-5除以2的结果。
其实不用纠结,这个原理我们只要知道就行,至于这些计算的事情,让计算机去干!
正数的右移运算等于做除法,负数的右移不等于做除法。
比如,上述的0111>>1,因为最低位的 1 被移出去了,就变成变为 011,又因为符号位为 0,所以高位补 0,就变成了 0011,也就是 3,而原来的 0111 是 7,对于整型来说:7/2=3,等于除以 2。
所以,在 2 进制中,正数右移 n 位,就等于除以 2的n次方倍。
右移运算在任何情况下都等于裁掉低位。
上述与运算我们说到,按位与可以消除指定位,如果配上右移,那就更合适了。比如我们用 1111 0100,这个数字的高 4 位表示年龄,低 4 位表示成绩,现在要知道这个人的年龄和成绩分别是多少。
用与运算获取成绩:11110100&00001111 = 0000 0100,得到成绩。
用右移运算获取年龄:11110100>>4=11111111,然后再用按位与运算去掉高 4 位的符号位,就得到 00001111,也就是年龄。
计算年龄有点费劲,我们有更简单的计算方法:无符号右移。

无符号右移(>>>)

无符号右移跟右移一样,只不过左边永远补 0。
还是上面获取年龄的例子,1111 0100,无符号右移 4 位,高位补 0,就成了 0000 1111,直接就获取到了年龄。
移位运算分为逻辑移位和算数移位,如果你用二进制表示数值,那么高位就是符号位,此时为算术移位,右移则高位补符号位。如果你用二进制表示逻辑,比如上述的flag,那此时就是逻辑移位,右移则高位永远补0.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值