1. 问题背景与核心挑战
这道LeetCode题目属于位运算系列中的经典问题,要求我们构造一个满足特定条件的最小数组。题目给出一个质数数组nums,要求返回一个数组ans,使得对于每个下标i,满足ans[i] OR (ans[i] + 1) == nums[i],并且在所有可能的解中选择最小的ans[i]。
这个问题的难点在于理解位运算OR的特性以及如何逆向推导出满足条件的最小数值。在实际编程面试中,这类位运算问题经常出现,因为它能很好地考察候选人对二进制操作的理解和数学思维能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 位运算基础与问题分析
2.1 OR运算的特性分析
OR运算(|)是位运算中的基本操作之一,它对两个数的每一位进行逻辑或运算。关键特性包括:
- 任何数与自身OR结果为自身:x | x = x
- 任何数与0 OR结果为原数:x | 0 = x
- 运算满足交换律和结合律
在本题中,我们需要特别关注x OR (x+1)的特殊性质。这个表达式的结果会将x二进制表示中最右边的0变为1,并将该位右侧的所有1变为0。
2.2 问题转化与逆向思维
题目要求我们找到满足x | (x+1) = nums[i]的最小x。我们可以将这个问题转化为:
- 给定nums[i],找出所有可能的x使得x | (x+1) = nums[i]
- 在这些x中选择最小的一个
通过观察我们可以发现,x | (x+1)的结果总是会将x的最右边的0变为1。因此,我们可以逆向思考:给定nums[i],如何恢复出原始的x。
3. 算法设计与实现细节
3.1 关键思路解析
核心算法思路可以分为以下几个步骤:
-
检查特殊情况:如果nums[i] == 2(唯一的偶质数),直接返回-1,因为x | (x+1)的结果最低位必须是1。
-
对于其他质数:
- 计算nums[i]的按位取反:t = ~nums[i]
- 找到t的最低位1(lowbit):lowbit = t & -t
- 将lowbit右移一位得到掩码
- 将nums[i]与该掩码异或得到结果
这个方法的数学依据是:x | (x+1)的结果中,最右边的0位在x中对应的是1,而该位右边的所有位在x中都是1。通过这种变换,我们可以逆向恢复出最小的x。
3.2 代码实现与语言特性
以下是各语言实现的详细解析:
Java实现
java复制class Solution {
public int[] minBitwiseArray(List<Integer> nums) {
int n = nums.size();
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
int x = nums.get(i);
if (x == 2) {
ans[i] = -1;
} else {
int t = ~x;
int lowbit = t & -t;
ans[i] = x ^ (lowbit >> 1);
}
}
return ans;
}
}
Java实现使用了基本的位运算操作,注意Integer的按位取反操作(~)和lowbit的计算方式。
C++实现
cpp复制class Solution {
public:
vector<int> minBitwiseArray(vector<int>& nums) {
for (int& x : nums) {
if (x == 2) {
x = -1;
} else {
int t = ~x;
x ^= (t & -t) >> 1;
}
}
return nums;
}
};
C++实现利用了引用直接修改原数组,减少了额外的空间开销。
Python实现
python复制class Solution:
def minBitwiseArray(self, nums: List[int]) -> List[int]:
for i, x in enumerate(nums):
if x == 2:
nums[i] = -1
else:
t = ~x
nums[i] ^= (t & -t) >> 1
return nums
Python实现简洁明了,注意Python中整数的位运算是无限精度的,与Java/C++有所不同。
Rust实现
rust复制impl Solution {
pub fn min_bitwise_array(nums: Vec<i32>) -> Vec<i32> {
let mut ans = Vec::new();
for x in nums {
if x == 2 {
ans.push(-1);
} else {
let t = !x;
let lowbit = t & -t;
ans.push(x ^ (lowbit >> 1));
}
}
ans
}
}
Rust实现体现了其所有权系统的特点,创建新的向量存储结果。
Go实现
go复制func minBitwiseArray(nums []int) []int {
for i, x := range nums {
if x == 2 {
nums[i] = -1
} else {
t := ^x
nums[i] ^= t & -t >> 1
}
}
return nums
}
Go实现简洁高效,利用了range遍历和位运算。
4. 复杂度分析与优化空间
4.1 时间复杂度
算法对每个数组元素执行固定次数的位运算操作,因此时间复杂度为O(n),其中n是数组长度。这是最优的时间复杂度,因为我们必须处理每个元素。
4.2 空间复杂度
大多数实现使用了O(1)的额外空间(除了可能需要存储结果的数组)。在原位修改的实现中(如C++、Python、Go),空间复杂度可以认为是O(1)。
4.3 可能的优化
虽然算法已经很高效,但可以考虑以下优化点:
- 并行处理:对于大型数组,可以利用多线程并行处理不同元素
- 预处理:如果多次调用,可以预先计算常见质数的结果
- SIMD指令:在某些平台上可以使用SIMD指令加速位运算
5. 边界条件与测试案例
5.1 特殊输入处理
需要考虑的特殊情况包括:
- 空数组输入
- 包含最小质数2的数组
- 包含最大质数(接近2^31-1)的情况
- 重复元素的数组
5.2 测试案例设计
有效的测试案例应包括:
java复制// 示例1
[2,3,5,7] → [-1,1,4,3]
// 示例2
[11,13,31] → [9,12,15]
// 边界案例1:最小输入
[2] → [-1]
// 边界案例2:大质数
[2147483647] → [1073741823]
// 重复元素
[3,3,3] → [1,1,1]
// 混合案例
[2,3,2,5] → [-1,1,-1,4]
6. 常见错误与调试技巧
6.1 常见实现错误
- 忘记处理nums[i]=2的特殊情况
- lowbit计算错误,特别是负数处理
- 位运算优先级问题导致的计算顺序错误
- 整数溢出问题(虽然本题中不太可能出现)
6.2 调试建议
- 打印中间变量:特别是t、lowbit等中间结果
- 对小规模输入手动计算验证
- 使用位运算可视化工具理解过程
- 编写单元测试覆盖各种边界条件
7. 位运算技巧总结
通过这道题目,我们可以总结出一些有用的位运算技巧:
- lowbit计算:x & -x 可以快速得到一个数最低位的1
- 最右边0变1:x | (x+1) 的经典用法
- 位反转与掩码操作:结合~和^操作可以实现复杂的位模式操作
- 位运算优先级:牢记位运算的优先级顺序,必要时使用括号
8. 实际应用场景
这类位运算技巧在实际开发中有广泛应用:
- 位图索引和压缩存储
- 哈希算法和校验码计算
- 网络协议中的标志位处理
- 高性能计算中的优化技巧
- 嵌入式系统中的寄存器操作
9. 扩展思考与变种问题
基于这道题目,可以延伸思考以下变种问题:
- 如果不是质数数组,算法需要如何调整?
- 如果条件改为x AND (x+1) == nums[i],如何解决?
- 如果要求第二小的解而非最小解,算法如何修改?
- 如果nums[i]范围扩大到所有正整数而不仅是质数,解法有何变化?
10. 面试技巧与准备建议
对于准备技术面试的候选人,这道题目提供了很好的练习机会:
- 理解题目后先尝试手动计算几个例子
- 从暴力解法开始,逐步优化到位运算解法
- 清晰地解释每个位运算步骤的含义和目的
- 考虑边界条件和特殊输入
- 讨论时间复杂度和可能的优化空间
在实际面试中,即使不能立即想到最优解,展示出清晰的思考过程和逐步优化的能力同样重要。
