学习王争的位运算,感觉没理解透,总结的也很一般
一,各种进制的转换
针对正整数的一些进制转换代码
共用的数组前后替换代码
private void swap(int[] arr, int n){
int i = 0;
int j = n - 1;
while(i < j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
}
1)整数转换成十进制数组
整数转换为十进制数组的逻辑是先对整数依次对10取余,并放入数组。
比如 1234 对10除余是 4, 再次除余是3,生成的数组是[4,3,2,1],此时再调转顺序,得到的就是转换好的十进制数组了。
public int[] convertToDeicimalArray(int a){
int[] arr = new int[10]; //整数最大值就10位
int i = 0;
while(a != 0){
arr[i++] = a%10;
a = a/10;
}
swap(arr, i); //调用公共的交换方法。
return arr;
}
2)整数转换成二进制数组
同上面的整数生成十进制的数组逻辑相同,转换成二进制的本质是对2取余,代码如下:
public int[] convertToBinaryArray(int a){
int[] arr = new int[32]; //整数的二进制是32位
int i = 0;
while(a != 0){
arr[i++] = a%2;
a = a/2;
}
swap(arr, i);
return arr;
}
3)二进制数组转整数
二进制数组转整数有两种方式
方式1,从后往前加
public int convertToInt(int[] arr){
int result = 0;
int n = arr.length - 1;
int weight = 1;
for(int i = n ; i >= 0; i--){
result = result + arr[i] * weight;
weight *= 2;
}
return result;
}
方式2:从前往后加
public int convertToInt(int[] arr){
int result = 0;
for(int i = 0; i <= arr.length - 1; i++){
result = result * 2 + arr[i];
}
return result;
}
4)整数转8进制或16进制数组
原理同转2进制。
另外8进制的表示方式为 089 这样,在数字前面加0
而16进制的表示方式为 0xA4/0XA4,在数字前面加0x/0X;
public int[] converToOct(int a){
int[] arr = new int[20];
int i = 0;
while( a != 0){
arr[i++] = a%8;
a = a/8;
}
swap(arr, i);
return arr;
}
//十六进制
public char[] converToHex(int a){
char[] arr = new char[10];
int i = 0;
while(a != 0 ){
int temp = a % 16; // a & OxF 也可以,速度更快
char c ;
if(temp > 9){
c = (char)('A' + (temp - 10));
}else{
c = (char)('0' + temp);
}
arr[i++] = c;
a = a/16;
}
swap(arr, i);
return arr;
}
二,如何存计算机中存储整数:补码(重要)
1)原码表示法
计算机没有专门的硬件来识别正,负号,只能识别0,1这样的二进制,对于一串二进制以最高位做为符号位,其余位做为数值位,下图以byte和int来说明。
这样的表示方式是原码表示法。
对于一个字节的byte数值,其取值范围为 -127(1111 1111 十六进制-Ox7F) ~ 127(0111 1111 十六进制0x7F)),比较特殊的是0,有1000 0000 (-0),0000 0000(+0)两种方式表示。
2)补码表示法
为什么要用补码来存储整数
对于正数的加法比较简单,按位相加,篷二进一,如:
5 + 3 化作二进制就是(假设数据长度是byte) 0000 0101 + 0000 0011 = 0000 10000 为8
但是如果变成减法,5-3呢
不考虑符号位,会变成 0000 0101 + 1000 0011 = 1000 1000 为-8,明显结果不对,而如果要考虑符号位,就需要重新设计电路,太麻烦,后面综合考虑设计出了补码表示法。
补码的基本概念
补码的基本原则就是,正数的补码就是其原码,
负数的补码相对复杂点,符号位不变,数值位取反,末位+1,如下图:
补码的两个特殊点:0与最小值
1,原码的0可以有 1000 0000 和0000 0000两种表示方法,而对于补码,只有 0000 0000这一种表示方法。-0没有对应的补码
2,对于长度为n(n为二进制位,如int为32)的数据类型,最高位为1,其余位为0的二进制串,
表示的是-2^(n-1)的补码,比如byte,1000 0000 表示的是-128(-2^(8-1))的补码。且其没有原码。
这就是为什么整数类型的数据范围负数比正数多1的原因,其是以补码进行存储的。
正数的减法
对于正数的加法,因为其补码与原码一样,所以和原码的加法没有区别
看看正数的减法,还是以5-3为例(假设长度为byte)
可以理解为 5+(-3)替换为补码的加法,而补码的加法是不区分符号位与数值位的,所有的都是按位相加,逢二进1,溢出则截取。
补码表示为: 0000 0101 + 1111 1101 = 1 0000 0010 高位进行截取,变为 0000 0010 结果为2.
补码的应用
溢出
以如下人码来说明
int a = 2148473647; //int的最大值0x7fffffff;
int b = 1;
int c = a + b;
System.out.println(c); //输出-2148473648
a和b都是正数,原码,补码相同,a的值转换为二进制的补码为 0x7fffffff,b为 0x00000001,相加后为 0x80000000; 其最高位为1,其余位为0,刚好是最大值的补码,即-2148473648, 特殊值-2^31的补码。
防止溢出可以使用如下代码
public int sum(int a, int b){
boolean downOverFlow = a < 0 && b < 0 && a < (Integer.MIN_VALUE - b);
boolean upOverFlow = a > 0 && b > 0 && a > (Integer.MAX_VALUE - b);
if(downOverFlow || upOverFlow){
throw new RuntimeException("OverFlow");
}
return a + b;
}
忌如下代码:
public int sum(int a, int b){
if((a + b) > Integer.MAX_VALUE || (a + b) < Integer.MIN_VALUE){
throw new RuntimeException("OverFlow");
}
return a + b;
}
整数自动类型转换
自动类型转换,以short转换为int为例,正数在其前面补0,负数则补1,这样可以保证其值的不变。
正数的好理解,左侧的零不会改变值。
负数:补码的规则是符号位不变,数值位取反,末位+1,
比如-3 是byte时,原码为1000 0011 补码为 1111 1101,发现其转换成补码后,后面的值有个取反+1的操作,而前面是直接将其原来的值取反,而整数自动类型转换都是由取值范围小的向范围大的,其原码原先的位置肯定是0,取反就是1,所以补1。如 -3 byte转换成short时,就是
原码 | 补码 | |
byte | 1000 0011 | 1111 1101 |
short | 1000 0000 0000 0011 | 1111 1111 1111 1101 |
另还可以观察下byte的原码与补码对比
同时有个知识点,补码的补码就是原码,网络证明如下,我绕进去没写出来,下面这个答案也没太看明白...
三,位运算
1)常见的位运算
与:&
或:|
非:!
异或:^ a^b^c == a^c^b; 同时a^a == 0; a^0 == a;
取反:~ ~~a = a;
如 byte a = 3 是 0000 0011 ~a 的字节码为 1111 1100。
位移: 算术位移:>> <<
逻辑位移: >>> <<<
2)逻辑位移和算术位移
不管逻辑位移还是算术位移都是针对补码
逻辑位移:>>>
逻辑位移不管是向右还是向左,不区分符号,都是在后面补0
算术位移:>>
算术位移右移
正数:在后面补0(与逻辑位移右移一样)
负数:在后面补1
算术位移左移
与逻辑左移一样,都是在后面补0,无论正反。
算术左移与逻辑左移都会截取掉高位,如上图,左移后负数变正数。
而负数的算术右移,如果一直下去,结果为会一直为-1,因为其高位补1,最后会一直是1111 1111 这样。
在不超出范围的情况下,算术右移>> 相当于除以2,对正数不停右移最后会为0,负数因为特殊会为-1。算术左移相当于乘以2,但得考虑超出范围被截取的情况。
所以算术左移与逻辑左移一样,都是后面补0,高位截图。相当于乘以2,但是很容易数据溢出,然后截取。
而算术右移当为正数时与逻辑右移一样,都是后面补0,相当于除以2,但是对负数,算术右移是补1,一直右移最后结果会一直为-1。