问题来源:Single Number II

问题描述:给定一个整数数组,除了一个整数出现一次之外,其余的每一个整数均出现三次,请找出这个出现一次的整数。

大家可能很熟悉另一个题目(Single Number):除了一个数出现一次之外,其余的均出现两次,找到出现一次的数。该问题很简单,大家肯定都知道解法:将所有的数异或,最后的结果即是出现一次的数。用到的知识是A^A=0,两个相同的数异或变为0,然后0^B=B,这样即可找到出现一次的数。

新问题有了变化,出现两次变成出现三次,整个问题的解法就不一样了。如何做到时间复杂度O(n),空间复杂度O(1)保持不变呢?最笨的方法就是计数,将每一个整数都看成一个长度位32的数组,然后统计32位中每一位出现1的次数。如果一个数出现3次,则其出现1的位肯定也是3次,这时如果某位出现4次,则意味着出现一次的数在该位也为1。通过分析所有的位,我们即可以找到这个出现一次的数。代码如下:

  1. int singleNumberII(int* A,int len)
  2. {
  3. int count[32],result=0;
  4. memset(count,0,sizeof(int)*32);
  5.  
  6. for (int i=0;i<len;i++)
  7. {
  8. for (int j=0;j<32;j++)
  9. {
  10. count[j]+=A[i]>>j&0x1;
  11. }
  12. }
  13.  
  14. for (int i=0;i<32;i++)
  15. {
  16. result|=(count[i]%3<<i);
  17. }
  18.  
  19. return result;
  20. }

很多人对上面的代码会有一个疑问:上面的代码分配了一个长度为32的数组,这样空间复杂度还算是O(1)吗?答案是肯定的,只要分配的空间是已知的固定值,空间复杂度都是O(1)。另一个例子,统计每个char字符出现的次数分配的长度为128的空间也属于O(1)。

上面的方法可以解决问题,但是显得不够优雅,是否存在一个和原始问题一样优雅的解法呢?答案是肯定的,但是理解起来会比较困难。今天我们就深入剖析一下这种优雅的解法,等你掌握之后就可以很容易地解决一系列的问题。

新解法用到了大学阶段大家都学过的数字逻辑电路知识,莫慌,用到的知识非常浅显,很容易就回忆起来。第一个概念真值表(truth table),是用0和1表示输入和输出之间全部关系的表格,异或的真值表如下:

A B P
0 0 0
0 1 1
1 0 1
1 1 0

其中,A和B是输入,共有四种组合,P是输出。给定一个真值表,我们还需要将其逻辑函数表达式写出来。共有两种方法,最小项推导法和最大项推导法。第二个概念最小项推导法(两个概念都很简单,我们只关注最小项),把输出为1的输入组合写成乘积项的形式,其中取值为1 的输入用原变量表示,取值为0的输入用反变量表示,然后把这些乘积项加起来。例如,上面异或真值表的逻辑函数表达式可以写为:

是不是很简单。有了这两个概念,我们就可以介绍新解法了。

一个32位int型整数可以看成32个独立的位,每一位都可以独立考虑,所以后面的描述都单指一个位。当一个数最多出现两次时,我们可以只用1 bit来描述,但是当一个数最多出现三次时,我们必须要用2 bit来描述。针对该问题,可以用00表示一个数未出现,01表示一个数出现一次,10表示一个数出现两次,当出现三次的时候按理应该是11,但是我们将其重置为00表示该数已经达到上限,肯定不是要找的数可以丢掉。所以给定一个数,其出现次数变化规律为00→01→10→00。针对这个变化规律,我们可以得到一个真值表:

high_bit low_bit input high_bit_output low_bit_output
0 0 0 0 0
0 1 0 0 1
1 0 0 1 0
0 0 1 0 1
0 1 1 1 0
1 0 1 0 0

其中,high_bit表示计数过程中的高位,low_bit是对应的低位,input表示下一个输入,high_bit_output是高位对应的输出,low_bit_output是低位对应的输出。前三行对应输入为0的情况,此时输出不变化;后三行对应输入为1的情况,需要注意的就是最后一行,10加1变成00。输入和输出为11的情况不会出现,未列出。

有了这个真值表,我们就可以针对高低两位利用最小项分别写出逻辑表达式:

最终的结果中low就表示出现一次的整数,因为0次、2次、3次对应的low值都是0。从这里也解释了为什么需要将11重置为00,否则就会出现两种情况low值为1。此外,这里还需要注意一点,两个公式中的输入都是利用旧值计算新值,所以当我们在计算出low之后,不能用该low值计算high值,需要用旧的low值计算high值。新的代码如下:

  1. int singleNumberII(int* A,int len)
  2. {
  3. int low = 0, high = 0;
  4. for(int i = 0; i < len; i++){
  5. int temp_low = (low ^ A[i]) & ~high;
  6. high =(high&~low&~A[i])|(~high&low&A[i]);
  7. low=temp_low;
  8. }
  9. return low;
  10. }

上述代码较最原始的代码优化很多,空间复杂度和时间复杂度都有明显下降,但是利用了一个局部变量,显得非常不美观。这个代码很像交换两个数时的代码,为了交换两个数,常规方法是引入一个局部变量,然后三次赋值操作。为了避免引入局部变量,一种优化是通过三次直接的位运算实现。在此我们也对上面的代码进行类似的优化,将局部变量删除。该怎么优化呢?我要放大招了!

low的计算保持不变,当我们计算完low之后,high的计算公式依赖的是旧的low值,我们设法将依赖旧low值改为依赖新low值。修改方法就是修改真值表,将low的输出重新作为输入,构造新的真值表:

high_bit low_bit input high_bit_output
0 0 0 0
0 1 0 0
1 0 0 1
0 1 1 0
0 0 1 1
1 0 1 0

上述真值表唯一的修改就是把之前真值表最后一列的输出拷贝到第二列中。然后我们针对修改后的真值表求逻辑表达式:

看到没,直接利用low的输出计算high会简化公式,同时也无需引入局部变量,最终代码如下:

  1. int singleNumberII(int* A,int len)
  2. {
  3. int low = 0, high = 0;
  4. for(int i = 0; i < len; i++){
  5. low = (low ^ A[i]) & ~high;
  6. high = (high ^ A[i]) & ~low;
  7. }
  8. return low;
  9. }

上面的代码是不是非常简洁和优雅!背后其实有非常坚实的理论基础。采用真值表的方法不光优雅,还具有非常好的扩展性。假设问题改为:只有一个数出现两次,其余出现三次,我们只需要返回high即可。又假如:有一个数出现一次或者两次,其余出现三次,我们只需要返回low|high即可。此外,不只是出现三次,出现五次、七次也可以用构造真值表的方法来解决,只需要增加输入位数即可。即使是最原始的问题,我们也可以用这种方法解决,只需要构造low和input的真值表即可,你会发现构造的真值表正好就是异或的真值表!

leetcode 之 Single Number II的更多相关文章

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

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

  2. Leetcode 137 Single Number II 仅出现一次的数字

    原题地址https://leetcode.com/problems/single-number-ii/ 题目描述Given an array of integers, every element ap ...

  3. 【题解】【位操作】【Leetcode】Single Number II

    Given an array of integers, every element appears three times except for one. Find that single one. ...

  4. LeetCode 137 Single Number II(仅仅出现一次的数字 II)(*)

    翻译 给定一个整型数组,除了某个元素外其余的均出现了三次. 找出这个元素. 备注: 你的算法应该是线性时间复杂度. 你能够不用额外的空间来实现它吗? 原文 Given an array of inte ...

  5. [LeetCode] 137. Single Number II 单独数 II

    Given a non-empty array of integers, every element appears three times except for one, which appears ...

  6. [LeetCode] 137. Single Number II 单独的数字之二

    Given a non-empty array of integers, every element appears three times except for one, which appears ...

  7. 【Leetcode】 - Single Number II

    Problem Discription: Suppose the array A has n items in which all of the numbers apear 3 times excep ...

  8. 详解LeetCode 137. Single Number II

    Given an array of integers, every element appears three times except for one, which appears exactly ...

  9. 【leetcode】Single Number II (medium) ★ 自己没做出来....

    Given an array of integers, every element appears three times except for one. Find that single one. ...

随机推荐

  1. The first week CorelDRAW 课总结:

    1.这节课学到了什么知识? 答:(1)认识了CorelDRAW X4的工作界面(由标题栏 菜单栏 工具栏 属性栏 工具箱 页面控制栏 状态栏 绘图区和调色板组成): (2)CorelDRAW X4的基 ...

  2. CountDownLatch与thread-join()的区别

    今天学习CountDownLatch这个类,作用感觉和join很像,然后就百度了一下,看了他们之间的区别.所以在此记录一下. 首先来看一下join,在当前线程中,如果调用某个thread的join方法 ...

  3. React Native(十五)——RN中的分享功能

    终于,终于,可以总结自己使用RN时的分享功能了-- 为什么呢?且听我慢慢道来吧: 从刚开始接触React Native(2017年9月中旬)就着手于分享功能,直到自己参与公司的rn项目开发中,再到现在 ...

  4. [LeetCode] Exclusive Time of Functions 函数的独家时间

    Given the running logs of n functions that are executed in a nonpreemptive single threaded CPU, find ...

  5. [LeetCode] Optimal Division 最优分隔

    Given a list of positive integers, the adjacent integers will perform the float division. For exampl ...

  6. 2018.4.16Spring.Net入门学习内容

    三大方面: IoC:Inversion of Control 控制翻转:就是创建对象的权利由开发人员自己控制New,转到了由容器来控制. DI:Dependency InjectionIt is a ...

  7. 使用数据库乐观锁解决高并发秒杀问题,以及如何模拟高并发的场景,CyclicBarrier和CountDownLatch类的用法

    数据库:mysql 数据库的乐观锁:一般通过数据表加version来实现,相对于悲观锁的话,更能省数据库性能,废话不多说,直接看代码 第一步: 建立数据库表: CREATE TABLE `skill_ ...

  8. 初学servlet之使用web.xml配置

    先写两个servlet,之后展示web.xml配置 package app01c;import java.io.IOException;import java.io.PrintWriter;impor ...

  9. 实验吧_who are you?(盲注)

    who are you? 翻翻源码,抓抓包,乱试一通都没有什么结果 题目中提示有ip,立马应该联想到X-Forwarded-For 虽然知道是这个方面的题,但完全不知道从何入手,悄咪咪去翻一下wp 才 ...

  10. ●POJ 1195 Mobile phones

    题链: http://poj.org/problem?id=1195 题解: 二维树状数组 #include<cstdio> #include<cstring> #includ ...