数的补数 -- 编程技巧

补数的概念:

今天在浏览各大编程练习题库时,发现一个快被自己忘却的概念:补数;
例如时钟指示为6点,想要它指向3点,可以按顺时针方向将分针转9圈,也可以逆时针方向转3圈,结果是一致的。
由于时针转一圈为12小时,而其中12在时钟里是不被显示而自动丢失的,即15-12=3,15点和3点在时钟表示上是一致的。
想象一下,在时钟这个问题上,任何时候时针向顺时针转9圈和逆时针转3圈的效果都是一致的,我们将方向定位正负,即-3与+9等价。而数学上称12为模写作(mod 12),而称+9是-3以12为模的补数。
记为:-3  +9(mod 12)             -4  +8(mod 12)               -5  +7(mod 12)
可以将模理解为一个足够大的正数,足够大到比你运算所有数字的绝对值都要大。
【以上截取自《计算机组成原理·第二版》唐朔飞著P221】
因此在模确定的情况下:
正数的补数为其自身。
一个正数和一个负数互为补数时,两数的绝对值之和为模。
一个负数可用它的正补数来代替,而这个正补数可以用模加上负数本身来得到。

分析:

在现在讨论的这个问题中(计算机存储),数的存储是以二进制进行存储的,所以模就是2的幂次方。
换而言之,此时的补数就相当于一个数(原码)的反码,即 补数 ==》反码
例如 5 的原码:101,  那么 5 的反码:010,  即 5 的补数是 2 ;
可能大家会觉得既然是反码,为何不直接用(~)取反喃。答案是肯定不行的。
因为一般而言就unsigned int来说是32bit,这简单举例一下:

8bit 16bit
5 0000 0101 0000 0000 0000 0101
~5 1111 1010 1111 1111 1111 1010

(~)符号不仅仅是将原码取反,还要将原码前面包含的所有的前导零一起取反。
因此不能简单的用(~)取反操作来进行反码的寻找。

步骤:

既然不能用(~)取反操作,那么应该怎么得到补数(反码)喃o(╯□╰)o???
例如LeetCode的这道题:https://leetcode-cn.com/problems/number-complement
当时欣赏(实则是仰慕)了一下其他大神的作品,仅仅三行代码,以为又是些垃圾文章一时间没反应过来心中直接一句国骂(* ̄︶ ̄)。回过神来仔细琢磨后,幸好当时我是躺在床上而不是站着,不然可能真会因腿软给跪了。

public static void main(String[] args){
    int num = 5;
    System.out.println(~num & (Integer.highestOneBit(num) - 1));
}

/* 这是段Java代码,以上各个符号的作用:	 
 * Integer.highestOneBit(num) :取num的二进制数最左边的最高位,且高位后面全部补零,最后返回int型的结果。
 * -1:经过上面的步骤,只有最高位为 1 其余位都为 0 这样再减一  就会导致借位 直到借到最高位  
 *     结果得到 最高位为0 其余位都为1  相当于取反了
 *  &:对两个二进制数进行按位与运算
 * ~ :对一个数的二进制取反
 */

这里实例解释一下,为简单起见用8bit存储来举例:
例如 5 在计算机中存储的方式是: 0000 0101,而结果是要 0000 0010;
而 ~5:1111 1010,要将其转化为 0000 0010,就需要想办法将倒数第4位及其前面的1变为0

现在问题就是如何让 1111 1010 转化为 0000 0010 ,当然八仙过海各显神通;
而大神的方法是:~num & (Integer.highestOneBit(num) - 1) ;

首先Integer.highestOneBit(num)   取5的不含前导位的二进制的最高位,即101  取最高位1,其他位都补0,得到100;
减一之后变为 011 ,这时再和 ~num 进行与运算, 因为 011 位数不够,所以前导部分需要填补0,即 :

~num 1111 1010
Integer.highestOneBit(num) - 1 0000 0011
& 0000 0010

想象一下,为什么要取到 num原码的最高位并后位补零,再减一?
因为 num 原码取反后最高位的 1 必定变成 0,而我们只需要除最高位的其他位的值;
而 ~num 后其前导位也会变成 1,为消除前导位的 1,引入Integer.highestOneBit(num) - 1;
因为Integer.highestOneBit(num) - 1使除最高位的其他位为 1,其余为 0,如此向 & 后就得到补数。


还有这第二种解法:类似于IP地址计算,先寻找一个num的掩码mask,对mask取反就可得到上述相同的效果;
例如 5 的二进制是 101,占3个bit,那么这个掩码 mask 的最后3个bit是0,其余全部是1;

5 0000 0101
mask 1111 1000 
~5 1111 1010 
~mask 0000 0111
(~5)&(~mask) 0000 0010
class Solution {
public:
    int findComplement(int num) {
        unsigned mask = ~0;    //掩码初始为二进制全部是1
        while(num & mask)
            mask <<= 1;        //左移掩码
        return (~num) & (~mask);
    }
};

参考:
https://blog.csdn.net/u012351768/article/details/76283904
https://blog.csdn.net/changyourmind/article/details/52369994

你可能感兴趣的:(编程巧妙技巧)