【中篇启航!】牛客剑指offer JZ56 数组中只出现一次的两个数字
一、题目描述
原题连接: JZ56 数组中只出现一次的两个数字
题目描述:
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
数据范围:2 <= n <= 1000,数组中的每个数的大小0 < val <= 1000000
要求:空间复杂度O(1),时间复杂度O(n)
示例1
输入:[1,4,1,6]
返回值:[4,6]
说明:返回的结果中较小的数排在前面
示例2
输入:[1,2,3,3,2,9]
返回值:[1,9]
二、解题
1、方法1——数组模拟哈希表
1.1、思路分析
由于题目中给出的元素最大为1000000,所以我们可以先创建一个长度为1000001的数组fre,然后对数组array进行遍历,每遍历到一个元素就执行fre[array[i]]++,记录array数组中每个元素出现的频数,然后再次遍历一遍数组array,当fre[array[i]] == 1时,将array[i]写入一个数组(我们可以直接将这两个数写到array[0]和array[1]。
因为这个方法点儿像是在使用哈希表,所以我称它为数组模拟哈希表。
1.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
// 写一个用于比较整型数据的函数
int cmp_int(const void* p1, const void* p2) {
return *((int*)p1) - *((int*)p2);
}
int* FindNumsAppearOnce1(int* array, int arrayLen, int* returnSize) {
assert(array && returnSize);
*returnSize = 0;
// 先对数组进行排序
int fre[1000001] = { 0 };
int i = 0;
for (i = 0; i < arrayLen; i++) {
fre[array[i]]++;
}
for (i = 0; i < arrayLen; i++) {
if (1 == fre[array[i]]) {
array[*returnSize] = array[i];
*returnSize += 1;
}
}
// 对array的前两个元素进行排序
qsort(array, 2, sizeof(array[0]), cmp_int);
return array;
}
时间复杂度:O(n),n为数组元素个数,我们需要遍历两遍次数组。排序的个数是固定的2
空间复杂度:O(1),我们只需要用到常数级的额外空间
2、方法2——排序
2.1、思路分析
我们知道一个数组在排序之后相同的元素一定是紧挨在一起的,例如:
所以我们可以在排序之后利用两个指针left和right遍历数组,left从左向右遍历,当出现array[left] != array[left + 1]时,将array[left]写入一个数组,right从右向左遍历,当出现array[right] != array[right - 1]时,将array[right]写入一个数组,例如:
如果相等,left和right在下一次遍历时,都要跳过一个元素(即left = left + 2, right = right - 2),例如:
2.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
int* FindNumsAppearOnce2(int* array, int arrayLen, int* returnSize) {
assert(array && returnSize);
*returnSize = 0;
// 先对数组进行排序
qsort(array, arrayLen, sizeof(array[0]), cmp_int);
int left = 0;
int right = arrayLen - 1;
while (left <= right) {
if (array[left] != array[left + 1]) {
array[*returnSize] = array[left];
*returnSize += 1;
left++;
}
else {
left += 2;
}
if (left < right) { // 有可能上面已经出现了left > right
if (array[right] != array[right - 1]) {
array[*returnSize] = array[right];
*returnSize += 1;
right--;
}
else {
right -= 2;
}
}
}
qsort(array, 2, sizeof(array[0]), cmp_int);
return array;
}
时间复杂度:O(n),n为数组元素个数
空间复杂度:O(1),我们只需要用到常数级的额外空间
3、方法3——分组异或运算
3.1、思路分析
我们都知道异或运算有以下规律:
a ^ a = 0;
a ^ 0 = a;
那么,假设数组中那两个只出现一次的数分别是m或n,如果我们将数组中所有的数都异或起来,得到的结果是不是就等于m ^ n呢?
但是光得到m ^ n好像也分析不出m和n到底是什么啊?
但若是我们能把n和m分成两组,n所在的组中只有n是只出现一次的其余都出现两次,m所在的组中也是只有m只出现一次,其余都出现了两次。然后对这两组分别进行全异或,是不是就得到了n和m具体是几了呢?
那么怎么将数组中的数分成两组且一组含有n一组含有m呢?
加下来我就给大家推演一下:
如果要我们分辨一个数是奇数还是偶数,熟悉二进制的我们肯定能想到,奇数的二进制序列的最低位肯定是1,而偶数的肯定是0。
那么我们可以将一个数与1按位与运算,如果得到的结果为1,那说明这个数是一个奇数,如果是0,那这个数就是一个偶数。
那这跟我们要将n和m分成两组有什么关系呢?
别急,请让我给大家举个例子,例如8^5 和 4 ^ 9(为了方便,我这里只写int的后八位):
有没有发现,只要是奇数和偶数相异或,得出的结果无论前面的序列如何不同,但最低位的1一定是相同的。
我们也可以这样理解:对于奇数和偶数只要找到它们二进制序列的第一个不同的位(从右往左),就能将它们以奇偶性”的不同分成两组。
那我们把思维扩展开来,对于任意两个数,是不是都可以找到它们二进制序列的第一个不同的位,就可以将它们以“某种性质”的不同,从而将他们分为两组呢?
当然可以,只是我们并不需要关心那“某种性质”到底是哪一种性质,这里我们需要做的只是分组而已。
3.2、代码实现
有了以上思路,那我们写起代码来也就水到渠成了:
int* FindNumsAppearOnce(int* array, int arrayLen, int* returnSize) {
assert(array && returnSize);
// 先异或数组中所有的数,得到n ^ m
int i = 0;
int n_m = 0;
for (i = 0; i < arrayLen; i++) {
n_m ^= array[i];
}
int div = 0; // 那个用于分组的数
// 再找出第一个不同的位
for (i = 0; i < 32; i++) {
if (((n_m >> i) & 1) == 1) {
div = 1 << i;
break;
}
}
// 取得n和m
int n = 0;
int m = 0;
for (i = 0; i < arrayLen; i++) {
if ((array[i] & div) == 0) {
n ^= array[i];
} else { // 另一种情况就是得到div本身
m ^= array[i];
}
}
// 将n和m写入array的前两个位置
*returnSize = 0;
array[*returnSize] = n;
*returnSize += 1;
array[*returnSize] = m;
*returnSize += 1;
qsort(array, 2, sizeof(array[0]), cmp_int);
return array;
}
时间复杂度:O(n),n为数组元素个数,我们至始至终都只是在一层循环中遍历数组。所以时间复杂度为O(n)。
空间复杂度:O(1),我们只需要用到常数级的额外空间。