【LeetCode】Single Number I & II & III

Posted on 2015-08-18 16:32  Maples7  阅读(1636)  评论(0编辑  收藏  举报

Single Number I :

Given an 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?

 


 

Solution:

解法不少,贴一种:

1 class Solution:
2     # @param {integer[]} nums
3     # @return {integer}
4     def singleNumber(self, nums):
5         ans = nums[0];
6         for i in range(1, len(nums)):
7             ans ^= nums[i]
8         return ans

 

其依据在于:

1、异或运算满足交换律;

2、a ^ a = 0;

3、b ^ 0 = b。

 

这题的关键就在于线性时间内把相同的一对 “消掉”,留下那个 “落单” 的。

异或运算给了这样的快捷的可能。

 

 

Single Number II:

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

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

 


 

 

Solution:

同样,考虑用位运算解题:

在 I 中巧妙地用异或运算解决了把重复的元素的“消除”,只留下“落单”的元素的问题。而在 II 中,除了要找的元素,每个元素都出现 3 次,I 的解法适用于出现偶数次的情况,但对于奇数次已不再适用。

考虑每个数用二进制展开,将各个数横向比较:

对于一个32位(或者64位)的整数,对于这32个位中的某个位而言,如果每个数都出现三次,那么对于所有数在这个位上“1”的个数,一定是 3 的倍数;而反之,如果存在某个数不是出现 3 次(也不是 3 的倍数次,这是题目未讲明处,I 亦同理如此),那么对于它的二进制展开后数为 1 的位而言,对于所有数在这个位上“1”的个数,一定不是 3 的倍数。

所以具体的解决办法是:

用一个 32 长的数组存储对于所有数的二进制展开,每一个位上总共 “1” 的个数和,最后看那些位上 “1” 的个数不是 3 的倍数,那么这一位在 ans 中就是 1。

算法是 O(32n) 的。

 

有一点需要注意的是:

不同的语言中对位运算尤其是符号位的处理是不尽相同的,比如 C++ 中最高位是符号位,如果不是 3 的倍数那么最后的 ans 就是负数,符号位可以和其他位一样处理;但如果是Python,由于其动态类型的特性,当超出数据范围时,会自动转化为更大范围的数据类型,而不会将其作为符号位处理。

 

C++版本:

 1 class Solution {
 2 public:
 3     int singleNumber(int A[], int n) {
 4         int bitnum[32] = {0};
 5         int res=0;
 6         for(int i = 0; i < 32; i++){
 7             for(int j = 0; j < n; j++){
 8                 bitnum[i] += (A[j] >> i) & 1;
 9             }
10             res |= (bitnum[i] % 3) << i;
11         }
12         return res;
13     }
14 };

Python版本:

 1 class Solution:
 2     # @param {integer[]} nums
 3     # @return {integer}
 4     def singleNumber(self, nums):
 5         bitNum = [0] * 32
 6         for i in range(32):
 7             for e in nums:
 8                 bitNum[i] += (e >> i) & 1
 9         ans = 0
10         for i, val in enumerate(bitNum):
11             if i == 31 and val % 3 != 0:
12                 ans = -((1 << i) - ans)
13             else:
14                 ans |= (val % 3) << i
15         return ans

其中对于逻辑上的符号位(第32位),单独判断并处理,如果为 1,则需要转化为对应的负数(对应的负数的绝对值 = 【模】- 不考虑符号位(符号位为0)的正数)。

 

 

Single Number III

Given an array of numbers nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once.

For example:

Given nums = [1, 2, 1, 3, 2, 5], return [3, 5].

Note:

  1. The order of the result is not important. So in the above example, [5, 3] is also correct.
  2. Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity?

 

Credits:
Special thanks to @jianchao.li.fighter for adding this problem and creating all test cases.

 


 

 

Solution:

打怪打到系列第三级,又回到了跟 I 类似的情况,也就是相同的数字是偶数个,不同的是,这里面有两个“怪胎”。

很容易联想到 I 的解法,把所有数异或起来。但是异或之后我们得到的是我们想要的那两个数的异或值,如何把它们从异或中拆分开呢?

 

假设我们要找的这两个数为 a, b, 而 x = a ^ b。

首先,a 肯定不等于 b,那么说明它们的二进制位一定是不完全相同的,所以 x 肯定不为 0。

也就是说,a 与 b 一定存在“某一位”,使得在它们中的某个数中是 0,而在另一个数中是 1,这是他们之间的一个差别。

我们可不可以利用这个差别来把这两个数从 x 中揪出来呢?

是可以的。

 

利用这个差别,我们可以将整个 nums 集合分成两个集合。一个集合中是这 “某一位” 为 0 的在nums中的所有数,假设为集合 A。而另一个集合是这 “某一位” 为 1 的在nums中的所有数。假设 a 的这 “某一位” 是 0 ,b 的 这个“某一位”是1,那么显然 a 在集合 A 中,b 在集合 B 中,这样问题就完全转化成了与 I 一样的两个子问题,于是可以得解。

 

关于具体的代码实现,还有一点说明:

 

我们如何找到这个 “某一位” 呢?理论上,只要是在这一位上 a与b的值不同,都可以合格的成为我们需要找的某一位。既然无更多限制,那我们肯定是找好找的某一位咯。

我们可以用很常规和易懂的方法去找,但一般而言,我们肯定是找最右边(低位)那边符合要求的“某一位”嘛。更进一步说,就是找到 x 中最低位的 1 嘛。那当然我们可以从最低位开始枚举每一位,直到找到我们需要找的那个“某一位”。

 

还有一种更trick利用位运算的办法:找到 x 中最低位的 1,仔细想想,这跟另外一个已经熟知的问题很像。

当我们统计某个数的二进制展开中1的个数的时候,我们使用了一个技巧,即用 n &= n - 1 每次来清除 n 中当前最右边的那个 1。

n-1 是把 n 的最低位的 1 变成 0,并且使更低位的 0 全部变成 1,然后异或一下就把 最低位的 1 及其更低位全部都变成了 0,即达到了“清除最低位的 1 的目的”。

(详见:统计二进制展开中数位1的个数的优化 优化解法1)

在这个地方,我们需要逆向思维,即 保留 最低位的1,并且最好使得其他位 都变成0,这样我直接与 nums 中的每一个数相与,就可以直接将它们分成 A 和 B 两个集合了。

逆向思维会是这样:

n-1 的过程其实是把 最低位 1 和 跟低位 都位反转 (Bit Flipping) 的过程,那我们这里 首先也将 n 的所有位反转得到 n'。

然后我们再把 n'+1。。。

Opps! What did we get? 

我们发现 n'+1 相对于 n 而言,最低位的1及其更低位的0 都没变,而其他位(比 最低位1 更高的位)都反转了。

那此时如果用 n & (n'+1) 得到的便是 除 n 中最低位 1 继续为 1 以外,其他各位都为 0 的数了。

n' 如何求?当然我们可以直接取反。但是联合 n'+1 来看,各个位取反再加一,这不正是计算机中 “负数补码” 的表示方式!

所以我们可以直接用 n &= -n 得到 “除 n 中最低位 1 继续为 1 以外,其他各位都为 0 的数”!

(注意正数的补码的补码是它本身,所以即便 n 本身是负数,-n是正数,但 -n 依然是求 n 的补码。)

完美!

 

代码如下:

 1 class Solution:
 2     # @param {integer[]} nums
 3     # @return {integer[]}
 4     def singleNumber(self, nums):
 5         diff = 0
 6         for e in nums:
 7             diff ^= e
 8         diff &= -diff
 9         ans = [0, 0]
10         for e in nums:
11             if diff & e != 0:
12                 ans[0] ^= e
13             else:
14                 ans[1] ^= e
15         return ans