LC 136,137 Single Number I / II 数组中几乎所有数都出现了N次,找出唯一一个出现1次的数

LC 136 Single Number

Given a non-empty array of integers, every element appears twice except for one. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

Example 1:

Input: [2,2,1]
Output: 1

Example 2:

Input: [4,1,2,1,2]
Output: 4

所有数字都出现了两次,只有一个数字出现了一次,找出那个数字。非常经典的面试题,用异或解决。因为两个相同的数异或结果为0,0异或一个数结果为那个数字本身,异或运算具有交换律,根据这三个性质,用0将数组从头到尾异或一遍就得到了出现一次的那个数字。

int singleNumber(vector<int>& nums) {
    int res = 0;
    for (int i = 0; i < nums.size(); i++) {
        res ^= nums[i];
    }
    return res;
}

LC 137 Single Number II

Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one.

Note:

Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

Example 1:

Input: [2,2,3,2]
Output: 3

Example 2:

Input: [0,1,0,1,0,1,99]
Output: 99

所有数字都出现了三次,只有一个数字出现了一次。这个问题不能用异或的方法解决,更准确的说是上一个题目比较凑巧、刚好能借助异或解决。解决这种问题有一个通用的解决思路:假设所有数都出现了N次只有一个数出现了1次,解决方法是记录数字每个位上出现1的数量的和,然后通过一种机制让出现N次的位回归0. 听着很抽象,先看下看这段代码:

int singleNumber(vector<int>& nums) {
   int res = 0;
   for (int i = 0; i < 32; i++) {
       int sum = 0;
       for (int j = 0; j < nums.size(); j++) {
           if (((nums[j] >> i) & 1) == 1) {
               sum += 1;
               sum %= 3;
           }
       }
       if (sum) {
           res |= sum << i;
       }
   }
   return res;
}

外层循环控制数组里每个数字的右移,其实是在提取每个数字的第i位。想象有3个不同的数字abc都出现了3次,对他们的最低位进行计数并mod 3,那么最后的结果一定是0。例如a=001, b=011, c = 010,那么三次a使得sum = 3,三次b使得sum = 6,三次b之后sum依旧为6,6 mod 3 == 0,所以这样sum就过滤掉了出现了三次的数字的位的信息,就可以单纯的记录出现了一次的数字的位的信息。这种方法的复杂度位O(32n),下面介绍一种更简单的方法。

int singleNumber(vector<int>& nums) {
    int ones = 0, twos = 0;
    for(int i = 0; i < nums.size(); i++){
        ones = (ones ^ nums[i]) & ~twos;
        twos = (twos ^ nums[i]) & ~ones;
    }
    return ones;
}

乍一看很难理解他在做什么,但是可以很直观的发现这种方法在复杂度上有很大的提高。在研究代码具体细节之前,先介绍一下思路,事实上思路和上面的mod3的方法很类似,不过用了更Genius的方法对出现过3次的数字进行过滤。由之前的方法我们可以看到,对数字的操作包含了对每个位的操作,他们之间又是没有相互影响的,所以可以通过观察一个位的操作来理解对一个数的操作。记录数字出现的次数除了用sum的方式,还可以用位与位异或的操作。考虑LC136,可以把对数组的异或理解成为通过位异或过滤出现两次的位的数据,例如有001出现了两次,010出现了一次,那么两次001异或就把0、0、1分别过滤掉变成了0、0、0. 这道题的问题在于如何用异或处理大于2的情况,因为异或只能完成0->1->0的循环,因此只有两种状态,只能记录最多出现两次(4,6,8...偶数次)的数据。用两个位就能记录出现3次的数据,但是要如何在两个位的基础上完成mod3的过滤呢?这需要通过自定义状态转换来完成。可以定义00->10->01->00(0->1->2->3->0)的状态转换,这样出现三次的数会被过滤成00,出现1次的数会被记录下来成为10,如果这样我们只需要提取状态机中第一位的1就能获得出现唯一一次的数据的相应位的值。用ones做状态机的第一位(高位),用twos做状态机的第二位(低位)。代码完成这样的判断逻辑:假设nums[i] == 1,ones ^ nums[i] & ~twos得到1,twos和nums[i]先异或得到1,在和~ones相与得到0,完成计数0->1(00->10)。将异或的结果和希望得到状态的结果画一个真值表,就能推断出&~ones的逻辑。

可见LC136和LC137还是有一些关联的,通过LC137可以对位异或计数的思想有更深刻的理解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值