我们都知道在计算机底层中,数值的运算是基于二进制运算,话不多说,我们直接进入运算学习:
二进制加法:在二进制中,运算为满2进一,即1+1=10、1+10=11、1+11=110...以此类推。这个较为简单,不再赘述,主要说明减法的运算。
二进制减法:二进制中,减法运算和加法运算类似,在十进制中,我们可以将两个数字的减法转换成一个数字加上另一个数字的负数,例如:10-5 ==> 10+(-5)。但是由于二进制中不存在负数,因此它规定,当二进制为八位时,最左边第一位0表示正数,最左边第一位是1表示负数。但是我们在实际计算的时候会发现一个新的问题:
1 + 2 = 3 ==> 0001 + 0010 = 0011 ;0011解码为3。
0 + (-1) = (-1) ==> 0000 + 1001 = 1001;1001解码为-1。
1 +(-1)=0 ==> 0001 + 1001 = 1010;1010解码为-2?
1010在进行解码时结果为-2,但是在十进制中1加-1是不可能等于-2的,因此使用到了反码和补码的概念。
补码:当二进制数字为八位时,不足八位应以0补齐不足位数,即十进制3在二进制为110,那么补码之后应为:0000 0110。以方便运算和进行数字的记录和保存。例如当1010 0101 与110相加,即可在补全之后在进行相加,即:1010 0101 + 0000 0110 = 1010 1111。其他情况类似,不再赘述。
反码:基于以上二进制减法中遇到的问题,我们可以使用反码进行运算:当一个数字为负数时,可以使用它的反码与其他二进制数字进行运算,把该负数的数字部分进行补码后再把这八个数字的0和1进行互换,然后再+1,即可得到这个负数的反码。例:(-3)的反码,首先应该将其数字部分转换成二进制即为:110;然后进行补码,即为:0000 0110;再将其的0和1进行互换,即为:1111 1001;最后用得到的二进制数加一就可以得到(-3)的反码;即为:1111 1010。
这时在进行计算就不会再出现之前的问题,我们还是用之前出现问题的式子进行运算和举例:
1 + (-1)= 0 ==>0000 0001 + 1111 1111 = 0000 0000 ;解码为0。
(ps:由于多出了一个第9位,我们的结果只取八位,因此将多出的部分删去,只取后八位)
完美解决问题。
(再再ps:之所以取八位是因为数据保存时的单位是字节(Byte),而一个字节占8个比特位(bit)每个二进制数字占一个比特位,因此我们多用八位表示二进制数字,当然由于int类型占4个字节,因此int类型储存数字时应补码成32位,前面用4位进行运算是为了界面简洁运算过程更直观。)
无符号右移:首先,为什么要有无符号右移,我们举一个栗子:二分查找,二分查找的实现就是在一个有序数组中,计算数组的中间值和目标是否相等,即:
int half(){
//设置一个长度为5的数组
int[] arr = new int[5];
//设置需要查找的数值
int targt = 4;
//定义左右两端索引并计算中间索引并自动向下取整
int left = 0,right = arr.length - 1;
int middle = (left + right) /2
//查找目标值并返回,以下代码并非本节重点内容,不再赘述...
while (left <= right) {
int middle = (left + right) >>> 1;
if (arr[middle] < number) {
left = middle+1;
}else if(arr[middle] > number){
right = middle-1;
}else{
System.out.println(Arrays.toString(arr));
return middle;
}
}
System.out.println(Arrays.toString(arr));
return -1;
}
但是我们也会有一个新的问题,由于int类型占4个字节,也就是说他的最大储存量是2^(4*8),也就是4294967296。由于int类型可以储存整数、负数和零,因此int类型的实际范围是:-2147483648~2147483647。
那么假如我们的数组非常大,大到是int类型的最大值,此时我们再运行以上代码就会发现即使数组中有目标值,但是返回还是-1,此时我们输出中间索引会发现是一个负数,这是因为如果目标值在中间值右边是,当你进行第二次平均值运算,计算的数值超出了int类型的范围,因此就会把第一位表示为符号位
一个开头为1的八位二进制数字,系统会判定为负数,因为int类型的取值范围是:-(2^16)~2^16-1,即只占八位二进制中的后七位,因此最开始的第一位为符号位,0表示正数,1表示负数,由于第二次运算为int正数最大值(right)和int最大值的一半(middle)相加,结果一定会溢出导致最开始的第一位变成1,因此转换成十进制就会判断为负数,即2147483647 +1073741823 = 3221225470。而3221225470的二进制为:10111111111111111111111111111110
一个负数的一半,肯定也是负数,所以我们需要使用无符号右移来解决这个问题。
无符号右移(>>>):在以上代码中,我们发现了在第五行代码中出现的int类型最大值溢出导致计算结果为负数,但是如果我们使用无符号右移就可以完美解决这个问题,示例如下:
//普通平均值计算,当right数值较大时会出现值溢出导致得到结果为负数
int middle = (left + right)/2;
//无符号右移,会完美解决值溢出问题
int middlePlus = (legt + right) >>> 1;
已经会用了,好好好,那么他的原理是什么呢?
其实原理非常简单,就是把所有的数字往右移动一位,你可以理解为八个人坐在座位上,当有一个人发出指令:退!退!退!所有人往右移一位,最右边的人直接跑路。而 >>> 后面的数字是几,就会往右移动几位。
因此10111111111111111111111111111110 >>> 1的结果就是:01011111111111111111111111111111 。这样一来就不会出现第一位是1而将结果判断为负数的情况了,于是又有了新的问题,对结果有什么影响。我们都知道十进制中的小数点往右移动一位,就是变大了十倍,在二进制中,所有的数字往右移动一位,整体变为了原先的1/2。那么如果最后一位是1和0会对数据有影响吗,完全没有,由于本来int类型就是向下取整,所以最后一位1还是0被移除没有任何影响。并且在javascript中,
总结一下,为什么要用无符号右移,主要是为了解决int类型运算结果溢出导致出现负数的问题,当然,即使不存在溢出问题依然可以用 >>> 1来表示除2,别问,问就是看起来逼格高。