190. 颠倒二进制位
颠倒给定的 32
位无符号整数的二进制位。
示例 1:
- 输入:
n = 00000010100101000001111010011100
- 输出:
964176192
(二进制表示为00111001011110000010100101000000
)
示例 2:
- 输入:
n = 11111111111111111111111111111101
- 输出:
3221225471
(二进制表示为10111111111111111111111111111111
)
提示:
输入是一个长度为 32
的二进制字符串。
进阶:
如果多次调用这个函数,你将如何优化你的算法?
位运算逐位处理
初始化结果为 0
;遍历 32
次,每次取输入值的最低位并将其放到结果的最高位,然后将输入值右移一位,结果左移一位以便下一次操作。
C++ ACM 模式实现
cpp
#include <iostream>
uint32_t reverseBits(uint32_t n) {
uint32_t ans = 0;
for (int i = 0; i < 32; ++i) {
ans = ans << 1;
ans |= n & 1;
n = n >> 1;
}
return ans;
}
int main() {
std::cout << reverseBits(43261596) << std::endl;
return 0;
}
代码说明
- 初始化
ans
为0
,用于存储颠倒后的二进制位。 - 遍历
32
次,每次处理一个二进制位:- 将
ans
左移一位,为下一个二进制位腾出位置。 - 使用位与操作
n & 1
取出n
的最低位,并将其与ans
进行位或操作,添加到ans
的最低位。 - 将
n
右移一位,以便下次处理下一个二进制位。
- 将
- 最后返回
ans
,即为颠倒后的32
位无符号整数。
时间复杂度
这种方法的时间复杂度为 O(1)
,因为循环次数固定为 32
次,不随输入规模变化。
191. 位1的个数
给定一个正整数 n
,编写一个函数,获取该正整数的二进制形式并返回其二进制表达式中设置位(即值为 1
的位)的个数,这被称为汉明重量。
示例 1:
- 输入:
n = 11
- 输出:
3
- 解释:输入的二进制表示为
1011
,其中有3
个设置位。
示例 2:
- 输入:
n = 128
- 输出:
1
- 解释:输入的二进制表示为
10000000
,其中有1
个设置位。
示例 3:
- 输入:
n = 2147483645
- 输出:
30
- 解释:输入的二进制表示为
1111111111111111111111111111101
,其中有30
个设置位。
提示
1 <= n <= 2^31 - 1
进阶
如果多次调用该函数,如何优化算法?
循环移位
通过循环将数字的每一位移到最低位,然后检查该位是否为 1
。
C++ ACM 模式实现
cpp
#include <iostream>
using namespace std;
int hammingWeight(uint32_t n) {
int count = 0;
while(n){
if(n&1)
count++;
n = n>>1;
}
return count;
}
int main(){
uint32_t n = 00000000000000000000000000001011;
cout<<hammingWeight(n)<<endl;
return 0;
}
代码说明
- 初始化计数器 :
int count = 0;
用于记录设置位的个数。 - 循环处理每一位 :在
while (n)
循环中,每次将n
右移一位,并检查最低位是否为 1。 - 检查最低位 :
count += n & 1;
如果最低位是 1,则增加计数器。 - 右移操作 :
n >>= 1;
将n
右移一位,以便在下一次循环中处理下一个二进制位。 - 返回计数器:循环结束后,返回计数器的值,即为设置位的个数。
时间复杂度
该算法的时间复杂度为 O(1)
,因为无论输入值的大小如何,都需要固定 32
次循环操作(对于 32
位整数)。
231. 2 的幂
给定一个整数 n
,判断该整数是否是 2
的幂次方。如果是,返回 true
;否则,返回 false
。如果存在一个整数 x
使得 n == 2^x
,则认为 n
是 2
的幂次方。
示例 1:
- 输入:
n = 1
- 输出:
true
- 解释:
2^0 = 1
示例 2:
- 输入:
n = 16
- 输出:
true
- 解释:
2^4 = 16
示例 3:
- 输入:
n = 3
- 输出:
false
提示:
-2^31 <= n <= 2^31 - 1
进阶:
能否不使用循环或递归来解决这个问题?
性质
要判断一个整数是否是 2
的幂次方,可以通过检查其二进制表示是否只有一个位是 1
来实现。具体来说,2
的幂次方的二进制形式只有一个 1
,其余位都是 0
。例如,2^0 = 1
的二进制是 1
,2^1 = 2
的二进制是 10
,2^2 = 4
的二进制是 100
,以此类推。
可以使用位运算来高效地解决这个问题。具体来说,可以利用以下性质:
- 如果
n
是2
的幂次方,那么n
的二进制表示中只有一个1
。 - 因此,
n & (n - 1)
的结果应该为 0,因为将n
减去1
后,二进制表示中的1
会变成0
,后面的0
会变成1
。
此外,还需要确保 n
是正数,因为负数和零不可能是 2
的幂次方。
C++ ACM 模式实现
cpp
#include <iostream>
bool isPowerOfTwo(int n) {
if (n <= 0) return false;
return (n & (n - 1)) == 0;
}
int main() {
std::cout << isPowerOfTwo(1) << std::endl;
std::cout << isPowerOfTwo(2) << std::endl;
std::cout << isPowerOfTwo(3) << std::endl;
std::cout << isPowerOfTwo(4) << std::endl;
}
代码说明
- 检查正数 :首先检查
n
是否大于零。如果n
小于或等于零,直接返回false
。 - 位运算 :使用
n & (n - 1)
操作来检查n
是否是2
的幂次方。如果结果为0
,则说明n
是2
的幂次方。
时间复杂度
该算法的时间复杂度为 O(1)
,因为它只涉及常数次的位运算。
338. 比特位的计数
给定一个整数 n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1 的个数,返回一个长度为 n + 1
的数组 ans
作为答案。
示例 1:
- 输入:
n = 2
- 输出:
[0, 1, 1]
- 解释:
0
的二进制表示为0
,有0
个1
。1
的二进制表示为1
,有1
个1
。2
的二进制表示为10
,有1
个1
。
示例 2:
- 输入:
n = 5
- 输出:
[0, 1, 1, 2, 1, 2]
- 解释:
0
的二进制表示为0
,有0
个1
。1
的二进制表示为1
,有1
个1
。2
的二进制表示为10
,有1
个1
。3
的二进制表示为11
,有2
个1
。4
的二进制表示为100
,有1
个1
。5
的二进制表示为101
,有2
个1
。
提示
0 <= n <= 10^5
进阶
能否在线性时间复杂度 O(n)
内用一趟扫描解决此问题?能否不使用任何内置函数?
动态规划
- 初始化数组 :创建一个数组
ans
,其中ans[i]
表示整数i
的二进制表示中 1 的个数。 - 动态规划转移方程 :对于每个整数
i
,其二进制中 1 的个数可以通过以下关系式计算:ans[i] = ans[i >> 1] + (i & 1)
- 其中,
i >> 1
表示将i
右移一位(相当于整数除以 2),i & 1
表示检查i
的最低位是否为 1。
- 遍历计算:从 0 到 n 遍历每个整数,利用上述转移方程计算每个整数的二进制中 1 的个数。
C++ ACM 模式实现
cpp
#include <iostream>
#include <vector>
using namespace std;
vector<int> countBits(int num) {
vector<int> res(num+1, 0);
for (int i = 1; i <= num; i++) {
res[i] = res[i >> 1] + (i & 1);
}
return res;
}
int main() {
int num = 5;
vector<int> res = countBits(num);
for (int i = 0; i < res.size(); i++) {
cout << res[i] << " ";
}
}
代码说明
总个数 :ans[i]
的值等于 ans[i >> 1]
(去掉最低位后的 1
的个数)加上 (i & 1)
(最低位是否为 1
)。
示例 :
以 i = 5
(二进制 101
)为例:
i >> 1 = 2
(二进制10
),ans[2]
已知为 1。i & 1 = 1
,因为5
的最低位是 1。- 因此,
ans[5] = ans[2] + 1 = 1 + 1 = 2
,即5
的二进制中有两个1
。
时间复杂度
该算法的时间复杂度为 O(n)
,因为每个整数从 0
到 n
都只需要进行常数次操作。
461. 汉明距离
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
示例 1:
- 输入:
x = 1
,y = 4
- 输出:
2
- 解释:
1
的二进制表示为0 0 0 1
4
的二进制表示为0 1 0 0
- 对应二进制位不同的位置如箭头所示。
示例 2:
- 输入:
x = 3
,y = 1
- 输出:
1
提示:
0 <= x, y <= 2^31 - 1
逐位比较
- 初始化计数器 :
int count = 0;
用于记录汉明距离,即不同位的数目。 - 循环条件 :
while(x != 0 || y != 0)
确保循环继续直到 x 和 y 都变为 0,这意味着所有二进制位都已被比较。 - 比较最低位 :
(x & 1)
和(y & 1)
分别获取 x 和 y 的最低位。如果这两个最低位不相同,if((x & 1) != (y & 1))
条件成立,计数器count
增加 1。 - 右移操作 :
x >>= 1
和y >>= 1
分别将 x 和 y 右移一位,以便在下一次循环中比较下一个二进制位。
C++ ACM 模式实现
cpp
#include <iostream>
using namespace std;
int hammingDistance(int x, int y) {
int count = 0;
while(x != 0 || y != 0){
if((x & 1) != (y & 1)){
count++;
}
x >>= 1;
y >>= 1;
}
return count;
}
int main(){
cout << hammingDistance(1,4) << endl;
return 0;
}
异或操作
- 异或操作 :计算
x
和y
的异或结果。异或操作会使得相同位为0
,不同位为1
,因此异或结果中的1
的个数即为汉明距离。 - 计算 1 的个数 :统计异或结果中
1
的个数,这可以通过循环移位和按位与操作来实现。
C++ ACM 模式实现
cpp
#include <iostream>
using namespace std;
int hammingDistance(int x, int y) {
int xorResult = x ^ y;
int count = 0;
while (xorResult){
if (xorResult & 1){
count++;
}
xorResult >>= 1;
}
return count;
}
int main(){
cout << hammingDistance(1,4) << endl;
return 0;
}
代码说明
- 异或操作 :
int xorResult = x ^ y;
计算 x 和 y 的异或结果。 - 初始化距离计数器 :
int distance = 0;
用于记录汉明距离。 - 循环处理每一位 :
distance += xorResult & 1;
检查异或结果的最低位是否为 1,如果是,则增加距离计数器。xorResult >>= 1;
将异或结果右移一位,以便处理下一位。
- 返回结果:循环结束后,返回距离计数器的值,即为汉明距离。
时间复杂度
这种方法的时间复杂度为 O(1)
,因为循环次数固定为 32
次(对于 32
位整数)。
136. 只出现一次的数字
给你一个非空整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1:
- 输入:
nums = [2, 2, 1]
- 输出:
1
示例 2:
- 输入:
nums = [4, 1, 2, 1, 2]
- 输出:
4
示例 3:
- 输入:
nums = [1]
- 输出:
1
提示
1 <= nums.length <= 3 * 10^4
-3 * 10^4 <= nums[i] <= 3 * 10^4
- 除了某个元素只出现一次以外,其余每个元素均出现两次。
异或操作
异或运算有一个特性:一个数异或自己等于 0
,一个数异或 0
等于其本身。因此,如果我们对数组中的所有元素进行异或运算,成对出现的元素都会抵消为 0
,最后剩下的就是那个只出现一次的元素。
C++ ACM 模式实现
cpp
#include <iostream>
#include <vector>
using namespace std;
int singleNumber(vector<int>& nums){
int ans = 0;
for(auto i : nums){
ans ^= i;
}
return ans;
}
int main(){
vector<int> nums = {4,1,2,1,2};
cout << singleNumber(nums) << endl;
return 0;
}
代码说明
- 初始化结果 :
int result = 0;
用于存储异或结果。 - 遍历数组:对数组中的每个元素进行异或运算。
- 返回结果 :最终
result
中存储的就是只出现一次的元素。
时间复杂度
这种方法的时间复杂度为 O(n)
,空间复杂度为 O(1)
。
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
- 除某个元素仅出现一次外,其余每个元素都恰出现三次。
性质
要解决这个问题,可以利用位运算的技巧来记录每个二进制位出现的次数。因为每个元素出现的次数要么是 1
次,要么是 3
次,所以可以通过统计每个二进制位上 1
出现的次数,并对 3
取模来消除出现三次的元素的影响。
- 统计每个二进制位上的 1 的个数 :对每个二进制位(总共
32
位),统计所有数字中该位上1
出现的次数。 - 对 3 取模 :因为出现三次的元素在每个二进制位上的
1
的个数都是3
的倍数,所以将每个二进制位的计数对3
取模后,剩下的就是只出现一次的元素在该位上的值。
C++ ACM 模式实现
cpp
#include <iostream>
#include <vector>
using namespace std;
int singleNumber(vector<int> &nums)
{
vector<int> bits(32, 0);
for (auto num : nums)
{
for (int i = 0; i < 32; ++i)
{
bits[i] += num & 1;
num >>= 1;
}
}
int result = 0;
for (int i = 0; i < 32; ++i)
{
result += bits[i] % 3 << i;
}
return result;
}
int main()
{
vector<int> nums = {2, 2, 3, 2};
cout << singleNumber(nums) << endl;
return 0;
}
代码说明
- 统计每个二进制位上的 1 的个数 :
bits
数组用于记录每个二进制位上的1
的个数。每个元素num
都被分解为32
位二进制数,每一位如果是1
则对应位置的计数加1
。 - 对 3 取模并构造结果 :将每个二进制位上的计数对
3
取模后,如果结果为1
,则表示该位属于只出现一次的元素。通过左移和按位或操作将这些位组合成最终结果。
260. 只出现一次的数字 Ⅲ
给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。你可以按任意顺序返回答案。
示例 1:
- 输入:
nums = [1, 2, 1, 3, 2, 5]
- 输出:
[3, 5]
- 解释:
[5, 3]
也是有效的答案。
示例 2:
- 输入:
nums = [-1, 0]
- 输出:
[-1, 0]
示例 3:
- 输入:
nums = [0, 1]
- 输出:
[1, 0]
提示
2 <= nums.length <= 3 * 10^4
-2^31 <= nums[i] <= 2^31 - 1
- 除两个只出现一次的整数外,
nums
中的其他数字都出现两次。
性质
- 异或所有元素:首先对数组中的所有元素进行异或操作。由于异或操作的性质,相同元素异或结果为 0,因此最终的异或结果实际上是两个只出现一次的元素的异或结果。
- 找到第一个不同的位 :找到异或结果中第一个为
1
的位。这个位表示两个只出现一次的元素在该位上不同。 - 分组异或 :根据上述找到的位,将数组中的元素分为两组。一组是该位为
1
的元素,另一组是该位为0
的元素。然后分别对这两组进行异或操作,得到两个只出现一次的元素。
C++ ACM 模式
cpp
#include <vector>
#include <iostream>
using namespace std;
vector<int> singleNumber(vector<int>& nums) {
int xor_all = 0;
for (int num : nums) {
xor_all ^= num;
}
int mask = 1;
while ((xor_all & mask) == 0) {
mask <<= 1;
}
int a = 0, b = 0;
for (int num : nums) {
if (num & mask) {
a ^= num;
} else {
b ^= num;
}
}
return {a, b};
}
int main() {
vector<int> nums = {1, 2, 1, 3, 2, 5};
vector<int> res = singleNumber(nums);
for (auto num : res) {
cout << num << " ";
}
cout << endl;
return 0;
}
代码说明
1. 初始化 mask
为 1
mask
用于标记两个只出现一次的元素在二进制表示中第一个不同的位。
2. 找到第一个不同的位
cpp
while ((xor_all & mask) == 0) {
mask <<= 1;
}
xor_all
是所有元素异或的结果,它实际上是两个只出现一次的元素的异或结果。由于异或的性质,相同元素异或后结果为0
,所以xor_all
保留了两个只出现一次元素的异或信息。(xor_all & mask) == 0
检查xor_all
的最低位是否为0
。如果是0
,说明两个元素在该位上的值相同,需要继续左移mask
查找下一个位。mask <<= 1
将mask
左移一位,直到找到xor_all
中值为1
的位。这个位就是两个只出现一次元素的第一个不同位。
3. 分组异或
cpp
int a = 0, b = 0;
for (int num : nums) {
if (num & mask) {
a ^= num;
} else {
b ^= num;
}
}
- 根据
mask
将数组中的元素分为两组:num & mask
为真的元素属于一组,这些元素在mask
所示的位上为1
。- 其他元素属于另一组,这些元素在
mask
所示的位上为0
。
a ^= num
对第一组元素进行异或操作。由于成对出现的元素会抵消为0
,最终a
保留了其中一个只出现一次的元素。b ^= num
对第二组元素进行异或操作,同理,b
保留了另一个只出现一次的元素。
4. 返回结果
最终,a
和 b
分别是两个只出现一次的元素。
371. 两整数之和
给你两个整数 a
和 b
,不使用运算符 +
和 -
,计算并返回两整数之和。
示例 1:
- 输入:
a = 1, b = 2
- 输出:
3
示例 2:
- 输入:
a = 2, b = 3
- 输出:
5
提示
-1000 <= a, b <= 1000
性质
- 异或运算 :异或运算可以模拟不考虑进位的加法。例如,
a ^ b
得到的结果是不考虑进位的和。 - 与运算和左移:与运算可以找出需要进位的位置,然后左移一位得到进位值。
- 循环处理进位:不断重复上述过程,直到进位值为零。此时的结果即为两整数之和。
C++ ACM 模式实现
cpp
#include <iostream>
using namespace std;
int getSum(int a, int b) {
while(b != 0) {
int carry = (unsigned int)(a & b) << 1;
a = a ^ b;
b = carry;
}
return a;
}
int main() {
cout << getSum(1, 2) << endl;
return 0;
}
代码说明
以 a = 1
和 b = 2
为例。
初始化:
a = 1
(二进制01
)b = 2
(二进制10
)
第一次循环:
-
计算进位:
carry = a & b
→carry = 1 & 2
- 二进制表示:
01 & 10 = 00
→carry = 0
-
计算不考虑进位的和:
a = a ^ b
→a = 1 ^ 2
- 二进制表示:
01 ^ 10 = 11
→a = 3
-
进位左移一位:
b = (unsigned int)carry << 1
→b = 0 << 1 = 0
循环条件检查:
b != 0
→0 != 0
→ 循环结束
在这个例子中,a
和 b
的二进制没有需要进位的地方,所以循环只执行了一次。最终,a
的值为 3
,即 1 + 2 = 3
。
这个过程展示了如何使用位运算来模拟加法操作。异或运算 (^
) 用于计算不考虑进位的和,而与运算 (&
) 和左移运算用于处理进位。通过不断重复这个过程,直到没有进位为止,最终得到正确的结果。
401. 二进制手表
二进制手表顶部有 4
个 LED
代表小时(0-11
),底部的 6
个 LED
代表分钟(0-59
)。每个 LED
代表一个 0
或 1
,最低位在右侧。
给你一个整数 turnedOn
,表示当前亮着的 LED
的数量,返回二进制手表可以表示的所有可能时间。你可以按任意顺序返回答案。
小时不会以零开头:
- 例如,"01:00" 是无效的时间,正确的写法应该是 "1:00"。
分钟必须由两位数组成,可能会以零开头:
- 例如,"10:2" 是无效的时间,正确的写法应该是 "10:02"。
示例 1:
- 输入:
turnedOn = 1
- 输出:
["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"]
示例 2:
- 输入:
turnedOn = 9
- 输出:
[]
提示
0 <= turnedOn <= 10
内置函数
- 枚举所有可能的时间组合 :小时范围是
0-11
,分钟范围是0-59
。 - 计算每个时间对应的二进制中 1 的个数 :对于每个时间,计算小时和分钟部分二进制表示中
1
的总数。 - 筛选符合条件的时间 :如果总数等于
turnedOn
,则将该时间添加到结果列表中。 - 格式化输出:确保小时不以零开头,分钟必须由两位数组成。
c++ ACM 模式实现
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<string> readBinaryWatch(int num) {
vector<string> res;
for (int h = 0; h < 12; ++h) {
for (int m = 0; m < 60; ++m) {
if (__builtin_popcount(h) + __builtin_popcount(m) == num) {
res.push_back(to_string(h) + (m < 10 ? ":0" : ":") + to_string(m));
}
}
}
return res;
}
int main() {
vector<string> res = readBinaryWatch(1);
for (auto &s : res) {
cout << s << endl;
}
return 0;
}
代码说明
__builtin_popcount
是 GCC(GNU Compiler Collection)
提供的一款内置函数,用于计算一个无符号整数的二进制表示中有多少个 1
。它的作用是快速统计二进制数中 1
的个数,这在位运算相关的编程问题中非常有用。
cpp
int __builtin_popcount(unsigned int x);
x
:一个无符号整数,函数将计算其二进制表示中1
的个数。
函数返回 x
的二进制表示中 1
的个数。