位运算的应用
1、不用加减乘除做加法
写一个函数, 求两个整数之和, 要求在函数体内不得使用+、-、×、÷ 四则运算符号
两个数相加的和 $=$ 异或和 $+$ 进位
即 $a + b = a ^ b + (a $ & $ b) << 1 $
其中, $a$ & $b$ 具有 $ \leq min(a, b) $ 的性质, 依据此式, 可迭代求出两数的和(类似迭代求$gcd$)
classSolution {public:int add(int num1, intnum2) {int ans = 0, carry = 0; //异或和 以及 进位
while(num2) { //当进位不为0时
ans = num1 ^num2;
carry= (num1 & num2) << 1;
num1=ans;
num2=carry;
}returnans;
}
};
2、找到数组中只出现一次的两个数字
一个整型数组里除了两个数字之外, 其他的数字都出现了两次, 找出这两个只出现一次的数字
异或的性质:
1.异或两次等于没异或
2.相当于不进位加法, 即不产生进位
可求出所有数的异或和, 出现两次的等于没异或, 即最终等于只出现一次的两个数的异或和
因为该两个数不相同, 所以其异或和在二进制下一定存在一位为"1", 记该位为第 $pos$ 位, 且该位1是其中一个数特有的(否则两个都等于1异或完了等于0)
然后求出数组中所有 $pos$ 位上为1的数的异或和, 即其中一个数(其余的数被异或了两遍等于没异或), 最终另一个数就是两个数的异或和再异或一遍第一个数
classSolution {public:
vector findNumsAppearOnce(vector&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{tot1, tot ^ tot1}; //另一个数就是两个数的异或和再异或一遍第一个数
}
};
3、数组中唯一只出现一次的数字
在一个数组中除了一个数字只出现一次之外, 其他数字都出现了三次, 请找出那个只出现一次的数字
法一($O(32n)$, 其实和$nlogn$差不多...)
按位确定那个数, 因为数组中其他数都出现了三次, 除去这个数后每一位上为1或0的数的个数都是3的倍数, 而加上这个数后有一边的个数会多1
所以可按位枚举, 遍历数组, 记录每位上是1和是0的个数为(1为 $tot1$, 最终根据 $tot1 % 3 == $ 0还是1来确定所求的数的该位
classSolution {public:int findNumberAppearingOnce(vector&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
}returnans;
}
};
法二(状态机模型, $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$ 即可
classSolution {public:int findNumberAppearingOnce(vector&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;
}returnones;
}
};