137. 只出现一次的数字 II
给你一个整数数组
nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。
示例 1:
输入:nums = [2,2,3,2] 输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,99] 输出:99
提示:
1 <= nums.length <= 3 * 10(4)
-2(31) <= nums[i] <= 2(31)1
nums
中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次
我们主要关注一个数的二进制数。如果某个元素出现了三次,那么这个元素的二进制数也将出现三次。
如果我们把所有元素的二进制数各位都累加起来,比如说二进制数第0位,一共有多少个1,第二位有多少个1,以此类推。
再依次对3取余,只出现一次的二进制数最终会得到1,出现了三次的二进制数会得到0。
再依次把对应的二进制数移到一个整数上即可。
class Solution {
public:int singleNumber(vector<int>& nums) {vector<int> sum(32);for (int i = 0; i < nums.size(); i++)for (int j = 0; j < 32; j++)sum[j] = (sum[j] + ((nums[i] >> j) & 1)) % 3;int ret = 0;for (int i = 0; i < sum.size(); i++)if (sum[i] == 1)ret |= 1 << i;return ret;}
};
vector<int> sum(32);
sum(32)
初始化了一个大小为32的向量,所有元素默认为0,用于统计每个位上的1的个数。
接下来,使用两层循环遍历数组nums
中的每个元素,以及这些元素的每一位。外层循环遍历数组中的每个元素,内层循环遍历这个元素二进制的每一位。
for (int i = 0; i < nums.size(); i++)
for (int j = 0; j < 32; j++)
nums.size()
获取数组nums
的大小,即元素的数量。
for (int j = 0; j < 32; j++)
则是对每个元素二进制的每一位进行遍历。
在内层循环内部,使用位运算来检查每一位上的值(0或1),并更新sum
向量。
sum[j] = (sum[j] + ((nums[i] >> j) & 1)) % 3;
(nums[i] >> j) & 1
计算nums[i]
的第j
位是0还是1。sum[j] + ...
将这个值加到sum[j]
上,% 3
确保如果同一位上的1出现了三次,就重置为0。
接下来,定义了一个名为ret
的整型变量,并初始化为0。这个变量用于存储最终结果。
int ret = 0;
再次遍历sum
向量,这次是为了重构出只出现一次的那个数。如果sum[i]
等于1,意味着该位在只出现一次的数字中是1,所以将这一位设置为1。
for (int i = 0; i < sum.size(); i++)
if (sum[i] == 1)
ret |= 1 << i;
if (sum[i] == 1)
检查每位上的计数是否为1,如果是,则通过ret |= 1 << i;
将结果中的对应位设置为1。
这段代码的时间复杂度是O(n),因为它需要遍历数组中的每个元素两次(一次计算每位上1的个数,一次重建结果)。
空间复杂度是O(1),因为使用的额外空间(sum
向量)的大小是固定的,与输入数组的大小无关。
另一种写法:
class Solution {
public:int singleNumber(vector<int>& nums) {int ret = 0;for (int i = 0; i < 32; i++) {int sum = 0;for (auto x : nums)sum += (x >> i) & 1;sum %= 3;if (sum == 1)ret |= 1 << i;}return ret;}
};
在函数内部,首先定义了一个名为ret
的整型变量,并初始化为0。这个变量将用于存储和返回最终的结果。
int ret = 0;
接下来,代码进入一个循环,遍历整数的每一位(从0到31位)。这是因为在32位整数中,每个整数都可以用32位二进制数表示。遍历每一位的意义是维护ret
的每一位。
for (int i = 0; i < 32; i++) {
在这个循环内部,首先定义了一个名为sum
的整型变量,并初始化为0。这个变量用于累加数组nums
中所有元素的第i
位上的值。
int sum = 0;
然后,通过一个范围for循环遍历数组nums
中的每个元素,对每个元素执行位运算,将第i
位上的值加到sum
变量上。
for (auto x : nums)
sum += (x >> i) & 1;
这里,(x >> i) & 1
是位运算的组合。x >> i
将元素x
右移i
位,将第i
位移动到最低位;然后与1进行按位与操作(&
),以获取该位的值(0或1)。
接下来,sum
变量对3取模,这是因为题目说明了每个元素都出现三次除了一个元素之外,所以任何出现三次的位贡献都将被消除。
sum %= 3;
如果sum
的结果是1,这意味着在这个特定的位上,只出现一次的元素贡献了一个1。因此,需要将这个位的贡献加到ret
变量上。
if (sum == 1)
ret |= 1 << i;
这里,1 << i
是将1左移i
位,创建一个在第i
位上有1的数,然后使用按位或操作(|=
),将这个位的贡献合并到ret
中。
最后,函数返回ret
变量,即那个只出现一次的元素的值。
这段代码的时间复杂度是O(n),其中n是数组nums
的长度。这是因为它需要遍历数组两次:一次是外部的32次迭代(对于每个位),另一次是内部循环遍历所有元素。
空间复杂度是O(1),因为使用的额外空间不依赖于输入数组的大小。
面试题 17.19. 消失的两个数字
给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O(N) 时间内只用 O(1) 的空间找到它们吗?
以任意顺序返回这两个数字均可。
示例 1:
输入: [1]输出: [2,3]
示例 2:
输入: [2,3]输出: [1,4]
提示:
nums.length <= 30000
异或操作的性质:
a^0=a
a^a=0
a^b^c=a^(b^c)
n=nums.size()
,n
表示数组的长度,很容易发现没有丢失数字的原数组范围应该是[1,n+2]
。
如果将原数组和现数组全部异或在一起,最后的结果是丢失的两个数字的异或结果。
异或的含义是,相同为0,不同为1。
丢失的两个数字一定是不同的,所以一定有1的存在,假设是第x位,意味着丢失的数字a第x位是1,丢失的数字b第x位是0,这就是划分的标准。
而我们要将这两个数字划分出来,就可以将原数组和现数组划分成两组。一组是第x位是1的一组,另一组是第x位是0的一组。
此时a和b被分为两组,每一组只有一个丢失的数字,分别对两组全部异或在一起,得到的结果就是两个丢失的数字。
class Solution {
public:vector<int> missingTwo(vector<int>& nums) {int n = nums.size();int ret = 0;for (const auto& x : nums)ret ^= x;for (int i = 1; i <= n + 2; i++)ret ^= i;int x = 0;for (int i = 0; i < 32; i++)if (((ret >> i) & 1) == 1) {x = i;break;}int a = 0, b = 0;for (const auto& x1 : nums)if (((x1 >> x) & 1) == 1)a ^= x1;elseb ^= x1;for (int i = 1; i <= n + 2; i++)if (((i >> x) & 1) == 1)a ^= i;elseb ^= i;return {a, b};}
};
int n = nums.size();
int ret = 0;
n
记录了数组nums
的大小。ret
用于累积异或结果。
然后,遍历数组nums
,对每个元素执行异或操作,将结果累积到ret
中。接下来,继续通过异或操作将从1到n+2
的所有数字累积到ret
中。由于异或操作的性质,两次遍历后ret
中存储的就是两个缺失数字的异或结果。
for (const auto& x : nums)
ret ^= x;
for (int i = 1; i <= n + 2; i++)
ret ^= i;
异或操作符^=
用于计算两个数字的异或。
接着,寻找ret
结果中为1的最低位,这个位将用于区分两个缺失数字。找到这个位后,将其索引存储在变量x
中。
int x = 0;
for (int i = 0; i < 32; i++)
if (((ret >> i) & 1) == 1) {
x = i;
break;
}
这段代码通过位移和位与操作找到ret
中第一个为1的位。
然后,定义了两个整型变量a
和b
,初始化为0。这两个变量将分别存储两个缺失数字。通过再次遍历数组nums
和从1到n+2
的所有数字,根据在x
位的值(0或1),将数字分成两组,并分别对每组进行异或操作。这样,最终a
和b
中就分别存储了两个缺失的数字。
int a = 0, b = 0;
for (const auto& x1 : nums)
if (((x1 >> x) & 1) == 1)
a ^= x1;
else
b ^= x1;
for (int i = 1; i <= n + 2; i++)
if (((i >> x) & 1) == 1)
a ^= i;
else
b ^= i;
通过if (((x1 >> x) & 1) == 1)
判断每个数在x
位的值,并根据这个值分组异或,最终找到缺失的两个数字。
最后,函数通过创建一个包含a
和b
的向量来返回这两个缺失的数字。
这段代码的时间复杂度为O(n),因为它需要遍历输入数组两次以及1到n+2
的范围两次。
空间复杂度为O(1),因为除了输入数组外,只使用了常数额外空间。
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!