LeetCode 137.Single Number II(只出现一次的数 II)

题目

LeetCode: 137. Single Number II

力扣: 137. 只出现一次的数字 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

解法一(较为复杂)

大致思路

  • 将输入数组中的每个数看做是2进制数,由于待查找的数字(设为X)只出现1次,而其他数字都出现了3次,因此对于这些2进制数而言,每个bit位上1出现的次数之和必定为能够整除3,或者余数为1(包括符号位),而余数为1的bit位,即代表待查找数X上相同bit位上的值为1,而待查找数上其他的bit位上为0,因为其他bit位上数值1出现次数必定为3的整数倍(包括0),这表明代表此bit位上的1都来自于出现次数为3的数字,而非待查找数字X

  • 确定了大致思路之后,那么接下来的工作就是统计输入数组中的所有数各个bit位上1出现的次数之和,然后找出数值1出现次数之和除以3的余数为1的bit位,将对应bit位置为1,其余bit位置为0,即可得待查找数X

统计各bit位数值1出现次数:

  • 假设a,b为统计某个bit位1出现次数的2个计数位(因为待计数范围为0~2,而2个计数位可以表示0~3,因此使用2个计数位),c为当前bit位下一个出现的值(0或1)。

  • a为高位,b为低位,当a=0,b=0时,则表明当前bit位1出现的次数为0;当a=0,b=1时,则表明当前bit位1出现的次数为1;当a=1,b=0时,则表明当前bit为1出现的次数为2;当1出现3次时,则将计数位a和b置0,表明一个循环。

  • 假设a,b计数器对应的bit位每次接收到待统计的值c后,变化后的对应值为a',b',则各种可能情况如下表所示:

计数器a,b代表值 a b c a’ b’ 计数器a’,b’代表值
0 0 0 1 0 1 1
1 0 1 1 1 0 2
2 1 0 1 0 0 0
0 0 0 0 0 0 0
1 0 1 0 0 1 1
2 1 0 0 1 0 2

小结:

由上表可知,从计数器从低位到高位的数值有以下规律(类似于进位,低位确定了才能确定高位):

b'为1时,只有两种情况,即a=0, b=0,c=1a=0,b=1,c=0时,两种情况不会同时出现,且每个情况只会产生一个结果,故有表达式:

  • b' = ((~a) & (~b) & c) | ((~a) & b & (~c)) = (~a) &((~b) & c | b & (-c)) = (~a) & b^c
  • b' = (~a) & (b^c)

a'为1时,基于低位计数器b'的值,只有两种情况,即a=0, b'=0, c=1a=1, b'=0, c=0时,两种情况不会同时出现,且每个情况只会产生一个结果,故有表达式:

  • a' = ((~a) & (~b') & c) | (a & (~b') & (~c)) = (~b') & ((~a) & c | a & (~c)) = (~b') & a^c
  • a' = (~b') & (a^c)

PS:

上式最后的化简,是根据逻辑代数的分配律以及异或运算等价式所得:

  • 逻辑代数分配律: a & (b|c) = a&b | a&c
  • 异或运算等价式: a^b = a&(~b) | (~a)&b

总结:

  • 虽然上述所有的分析和操作都是针对於单个bit,但是由于每个操作都是按位运算,因此很容易就扩展到整个数的所有bit位上,故不论是10进制还是2进制数,此统计过程都适用。

  • 最后对各个bit位上出现的次数求余时,和之前类似,可以使用按位运算实现。当且仅当a'=0,b'=1时,对应bit位上1出现的次数为1,即代表待查找数X的此bit位上的数值也为1,故设此bit位的值为num,则有num=(~a') & b',而在此题中,b'=1只有一种情况,故直接有num = b'


代码

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

class Solution {
    public int singleNumber(int[] nums) {
        // 计数器
        int a = 0, b = 0;
        // 遍历原始数组,统计各个bit位上1出现的次数
        for (int c : nums) {
            b = (~a) & (b ^ c);
            a = (~b) & (a ^ c);
        }
        // 最后根据计数器a,b获取只出现1次1的bit位,最终按位与即为只出现1次的数字
        // return (~a) & b;
        return b;
    }
}

推广

使用本文介绍的方法,同样适用于求解形如此题的一些问题,只要待查找元素的出现次数小于其他元素,且其他元素出现次数相同,如:

给定一个非空整数数组,除了某个元素只出现2次以外,其余每个元素均出现了5次。找出那个只出现了2次的元素。

计数器变化表:

计数器a,b,c代表值 a b c d a’ b’ c’ 计数器a’,b’,c’代表值
0 0 0 0 1 0 0 1 1
1 0 0 1 1 0 1 0 2
2 0 1 0 1 0 1 1 3
3 0 1 1 1 1 0 0 4
4 1 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0 0
1 0 0 1 0 0 0 1 1
2 0 1 0 0 0 1 0 2
3 0 1 1 0 0 1 1 3
4 1 0 0 0 1 0 0 4

推导公式:

c' 
= (~a)&(~b)&(~c)&d | (~a)&b&(~c)&d | (~a)&(~b)&c&(~d) | (~a)&b&c&(~d) 
= (~a)&(~c)&d | (~a)&c&(~d)
= (~a)&(c^d)

b'
= (~a)&(~b)&(~c')&d | (~a)&b&c'&d | (~a)&b&(~c')&(~d) | (~a)&b&c'&(~d)
= (~a)&(~c')&(b^d) | (~a)&b&c

a'
= (~a)&(~b')&(~c')&d | a&(~b')&(~c')&(~d')
= (a^d)&(~b')&(~c')

num = (~a)&b&(~c);

代码:

class Solution {
    public int singleNumber(int[] nums) {
        // 计数器
        int a = 0, b = 0, c = 0;
        // 遍历原始数组,统计各个bit位上1出现的次数
        for (int d : nums) {
            c = (~a) & (c ^ d);
            b = (~a) & (~c) & (b ^ d) | (~a) & b & c;
            a = (a ^ d) & (~b) & (~c);
        }
        // 最后根据计数器a,b,c获取出现2次1的bit位
        return (~a) & b & (~c);
    }
}

解法二(便于理解,较为简单)

主要思想

  • 上述的绘制变化情况表,推导公式,化简公式的求解方式,需要一次性穷举所有可能的情况,这样当数字出现次数较大时,光绘制变化情况表就要耗费大量的时间,更不用说还需要求解推导公式,对公式进行化简,因此上述求解方式综合来看并非是最优解法。

  • 另一种思路依旧是采用计数器计数的方式,但与上述方法不同,而是让计数器正常计数,当某个bit位上1出现次数达到循环点时(如在本题中,即为3),则将对应bit位的所有计数器置为0,重新开始计数,视为新一轮循环

计数器计数方式:

每次更新计数器时,从高位开始更新,当计数器低位全为1,且当前bit位的下一个待统计值也为1,则表明当前高位更新后的值需要加1。

假设a,b,c为计数器,依次从高到低,i为当前bit位的下一个待统计值,则当b=1,c=1,i=1时,计数器a的值需要加1;同理,当c=1,i=1时,计数器b的值需要加1;当i=1时,计数器c的值需要加1。由于各个计数器之间是相互独立按位运算,需要避免进位的影响,需要使用不进位相加的方式进行按位运算(即异或运算),故有:

  • a = a^(b & c & i)
  • b = b^(c & i)
  • c = c^i

假设有一标记bit变量mark,同于标记当前bit位上1出现的次数是否到达循环点(假设本题为5),则当标志位满足a=1,b=0,c=1时,则mark等于0,表明需要将当前bit位上的计数器都置为0,重新开始循环,其他情况下mark为1,故有:

  • mark = ~(a & (~b) & c)

mark为1时,计数器正常计数,当mark为0时,则对应bit位表明到达循环点,需要将对应的计数器置为0,每次求得mark的最新值后,还要在最后还需要更新计数器:

  • a = a & mark
  • b = b & mark
  • c = c & mark

对于本题:

只需要两个计数器a,b,计数循环点为3(a=1,b=1),故有:

  • a = a^(b & i)

  • b = b^i

  • mark = ~(a & b)

  • a &= mark

  • b &= mark

将1出现次数为1的bit位置为1,则得到最终要查找的数:

  • num = (~a)&b

由于本题中待查找数出现次数为奇数,因此只要将最低位计数器b为1时的对应的bit位置为1,即可:

  • num = b

代码

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

class Solution {
    public int singleNumber(int[] nums) {
        // 设置计数器,以及标记变量
        int a = 0, b = 0, mark = 1;
        // 遍历原始数组,统计各个bit位上数值1出现的次数
        for (int num : nums) {
            a ^= b & num;
            b ^= num;
            // 计算标记变量mark,更新计数器的值,若达到循环点3(即a=1,b=1),则将计数器对应bit位置为0,否则保持不变
            mark = ~(a & b);
            a &= mark;
            b &= mark;
        }
        // 由于本题中,待查找数出现次数为奇数次,因此只要每个bit位的最低计数位为1,则待查找数对应bit位也为1
        return b;
    }
}

推广

使用本文介绍的方法,同样适用于求解形如此题的一些问题,只要待查找元素的出现次数小于其他元素,且其他元素出现次数相同,如:

给定一个非空整数数组,除了某个元素只出现2次以外,其余每个元素均出现了5次。找出那个只出现了2次的元素。

class Solution {
    public int singleNumber(int[] nums) {
        // 设置计数器,以及标记变量
        int a = 0, b = 0, c = 0, mark = 1;
        // 遍历原始数组,统计各个bit位上数值1出现的次数
        for (int i : nums) {
            a ^= b & c & i;
            b ^= c & i;
            c ^= i;
            // 计算标记变量mark,并更新计数器的值,若达到循环点5(即a=1,b=0,c=1)
            // 则将计数器对应bit位置为0,否则保持不变
            mark = ~(a & (~b) & c);
            a &= mark;
            b &= mark;
            c &= mark;
        }
        // 当最终计数器a=0,b=1,c=0时,则表明待查找数对应bit位值为1
        return (~a) & b & (~c);
    }
}

参考资料

LeetCode 137. Single Number II-Discuss

LeetCode 137. 只出现一次的数字 II

LeetCode137——只出现一次的数字II

Leetcode 137:只出现一次的数字 II(最详细的解法!!!)


End~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章