问题要求
假设一个机器存储一个标号为ID的记录(假设ID是小于10亿的整数),假设每份数据保存两个备份,这样就有两个机器存储了同样的数据。
1. 在某个时间,如果得到一个数据文件ID的列表,是否能够快速找出这个表中仅出现一次的ID?
2. 如果已经知道只有一台机器死机(也就是说只有一个备份丢失)呢?如果有两台机器死机呢?(假设同一个数据的两个备份不会同时丢失)?
分析
对于题目,我们可以转化为,有很多ID,其中只有一个ID出现的次数小于2,其他正常的ID出现次数都等于2,如何找到这个次数为1的ID?
解法一
分析
常规思路来说,一般我们会通过遍历整个数组,通过记录每个ID的出现次数,以此判断次数为1的ID。所以如果数组中有N个元素,那么我们可以知道它的时间复杂度为O(N),空间复杂度为O(N)。
代码
我通过STL中的vector来存储ID,其中的N为“极大值”
// 解法一
// 时间复杂度(N),空间复杂度(N)
void FindOnlyOne(vector<int> array)
{
vector<int> arrayB;
// 默认为0
for(int i = 0; i < N; ++i) {
arrayB.push_back(0);
}
// 进行遍历,记录ID出现的次数
for(vector<int>::iterator i = array.begin(); i != array.end(); ++i) {
arrayB[*i]++;
}
// 输出次数为1的ID
for(int i = 0; i < N; ++i) {
if(arrayB[i] == 1) {
cout << i << " ";
}
}
cout << endl;
}
解法二
分析
在解法一中,我们arrayB的大小为N,而我们题目所说的只需要我们找到仅出现一次的ID,说明只需要记录次数为1的ID值即可。那么,我们可以通过判断如果当前ID出现了2次,那么我们就释放当前ID的内存,通过这样,我们可以极大的减少了内存的使用。
算法的空间赋值度变为O(1)~O(N),时间复杂度为(N)
代码
为了简单,我通过map来存储数据
// 解法二
// 空间复杂度(1-N),时间复杂度(N)
void FindOnlyOne(vector<int> array)
{
map<int, int> map_arry;
int size = array.size();
for(int i = 0; i < size; ++i) {
map_arry[array[i]]++;
if(map_arry[array[i]] == 2) {
map_arry.erase(array[i]);
}
}
for(map<int, int>::iterator i = map_arry.begin(); i != map_arry.end(); ++i) {
cout << i->first << " ";
}
cout << endl;
}
解法三
分析
解法二中的空间复杂度O(1)~O(N),那么我们能不能将空间复杂度稳定在O(1)呢?异或运算可以实现这一可能。
由于X^X=0,X^0=X。因此如果存在两个相同的ID,那么通过异或运算该值变为0。
代码
// 空间复杂度为(1),时间复杂度为(N-1)
// 只能解决一台故障机器,如果存在多台该算法就会失效
void FindOnlyOne(int* a)
{
int t = a[0];
for(int i = 1; i <N; i++) {
t ^= a[i];
}
cout << t << endl;
}
进一步分析
对于第二问,如果存在两台故障机器,那么我们假设这两台故障机器的ID分别为A,B。
假设该ID表为存放的ID为{1,2,5,4,2,1,7,4},我们可以知道令A=5,B=7
通过异或运算(过程如上述代码),我们可以知道t=5^7=2,我们通过二进制来查看以下
5(D) = 00000101(B)
7(D) = 00000111(B)
5 ^ 7 = 00000101 ^ 00000111 = 00000010 = 2
我们可以看到,如果A^B不等于,那么这个异或值的二进制中某一位为1,并且A和B中有且仅有一个数的相同位上也为1。当然你可能不相信这个结论,我们可以通过更多的例子来证明。
3 ^ 2 = 00000011 ^ 00000010 = 00000001
10 ^ 20 = 00001100^ 00010100 = 00011000
因此,我们可以把ID分为两类,一类在这位上为1,一类在这位上为0.
代码
// 时间复杂度(N),空间复杂度(1)
unsigned int FindFirstBitIs1(int num)
{
int indexBit = 0;
while((num & 1) == 0 && indexBit < 32) {
++indexBit;
num >>= 1;
}
return indexBit;
}
bool IsBit1(int num, unsigned int indexBit)
{
num >>= indexBit;
return (num & 1);
}
void FindTheNumber(vector<int> array)
{
int size = array.size();
// 此处排序有没有都没有关系
sort(array.begin(), array.end());
// 记录A和B异或后的值
int a = array[0];
for(int i = 1; i < size; ++i) {
a ^= array[i];
}
// 找到第一个为1的值
unsigned int indexBit = FindFirstBitIs1(a);
int num1 = 0;
int num2 = 0;
for(int j = 0; j < size; ++j) {
if(IsBit1(array[j], indexBit))
num1 ^= array[j];
else
num2 ^= array[j];
}
cout << "num1 = " << num1 << ", " << "num2 = " << num2 << endl;
}
解法4
分析
解法四有一定的局限性,前提是我们要知道原有机器ID的表格。
我们可以通过计算ID总和的差值找到A(如果只有一台机器出故障)或为A+B(如果有两台机器出故障),再通过ID的乘积得到A×B
或者看为
A+B(当只有一台机器出故障的时候B为1)
A×B(当只有一台机器出故障的时候B为1)
代码
// 空间复杂度为(1),时间复杂度为(N)
void findfour(vector<int> array1, vector<int> array2)
{
int size1 = array1.size();
int size2 = array2.size();
int sum1_add = 0;
int sum2_add = 0;
int sum1_mul = 1;
int sum2_mul = 1;
for(int i = 0; i < size1; ++i) {
sum1_add += array1[i];
sum1_mul *= array1[i];
}
for(int i = 0; i < size2; ++i) {
sum2_add += array2[i];
sum2_mul *= array2[i];
}
int dvalue_add = sum1_add - sum2_add;
int dvalue_mul = sum1_mul / sum2_mul;
for(int i = 0; i < dvalue_add; ++i) {
for(int j = 0; j < dvalue_add; ++j) {
if((i * j) == dvalue_mul && (i + j) == dvalue_add) {
cout << "num1 = " << i << ", " << "num2 = " << j << endl;
return;
}
}
}
}
总结
这题是很常见的大数组问题之一。通过简单的异或运算,我们可以简化代码的空间复杂度和编程难度。