异或运算(^):同0非1,即相同的数异或结果为0,不同的数异或结果为1。可以理解为不带进位的二进制加法。
由于可以理解为不带进位的二进制加法,那么显然异或运算是满足交换律的:
A⊕B=B⊕A
A\oplus B=B\oplus A\
A⊕B=B⊕A
一个数异或自己本身就会变成0,而一个数异或0则不改变该数,即:
A⊕A=0A⊕0=A
\begin{align}A \oplus A=0 \\
A\oplus 0=A\end{align}
A⊕A=0A⊕0=A
设A与B是不相同的两个数,则:
A⊕B⊕B=A⊕0=A
A \oplus B \oplus B=A\oplus 0=A
A⊕B⊕B=A⊕0=A
请理解异或的规则,接下来便是实际应用:
1.1交换两个变量(不使用第三变量)
对于常规的交换两个变量的值非常简单,只需要额外定义一个第三变量即可,在这里我们不使用第三变量。
我们可以得到采用异或来交换两个数。代码如下:
A = A ^ B;
B = A ^ B; // B = A ^ B ^ B = A
A = A ^ B; // A = A ^ B ^ A = B
成功交换,核心思想就是将A^B的值存入A中,在进行相应异或操作。
1.2找出出现奇数次数的数(位运算)
- 有这么一道题目:给定一个整型数组,里面只有一个数出现了奇数次,其他的数都是偶数次,请找出这个数,要求时间复杂度为O(n)O(n)O(n)。
由于时间复杂度为O(n)O(n)O(n),暴力求解肯定不行,并且暴力求解也并不好书写。
根据异或的思想,一个数异或自己本身的值为0,那么只要是偶数次的数,异或后的值都为0。而0与一个数异或等于这个数本身,因此我们只需要将数组中的所有数异或一次便可以得到那个出现了奇数次的数。
- 这道题目升级一下:给定一个整型数组,里面只有两个数出现了奇数次,其他的数都是偶数次,请找出这个两数,要求时间复杂度为O(n)O(n)O(n)。
题目由原来的一个数变为了两个数,我们假设这两个数分别为a和b。按照上面全部异或一次的方法后,那么得到的结果为a^b。
现在问题是如何通过a^b将a和b分别提取出来呢?
观察a^b的特点,由于异或运算的性质,只有当两个数完全相同时结果才会等于0,而a与b显然不相等,因此得到的结果里(二进制)至少有一位的值为1。且在这一位上,a与b必然一个为0,另一个为1。
右往左找到结果中第一个为1的位(bit),可以通过该数与其补码相与得到。通过一个实例来说明:
假设结果为10010100,求其补码:01101100,两者相与,得到00000100,刚好是右边第一个1,将这个数记作eor。
[!TIP]
补码的求法为按位取反加1。
观察补码形式,还有一种更加简单的求法:从右往左找到第一个1,在这个1的左边的所有数字全部取反即可得到补码。这也是为什么通过一个数与其补码相与便可以得到该数右边的第一个1。
有了这个eor,我们将eor与数组中所有的数相与,如果结果为0,则该数这一位上一定为0,而a与b中必然只有一个数的该位上为0,我们只需要将所有该位上位0的数进行异或操作,便可以得到a和b中的其中一个(onlyOne)。
那么剩下的另一个数只需要计算abonlyOne便可以得到。代码如下:
#include <iostream>
#include <string>
using namespace std;
// 给定一个整型数组,里面只有两个数出现了奇数次,
// 其他都是偶数次,请找出这个数,要求O(n)
int main()
{
int array[] = {10, 10, 20, 20, 17, 19, 30, 30, 50, 50};//这里的数组只是示例
int eor = 0;
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
{
eor = eor ^ array[i];
}
// 此时eor=a^b
// 由题目知,eor!=0,故其必至少有一个位上为1
// 找出这个1:
int rightOne = eor & (~eor + 1); // 找到右边第一个1
int onlyOne = 0;
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
{
if ((array[i] & rightOne) == 0) // 找到这一位为0的所有数
// if ((array[i] & rightOne) != 0) 找到这一位为1的数
{
onlyOne ^= array[i];
}
}
cout << onlyOne << " " << (onlyOne ^ eor) << endl;
return 0;
}