按奇偶排序数组 图解题解
这道题到底在问什么
- 输入
- nums=[3,1,2,4]
- 输出
- [4,2,1,3] ([2,4,3,1] 等也算对,只要偶在前奇在后)
- 输入
- nums=[0]
- 输出
- [0] (单个元素直接返回)
最优解:为什么这么做
一句话答案:LeetCode 905 按奇偶排序数组:允许任意顺序,就用对撞双指针在原地分区——左指针遇偶右移、右指针遇奇左移、左奇右偶就交换,一趟分好,时间 O(n)、空间 O(1)。
把偶数全挪到奇数前面,要返回什么才算数
给一个整数数组 nums,重排它,让所有偶数排在所有奇数前面,返回满足条件的任一数组即可;偶数、奇数各自谁先谁后都不管。题面 nums=[3,1,2,4],输出 [4,2,1,3],其实 [2,4,3,1] 也算对,只要偶在前奇在后。再看 nums=[0],单个元素直接返回 [0]。「任一」是重点:不排序、也不强求稳定。
先分两个表再拼起来,差在哪儿
扫一遍 nums,偶数进一个表、奇数进另一个表,最后两表拼回去,答案就有了。逻辑没错,时间也是 O(n),可这一路白开了两个和 nums 一样长的表,多花 O(n) 额外空间。题目只要偶在前奇在后、不追究具体顺序,这份空间能省掉。
凭什么两个指针相向走一趟就能分好
既然允许任意顺序,就不必搬新表,直接在原数组上把偶数拨到左、奇数拨到右。摆两个指针:i 从最左、j 从最右,相向往中间走。i 只要停在偶数上就往右迈、j 只要指着奇数就往左退;只有 i 指奇数、j 指偶数、两边都站错时才交换,双双归位后再各进一步。一头一尾对着夹,中间没分好的窗口越缩越小,碰上就分完。在原数组上按一个判定拆成两段,就是对撞双指针原地分区。
左指针右指针,各自碰到什么才动
每一轮先看 i:nums[i] 是偶数(nums[i] % 2 == 0)就站对,i 右移;不是偶数,再看 j,nums[j] 是奇数(nums[j] % 2 == 1)也站对,j 左移;两条都不满足,只剩 i 指奇数、j 指偶数这一种,交换 nums[i] 与 nums[j],随后 i 右移、j 左移。循环条件是 i 比 j 小,等两者相遇或错开就停下返回 nums。判断先后有讲究:先放过站对的偶数、再退走站对的奇数,剩下的才是「左奇右偶」那种一换解决俩的情形;右边站对的奇数别硬换,否则会把归位的奇数又拖回窗口。
[3,1,2,4] 分两步落位,一步一步看
起手 i 指向下标 0、j 指向下标 3。nums[0]=3 奇数,i 不动;nums[3]=4 偶数、非奇数,j 也不退;落到交换,nums[0] 和 nums[3] 对调,数组变成 [4,1,2,3],i 到下标 1、j 到下标 2。第二轮 nums[1]=1 奇、nums[2]=2 偶,又交换,nums[1] 和 nums[2] 对调,数组成 [4,2,1,3],i 到下标 2、j 到下标 1。这时 i 比 j 大,循环停,返回 [4,2,1,3]——偶数 4、2 在前,奇数 1、3 在后,偶在前奇在后就对了。
循环条件用 i 小于等于 j,会白空转一轮
i 和 j 合起来只把数组从两头扫过一遍,时间 O(n);全程只多用 i、j 两个下标、就地交换,空间 O(1)。几种边界都不触发交换、原样返回就对:单元素如 nums=[0],i 和 j 起手同位,条件 i 比 j 小不成立,一轮不进直接返回;全是偶数则 i 右移到头、全是奇数则 j 左移到头,都不交换。收尾一个爱写错的点:循环写成 i 小于等于 j,等 i、j 落到同一元素上还会多进一轮,对已定的位置空转,改成 i 比 j 小才干净。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套「左偶就 i++、右奇就 j--、左奇右偶就交换」,下面每一帧都在套它。
- 4双指针就位:i(左指针)站在下标 0,j(右指针)站在下标 7。中间紫色窗口是还没分好的区域。我们要让窗口里的偶数都流到左边、奇数都流到右边。
- 5先看左指针。nums[0] = 3 是奇数,奇数该去右边,这一位站错了,先记着它,再去看右指针。
- 6再看右指针。nums[7] = 7 也是奇数,而奇数本来就该待在右边,这一位其实是站对的。
- 7左边奇数虽然站错,但右边是个已经站对的奇数,没法跟它换。所以先把右指针 j 往左收一格,把这个奇数定下来。
- 8j 退到下标 6。下标 7 的 7 归入右边的奇数区(蓝色)。左指针那个奇数下一轮再处理。
- 9先看左指针。nums[0] = 3 是奇数,按规矩它该去右边,是个站错位的。
- 10再看右指针。nums[6] = 8 是偶数,按规矩它该来左边,也是个站错位的。两个刚好反着。
- 11这是最划算的一步:左边的奇数和右边的偶数一交换,两个就都各就各位了。准备交换。
- 12换完:下标 0 变成偶数 8,下标 6 变成奇数 3,都站对了。于是 i 右移到 1、j 左移到 5,窗口同时缩小两头。
- 13先看左指针。nums[1] = 2 是偶数,偶数本来就该待在左边,这一位已经站对了。
- 14既然左指针指的是偶数、位置正确,就不用动它,直接让 i 往右迈一步,去检查下一个。
- 15i 走到下标 2。下标 1 的 2 正式归入左边的偶数区(绿色)。继续看新的左指针。
- 16先看左指针。nums[2] = 1 是奇数,奇数该去右边,这一位站错了,先记着它,再去看右指针。
- 17再看右指针。nums[5] = 5 也是奇数,而奇数本来就该待在右边,这一位其实是站对的。
- 18左边奇数虽然站错,但右边是个已经站对的奇数,没法跟它换。所以先把右指针 j 往左收一格,把这个奇数定下来。
- 19j 退到下标 4。下标 5 的 5 归入右边的奇数区(蓝色)。左指针那个奇数下一轮再处理。
- 20先看左指针。nums[2] = 1 是奇数,按规矩它该去右边,是个站错位的。
- 21再看右指针。nums[4] = 6 是偶数,按规矩它该来左边,也是个站错位的。两个刚好反着。
- 22这是最划算的一步:左边的奇数和右边的偶数一交换,两个就都各就各位了。准备交换。
- 23换完:下标 2 变成偶数 6,下标 4 变成奇数 1,都站对了。于是 i 右移到 3、j 左移到 3,窗口同时缩小两头。
- 24两个指针在下标 3 撞上了,中间再没有未处理的元素,循环停止。此刻左边四个全是偶数、右边四个全是奇数。
- 25分区完成:绿色 8、2、6、4 都是偶数,蓝色 1、5、3、7 都是奇数。偶在前、奇在后,这就是一个合法答案。
⚠️ 容易写错的地方
✗ 错:用「新建偶数表 + 奇数表再拼接」
✓ 对:双指针原地交换
题目允许任意顺序,原地两指针更省空间,O(1)
✗ 错:循环条件写成 i ≤ j
✓ 对:应为 i 比 j 小
i 等于 j 时指向同一个元素,无需再处理,写 ≤ 会多绕一步
✗ 错:右边是奇数时还去交换
✓ 对:右奇数已站对,只需 j 左移
盲目交换会把已归位的奇数又换走,破坏分区
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def sortArrayByParity(self, nums: List[int]) -> List[int]:
i, j = 0, len(nums) - 1
while i < j:
if nums[i] % 2 == 0:
i += 1
elif nums[j] % 2 == 1:
j -= 1
else:
nums[i], nums[j] = nums[j], nums[i]
i, j = i + 1, j - 1
return numsC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> sortArrayByParity(vector<int>& nums) {
int i = 0, j = nums.size() - 1;
while (i < j) {
if (nums[i] % 2 == 0) {
++i;
} else if (nums[j] % 2 == 1) {
--j;
} else {
swap(nums[i++], nums[j--]);
}
}
return nums;
}
};Java
import java.util.*;
class Solution {
public int[] sortArrayByParity(int[] nums) {
int i = 0, j = nums.length - 1;
while (i < j) {
if (nums[i] % 2 == 0) {
++i;
} else if (nums[j] % 2 == 1) {
--j;
} else {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
++i;
--j;
}
}
return nums;
}
}复杂度
时间
O(n)
i 与 j 合起来只把数组扫过一遍
空间
O(1)
原地交换,只用 i、j 两个下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 按奇偶排序数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和快速排序里的分区(partition)是一回事吗?+
本质是一回事,都是把数组按某个判定分成两段。这里的判定是「奇还是偶」,快排里的判定是「和基准比谁大谁小」,判定换了、套路没变:两个指针一左一右相向夹逼,碰到两边都不满足判定就交换一次,让它们各归各段。所以把这题的双指针分区想透了,再回头看快排的 partition 会顺很多,它俩就是同一个骨架换了张皮。
如果还要求偶数之间保持原来的相对顺序(稳定)怎么办?+
那就不能原地交换了。原地对撞的双指针会把靠后的偶数换到靠前,偶数之间的原始先后被打乱。想要稳定,得另开一个数组,先按原顺序把偶数依次放进去、再接着放奇数,扫一遍填完。这样偶数、奇数各自的相对顺序都留住了,代价是空间从 O(1) 涨到 O(n)。本题不要求稳定,才有资格用原地交换省下这份空间。
右指针指着奇数时,为什么只让它左退、不参与交换?+
因为奇数本来就该待在数组右边,j 指着的这个奇数已经站在它该在的区域,位置是对的,没有换的必要。这时只要把 j 往左收一格,把这个站对的奇数排除在待分窗口之外就行。要是不管三七二十一就交换,会把这个已经归位的奇数又换回左边窗口,白白破坏已经分好的部分,还得多绕几轮才能重新收拾干净。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 按奇偶排序数组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。