问题来源:Single Number II

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

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

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

int singleNumberII(int* A,int len)
{
int count[32],result=0;
memset(count,0,sizeof(int)*32); for (int i=0;i<len;i++)
{
for (int j=0;j<32;j++)
{
count[j]+=A[i]>>j&0x1;
}
} for (int i=0;i<32;i++)
{
result|=(count[i]%3<<i);
} return result;
}

很多人对上面的代码会有一个疑问:上面的代码分配了一个长度为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值。新的代码如下:

int singleNumberII(int* A,int len)
{
int low = 0, high = 0;
for(int i = 0; i < len; i++){
int temp_low = (low ^ A[i]) & ~high;
high =(high&~low&~A[i])|(~high&low&A[i]);
low=temp_low;
}
return low;
}

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

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会简化公式,同时也无需引入局部变量,最终代码如下:

int singleNumberII(int* A,int len)
{
int low = 0, high = 0;
for(int i = 0; i < len; i++){
low = (low ^ A[i]) & ~high;
high = (high ^ A[i]) & ~low;
}
return low;
}

上面的代码是不是非常简洁和优雅!背后其实有非常坚实的理论基础。采用真值表的方法不光优雅,还具有非常好的扩展性。假设问题改为:只有一个数出现两次,其余出现三次,我们只需要返回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. JavaScript中内存使用规则--堆和栈

    堆和栈都是运行时内存中分配的一个数据区,因此也被称为堆区和栈区,但二者存储的数据类型和处理速度不同.堆(heap)用于复杂数据类型(引用类型)分配空间,例如数组对象.object对象:它是运行时动态分 ...

  2. Terminal,git,vim常用命令整理以及删除本地git仓库

    Terminal常用命令 目录操作 ll 打开目录列表 ls -a 打开目录下所有文件列表 cd 切换到某目录 mkdir 创建目录 rmdir 删除空目录 rm -rf 目录名字 强制删除目录 mv ...

  3. java8接口定义增强

    java1.7之前,接口中只允许有全局常量和抽象方法,而1.8之后允许在接口中扩充default修饰的普通方法和static修饰的静态方法 其目的是在修改接口中方法的时候,子类就不必去一一修改 pac ...

  4. [MongoDB教程] 1.简介

    MongoDB (名称来自「humongous (巨大无比的)」), 是一个可扩展的高性能,开源,模式自由,面向文档的NoSQL,基于 分布式 文件存储,由 C++ 语言编写,设计之初旨在为 WEB ...

  5. NC的开发模型

    2018-04-1622:35:12 NC的开发模型 系统前端为:客户UI代码,UI代码继承ToftPanel,UI代码通过调用远程组件和服务端进行交互,中间传递的数据模型为VO,远程组件调用底层的业 ...

  6. [SCOI 2005]王室联邦

    Description “余”人国的国王想重新编制他的国家.他想把他的国家划分成若干个省,每个省都由他们王室联邦的一个成员来管理.他的国家有n个城市,编号为1..n.一些城市之间有道路相连,任意两个不 ...

  7. [Codeforces 864F]Cities Excursions

    Description There are n cities in Berland. Some pairs of them are connected with m directed roads. O ...

  8. [SDOI2009]HH的项链

    题目描述 HH 有一串由各种漂亮的贝壳组成的项链.HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义.HH 不断地收集新的贝壳,因此,他的项链变得越来越长. ...

  9. ●BZOJ 4278 [ONTAK2015]Tasowanie

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=4278 题解: 把两个串连接起来,用一个大数连接(必须要用大数).倍增算法求出后缀排名.然后两 ...

  10. ubuntu 安装 WPS for Linux(ubuntu)字体配置(字体缺失解决办法)及卸载libreoffice

    从官网下载安装wps for Linux sudo dpkg -i wps-office_10.1.0.5672~a21_amd64.deb 启动WPS for Linux后,出现提示"系统 ...