Leetcode Single Number系列
Single number,只出现一次的数字,是很经典的算法问题,leetcode上目前有3道相关题目,分别是136,137和260.本文将分享这三道题的思路。
136 只出现一次的数字
本题是此类问题的原型题,对于刷题比较少的朋友本题也是一道“开脑洞”的题目。
这道题 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
数组的元素只有一个只出现了一次,其余都出现了2次。如果直接暴力求解,能想到2种解法。
第一是将数组排序,这样相同的数就会变到相邻的位置,只需要比较相邻元素是否存在相等元素,如果没有那就是唯一只出现一次的数,这种解法不需要额外空间,时间复杂度为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)。
第二种是哈希表的方法,遍历数组,每个元素放在哈希表中,最后检查每个元素出现次数。这种方法时间复杂度和空间复杂度都是
O
(
N
)
O(N)
O(N)
不过上述两种方法都没有用到“其他元素出现两次”的信息,有没有什么方法能够记录出现次数,累积2次就归零呢?异或运算具有 a ∧ 0 = a , a ∧ a = 0 a \wedge 0 = a, a \wedge a = 0 a∧0=a,a∧a=0的特点,可以做到将出现两次的元素再次归零。
因此本题只需要遍历所有元素,求所有元素异或的结果,最终结果就是只出现一次的数。
C++代码如下:
int singleNumber(vector<int>& nums) {
int ans=0;
for(auto n:nums) ans^=n;
return ans;
}
137 只出现一次的数字 II
本题增加了一些难度,一个数字出现一次,其他数出现了3次,那么就无法直接通过异或求解。
本题同样可以通过排序后比较相邻元素或哈希表的方法暴力求解。
这类问题还有一个思路是按位求解,考虑二进制下的每个元素,将所有元素这一位相加,显然除了只出现一次的数,其他数这一位都被加了3次,因此结果对3取余就是出现一次的数这一位的值。每一位都这么做最后就可以得到这个数。
C++代码如下:
int singleNumber(vector<int> & nums)
{
int len = nums.size(), ans = 0;
for (int i = 0; i < 32; ++i) {
int count = 0;
int mask = 1 << i;
for (int j = 0; j < len; ++j) {
if (nums[j] & mask)count++;
}
if (count % 3) ans |= mask;
}
return ans;
}
但是复杂度仍然较高,我们依然可以通过逻辑运算得到
O
(
N
)
O(N)
O(N)的解法。
由于其他数出现了三次,我们需要累积到3归零,也就是至少能表示出现0(3)次、1次、2次三种状态,那么一个变量就无法做到了,我们可以通过2个变量实现。
对于输入的x,我们通过a, b两个变量通过以下的方式运算:
b = (b ^ x) & (~a);
a = (a ^ x) & (~b);
初始化ab为0,当多次遇到x时变化如下:
a | b | |
---|---|---|
0 | 0 | 0 |
1 | 0 | X |
2 | X | 0 |
3 | 0 | 0 |
这样出现3次的数就会归零,最后b会保存只出现一次的数的信息。
C++代码如下:
int solution::singleNumber(vector<int> & nums)
{
int a = 0, b = 0;
for (auto n : nums) {
b = (b ^ n) & (~a);
a = (a ^ n) & (~b);
}
return b;
}
260 只出现一次的数字 III
本题在136的基础上将只出现一次的数增加为2个,也就是有两个不同的数都只出现了一次,而其他数都出现了2次。
本题仍然可以用异或的运算排除其他数,但结果将是这两个数的异或结果。显然由于两个数不一样,异或结果二进制表示中至少存在一个1,我们找到一个(比如最右边的)1,以此为标准将原数组分为两组,这两个数必然不在同一组。(在这一位上一个是0一个是1,否则异或结果这一位就不是1了)。其他数分在哪组无所谓,因为一定是成对出现,对每一组再异或一次就分别得到这两个数了。
C++代码如下:
vector<int> singleNumber3(vector<int> & nums){
int sum=0;
for(auto n:nums) sum^=n;
sum=sum&(-sum);
int a=0,b=0;
for(auto n:nums){
if(n&sum)a^=n;
else b^=n;
}
return vector<int>{a,b};
}
这里sum=sum&(-sum)是求sum最右边的1(保留这个1而将其它位置0)。原因在于有符号整数补码存储的方式使得互为相反数的两个整数补码是按位取反加一的,对于xxxx1000这个二进制数,取反后为yyyy0111,再加一就是yyyy1000,可见最右的1左边都是按位取反的,做与运算一定会全是0,而1以右全是0,1保持不变,这样按位与就得到00001000保留了最右的1.