异或思想的算法题
1.消失的数字
数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
示例 1:
输入:[3,0,1]
输出:2
示例 2:
输入:[9,6,4,2,3,5,7,0,1]
输出:8
采用异或的思想
我们知道 异或的规则是相同为0 不同为1 , 我们让一个数字去和0~numsize之间的数去异或,再让它去跟数组里的值去异或,由于消失的数字只会出现一次,其他数字会出现两次,而出现两次数字对应的二进制位的1 终究会被抵消,因此最后二进制位留下来的1 和 0 就是消失的那个数字的二进制位
代码如下:
c
int missingNumber(int* nums, int numsSize)
{
// 让missing 一开始等于这个数组的元素个数
int missing = numsSize;
// 创建循环去异或 0~numSize之间的每一个数
// 再让其去去异或数组中的每一个数
for(int i = 0; i<numsSize; i++)
{
missing ^= i;
missing ^= nums[i];
}
// 走到这里就说明,missing中存储的二进制数就是消失的数字的二进制数
return missing;
}
首先,在代码中, missing
初始化为 numsSize
,因为我们预设缺失的数字是 numsSize
。
然后,遍历数组,对于每个索引 i,我们使用异或操作:
- 第一步,我们将当前遍历到的索引值 i 与
missing
进行异或操作。这会使missing
中存储的值减少或增加 i,取决于当前遍历到的位置。 - 第二步,我们将当前位置 i 对应的 nums[i] 与
missing
进行异或操作。由于数组中所有正常的数都会在某个时刻出现两次(一次作为索引,一次作为元素),而那个缺失的数只会被用一次,因此在经历了全部异或操作后,那个缺失的整数就会留在missing
变量中。
通过以上过程和异或运算的特性,最终得到的 missing
即为缺失的整数值。
我们来看一个难度更高的题目:
2.数组中有两个数字只出现了一次
题目:
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
你必须设计并实现线性时间复杂度 的算法且仅使用常量额外空间来解决此问题
示例 1:
输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。
示例 2:
输入:nums = [-1,0]
输出:[-1,0]
示例 3:
输入:nums = [0,1]
输出:[1,0]
分析:
如图所示:
- 我们首先让 ret = 0 去异或所有数组里的数,那么出现两次的数就会被抵消
- 因此ret的结果就是两个出现一次的数异或的值
- 那我们就要想办法把这两个数给分离出来
- 那么我们采取去找到ret中 的 1 也就是二进制中 为 1 的地方,因为如果是1就说明两个只出现一次的数的二进制 在这个位上面 一个为0 一个为1
- 如上图所示,ret中第M位为1的,就说明x1,x2两个数的第M位一个是1,一个是0
- 接下来我们就去数组中分离出x1和x2
- 让第M位为1的为一组。第M位为0的为一组,就说明x1和x2在不同的一组中
- 那我们就让ret的每一个位都去&上一个1 如果结果是1 就说明ret的该位就是1,那我们就跳出循环。1<<m的意思是让1左移m个位
-
当我们找到了为1的位了,我们就要分离。
-
上图我们让数组的每一个数进行分离,第M位为1 的数就让x1去异或,第M位是0的就让x2去异或,x1异或完全部的数,第M位为1且出现两次的都会抵消,最后剩下的就是只出现一次且第M位为1的数。x2也是同理,第M位为0的数且出现两次的数会被抵消,最后剩下的x2的值就是只出现一次且第M位为0的数
-
有一个需要注意的点,我们需要将(1<<m)改成((unsigned int)1 << m),因为int是有符号类型,最高位32位是符号位,不能比较,但是如果传入的数用到了32位,那么我们的程序就无法完成功能的实现。
代码实现:
c
int* singleNumber(int* nums, int numsSize, int* returnSize)
{
// 我们要让ret去异或数组中所有的数
int ret = 0;
// 遍历数组
for(int i = 0; i < numsSize;i++)
{
ret ^= nums[i];
}
// 此时的ret是只出现一次的两个数异或的结果
// 我们要去检测第m位出现1 要找到这个m 这样我们后面才能对两个数进行分离
// 为什么要检测第m位出现1呢,因为如果异或的结果是1 就说明这两个数在这个位上面,一个是1 一个是0
// 这个不同点就是我们的突破口
int m = 0;
while(m < 32) // 32是因为一个int类型是4个字节 32个比特位
{
// 为什么要让1强制转化成unsigned int类型呢?
// int 是有符号类型,最高位(第32为)为符号位,不能比较最高位
// 若我们只比较前31位 就会导致一部分int类型无法被我们的程序正确运行
if(ret & ((unsigned int)1<<m)) // 1<<m 就是让1左移m个位
break; // 如果走进这个循环就说明 第m位就是1
else
m++; // 如果走到这里说明第m位不是1 让m++ 让1继续左移
}
// 找到了1 之后就要对原数组里的数进行分离
// 第m位为1 的数 一组 因为只出现一次的数的其中一个数的第m位也是1
// 第m位为0的数 一组 ,因为另外一个只出现一次的数 的第m位是0
int x1 = 0, x2 = 0;
for(int i = 0; i < numsSize; i++)
{
// 为什么要让1强制转化成unsigned int类型呢?
// int 是有符号类型,最高位(第32为)为符号位,不能比较最高位
if(nums[i] & ((unsigned int)1<<m)) // 判断数组下标为i的元素的第m位是否为1
{
x1 ^= nums[i];
// 让第m位为1的数全部异或,出现两次的数抵消,剩下的就是只出现一次的并且第m位是1的数
}
else
{
x2 ^= nums[i];
//让第m位为0的数全部异或,出现两次的数抵消,x2最后剩下的就是只出现一次且第m位是0的数
}
}
// 走到这里x1 和x2 就是我们要的只出现一次的数
int* retArr = (int*)malloc(sizeof(int) * 2);
retArr[0] = x1;
retArr[1] = x2;
*returnSize = 2;
return retArr;
}