戳这里:LeetCode 136: 只出现一次的数字 I
题目要求: 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
具有线性时间的复杂度,因此直接使用双层for循环的方法排除了
不使用额外空间,因此hash表的方法也排除了
这里使用的是位异或的方法
按位异或的特点:两个相同的数字异或得到的是0、一个数字与位全0的数字异或得到的是数字本身、一个数字与位全1的数字异或得到的是每一位取反
例如:
(a ^ a)^(b ^ b) == a ^ b ^ a ^ b == 0
(a ^ 0) == a
(0110 ^ 1111) == 1001
根据题意:所有数字只有一个数字出现一次,其余的数字都出现了两次,由上面例一可知,异或的顺序可以随意交换
题解:定义一个数字 ret = 0、循环所有数字与之异或,最终得到的就是只出现一次的数字
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (int i = 0; i < nums.size(); ++i) {
ret = ret^nums[i];
}
return ret;
}
};
戳这里:LeetCode 137: 只出现一次的数字 II
题目要求:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
解法一: 首先这道题不能使用上一题的解法
第一种方法介绍一种数学方法
所有的数字去重后相加的三倍减去所有数字相加除以二
这一长串公式很好理解、公式表示为:
对于 a b a c a b b :3 * (a+b+c) - (a+b+a+c+a+b+b) = 2 * c 然后除以2得到的就是 c
代码实现:
class Solution {
public:
int singleNumber(vector<int>& nums) {
set<int> s; // 去重
long long sum1 = 0; // 使用long long防止溢出
for (int i = 0; i < nums.size(); ++i) {
sum1 += nums[i];
s.insert(nums[i]);
}
long long sum2 = 0;
auto it = s.begin();
while (it != s.end()) {
sum2 += *it;
++it;
}
int ret = (3*sum2-sum1)>>1;
return ret;
}
};
上面这种方法虽然可以,但是效率是非常低的
那么如何,让我们的算法具有线性时间复杂度,并且不使用额外空间来实现呢?
第二种方法: 位运算方法
举例:5 1 9 5 1 9 5 1 9 4
将它们都转化为二进制观察
十进制 | 二进制 |
---|---|
5 | 0 1 0 1 |
1 | 0 0 0 1 |
9 | 1 0 0 1 |
5 | 0 1 0 1 |
1 | 0 0 0 1 |
9 | 1 0 0 1 |
5 | 0 1 0 1 |
1 | 0 0 0 1 |
9 | 1 0 0 1 |
4 | 1 0 0 0 |
仔细观察发现,每一列 1 的个数一定是3的倍数,如果不是一定是因为只出现一次的数字贡献了1
跟随着这个规律,就可以开始编写代码了
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (int i = 0; i < 32; ++i) {
int count_1 = 0; // 统计第i位1的个数
for (int j = 0; j < nums.size(); ++j) {
if ((nums[j] & (1<<i)) != 0) {
count_1++;
}
}
// 1个数不为三的倍数,说明只出现一次的数字贡献了1
if (count_1 % 3 != 0)
ret |= (1 << i);
}
return ret;
}
};
戳这里:LeetCode 260: 只出现一次的数字 III
题目要求: 给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
这里只介绍异或方法
在只出现一次的数字 I
中我们将所有的元素进行异或得到了结果,这道题有两个只出现一次的数字,因此将所有元素异或之后得到的是两个只出现一次的数字的异或值
例如:1 5 3 1 3 9
只出现一次的数字是 5 9 所有元素异或,得到结果 12
那么我们如何将12重新拆分成为5 9呢?
从二进制来分析,为了便于观察,我直接将元素进行排序后展示(实际并不排序)
十进制 | 二进制 |
---|---|
1 | 0 0 0 1 |
1 | 0 0 0 1 |
3 | 0 0 1 1 |
3 | 0 0 1 1 |
5 | 0 1 0 1 |
9 | 1 0 0 1 |
很抽象,我直接给方法吧!
12 的二进制 1 1 0 0 其中1的位置是5 和 9 不同的位,0的位置是5和9相同的位,利用的就是5和9从低开始第一个不同的位来将12重新拆分为5和9(也就是第三位
)
从表格中观察所有数字的第三位,1 和 3 的第三位都是成对出现,只有5和9的第三位不是成对出现,并且不同(一个是1,另一个是0)
那么我们将所有第三位为1的数字进行异或(除了5之外其他都是成对出现),因此得到的就是5喽(第一题的思想),然后将12和5异或得到9,==> 完毕!
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
if (nums.size() < 2)
return {};
// num是将所有元素的异或值
int num = 0;
for (auto e : nums) {
num ^= e;
}
// pos是从低开始第一个不同的位
int pos = 0;
for (int i = 0; i < 32; ++i) {
if (num & (1 << i)) {
pos = i;
}
}
// ret1和ret2 ⇒ 存储返回结果
// 将所有低pos位为1的数字进行异或得到其中一个结果
// 将之与num异或得到第二个结果
int ret1 = 0;
int ret2 = 0;
for (auto e : nums) {
if (e & (1 << pos)) {
ret1 ^= e;
}
}
ret2 = num^ret1;
return {ret1,ret2};
}
};