位运算的应用
位运算的应用
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$差不多...)
按位确定那个数, 因为数组中其他数都出现了三次, 除去这个数后每一位上为1或0的数的个数都是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;
}
};