找出数组中三个只出现一次的数

一个整型数组 nums 里除 3 个数字之外,其他数字都出现了两次,请写程序找出这3个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)

方法:
受到剑指 Offer 56的启发,我们寻找一种方法将出现一次的3个数,拆成1个和2个分别放在两组中,其他出现两次的数字被分到相同的组中

定理:我们知道,如果任意的三个数的异或值为0,则若其中一个数在任意的n 位上为 1,那么另两个数在第 n 位上必然一个为 1,另一个为 0(否则这三个数的异或值就不会为0),因而当出现 三个数异或值为0 时,可以根据第 n 位为1,可以将这三个数分成两部分

假设这三个不同的数是:A、B、C,则它们的异或值为 X = A ⊕ \oplus B ⊕ \oplus C,X 的值可能为 0(如 3^5^7=1),也可能不为 0(如 3^5^6=0)。当 X 的值不为 0 时,很难找到一种方法,将原来的数组划分为两部分,使得这三个数不都在同一部分。因此,要先对原来的数组进行一次替换:将数组每个数 n u m i num_i numiX 进行异或。这样原来的三个数就变成了:B ⊕ \oplus C、A ⊕ \oplus C、A ⊕ \oplus B,记 a = B ⊕ \oplus Cb = A ⊕ \oplus Cc = A ⊕ \oplus B。新的异或值 x = a ⊕ \oplus b ⊕ \oplus c = 0出现了三个数的异或值为0的情况

由于 A、B、C 互不相等,显然它们间的异或值 abc 都不为0,且互不相等(证:若a等于b,则 0 = a ⊕ \oplus b = (B ⊕ \oplus C) ⊕ \oplus (A ⊕ \oplus C) = A ⊕ \oplus B ,与 A ⊕ \oplus B != 0自相矛盾)

f(n) 为 n 的二进制表示中最低位 1 的位置,则 f(a)、f(b)、f(c)这三个数中有且只有两个数相等(证明:不妨设 f(a)、f(b)、f(c) 中最小的是 f(a) ,根据上面的定理,a ⊕ \oplus b ⊕ \oplus c = 0, 若 f(a) 的值为 k, 表示 a 在第 k 位上为1,则 b、c 在第 k 位上必然是一个为 1,一个为0,不妨设 b 在 k 位为 1 ,则根据 f(n) 的定义以及 f(a) 最小的假设,可得 f(b) = f(a)f(c) > f(a)
因此,新数组的所有元素 n e w n u m i newnum_i newnumi 对应的 f ( n e w n u m i ) f(newnum_i) f(newnumi) 的总异或值等于 f(c)

假设 f(a) = f(b),上面求出了 f(c) = m $\to $ c 在第 m 位上为 1,不妨设 b 在第 m 位也为 1 ,则 a 在第 m 位为 0。根据第 m 位(第f(c)位)是否为 1,可将新数组分为两部分(一部分包含元素 b=A ⊕ \oplus C 和 c=A ⊕ \oplus B 及其它成对元素,另一部分包含元素 a=B ⊕ \oplus C 及其它成对元素),每部分的异或值恰好都是 B ⊕ \oplus C,故可以求出数A(等于B ⊕ \oplus C ⊕ \oplus X = B ⊕ \oplus C ⊕ \oplus (A ⊕ \oplus B ⊕ \oplus C) = A)
最后将数 A 放入原数组,问题转为“只有两个数出现一次”的情况,利用剑指 Offer 56的方法算出另两个数

int f(int n) { // lowbit
    return n&(-n);
}

vector<int> three_unique_number(vector<int>& nums) {
    int n = nums.size();
    int X = 0; // X = A ^ B ^ C
    for(auto i:nums) X^=i;
    // a = A ^ B, b = A ^ C, c = A ^ B
    // x = a ^ b ^ c == 0
    int fx = 0; // fx = f(a) ^ f(b) ^ f(c) = f(c)  ( f(a)、f(b)、f(c) 这三个数中有且只有两个数相等, 设f(a)=f(b))
    for(int i = 0; i < n; ++i) {
        fx ^= f(nums[i] ^ X); // 替换原数组num为新数组newnum, newnum[i] = nums[i] ^ X 
        // 为了节省空间,每次要查看新数组newnum时可以遍历一遍原数组num实时生成新数组
    }
    // 此时fx = f(c)
    int A = X; 
    // 根据新数组的元素第 f(c) 位是否为 1,可将新数组分为两部分, 一部分包含元素 b=A^C 和 c=A^B 及其它成对元素,另一部分包含元素 a=B^C 及其它成对元素
    // 每部分的异或值恰好都是 B ^ C,故可以求出数A(等于B^C^X=B^C^(A^B^C)=A)
    for(int i = 0; i < n; ++i) {
        // nums[i] ^ X 为新数组newnum的元素,为了节省空间,每次要查看新数组newnum时可以遍历一遍原数组num实时生成新数组
        if((nums[i] ^ X) & fx) {
            A ^= nums[i] ^ X;
        }
    }
    // 此时求出了 A
    // 将数 A 放入原数组,问题转为“只有两个数出现一次”的情况,利用剑指offer56题方法singleNumbers算出另两个数 B、C
    int xorsum = X^A ;
	  int div = xorsum & (-xorsum);
    int B = 0, C = 0;
    for(auto i:nums) { 
        if(div & i) { 
            B ^= i;
        } else { 
            C ^= i;
        }
    }
    if(div & A) {
        B ^= A;
    } else {
        C ^= A;
    }
    return vector<int>{A, B, C};
}

int main(vector<int>& nums) {
    vector<int> nums = {2, 2, 4, 4, 6, 6, 21, 21, 23, 521, 666};
    vector<int> ans = three_unique_number(nums);
    for(auto a:ans) {
        cout<<a<<" ";
    }
    cout<<endl;
    // 521 23 666
    return 0;
}

参考:http://www.cppblog.com/flyinghearts/archive/2013/03/21/198695.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值