我在查看其他人关于单身狗问题的解决方法时,他们总是很笼统的提出一些逻辑或者逻辑看起来很复杂然后就把代码写出来了,但是实际代码是怎么运行的对于其他人甚至他自己而言可能都是不清晰的,所以我想写一篇行行都有注释的找单身狗问题,方便大家在解决这个问题的时候能对里面涉及的各种知识点有一个回顾。
问题描述:
一个数组中只有两个数字是出现一次,其他所有数字都出现了两次,编写一个函数找出这两个只出现一次的数字。
例如:
有数组的元素是:1,2,3,4,5,1,2,3,4,6
只有5和6只出现1次,要找出5和6。
我在这里总结了两种办法供大家参考。同时,与以往的问题分析不同的是我在这里将针对两种不同的方法分开进行问题分析。
方法一:
既然是寻找单身狗了,那么我们就把所有成对出现的数字 (情侣) 集中在一起,那两个被孤立的就是单身狗了。我们通过冒泡排序实现这一功能:
void bubble(int* a, int sz) //(数组首元素地址,数组长度) { int j, i; for (i = 0; i < sz; i++) //判断循环次数 { for (j = 0; j <sz - 1 - i; j++) //冒泡排序中每次循环都会比上次循环需要进行交换个数减少一个,又因为i是从0开始的所以还要减去一个1 { if (*(a + j) > *(a + j + 1)) //如果左边大于右边交换左右交换位置 { int* tmp = *(a+j); *(a+j) = *(a + j + 1); *(a + j + 1) = tmp; } } } }
下面是大致的交换过程:
经过排序,我们就会将11、22、33、44这些情侣配对,以及将5,6这两个单身狗与他们分开。接下来做的就是将两个单身狗输出了, 同样的也是依靠循环。
void find(int* a, int sz, int* num) //num是用来存放单身狗的新数组 { bubble(a, sz); //将最后排序完成的结果传递给最后的find函数 int i = 0; int j = 0; for (i = 0; i < sz;) //这里对i的改变被移至if中去 { if (a[i] == a[i + 1]) //如果第一个和第二个数字相同那么就从第三个数字和第四个数字比较,能这样干的原因是因为冒泡排序的已经将成对的数字匹配过了如果不是求单身狗的题目限制这个判断条件就不适用了。 { i += 2; } else //前面的几组都能相等主要就是最后不等的5和6了 { num[j] = a[i]; //当到5时,应该是a[8] != a[9],将a[8]赋值给新数组num[0]中 i++; //这时i增加一次到达6,而此时数组已经结束了if判断条件必然为假 j++; //自增之后j为1,也就是新数组第二个位置,这个位置用来存放6 } } }
最后的主函数如下:
int main() { int arr[] = { 1,2,3,4,5,1,2,3,4,6 }; int num[2] = { 0 }; //用来存放单身狗的数组 int sz = sizeof(arr) / sizeof(arr[0]); find(arr, sz, num); printf("%d,%d", num[0], num[1]); //最后打印两个单身狗 return 0; }
方法二:
方法一我们采用了将情侣数字配对分离单身狗的办法,方法二我们采用异或运算的办法来解决这个问题,首先我们要知道关于异或运算的三个特点是:
1、异或运算的规则是相同为0不同为1
2、两个相同数字异或运算结果为0
3、两个不同数字异或结果总有一个或者多个位为1
我们首先需要的是将所有的数字全部进行异或,那么这样相同数字结果异或肯定全部位置是零,比如:{1,1,2,3,4,4},但是我们现在的数组是: { 1,2,3,4,5,1,2,3,4,6 }; 那么肯定是不能全部变为0的,5和6异或的结果肯定会有1出现。5^6 = 3 (0011) 这就证明这两个数二进制的后两位数字是不同的。当然这里只是因为我们为了方便理解直接计算了5和6的结果,实际情况中我们可能是不知道数组中具体的数字的只是为了让大家理解这段代码的意义在哪里。
int num = 0; for(int i = 0;i<sz;i++) { sum ^= arr[i]; }
我们通过全体异或知道了这两个单身狗二进制位中有哪两位是不同的,那么这两个数的最后一位一定分别是0和1的,倒数第二个位置同理,所以我们在接下来的记录中只需要记录其中的一个位置即可,因为将数组中二进制位在该位置上为1的数全部选出后,剩余的就是数组中二进制位在该位置上为0的数了只需要一个else就可以解决。
int pos; for (i = 0; i <32; i++) //整型数字占四个字节,三十二个比特位也就是有32个0和1 { if (sum & 1 << i) //当变量 sum 的二进制表示中,从右边开始数的第 i 位为 1 时,表达式 sum & 1 << i 的结果为真,然后记录此时i在32个比特位中的位置 { pos = i; break; //结果为1跳出for循环 } }
1、当& 为双目运算符时,它代表按位与而非取地址操作符(单目操作符),优先级小于 <<(单目和双目的区别可以简单理解为&两边的操作数个数)
2、左移位操作符的移位规则是:左边抛弃右边补零
3、&:同一为一,不同为零
所以 sum&1<<i 的运算顺序为先1<<i 移位,后再与sum按位与。这样就可以通过循环i++判断出sum的哪一位是1了,然后将此时i的值(即num中的1在32个比特位中哪个位置记录下来)。
终于到最后一步了😍,我们现在要做的就是,利用标记的pos = i将数组中所有数字二进制位该位置上为1的和为0的两种情况分开,然后将分开后得到的两组数字利用^=分别进行多次异或运算,最后两组中异或运算的结果就是数组中的两个单身狗了。(由于并不确定给的数组中成对儿元素的具体情况且异或运算时是以二进制形式进行的,相同的肯定能抵消掉,多次异或然后相同抵消剩下的肯定就是那个单身狗了)
int dog1 = 0, dog2 = 0; //初始化两个变量用来存放辨认出的两个单身狗 for (int i = 0; i < sz; i++) { if (arr[i] & 1 << pos) //移位时arr[i]代表的数字会转化成相应的二进制数字,向右移动i个位置后与1做与运算 dog1 ^= arr[i]; else dog2 ^= arr[i]; }
最后展示整体代码如下:
int main() { int arr[] = { 1,2,3,4,5,1,2,3,4,6 }; int sum = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < sz; i++) { sum ^= arr[i]; } int pos; for (int i = 0; i < 32; i++) { if (sum & 1 << i) { pos = i; break; } } int dog1 = 0, dog2 = 0; for (int i = 0; i < sz; i++) { if (arr[i] & 1 << pos) dog1 ^= arr[i]; else dog2 ^= arr[i]; } printf("第一个单身狗%d\n 第二个单身狗%d", dog1, dog2); return 0; }
!!!切记思考时与运算和异或运算的运算规则别搞混了!!!
小补充:
解决完找两个单身狗的问题,我们现在来个初阶的找单身狗问题:找到数组中的一个单身狗。
具体代码也很简单,请大家自行理解吧~
int main() { int arr[] = { 1,2,2,3,3,1,4 }; int dog = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < sz; i++) { dog ^= arr[i]; } printf("单身狗 是%d", dog); return 0;