我们来看看阿里面试中的三道题目,都跟唯一数有关,层层递进,非常有趣。
小学生也能读懂题目的意思,但并不好做。话不多说,早晨起来,手绘一幅。
涛哥手绘
There is only you in my heart
题目一(简单)
在n个整数中,仅有1个整数出现1次,其余的整数都出现了偶数次,求这个仅出现1次的整数。要求时空复杂度尽可能低。
分析1:用记录次数的方法,空间复杂度不过关,无法通过阿里的面试。
分析2:用排序的方法,时间复杂度不过关,自然无法通过阿里的面试。
分析3:采用异或的方法,直接得出结果,可以通过阿里的面试,程序:
- #include <iostream>
- using namespace std;
-
- int main()
- {
- int a[] = {3, 4, 6, 5, 5, 6, 4};
- int n = sizeof(a) / sizeof(a[0]);
- int result = 0;
- for(int i = 0; i < n; i++)
- {
- result ^= a[i];
- }
-
- cout << result << endl; // 3
- return 0;
- }
上述程序的结果是3,其背后的逻辑是:
-
- result = 3 ^ 4 ^ 6 ^ 5 ^ 5 ^ 6 ^ 4
- = 3 ^ (4 ^ 4) ^ (5 ^ 5) ^ (6 ^ 6)
- = 3 ^ 0 ^ 0 ^ 0
- = 3
题目之二(不难)
在n个整数中,有1个奇数仅出现一次,有1个偶数仅出现一次,其余的整数都出现了偶数次,求奇数和偶数的值。要求时空复杂度尽可能低。
分析:计数法和排序法都无法通过阿里面试,考虑用异或法。设所有整数为:3,8,4,6,5,5,6,4,把所有整数分为奇数派和偶数派,如下:
奇数派 | 偶数派 |
3,5,5 | 8,4,6,6,4 |
很显然,可以分别对奇数派和偶数派进行求解,程序如下:
-
- #include <iostream>
- using namespace std;
-
- int main()
- {
- int a[] = {3, 8, 4, 6, 5, 5, 6, 4};
- int n = sizeof(a) / sizeof(a[0]);
- int result1 = 0;
- int result2 = 0;
- for(int i = 0; i < n; i++)
- {
- if (a[i] % 2 != 0) // 奇数
- {
- result1 ^= a[i];
- }
- else // 偶数
- {
- result2 ^= a[i];
- }
-
- }
-
- cout << result1 << endl; // 3
- cout << result2 << endl; // 8
- return 0;
- }
经自测,程序结果正确。结果为3和8.
题目之三(困难)
在n个整数中,有2个不同的整数分别出现1次,其余的整数都出现了偶数次,求仅出现1次的2个整数。要求时空复杂度尽可能低。
分析:这道题的难度更大了。计数法和排序法都无法通过阿里面试,不予考虑。以3,7,4,6,2,2,4,6为例,该怎样把3和7区分开来呢?通过奇偶肯定不行。我们来看下3和7的二进制:
3 | 7 | |
二进制 | 0011 | 0111 |
可以看到,在3和7的二进制中,倒数第三位不同。而且,需要说明的是,由于3和7不同,所以它们的二进制数中,必然存在不同的位。根据这个特征,可以把原来的整数进行划分,分别定义为蓝系和红系,如下:
整数 | 二进制 | 类别 |
3 | 0011 | 蓝系 |
7 | 0111 | 红系 |
4 | 0100 | 红系 |
6 | 0110 | 红系 |
2 | 0010 | 蓝系 |
2 | 0010 | 蓝系 |
4 | 0100 | 红系 |
6 | 0110 | 红系 |
然后,分别对蓝系和红系进行异或,就求出了3和7,程序如下:
- #include <iostream>
- using namespace std;
-
- int getOXR(int a[], int n)
- {
- int result = 0;
- for(int i = 0; i < n; i++)
- {
- result ^= a[i];
- }
-
- return result;
- }
-
- int getMask(int r)
- {
- int mask = 1;
- while((mask & r) == 0)
- {
- mask <<= 1;
- }
-
- return mask;
- }
-
- void getResult(int a[], int n, int &A, int &B)
- {
- int result = getOXR(a, n);
- int mask = getMask(result);
-
- for (int i = 0; i < n; i++)
- {
- if (mask & a[i])
- {
- A ^= a[i];
- }
- }
-
- B = A ^ result;
- }
-
- int main()
- {
- int a[] = {3, 7, 4, 6, 2, 2, 4, 6};
- int n = sizeof(a) / sizeof(a[0]);
- int A = 0, B = 0;
- getResult(a, n, A, B);
- cout << A << endl; // 7
- cout << B << endl; // 3
-
- return 0;
- }
经自测,程序结果正确。结果为7和3.
异或逻辑简介
异或,是一种很巧妙的思维,在实际开发中,也会偶尔用到异或,而且,这类按位运算是非常快的。
我们来看如下电路图的动图,它实现的是一位二进制的异或逻辑,其实就是不带进位的二进制加法。
可见:当D1和D2都亮或者都熄灭时,D3熄灭; 而当D1和D2有且仅有一个亮时,D3亮。具体逻辑如下:
D1 | 操作 | D2 | D3 |
0 | 异或 | 0 | 0 |
0 | 异或 | 1 | 1 |
1 | 异或 | 0 | 1 |
1 | 异或 | 1 | 0 |