二进制学习

二进制是计算机的基础,计算机只识别二进制数据,其基础运算是采用2进制。编程语言写好的程序经过编译后变成计算机能识别的2进制数据,人不可能直接写2进制数据,其中间需要通过编程语言进行协调,所以编程语言就是连接人类和计算机之间的桥梁,下面补充学习二进制基础知识。

二进制

(1)计算机内部只有2进制数据,别的一概没有。Java编程语言,利用算法支持10进制,使用户感受上可以使用10进制,比如System.out.println(50)运行输出了50,其实其底层调用了Integer.toString()方法进行了转换,将2进制110010转换成10进制50。底层算法是如下方法:

(2)Java底层是有方法实现2进制数据和10进制数据的相互转换,具体如下:

   将10进制字符串转换为2进制int ---->Integer.parseInt()

   将2进制int转换为10进制的字符串 ---->Integer.toString()

 了解2进制规则时,需要配套了解16进制规则,因为两者可以相互转换,4位2进制可以简写为一个16进制数,可以通过排列组合就可以理解了。

(3)2进制规则:

 ①逢二进一的计数规则,可以参考只有2个算珠的算盘,移动算珠到上面代表0位,移动算珠下来代表1位

 ②权:64,32,16,8,4,2,1 (1的2倍数)

 ③基数:2

 ④数字:01

(4)16进制规则:

 ①逢十六进一的计数规则

 ②权:256,16,1(1的16倍数)

 ③基数:16

 ④数字:0123456789abcdef

二进制补码

计算机用来处理(有符号数)负数问题的算法

补码算法:位数补码

①总的位数是4位数

②如果计算结果超过4位数,自动溢出舍弃

先来看看只有4位的2进制数,发现范围是从0000~1111,代表10进制的0~15,如果1111加1,会变成10000,超过4位,最高位1自动溢出舍弃,变成0000,发现又回到起点,所以如果只是4位的2进制数字,采用补码规则,最多只能表示16个数,现在前人根据这样,将2进制高位为0的分成1半,2进制高位为1的分成1半,前者用来表示正数,代表0~7,后者用来表示负数,代表-1~-8。具体参考如下图片:

其中0~7的对应的2进制数为0000~0111,其中-1~-8的对应2进制数为1111~1000,其实计算机原本只有正数,-1~-8的这几个,在计算机底层还是正数,通过算法Integer.toString(),变成负数展示给人看,让人感觉好像有负数,上面通过4位来表示一个数,取值范围就是-2^3~2^3-1 ,这些数是完全不能满足生活需求的,因此就扩大了位数,用来展现更多的数字,就有了如下几种数据类型:

①byte,通过8位来表示一个数,取值范围为-2^7~2^7-1,取值范围-128~127,单位为1个byte,即一个字节

②short,通过16位来表示一个数,取值范围为-2^15~2^15-1,取值范围-32768~32767,单位为2个字节,即2byte

③带符号int,通过32位来表示一个数,取值范围为-2^32~2^31-1,取值范围-2147483648~2147483647,单位为4个字节,即4byte

④无符号int,也是32位,取值范围0~2^32-1

⑤long,通过64位来表示一个数,取值范围-2^63~2^63-1,取值范围-9223372036854774808~9223372036854774807,单位为8个字节,即8byte

以下为浮点型:
(1)float,4byle,32位
(2)double,8byte,64位
字符型:
  (1) char,2个字节,16位
布尔型:
(1)boolean,可能就是一个bit,也可能是一个byte

互补对称公式:-n=~n+1,意思就是一个数对应的另外一个符号对称数,等于2进制位数取反后加1。可以参考上图圆盘就一目了然。

@Test
    public void testBinary8() {
        /**
         * 测试互补对称公式 -n=~n+1
         */
        System.out.println(~8+1);//-8
        System.out.println(Integer.toBinaryString(~8+1));//11111111111111111111111111111000
        System.out.println(~8);//-9
        System.out.println(Integer.toBinaryString(~8));//11111111111111111111111111110111
        System.out.println(~-8);//7
        System.out.println(Integer.toBinaryString(~-8));//111
        System.out.println(~-8+1);//8
        System.out.println(Integer.toBinaryString(~-8+1));//1000
    } 

二进制运算符

① ~ 取反
② & 与运算 主要用于截取
0&0=0
0&1=0
1&0=0
1&1=1
③ | 或运算 主要用于合并
 0|0=0
 0|1=1
 1|0=1
 1|1=1
④ >>> 逻辑右移动运算
 将二进制数值总体往右边移动一定位数,低位溢出不要,高位不够用0补齐
 比如 n=00000000 00000000 00000000 11010001
 m=n>>>1运算后
 变成 (0)00000000 00000000 00000000 1101000(1)
 整体往右边移动了1位,低位的1去掉,高位补上0,变成如下形式:
 000000000 00000000 00000000 1101000
⑤ >> 数学右移动运算
⑥ << 数学左移动运算

编码方案

字符是16位的,而流(文件/互联网)按照byte(8位)进行处理,将文字进行传输必须拆分为byte,这个拆分方法,成为文字的编码。把文字拆开,变成8位8位的格式,即编码,反过来叫做解码。
编码方案学习:
首先需要了解一下几个概念,unicode,ASCII,UTF-8之间到底有啥区别?
unicode:一个字符对应一个数,就是将全世界所有知道的字符统计进去,目前到了10万+
UCS unicode3.2标准 0~65535 java char就是这个UCS
UTF-8:变长编码,1-4字节,短的1字节,长的4字节,具体有以下几种:
① 0~127 1字节编码,这个范围的也叫做ASCII码,主要显示英文和其他西欧语言,格式为 0XXXXXXXX
② 128~2048? 2字节编码,格式为110XXXXX 10XXXXXX
③ 2048?~65535 3字节编码,中文,韩文日文等都在这个范围内,格式为1110XXXX 10XXXXXX 10XXXXXX
④ 65535~10万 4字节编码,格式11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
比如'中'这个字符,对应的unicode编码为0x4e2d,转化成二进制为0100 1110 0010 1101
然后需要使用三个字节编码,将二进制格式按顺序填充到格式1110XXXX 10XXXXXX 10XXXXXX中,变成11100100 10111000 10101101,这个就是UTF-8编码,所以将字符的unicode索引添加到UTF-8的字节位置上,叫做编码,然后网络通过8位一个字节一个字节的传输,然后如果将网络上传输的字节一个一个的组合起来,反过来从其中取出对应位置的的数,再和unicode索引对照找到对应的字符,这个过程就做解码。

编码实例分析 

接下来用'中'字符进行编码 中字符 unicode编码为:0100 1110 0010 1101 ,在java中会将前面的位补齐,变成如下形式
 00000000 00000000 01001110 00101101
 然后需要编码的格式为:
 1110XXXX 10XXXXXX 10XXXXXX
 一般按照3部分进行编码,按照先后顺序分别是b1 b2 b3
 step 1
 先得到b3,b3需要得到中字符unicode编码的最后6位,需要使用与运算符,截取最后6位,使用如下字符进行与运算 00000000 00000000 00000000 00111111,运算先转换成16进制再进行运算,这个目的就是截取了unicode最后6位然后还需要将b3的前面两个已经规定好的10给添加到得到的结果的前面,使用或运算符 刚得到的与运算结果应该是00000000 00000000 00000000 00101101。使用或运算符,将10添加上,需要还进行或运算的值为 00000000 00000000 00000000 10000000,换算成16进制为0x80。
 所以b3的最后结果为(0x4e2d&0x3f)|0x80
 step2
 然后需要得到b2,b2部分也需要截取6位,但是截取的是unicode字符刚截取部分的前面6位,这个时候如果使用逻辑右移动运算符,可以将需要截取的又放到最后6位,又可以按照刚才的方法进行截取和拼接了,先逻辑右移动6位
 0x4e2d>>>6
 然后再截取最后6位,使用与运算
 (0x4e2d>>>6)&0x3f
 最后再将10拼接到前面,使用或运算符
 ((0x4e2d>>>6)&0x3f)|0x80
 step3
 最后得到b1,可以参考前面的方法, 先逻辑右移动12位,使用逻辑右移动运算
 0x4e2d>>>12
 然后再截取最后4位,使用与运算
 截取4位运算值为00000000 00000000 00000000 00001111
 (0x4e2d>>>12)&0xf
 再将1110拼接上去
 拼接运算值00000000 00000000 00000000 11100000
 ((0x4e2d>>>12)&0xf)|0xe0

解码实例分析 

如果是解码,就是把刚才编码得到的三个字节b1 b2 b3,取出编码时存入的部分,再重新按顺序拼接
 b1需要取出最后4位
 b1&0xf
 b2需要取出最后6位
 b2&0x3f
 b3需要取出最后6位
 b3&0x3f
 最后在将取出的部分拼接起来,注意,b1需要左移动12位,b2需要左移动6位,b3不要移动
 ((b1&0xf)<<12)|((b2&0x3f)<<6)|b3&0x3f
 最终将得到'中'字符对应的unicode编码

以下是编码解码过程代码:

@Test
    public void testBinary9() {
        /**
         * 打印出中这个字符的unicode索引的二进制形式
         */
        int i='中';
        System.out.println(Integer.toBinaryString(i));//100111000101101 省略了最前面的0
        //其实就是0100 1110 0010 1101,变成16进制为4e2d
    }
    @Test
    public void testBinary10() throws UnsupportedEncodingException {
        /**
         * 将'中'字进行编码,变成UTF-8编码
         * step1 先获得UTF-8的b3部分,使用&和|运算
         */
        int a='中';
        int m=0x3f;
        int n=0x80;
        int b3=((a&m)|n);
        System.out.println(Integer.toBinaryString(a));//100111000101101
        System.out.println(Integer.toBinaryString(a&m));//101101
        System.out.println(Integer.toBinaryString(b3));//10101101
        /**
         * step2 再获取UTF-8的b2部分 使用>>>,&,|运算
         */
        int k=a>>>6;
        System.out.println(Integer.toBinaryString(k));//100111000
        int b2=(k&m)|n;
        System.out.println(Integer.toBinaryString(b2));//10111000
        /**
         * step3 再获取UTF-8的b1部分,使用>>>,&,|运算
         */
        int s=a>>>12;
        int o=0xf;
        int p=0xe0;
        int b1=(s&o)|p;
        System.out.println(Integer.toBinaryString(b1));//11100100
       
        //JDK提供了UTF-8到char的解码
        byte[] bytes= {(byte) b1,(byte) b2,(byte)b3};
        String result=new String(bytes,"utf-8");
        System.out.println(result);//暂时输出乱码
        
        /**
         * 也可以手动解码,将刚才得到的三个字节按照编码方式,将放到对应位置的bit截取后重新拼接
         */
        //截取b3最后6位
        int c1=b3&0x3f;
        //截取b2最后6位,并左移动6位
        int c2=(b2&0x3f)<<6;
        //截取b1最后4位,并左移动12位
        int c3=(b1&0xf)<<12;
        //拼接
        int cResult=c3|c2|c1;
        System.out.println((char)(cResult));
        
    }

 移位运算符的数学意义 

 十进制
 移动小数点运算
 123456. 小数点向右移动
 1234560. 小数点向右移动1次,数字*10
 12345600. 小数点向右移动2次,数字*10*10
 如果小数点位置不变,数字向左移动
 123456. 数字向左移动
 1234560. 数字向左移动1次,数组*10
 12345600. 数字向左移动2次,数组*10*10

 二进制依次类推
 移动小数点运算
 000000000 00000000 00000000 00110010. 小数点向右移动
 00000000 00000000 00000000 001100100. 小数点向右移动1次,数字*2
 0000000 00000000 00000000 0011001000. 小数点向右移动2次,数字*2*2
 如果小数点位置不变,数字向左移动
 000000000 00000000 00000000 00110010. 数字向左移动
 00000000 00000000 00000000 001100100. 数字向左移动1次,数组*2
 0000000 00000000 00000000 0011001000. 数字向左移动2次,数组*2*2

 数学右移位运算
 1 相当如将原数据进行除法,结果向小数点方向取整数
 2 >>数学移位:正数高位补0,负数高位补1,有数学意义
 3 >>> 逻辑移位,不管正数负数,高位都是补0,没有数学意义

 n=11111111 11111111 11111111 11110111 使用互补对称公式得出为-9
 n>>1变成-5
 n=111111111 11111111 11111111 1111011 使用互补对称公式得出为-5
 如果n>>>1
 n=011111111 11111111 11111111 1111011 非常大的一个数

 部分经典面试题

(1)经典面试题:
 int i=0x32;
 System.out.println(i); //3*16^1+2*16^0=50

(2)经典面试题:
 int n=0xffffffff;
 System.out.println(n);//使用互补对称公式得出-1

(3)经典面试题:
 正数的溢出为负数,负数的溢出为正数
 答案:错,根据圆盘转圈,只能说不一定,要看溢出多少了,所以都有可能

(4)经典面试题:
 System.out.println(~8+1);
 答案:-n=~n+1公式,得到-8

(5)经典面试题:
 如何优化n*8计算?
 答案:n<<3; //左移位比乘法快

 

总结:二进制部分为计算机基础知识,有必要补充学习,加深对基础知识的理解。

参考博客;

(1)https://www.jianshu.com/p/53fbd4d91a31

posted @ 2019-03-03 11:58  斐波那切  阅读(3210)  评论(0编辑  收藏  举报