问题引入
很久没有巩固计算机的基础理论知识了,今天突然想起一道编程题:如何将一个整数转换成二进制。看这道题的第一眼,觉得这可太简单了。用这个整数对2取余并循环除以2,直到最后循环的数等于0,把取余的结果拼接起来即可。
上面的方法我估计大部分人都想得出来,但考虑不全。如果加一个条件,要考虑负数,这个问题突然看着不是那么简单了。
说到负数的时候,一时忘记了负数在计算机中的表现形式。那么来巩固一下这个非常基础的计算机基础知识。
正数和负数
在计算机中,整数的表达方式区分正数和负数,正数用原码表示,负数用补码表示。除此之外,正数和负数还可以通过符号位区分,即最高位为符号位,最高位为0表示正数,最高位为1表示负数。
原码、反码和补码
正数的原码、反码、补码相同,如10的原码、反码、补码都是 00001010;
负数的原码为其对应正数的原码,但符号位为1,比如-10,其对应的正数10的原码为 00001010,那么-10的原码为 10001010;
负数的反码为其原码除符号位之外各位取反,如-10的原码为 10001010,除符号位外其余全部取反 11110101;
负数的补码为其反码加1,如-10的反码为 11110101,加1后,得到 111101010。
为什么有反码和补码
可以看出,对正数而言,反码和补码是没有意义的。反码和补码的意义只是针对负数,因为在计算机中负数的表现形式就是补码。
那么就有一点疑问了,负数为什么要用补码表示,用原码行不行?
来看看,负数用原码表示会有什么问题。还是以-10为例,其原码为 10001010,也就是符号位为1,其余位和正数一致。用原码这种方式很容易理解,但有个问题。一个字节能表示的最大负数只能为 11111111,也就是-127。这么看貌似就不对了,一个字节的表示范围只能是-127~127,-128表示不出来。而0的表示却有正0(00000000)和负0(10000000)两种表达方式,也就是说,用原码表示负数,负0是没什么用的。
那么如果用负0表示-128可以吗?-127为 11111111,-128为 10000000,两个数字跨度太大,对计算机而言不方便计算。
所以计算机的负数用补码表示。-127为 10000001,-128为 10000000,两个数字相差为1,方便计算。且不用去记原码表示时负数表示数字的特殊性,补码对所有负数表示的方式是一致的。
如何将一个整数转换成二进制
回归到开头引入的问题,如何将一个整数转换成二进制呢?
根据上面的规则,正数的二进制是其原码,负数的二进制是其补码。那么我们就可以根据这个规则来算,用golang来实现:
// toBinary 将正数转换成二进制
func toBinary(n int) string {
// 正数,求原码
if n >= 0 {
var s string
for n > 0 {
if n%2 == 1 {
s = s + "1"
} else {
s = s + "0"
}
n /= 2
}
return s
}
// 负数,先求其正数的原码
origin := toBinary(-n)
// 根据原码求反码
var back string
for _, v := range origin {
if v == '0' {
back += "1"
} else {
back += "0"
}
}
// 根据反码求其补码
carry := "1"
var completion string
for i := len(back) - 1; i >= 0; i-- {
if back[i] == '0' {
if carry == "1" {
completion = completion + "1"
carry = "0"
} else {
completion = completion + "0"
}
} else {
if carry == "1" {
completion = completion + "0"
carry = "1"
} else {
completion = completion + "1"
}
}
}
return completion
}
上面的实现看着很复杂,但完全根据 正数的二进制是其原码,负数的二进制是其补码规则来完成。
那么是否还有更好的办法来实现呢?我们想想在计算机中,整数都是用二进制表示的,只是我们看到的是10进制,那么是否可以通过一些二进制的计算来完成呢?答案是可以,即通过移位运算加上与运算,就可以得到一个整数的二进制,同样以golang为例:
// toBinary2 将正数转换成二进制
func toBinary2(n int) string {
// 这里认为n暂用8个字节
var s string
for i := 63; i >= 0; i-- {
bit := (n >> i) & 1
s += fmt.Sprintf("%d", bit)
}
return s
}
上面的代码看着简洁很多,通过移位运算,可以得到第i+1位的值,并跟1做与运算可以求得第i+1位是0还是1