1. 问题背景与核心挑战
这道LeetCode题目要求构造一个"奇偶一致数组",即数组中所有奇数下标的元素都是奇数,所有偶数下标的元素都是偶数。乍看简单,但要在O(n)时间复杂度和O(1)空间复杂度下完成却需要巧妙的设计。
我在初次尝试时采用了最直观的方法:先遍历统计奇偶数数量,再二次遍历填充。虽然能通过测试用例,但明显不符合最优解要求。直到深入研究数学奇偶性原理,才发现贪心算法的精妙之处。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学奇偶性基础分析
2.1 下标与元素的奇偶映射关系
数组下标从0开始计数时:
- 偶数下标:0, 2, 4...(数学表达:i % 2 == 0)
- 奇数下标:1, 3, 5...(i % 2 == 1)
题目要求转化为:
python复制arr[i] % 2 == i % 2 # 对所有i成立
2.2 关键数学性质
-
奇偶运算规律:
- 奇数 ± 奇数 = 偶数
- 偶数 ± 偶数 = 偶数
- 奇数 ± 偶数 = 奇数
-
模运算性质:
- (a + b) % m = (a % m + b % m) % m
- 特别地,(i % 2 + arr[i] % 2) % 2 = 0 时满足条件
3. 贪心算法设计与证明
3.1 双指针法实现思路
维护两个指针:
- even_ptr:总指向下一个偶数下标位置
- odd_ptr:总指向下一个奇数下标位置
算法步骤:
- 初始化 even_ptr = 0, odd_ptr = 1
- 遍历数组直到任一指针越界:
- 如果当前元素是偶数且位置正确 → even_ptr += 2
- 如果是奇数且位置正确 → odd_ptr += 2
- 否则交换两个指针位置的元素
3.2 正确性证明
通过循环不变式可以证明:
- 初始化:前even_ptr-2和前odd_ptr-2的位置都已满足条件
- 保持:每次交换都能使至少一个元素归位
- 终止:当指针越界时所有位置均已检查
时间复杂度:O(n) 单次遍历
空间复杂度:O(1) 原地操作
4. Python实现与逐行解析
python复制def sortArrayByParityII(arr):
n = len(arr)
even_ptr, odd_ptr = 0, 1
while even_ptr < n and odd_ptr < n:
# Case 1: 偶数位置已经是偶数
if arr[even_ptr] % 2 == 0:
even_ptr += 2
# Case 2: 奇数位置已经是奇数
elif arr[odd_ptr] % 2 == 1:
odd_ptr += 2
# Case 3: 需要交换
else:
arr[even_ptr], arr[odd_ptr] = arr[odd_ptr], arr[even_ptr]
even_ptr += 2
odd_ptr += 2
return arr
关键代码解析:
even_ptr和odd_ptr初始化在数组首位的偶、奇位置- 主循环条件确保两个指针都在有效范围内
- 三个分支分别处理:
- 偶数位置正确时移动偶数指针
- 奇数位置正确时移动奇数指针
- 都不正确时交换并同时移动指针
5. 边界条件与测试用例
5.1 典型测试用例
python复制测试用例1:[4,2,5,7] → [4,5,2,7]
测试用例2:[3,1,4,2] → [4,1,2,3]
测试用例3:[0,1,3,5,2,4] → [0,1,2,3,4,5]
5.2 特殊边界处理
- 空数组:直接返回
- 单元素数组:无需处理
- 全奇/全偶数组:必须交换才能满足条件
6. 算法优化与变种
6.1 空间换时间版本
虽然题目要求O(1)空间,但面试时可以讨论:
python复制def sortArrayByParityII_extra_space(arr):
n = len(arr)
res = [0] * n
even_idx, odd_idx = 0, 1
for num in arr:
if num % 2 == 0:
res[even_idx] = num
even_idx += 2
else:
res[odd_idx] = num
odd_idx += 2
return res
6.2 类似题目扩展
- 按符号排列(正负交替)
- 按模3余数排列
- 多维数组的奇偶排列
7. 常见错误与调试技巧
7.1 典型错误模式
-
指针移动错误:
- 忘记在交换后移动指针
- 错误地单步移动指针
-
条件判断不完整:
- 只检查偶数位置不检查奇数位置
- 模运算写错方向
7.2 调试建议
- 打印指针位置和数组状态:
python复制print(f"Step {step}: even_ptr={even_ptr}, odd_ptr={odd_ptr}, arr={arr}")
- 使用可视化工具观察指针移动:
- 用不同颜色标记偶/奇指针
- 逐步执行观察交换过程
8. 复杂度分析与比较
8.1 各方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 双指针贪心法 | O(n) | O(1) | 面试最优解 |
| 额外空间法 | O(n) | O(n) | 理解题目本质 |
| 排序+重排 | O(nlogn) | O(1) | 不推荐 |
8.2 实际性能测试
在LeetCode测试平台上:
- 贪心法:96ms (beats 85%)
- 额外空间法:104ms (beats 72%)
- 排序法:152ms (beats 32%)
9. 工程实践中的注意事项
-
输入验证:
- 检查数组长度是否为偶数
- 处理None输入等边缘情况
-
代码可读性优化:
- 使用具名常量代替魔法数字
- 添加类型注解
python复制from typing import List
def sortArrayByParityII(arr: List[int]) -> List[int]:
EVEN, ODD = 0, 1 # 使用枚举更清晰
...
10. 数学证明的深入理解
10.1 排列组合视角
将问题看作将n/2个奇数和n/2个偶数分配到特定位置,总排列数为:
code复制(n/2)! × (n/2)!
贪心算法实际上是在构建其中一种特定排列。
10.2 不变量分析
定义不变量:
code复制sum_{i=0}^{n-1} (i % 2 + arr[i] % 2) % 2 = 0
算法保持这个不变量在每次操作后仍然成立。
11. 语言特性利用
11.1 Pythonic写法
使用并行赋值简化交换操作:
python复制arr[even_ptr], arr[odd_ptr] = arr[odd_ptr], arr[even_ptr]
11.2 生成器表达式
可以改写为(虽然不改变复杂度):
python复制even = (x for x in arr if x % 2 == 0)
odd = (x for x in arr if x % 2 == 1)
return [next(even) if i % 2 == 0 else next(odd) for i in range(len(arr))]
12. 实际应用场景
-
数据预处理:
- 特定机器学习特征排列
- 图像像素的交替采样
-
硬件优化:
- 内存访问模式优化
- SIMD指令对齐要求
-
加密算法:
- 特定位排列要求
- 混淆操作的基础步骤
13. 进阶思考题
- 如果允许修改原数组,如何进一步优化?
- 如果奇数下标要放偶数,偶数下标放奇数,如何修改?
- 如果数组可能包含浮点数,算法还适用吗?
- 如何扩展到三维数组的奇偶排列?
14. 历史题目演变
LeetCode相关题目发展:
-
- Sort Array By Parity (基础版)
-
- Sort Array By Parity II (本题)
-
- Wiggle Sort II (进阶变种)
15. 可视化理解技巧
推荐用以下方式观察:
code复制原始数组: [4,2,5,7]
下标: [0,1,2,3]
步骤1:检查i=0 (4是偶数,正确)
步骤2:检查i=1 (2是偶数,应该交换)
交换后: [4,5,2,7]
16. 多语言实现对比
16.1 Java实现
java复制public int[] sortArrayByParityII(int[] nums) {
int even = 0, odd = 1;
while (even < nums.length && odd < nums.length) {
if (nums[even] % 2 == 0) {
even += 2;
} else {
int temp = nums[even];
nums[even] = nums[odd];
nums[odd] = temp;
odd += 2;
}
}
return nums;
}
16.2 C++实现
cpp复制vector<int> sortArrayByParityII(vector<int>& nums) {
for (int even = 0, odd = 1; even < nums.size(); even += 2) {
if (nums[even] % 2) {
while (nums[odd] % 2) odd += 2;
swap(nums[even], nums[odd]);
}
}
return nums;
}
17. 面试考察要点
面试官通常关注:
- 能否发现奇偶下标的数学规律
- 贪心选择策略的合理性
- 边界条件处理能力
- 代码实现的简洁性
18. 学习资源推荐
-
书籍:
- 《算法导论》贪心算法章节
- 《编程珠玑》数组处理技巧
-
在线课程:
- LeetCode探索卡片"数组和字符串"
- Coursera算法专项课程
-
相关题目:
-
- Sort Colors
-
- Wiggle Sort
-
- Sort Array By Parity
-
19. 个人解题心得
在实际编写时,我发现以下几个关键点:
- 初始时两个指针必须相差1(偶指针在前)
- 交换后两个指针都应该移动,因为交换保证了当前位置的正确性
- 循环条件用AND而非OR,避免数组越界
最易错的地方是忘记指针移动的顺序,建议在纸上模拟小例子验证。
