LintCode位运算题总结

LintCode上位运算&二进制的题目不多,加起来就10道题左右,把他们全部都AC一下,位运算这块就差不多了。面试的时候位运算也不是经常考察的点。因为这东西你知道就指导,不知道就不知道,没有啥深入探究讨论的余地。

1. A + B Problem

对两个32位的整数求和,这道题要是直接回答return A + B那就只能等着收拒信了。这道题考查的是会不会用位运算做加法,如果用位运算的话,运行时间是比直接return A+B快得多的。

主要利用异或运算来完成,异或运算,相等为0,不同为1。如果转换为加法,异或运算有一个别名叫做:不进位加法。那么a ^ b就是a和b相加之后,该进位的地方不进位的结果。

然后下面考虑哪些地方要进位,自然是a和b里都是1的位置需要进位。a & b就是a和b里都是1的那些位置,a & b << 1 就是进位之后的结果。所以:a + b = (a ^ b) + (a & b << 1) 

令x = a ^ b, y = (a & b) << 1   可以知道,这个过程是在模拟加法的运算过程,进位不可能一直持续,所以b最终会变为0。因此重复做上述操作就可以求得a + b的值。

    public int aplusb(int a, int b) {
        while (b != 0) {
            int x = a ^ b;
            int y = (a & b) << 1;
            a = x;
            b = y;
        }
        return a;
    }

408. Add Binary

对两个用String类型表示的二进制串进行加法操作。思路很简单,就是模拟加法,首先要把2个string补成长度相等,比如A='1', B='11',那就要在A前面补一个0。当2个字符串长度相等后,再用循环来对每一位进行加法操作,用一个额外变量carry表示进位。

    public String addBinary(String a, String b) {
        // 补位,让2个string等长
        int length = a.length() > b.length() ? a.length() : b.length();
        if (a.length() < length) {
            int toGo = length - a.length();
            for (int i = 0; i < toGo; i++) {
                a = "0" + a;
            }
        }
        if (b.length() < length) {
            int toGo = length - b.length();
            for (int i = 0; i < toGo; i++) {
                b = "0" + b;
            }
        }
        // 模拟加法操作
        String res = "";
        int carry = 0;
        for (int i = length - 1; i >= 0; i--) {
            int val = (int)(a.charAt(i) - '0') + (int)(b.charAt(i) - '0') + carry;
            if (val > 1) {
                carry = 1;
                val = val % 2;
            } else {
                carry = 0;
            }
            res = val + res;
        }
        if (carry == 1) {
            res = 1 + res;
        }
        return res;
    }

365. Count 1 in Binary

输入一个32位整数,求它里面有多少个二进制的1存在。

每次“&”都会去掉num最右边的1.

如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。

举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.

我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。

如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。

    public int countOnes(int num) {
        int count = 0;
        while (num != 0) {
            num = num & (num - 1);
            count++;
        }
        return count;
    }


411. Gray Code

格雷码(Gray Code)是一个数列集合,每个数使用二进位来表示,假设使用n位元来表示每个数字,任两个数之间只有一个位元值不同。 

例如以下为3位元的格雷码: 000 001 011 010 110 111 101 100 。 

如果要产生n位元的格雷码,那么格雷码的个数为2^n. 

假设原始的值从0开始,格雷码产生的规律是:

第一步,改变最右边的位元值;

第二步,改变右起第一个为1的位元的左边位元;

第三步,第四步重复第一步和第二步,直到所有的格雷码产生完毕(换句话说,已经走了(2^n) - 1 步)。 

用一个例子来说明: 假设产生3位元的格雷码,原始值位 000 

第一步:改变最右边的位元值: 001 

第二步:改变右起第一个为1的位元的左边位元: 011 

第三步:改变最右边的位元值: 010 

第四步:改变右起第一个为1的位元的左边位元: 110 

第五步:改变最右边的位元值: 111 

第六步:改变右起第一个为1的位元的左边位元: 101 

第七步:改变最右边的位元值: 100

如果按照这个规则来生成格雷码,是没有问题的,但是这样做太复杂了。如果仔细观察格雷码的结构,我们会有以下发现除了最高位(左边第一位),格雷码的位元完全上下对称(看下面列表)。比如第一个格雷码与最后一个格雷码对称(除了第一位),第二个格雷码与倒数第二个对称,以此类推。

000

001

011

010

110

111

101

100

所以,在实现的时候,我们完全可以利用递归,在每一层前面加上0或者1,然后就可以列出所有的格雷码。 比如: 

第一步:产生 0, 1 两个字符串。 

第二步:在第一步的基础上,每一个字符串都加上0和1,但是每次只能加一个,所以得做两次。这样就变成了 00,01,11,10 (注意对称)。 

第三步:在第二步的基础上,再给每个字符串都加上0和1,同样,每次只能加一个,这样就变成了 000,001,011,010,110,111,101,100。 

好了,这样就把3位元格雷码生成好了。 

如果要生成4位元格雷码,我们只需要在3位元格雷码上再加一层0,1就可以了: 0000,0001,0011,0010,0110,0111,0101,0100,1100,1101,1110,1010,0111,1001,1000. 

也就是说,n位元格雷码是基于n-1位元格雷码产生的。 如果能够理解上面的部分,下面部分的代码实现就易理解了。

    public ArrayList<Integer> grayCode(int n) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        if (n <= 0) {
            res.add(0);
            return res;
        }
        res = grayCode(n - 1);
        for (int i = res.size() - 1; i >= 0; i--) {
            int num = res.get(i);
            num += (1 << (n - 1));
            res.add(num);
        }
        return res;
    }

181. Flip Bits

判断2个数的二进制位上有多少位是不同的。就先对两个数进行一下异或操作,得到的结果就是一个包含2个数不同位信息的数,比如100和011异或的结果是111,因为100和011三个位置都不同,所以有3个位置不同,所以结果是111.然后我们用位操作统计这个111里面有多少个1,就行了

    public static int bitSwapRequired(int a, int b) {
        // 异或的结果就是两数不同的位置的值为1
        int xor = a ^ b;
        // 然后统计这个数里面有多少个1
        int count = 0;
        while (xor != 0) {
            xor = xor & (xor - 1);
            count++;
        }
        return count;
    }

142. O(1) Check Power of 2

用O(1)的时间检查一个数是否是2的平方。这里可以用位运算。我们可以先列举一下,2的倍数的二进制数分别是10、100、1000、10000、100000等等。找到规律后,可以发现

符合这种规律的数都是最左边是一个1,然后右边都是0.那我们把它减去1之后再看看,分别是01、011、0111、01111、011111等等。然后再把他两做&操作,发现得到的都是0了。这个规律是肯定成立的,利用这个规律就可以在O(1)时间内判断了:

    public boolean checkPowerOf2(int n) {
        if (n <= 0) {
            return false;
        }
        return (n & (n - 1)) == 0;
    }


179. Update Bits

这道题如果转换为用二进制串俩做是比较简单的,如果一定要用位运算来做,其实这道题并不难,至少不涉及太多的数据结构和算法,需要的只是一些数学知识和位运算的知识,和一些细心与耐心。 首先,因为涉及到位运算,当然要将所有数字表示成32位二进制数的形式(题目中给出的不满32位的形式不利于解题)。拿题目中的例子来说: 

N=(10000000000)2 M=(10101)2 即N=1024,将其表示成32位的形式,即在前面补0得到

N=(00000000 00000000 00000100 00000000)2 M=(00000000 00000000 00000000 00010101)2。Update之后N=(10001010100)2,

即N=(00000000 00000000 00000000 00000100 01010100)2。其中加粗的部分即M。与Update之前的N对比,可以发现,除了i到j这一部分,其他部分与原数据相同。 

由位运算的性质可知,要保留一个数不变,需要与1做与运算,而变成M的部分可以先将该部分变成0,然后再与M做或运算,或者直接将M移位后做加法。好了,思路有了之后,我们来看代码。 

    public int updateBits(int n, int m, int i, int j) {
        int mask;
        if (j < 31) {
            mask = ~((1 << (j + 1)) - (1 << i));
        } else {
            mask = (1 << i) - 1;
        }
        return (mask & n) + (m << i);
    }

其中的mask即我们需要的掩码,以题目中的例子为例,mask=(11111111 11111111 11111111 10000011)2,这样与n做与运算,就可以做到我们前面说的保留i到j之外的部分,同时i到j之间的部分清0,然后在和左移i位的m相加就得到最后的结果。 


180. Binary Representation

首先,这个的输入是小数,这里就涉及到小数的二进制表示法。整数部分表示成二进制相信大家都会,只要不断的对2取余,然后再将余数反转就可以。那么小数部分如何表示成二进制,方法如下:

对十进制小数乘2,得到的整数部分和小数部分,整数部分既是相应的二进制数码,再用2乘小数部分(之前乘后得到新的小数部分),又得到整数和小数部分。如此不断重复,直到小数部分为0或达到精度要求为止。第一次所得到为最高位,最后一次得到为最低位如:0.25的二进制0.25*2=0.5 取整是0。0.5*2=1.0 取整是1即0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)。例子如下: 

0.8125的二进制 

0.8125*2=1.625 取整是1 

0.625*2=1.25 取整是1 

0.25*2=0.5 取整是0 

0.5*2=1.0 取整是1 

即0.8125的二进制是0.1101(第一次所得到为最高位,最后一次得到为最低位)

在上述的计算方法中,可能出现无限循环的情况,因此必须控制精度。题目中已经给出32位小数精度,超过则返回ERROR。

    private String left(String n) {
        int num = Integer.parseInt(n);
        String res = "";
        while (num != 0) {
            res = (num % 2) + res;
            num /= 2;
        }
        return res;
    }
    private String right(String n) {
        double num = Double.parseDouble(n);
        if (num == 0.0) {
            return "0";
        }
        String res = "";
        while (num < 1) {
            if (res.length() >= 32) {
                return "ERROR";
            }
            num = num * 2;
            if (num < 1) {
                res += "0";
            } else if (num == 1) {
                res += "1";
                return res;
            } else {
                res += "1";
                num -= 1;
            }
        }
        return res;
    }
    public String binaryRepresentation(String n) {
        if (n == null || Double.parseDouble(n) == 0) {
            return "0";
        }
        int pos = n.indexOf('.');
        // 只有整数部分
        if (pos == -1) {
            return left(n);
        }
        String l = left(n.substring(0, pos));
        String r = right("0." + n.substring(pos + 1));
        // 右边假如就是0.0000,那就直接返回整数部分
        if (r == "0") {
            return l;
        }
        // 右边假如是0.XXXX的形式
        if (pos == 1 && n.charAt(0) == '0') {
            return "0." + right(n);
        }
        // 超过32位精度
        if (r == "ERROR") {
            return r;
        }
        // 整数和小数部分都有
        return l + "." + r;
    }
题目不算难,但我硬是提交了5次才完全AC。主要是处理Corner case不到位,这也是为啥这道题也被归类为Hard的原因吧!



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值