位运算的应用

位运算的应用

1、不用加减乘除做加法

写一个函数, 求两个整数之和, 要求在函数体内不得使用+、-、×、÷ 四则运算符号

 

两个数相加的和 $=$ 异或和 $+$ 进位
即 $a + b = a ^ b + (a $ & $ b) << 1 $

其中, $a$ & $b$ 具有 $ \leq min(a, b) $ 的性质, 依据此式, 可迭代求出两数的和(类似迭代求$gcd$)

class Solution {
 public:
    int add(int num1, int num2) {
        int ans = 0, carry = 0;     //异或和 以及 进位
        while(num2) {       //当进位不为0时
            ans = num1 ^ num2;
            carry = (num1 & num2) << 1;
            num1 = ans;
            num2 = carry;
        }
        return ans;
    }
};

 

2、找到数组中只出现一次的两个数字

一个整型数组里除了两个数字之外, 其他的数字都出现了两次, 找出这两个只出现一次的数字

 

异或的性质:

  1.异或两次等于没异或

  2.相当于不进位加法, 即不产生进位

 

可求出所有数的异或和, 出现两次的等于没异或, 即最终等于只出现一次的两个数的异或和
因为该两个数不相同, 所以其异或和在二进制下一定存在一位为"1", 记该位为第 $pos$ 位, 且该位1是其中一个数特有的(否则两个都等于1异或完了等于0)
然后求出数组中所有 $pos$ 位上为1的数的异或和, 即其中一个数(其余的数被异或了两遍等于没异或), 最终另一个数就是两个数的异或和再异或一遍第一个数

class Solution {
 public:
    vector<int> findNumsAppearOnce(vector<int>& nums) {
        
        int tot = 0; //所有数的异或和
        for(int i = 0; i < nums.size(); i++) tot ^= nums[i]; //求出异或和
        
        int pos = 0, tot1 = 0;
        
        while(!(tot >> pos & 1)) pos++; //求出两个数异或和tot在二进制表示下的第一位1
        
        for(int i = 0; i < nums.size(); i++)
            if(nums[i] >> pos & 1) //求出数组中pos位上是1的数的异或和, 即其中的一个数(其余的数被异或了两遍等于没异或)
                tot1 ^= nums[i];

        return vector<int>{tot1, tot ^ tot1}; //另一个数就是两个数的异或和再异或一遍第一个数
    }
};

 

3、数组中唯一只出现一次的数字

 在一个数组中除了一个数字只出现一次之外, 其他数字都出现了三次, 请找出那个只出现一次的数字

 

法一($O(32n)$, 其实和$nlogn$差不多...)

按位确定那个数, 因为数组中其他数都出现了三次, 除去这个数后每一位上为10的数的个数都是3的倍数, 而加上这个数后有一边的个数会多1
所以可按位枚举, 遍历数组, 记录每位上是1和是0的个数为(1为 $tot1$, 最终根据 $tot1 % 3 == $ 0还是1来确定所求的数的该位

class Solution {
 public:
    int findNumberAppearingOnce(vector<int>& nums) {
        int ans = 0;    //确定答案
        for(int i = 0; i < 32; i++) {       //按位枚举
            
            int tot0 = 0, tot1 = 0;     //数组中第i位为0和1的数的个数
            
            //求出个数
            for(int j = 0; j < nums.size(); j++)
                if(nums[j] >> i & 1) tot1++;
                else tot0++;
            
            if(tot1 % 3 == 1) ans |= 1 << i;    //如果1的个数 % 3后多了一个, 那么该位就是1
        }
        return ans;
    }
};

 

法二(状态机模型, $O(n)$)

 

本题与前一题思路类似, 前一题中, 其他数都出现了两次, 因此需要的状态转移方式是, 如果出现两个1就抵消为0, 用一个变量和异或运算即可实现
而本题是需要1出现三次时才会抵消, 因此有三种状态, 即1出现的次数为 $3k, 3k + 1, 3k + 2$ 次

逐位来看, 要设计一个两位的状态转移, 出现三个1时抵消, 出现0时不变
一个变量只能表示两种状态, 要用两个变量来表示, 用 $one$ 和 $two$ 两个变量来记录1出现次数, 00表示1出现 $3k$ 次, 01表示1出现 $3k + 1$ 次, 10表示1出现 $3k + 2$ 次

                          真值表

(one, two)二元组 x(当前位, 0/1) (one, two)二元组
(0, 0) 0 (0, 0)
(0, 0) 1 (0, 1)
(0, 1) 0 (0, 1)
(0, 1) 1 (1, 0)
(1, 0) 0 (1, 0)
(1, 0) 1 (0, 0)

可以看到, 该状态机有三种状态, 碰到0不发生改变, 碰到1进入下一个状态

 $one$ 的状态转移方程

   $ one = ($~$one$ & ~$ two $ & $ x) $ | $ (one $ & $ ~two $ & $ ~x) $

               $ = $~$two$ & $(($~$one$ & $x)$ | $(one$ & ~$x))$
               $ = $~$two$ & $(one$ ^ $x)$

同理, 再用转移后的 $one$ 来求 $two$ 的状态转移方程

其中 $one$ 为1当且仅当1出现次数为 $3k + 1$, $tow$ 为1当且仅当1出现次数为 $3k + 2$
因此如果题目改为, 有一个数出现了两次, 则返回 $two$ 即可

class Solution {
 public:
    int findNumberAppearingOnce(vector<int>& nums) {
        int ones = 0, twos = 0;     //状态机的两个变量(需要表示出3种状态)
        
        for(int i = 0; i < nums.size(); i++) {      //迭代求出最终的状态(即答案)
            int x = nums[i];
            ones = (ones ^ x) & ~twos;
            twos = (twos ^ x) & ~ones;
        }
        
        return ones;
    }
};

 

posted @ 2020-10-06 17:16  yikanji  阅读(180)  评论(0)    收藏  举报