《编程之美》-- 快速找出故障机器


问题要求

假设一个机器存储一个标号为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;
            }
        }
    }
}

总结

这题是很常见的大数组问题之一。通过简单的异或运算,我们可以简化代码的空间复杂度和编程难度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值