LintCode解题记录 - 位操作

Tips:已经正式作为一名研究生啦:D.

LintCode Product of Array Exclude Itself

给定一个数组A,定义B[i] = A[0]…*A[i-1]*A[i+1]…*A[n-1].求数组B,但是在计算的时候请不要用除法。

思路

可以把B[i]看成两部分,即i的前缀积和i的后缀积,其中后缀积如果采用倒序遍历的方式掉话也就相当于前缀积。两次遍历,时间复杂度O(2n),空间复杂度O(n)。

注意

数据的范围是long long。

代码
    vector<long long> productExcludeItself(vector<int> &nums) {
        // write your code here
        vector<long long> res(nums.size(), 1);
        long long sum = 1;
        //正序遍历一次,对于nums[i],求得nums[0]*...*nums[i-1]
        for (int i = 1; i < nums.size(); i++) {
            sum *= nums[i-1];
            res[i] = sum;
        }
        sum = 1;
        //倒序遍历一次,对于nums[i],求得nums[n-1]*...*nums[i+1]
        for (int i = nums.size()-2; i >= 0; i--) {
            sum *= nums[i+1];
            res[i] *= sum;
        }
        return res;

    }

LintCode Delete Digits

输入一个字符串A,其由N位数字组成,代表一个N位正数。给定一个正数k,要求从该N位数中删除任意k位数,并把剩余数字按照原有顺序组成一个新的数,输出这个最小的新数。

思路

贪心Greedy。什么情况下可以保证删除一位数字之后,后面的数字依次往前进一位得到的新数比删除其余位的数字得到的更小呢?仔细想一下可以发现如果A[i]>A[i+1],就可以删掉A[i],满足题意。如果i到了最后一位,那么就说明之前的数都是递增的,此时直接删除最后一位数字即可。

注意

删除之后的新数可能会有前导0的存在,所以要剔除前导0。同时本题用’?’代表删除一位数字。另外,如果删除了一位数之后,应该跳出循环,从头开始再判断。而不是接着继续判断下去。

代码
    string DeleteDigits(string A, int k) {
        // wirte your code here
        string res = "";
        int len = A.size();

        while (k) {
            for (int i = 0; i < len; i++) {
                if (A[i] == '?') continue;
                int next = i+1;
                while (next < len && A[next] == '?') next++;
                if (next < len && A[i] > A[next] || next == len) {
                    A[i] = '?';
                    k--;
                    break;
                }
            }
        }
        int i = 0;
        while (A[i] == '?' || A[i] == '0') i++;
        for (; i < A.size(); i++) {
            if (A[i] == '?') continue;
            res += A[i];
        }
        return res;
    }

LintCode Find the Missing Number

输入一个数组,大小为N,包含的数为0~N,显然要少一个数,问你这个数是多少?

思路1

先求和0~N,然后减去给的数组的元素和得到的就是缺失元素。(惊了个呆我竟然没想到)

代码1
int findMissing(vector<int> &nums) {
    // write your code here
    int sum = nums.size()*(nums.size()+1) / 2;
    for (auto n: nums) {
        sum -= n;
    }
    return sum;
}
思路2

位操作之异或操作。把原书组和0~N的所有数都异或一遍,除了缺失的那个数外其余的所有数都出现了两次,他们的异或值均为0,所以最终的异或值就是缺失的那个数。

代码2
    int findMissing(vector<int> &nums) {
        // write your code here
        int res = 0;
        for (auto n: nums) {
            res ^= n;
        }
        for (int i = 0; i <= nums.size(); i++) {
            res ^= i;
        }
        return res;
    }
思路3

如果用Hash来做就很简单,但题目要求是O(1)的空间复杂度,受到Sort ColorsII思想的启发,可以用这个数组本身来存储Hash信息。这里用nums[i]=-1代表数i出现过一次。

代码3
    int findMissing(vector<int> &nums) {
        // write your code here

        for (int i = 0; i < nums.size();) {
            if (nums[i] < 0 || nums[i] == nums.size()) {
                i++;
                continue;
            }
            if (nums[i] == i) {
                nums[i++] = -1;
                continue;
            }
            int temp = nums[nums[i]];
            nums[nums[i]] = -1;
            nums[i] = temp;
        }

        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] == -1) continue;
            return i;
        }
        return nums.size();

    }

Tips: 强大的位操作,再有些时候能起到不一样的效果。

LintCode Single Number

输入一个数组,其中只有一个数出现过1次,其余所有数都出现了两次,求这个独苗。
要求一次遍历,不使用额外空间。

思路

异或操作,即找不同操作。异或操作满足交换率和结合率,比如[1,3,2,2,5,3,5],逐个异或的结果就是
1 xor 3 xor 2 xor 2 xor 5 xor 3 xor 5 = 1 xor (2 xor 2) xor (3 xor 3) xor (5 xor 5) = 1。

注意

所有的位操作都是对于二进制而言。

代码
    int singleNumber(vector<int> &A) {
        // write your code here
        int res = 0;
        for (auto n: A) {
            res ^= n;
        }
        return res;
    }

LintCode Single NumberII

输入一个3n*1的数组,只有一个数出现了一次,其余所有数都出现了k次,求这个独苗。

思路1

和Single Number很像,但是由于出现了三次,所以异或操作并不起作用。如果从二进制的角度来看,如果一个数出现了3次,那么其二进制上对应每一位也都出现了三次。把其每一位都加起来模3就是0,最后剩下的就是Single。由于int是32位的,所以我们用一个变量从0到31表示其第i位的值。这种思路的3也可以延伸为k,只需要改为模k即可。

代码1
    int singleNumberII(vector<int> &A) {
        // write your code here
        int res = 0;
        for (int i = 0; i < 32; i++) {
            int sum = 0;
            for (int j = 0; j < A.size(); j++) {
                sum += (A[j] >> i) & 1;//与1相与就是取其最低位而其余位全部为0
            }
            res |= (sum % 3) << i;//这里还要把模的结果还原到其原本的位置上去,然后利用或操作添加到res的二进制上。
        }
        return res;
    }
思路2

我们只用三个变量one,two,three来表示每一位出现1次、2次、3次1的个数。比如数组[1,3,3,3],换成二进制就是[001, 101, 101, 101],那么one=001, two=101, three=101。one的第i位就代表对于这个数组中的所有数的二进制表示中,第i位是否出现了一次1,即可以理解为出现1次1的分布。two代表是否出现了两次或三次,three就代表出现了三次。

代码2
    int singleNumberII(vector<int> &A) {
        // write your code here
        int one = 0, two = 0, three = 0;
        for (int i = 0; i < A.size(); i++) {
        //只要第二次以上出现,就为1
            two |= one & A[i];
            //出现奇数次保留,偶数次就清空。 
            one ^= A[i];
            //出现2次即以上且出现奇数次,根据题意就是出现了三次,three就置1
            three = one & two;
            //将three取反,将该位清0,然后修改1和2,就相当于是已经有三个重复的数出现了,然后删除这三个重复的数
            one &= ~three;
            two &= ~three;
        }
        return one;
    }

LintCode Single Number III

给定一个2*n+2个数的数组,其中有两个数出现了一次,其余数都出现了两次,求这两个数。

思路

这道题思路还是跟Single Number很像的,但是异或得到的是这两个数的异或值。如果能把这个数组劈开,使这两个独苗分在不同的小组,那么调用Single Number的方法就很可以得到这两个数了。那么按照什么规则可以把这两个独苗区分开呢?根据异或的要求,如果这两个数某一位上异或位1说明他们不同,那么他们和1相异或得到的结果也肯定不同,于是这两个数就可以根据这个条件分割开来了。同时其余数组内的所有数也会根据这个条件(某一位与1相异或)而分成两个小组。

注意

理论上来说可以取异或值的任一的’1’,本题中采用了取最右边的1,这种方法在树状数组中曾经提到过,即
x & (-x),这里复习一下补码的知识,正数的补码等于其原码本身,负数的补码等于符号位为1,其余位等于该数绝对值的原码按位取反再加1。比如x=6,写成二进制就是0000 0110,-6的二进制代码就是 1111 1010,两者按位相与得到0000 0010,即6的最右边的1。用数组里的数和0000 0010相与的结果可以对数组进行 划分。

代码
    vector<int> singleNumberIII(vector<int> &A) {
        // write your code here
        vector<int> res(2, 0);
        int diff = 0;
        for (auto n: A) {
            diff ^= n;
        }
        diff &= -diff;
        for (auto n: A) {
            if (n & diff) res[0] ^= n;
            else res[1] ^= n;
        }
        return res;


    }

总结

位操作很重要,常见的位操作有 按位与、或、异或、平移、以及取反。进行位操作时,一定要用二进制的眼光来看待问题,来思考问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值