一.什么是单身狗问题?
- 单身狗往往是这样的,在别人都成双成对的时候,只有你一个人孤独寂寞冷(比如博主)。而单身狗问题往往是找出一系列数中落单的那个
- 对于这种问题往往可以暴力求解,但这样显得不够“优雅”,毕竟我们是“单身贵族”嘛!所以我们这里就介绍一种时间复杂度和空间复杂度都比较低的方法——位运算法
找出一个单身狗
- 先举出最简单的例子
- 问题如下
//找出单身狗-版本1 //有一个数组只有一个数组出现一次,其余数字都是成对出现的 //请找出只出现一次的数字 //1 2 3 4 5 1 2 3 4
- 5就是这里的单身狗`
- 那我们怎样用位运算法求解呢?
- 我们知道对于异或来说相同为0,相异为1
- 而异或又有这样的特性
//a^a=0; //a^0=a;
- 同时,异或也满足交换律和结合律,我们就能写出以下代码
int find_single_dog1(int arr[], int sz) { int i = 0; int ret = 0; for (i = 0; i < sz; i++) { ret ^= arr[i]; } return ret; } int main() { int arr[] = { 1,2,3,4,5,1,2,3,4 }; int sz = sizeof(arr) / sizeof(arr[0]); int dog = find_single_dog1(arr, sz); printf("%d\n", dog); return 0; }
- 什么意思呢?
- 我们通过一个函数把所有的元素都给异或在一起,我们知道相同元素异或在一起为0,而一个元素异或0等于它本身,这样,就可以得到这个“单身狗”了。
找两条单身狗
- 从上面这题我们又可以深入一点,如果此时有两个人,但是他们是同性或者是异性但是不适合呢?也就是说有两条单身狗我们又该如何解决呢?
//找出单身狗版本2 //一个数组中只有两个数字是出现一次,其他所有数字都出现了两次。、 //编写一个函数找出这两个只出现一次的数字。 //例如: //有数组的元素是:1,2,3,4,5,1,2,3,4,6 //只有5和6只出现1次,要找出5和6.
- 此时的难点在于,我们也可以通过把所有的元素都异或起来的方法找到这两条单身狗异或的值,但是我们怎样通过他俩异或的值找到相应的两个元素呢?
- 我们可以这样考虑,我们如果能把两条单身狗分到不同的组里,再找出每一个组中的单身狗不就和第一种情况一样了吗?
- 此时我们分组的条件就应该是两者一定存在差异的地方,哪里能凸显两者的差异呢?
- 先把代码放这,让大家自己想一下,下面会有详细的解释的
void find_single_dog2(int arr[], int sz, int* pd1, int* pd2) { //1. 所有数字异或在一起 int ret = 0;//5 ^ 6 int i = 0; for (i = 0; i < sz; i++) { ret ^= arr[i]; } //2. 计算ret的第几位是1,从而找出不同进行分组 int pos = 0; for (i = 0; i < 32; i++) { if (((ret >> i) & 1) == 1) { pos = i; break; } } //分组 //计算数组中元素的第pos为1的异或 for (i = 0; i < sz; i++) { if (((arr[i] >> pos) & 1) == 1) { *pd1 ^= arr[i]; } } //计算数组中元素的第pos为0的异或 *pd2 = ret ^ *pd1; } int main() { int arr[] = { 1,2,3,4,5,1,2,3,4,6 }; int dog1 = 0; int dog2 = 0; int sz = sizeof(arr) / sizeof(arr[0]); find_single_dog2(arr, sz, &dog1, &dog2); printf("%d %d", dog1, dog2); return 0; }
- 看懂了吗?我来解释一下
- 我们这个ret是把所有数异或进来的,此时只剩下了两条单身狗异或的值,异或相异为1
也就是说找到ret二进制位是1的位置是不是就找到两条单身狗的不同之处啦?接下来通过分组就可找出单身狗
- 注意:相同的元素相应的二进制位一定相同,也就是非单身狗们一定会被分到同一组中,从而保证这组中异或后有且仅有一条单身狗。
- 这里还有一个能偷懒的地方,我们知道了两条单身狗异或的值,又找出了一条单身狗,通过异或法是不是就能快速找出另一条单身狗了?
a^b^a=b
二.LeetCode单身狗进阶题
- 题目的链接放在这里
错误集合 - 说到底,这个题就是单身狗题目的变种,我们同样是通过异或先找出重复出现的值和丢失的值,然后再对它们进行分组,依次找出即可
int* findErrorNums(int* nums, int numsSize, int* returnSize){ int ret=0; int*returnNums=(int*)malloc(sizeof(int)*2); *returnSize=2; int i=0; //通过把数组中元素和1到n中元素异或起来,找到多余元素以及缺少的元素 (找两条单身狗) for(i=0;i<numsSize;i++) { ret^=(i+1); ret^=nums[i]; } int pos=0; i=0; //异或后不同位二进制为1,找1分组 while(i<32) { if(((ret>>i)&1)==1) { pos=i; break; } i++; } //找出不同位后分组,只用找一个就行 int nums1=0; int nums2=0; for(i=1;i<=numsSize;i++) { if(((i>>pos)&1)==1) { nums2^=i; } } for(i=0;i<numsSize;i++) { if(((nums[i]>>pos)&1)==1) { nums1^=nums[i]; } } nums1^=nums2;//分组后的元素异或1-n找相同元素消掉 //多的或者少的那个元素,不确定,我们需要判断一下 int flag=0;//标记 for(i=0;i<numsSize;i++) { if(nums[i]==nums1) { flag=1; returnNums[0]=nums1; break; } } if(flag==1)//数组中找到了这个元素,为重复元素,另一个为缺少元素 returnNums[1]=ret^nums1; else { returnNums[1]=nums1; returnNums[0]=nums1^ret; } return returnNums; }
除了注释中提到的,我这里还想提两点需要注意的地方
与找单身狗不同,这里我们没有成对的元素,因此我们在分组前后都通过异或1到n的数找相同元素从而消掉,剩下的就是我们的单身狗了(有些人可能不理解重复的元素咋消掉啊,重复的元素在ret里就因为相同而消掉了,因此照样能找到这条单身狗哦)
与普通题不同的地方在于,我们这里找出两条单身狗后还有辨别一下哪个是重复元素,哪个是缺少元素,在区分时我们通过flag做了一个标记,flag的值被修改也就是数组存在这个数,说明它就是重复元素啦,另一个自然是缺少元素,反之未被修改就与这种情况相反。
总结
- 今天的内容到这里就结束了,有关位运算的地方如果不太清楚最后自己画画逻辑图就能弄懂,而有关题目博主建议如果看懂了的话不妨自己动手试试哦!!
- 好了,如果你有任何疑问欢迎在评论区或者私信我提出,大家下次再见啦!