最近做了Leetcode上有三道相似的关于位运算的题,记录一下思路。
做了第一道比较简单的题之后再做另外两道时,虽然知道要使用异或,但就是做不出来。
这三道题常规解法都不难,但如果你想真正达到题目作者的要求就有难度,难就难在你需要用O(n)的时间复杂度以及O(1)的空间复杂度。重点在于O(1)的空间复杂度。
136.Single Number
Given a non-empty 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?
Example 1:
Input: [2,2,1]
Output: 1
题目就是给你一个序列,这个序列中只有一个数字只出现一次,其他数字都是出现两次,要你找出那个只出现过一次的数字。
思路:
用异或解决问题。如果本来不知道异或的特殊性质那基本想不到可以这样去解题了。
①a^a=0:一个数和它自身异或的结果等于0,因为异或就是要判断两个数的每一位是否相等,相等则结果为1不等则结果为0。
②b^0=b:一个数和0异或会得到它本身。
③a^a^b=a^b^a:即异或运算满足交换律。有位大佬跟我说过,异或可以看作是一种无进位的加法,所以异或跟普通加法一样也满足交换律。
▲结合上述三点,就有a^a^b=a^b^a=b^a^a=b。那继而就有a^c^B^a^d^d^c=B...
所以题解为:
class Solution {
public:
int singleNumber(vector<int>& nums) {
for(int i=0;i<nums.size()-1;++i)
nums[i+1]^=nums[i];
return nums[nums.size()-1];
}
};
137.Single Number Ⅱ
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
题意就是一个序列中只有一个数字只出现一次,其他数字都出现了三次,要你找出那个只出现一次的数。
思路:
通过上一题我们学到了异或的特殊性质,我们知道a^b^a=b,所以a^a^a=a,因此要上一题的方法将序列中所有数字异或的话我们最终得到的是序列中出现过的数字的异或和,即如果序列中包含了[a,b,c,d]这四个不同的数,则最后异或的结果就是a^b^c^d。
而我们的目的是得到a,所以我们可以尝试寻找一种计算/计数方法(记为(^)),让a(^)a(^)a=0,那这样我们就能通过对序列中所有数字用(^)方法计算最终得到我们要找我数字。
上一题实际上我们是用一个变量来计数,设该变量为D,即当某个数出现一次时,D=该数,当某个数出现两次时,D=0,当某个数出现三次时D又置为该数字。
常规是异或是:当某个数出现一次时,D=该数,当某个数出现两次时,D=0;现在为了满足我们的构想,我们需要使得当某个数出现三次时D仍然为0。为了实现这种构想,我们再使用一个变量作为辅助。
a | b | |
初始时 | 0 | 0 |
X出现一次时 | X | 0 |
X出现两次时 | 0 | X |
X出现三次时 | 0 | 0 |
如上表,a是我们的主计数变量,b是辅助计数变量,我们“让b比a慢一步计数”。
为了实现上表的功能,我们尝试出a,b的计算公式:
a = (a^X)&(~b);b=(a^X)&(~a);
即a与X异或之后的结果再与b取反后的结果相与。也就是我们上述定义的运算(^)
这样我们就确保了对任意一个数,当它出现一次时,a=x,b=0,当它出现两次时a=0,b=x,当它出现三次时a=0,b=0。
所以可以这么说,我们用两位来对一个数字进行计数,这种方法也可以用于求解这样的问题:一个序列中只有一个数只出现了两次,其他数都出现三次,要求找出那个出现两次的数。如果是这样的问题,那么我们使用同样的计数方法就可以知道,我们要找的数最后会被b记录下来。
所以题解为:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int a=0,b=0;
for(int num:nums)
{
a=(a^num)&(~b);
b=(b^num)&(~a);
}
return a;
}
};
还有另一种万金油思路,由我的室友代码王提供:
对每个数字的每一位做模3的加法。这样时间复杂度是O(32*n+32),空间复杂度是O(32) .
260.Single Number Ⅲ
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.
Example:
Input:[1,2,1,3,2,5]
Output:[3,5]
Note:
- The order of the result is not important. So in the above example,
[5, 3]
is also correct. - Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity?
题意就是一个序列中有两个数只出现过一次,其他数都出现了两次,要你找出这两个数。
这题比第一题多了一个要找的数,假设我们要找的两个数是a和b,使用第一题的做法我们最后会得到a^b。解题的切入点就是这个a^b。
我们要认识到异或的含义,就是看两个数哪一位是相同的哪一位是不同的,通过a^b的结果我们可以知道a和b哪一位不同,假设不同的位是第i位,那么a和b这两个数中必有一个数的第i位是1而另一个数的第i位是0。进而我们可以通过这一个不同的位将a和b区分开,甚至将原序列分为两个不重叠的子序列A和B,其中A的所有数字的第i位都为1,B的所有序列的第i位都为0,这样我们就把这个问题划分为我们解决的第一个single number的问题了。
还有一个问题没解决,就是我们要怎么找到a和b不同的一位,也就是找到a^b中的某一个1。比较容易找的就是找a^b最右边的那个1。
有这样一个计算公式 rightone(最右一个1)=(x&(x-1))^x。这里利用了x-1肯定会改变x最右边的那个1的值。
假设最右一个1是数字x的第i位,那么x-1与x相比,它们的第i位左边的位都一样,而第i位及其右边的位都相反。让x&(x-1),得到的结果是第i位左边的位的值不变,第i位及其右边的位的值都变为0。接着(x&(x-1))^x,因为异或的两个操作数的第i位的左边一样,所以异或结果是0;而左操作数的第i位及其右边全为0,右操作数的第i位为1,其右边的数字为0,所以异或后得到的数字将会是0...010..0,即一个只有第i位为1的数字。
好了,我们就利用得到的这个数来划分原序列。
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int axorb=0;//xor
for(int num:nums)
axorb^=num;
axorb^=(axorb&(axorb-1));//最低一位为1是哪一位
int a=0,b=0;
for(int num:nums)
{
if(axorb&num)
a^=num;
else
b^=num;
}
return {a,b};
}
};
小结:
有关位运算的骚操作一般都跟异或有关;
记住异或的特殊性质,以及异或满足交换律;
可以用异或的思想来计数,根据计数上限的不同采用不同数量的变量。