目录

题目描述

922. 按奇偶排序数组 II

image-20230311222317232

题意分析

给一个长度为偶数的数组,其中恰好一半是偶数、一半是奇数。要求重排它,使得每个偶数下标上放的是偶数、每个奇数下标上放的是奇数。任何满足条件的排列都算正确答案。

有三个约束信号值得单独拎出来。第一,「奇偶各占一半」是题目给的硬保证,不是需要验证的条件,这意味着不存在无解情况,代码里不必写失败分支。第二,题目明说答案不唯一,所以完全不需要考虑元素间的相对顺序,也谈不上真正意义上的排序。第三,值域是非负整数,取模判断奇偶不会遇到负数取模的符号问题。

再看目标本身:这里唯一被约束的是「下标的奇偶性」与「元素的奇偶性」要对上,元素的大小关系毫无作用。想清楚这一点,题目就从「排序」退化成了「分类归位」。

边界上,数组长度至少为 2,所以不会出现空数组;长度必为偶数,因此最后一个下标一定是奇数下标,两类位置的数量严格相等。

解法:奇偶下标分流填充

核心思路

一个自然但走偏的想法是排序:把偶数全排前面、奇数全排后面,再想办法交错。这既做了多余的工作(题目不关心顺序),又没有直接解决交错问题,反而更绕。瓶颈在于把「归位」误当成了「定序」。

退回到问题本身:每个元素只有两种身份(奇或偶),每个位置也只有两种身份(奇下标或偶下标),而且两边的数量恰好一一匹配。这是一个纯粹的分配问题——把每个元素投递到对应类型的下一个空位即可,谁先谁后完全无所谓。

于是开一个等长的结果数组,用两个写指针分别管理两类空位:evenIdx 指向下一个待填的偶数下标,从 0 起步;oddIdx 指向下一个待填的奇数下标,从 1 起步。每写一个就把对应指针加 2,自然跳到同类的下一个位置。

不变量是:任意时刻,evenIdx 恒为偶数且 res 中所有小于 evenIdx 的偶数下标都已填入偶数;oddIdx 恒为奇数且 res 中所有小于 oddIdx 的奇数下标都已填入奇数。 两个指针步长都是 2,起点奇偶性不同,所以它们的取值集合永不相交,写入互不覆盖。由于奇偶元素各占一半,遍历结束时两个指针恰好都越过数组末尾,所有位置被填满且无一重复。

解题步骤

  • 新建与 nums 等长的结果数组 res。用新数组而不是就地交换,是为了让每个元素的落位一次确定,不需要处理「换过去的那个又该放哪」的连锁问题。
  • 初始化 evenIdx = 0oddIdx = 1。起点必须分别取最小的偶数下标和最小的奇数下标,这样加 2 的步进才能覆盖各自的全部位置。
  • 顺序遍历 nums 中的每个 num,只判断它自身的奇偶性,不看它原来在哪个下标。原下标信息在本题里没有任何价值。
  • num % 2 == 0,写入 res[evenIdx] 并令 evenIdx += 2;否则写入 res[oddIdx] 并令 oddIdx += 2。写入与推进必须成对出现,漏掉推进会导致同一个位置被反复覆盖。
  • 全部遍历完直接返回 res。因为题目保证了数量匹配,不需要任何越界检查或补漏逻辑。

nums = [4,2,5,7] 走一遍:初始 res = [_,_,_,_]evenIdx = 0oddIdx = 1。第一个元素 4 是偶数,写入 res[0]res 变成 [4,_,_,_]evenIdx 推进到 2。第二个元素 2 是偶数,写入 res[2]res 变成 [4,_,2,_]evenIdx 推进到 4(已越界,但之后不会再有偶数,永远不会被使用)。第三个元素 5 是奇数,写入 res[1]res 变成 [4,5,2,_]oddIdx 推进到 3。第四个元素 7 是奇数,写入 res[3]res 变成 [4,5,2,7]oddIdx 推进到 5。返回 [4,5,2,7]:下标 02 上是偶数 42,下标 13 上是奇数 57,完全符合要求。再以 nums = [3,1,4,2] 走一遍:3 是奇数写 res[1]1 是奇数写 res[3]4 是偶数写 res[0]2 是偶数写 res[2],最终 res = [4,3,2,1],同样合法。

代码实现

class Solution {
    public int[] sortArrayByParityII(int[] nums) {
        int[] res = new int[nums.length];
        int evenIdx = 0;
        int oddIdx = 1;

        // 偶数只写偶数位,奇数只写奇数位,两个指针互不干扰。
        for (int num : nums) {
            if (num % 2 == 0) {
                res[evenIdx] = num;
                evenIdx += 2;
            } else {
                res[oddIdx] = num;
                oddIdx += 2;
            }
        }

        return res;
    }
}
func sortArrayByParityII(nums []int) []int {
    res := make([]int, len(nums))
    evenIdx := 0
    oddIdx := 1

    // 偶数只写偶数位,奇数只写奇数位,两个指针互不干扰。
    for _, num := range nums {
        if num%2 == 0 {
            res[evenIdx] = num
            evenIdx += 2
        } else {
            res[oddIdx] = num
            oddIdx += 2
        }
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组长度。只做一趟遍历,每个元素上的工作是一次取模、一次写入、一次加法,都是常数时间。
  • 空间复杂度:$O(n)$,额外开了一个等长的结果数组;除此之外只有两个整型指针。若把返回数组算作必要输出,则额外开销为 $O(1)$。

关键点总结

  • 先分清题目要的是「定序」还是「归位」。本题只约束奇偶匹配、不约束相对顺序,识破这点就能直接跳过排序,把复杂度从 $O(n \log n)$ 降到 $O(n)$。
  • 两类目标位置各配一个步长为 2 的写指针,是处理「交错填充」的通用模板。起点取不同奇偶、步长相同,就能保证两条写入轨道天然不冲突。
  • 题目给出的数量保证要主动利用。既然奇偶各占一半,就不必写越界保护和无解分支,代码更短也更不容易错。
  • 面试视角:这题的进阶要求是常数额外空间。要能说出就地双指针的做法:i 走偶数下标、j 走奇数下标,各自跳过已经匹配的位置,一旦同时发现 nums[i] 是奇数且 nums[j] 是偶数就交换两者;由于数量匹配,两个指针会同步耗尽。
  • 面试视角:被问「为什么就地版本一定能配上对」时,答案是计数论证——偶数下标上多出来的奇数个数,必然等于奇数下标上多出来的偶数个数,所以每次都能凑成一对交换。能讲清这句话比写出代码更有说服力。

易错点总结

  • 错误写法oddIdx 初始化为 0 或与 evenIdx 同起点。用例 [4,2,5,7] → 奇数被写到偶数下标上,res[0] 先被 4 填再被 5 覆盖,结果既有覆盖又有空位。
  • 错误写法:写入后指针加 1 而不是加 2。用例 [4,2,5,7]evenIdx 依次取 01,把偶数 2 写进了奇数下标 1,奇偶匹配被破坏。
  • 错误写法:写入后忘记推进指针。用例 [4,2,5,7] → 两个偶数都写进 res[0]42 覆盖,res[2] 保持默认值 0,输出错误。
  • 错误写法:按下标的奇偶性去分类元素,即写成 if (i % 2 == 0) 来决定往哪写。用例 [4,2,5,7] → 分类依据用错了对象,原下标与元素奇偶无关,结果等于原样拷贝。
  • 错误写法:先对数组整体排序,再把前一半填进偶数下标、后一半填进奇数下标。用例 [2,3,4,5] → 排序后仍是 [2,3,4,5],前一半 23 分别落到下标 02,奇数 3 被放进了偶数下标,判定失败;这种做法既不成立又白付了 $O(n \log n)$。
  • 错误写法:就地交换版本里只推进其中一个指针。用例 [4,2,5,7] → 交换后没有同时推进两个游标,会重复检查同一对位置,轻则死循环,重则把已经归位的元素换回去。
  • 错误写法:用 num & 1 == 0 判断偶数但忘了运算符优先级,在 Java 中写成 if (num & 1 == 0)。用例任意输入 → == 优先级高于 &,表达式类型不匹配直接编译失败。
  • 错误写法:直接返回 nums 而不是 res。用例 [4,2,5,7] → 原数组未被修改,返回的仍是不满足条件的原序列。
  • 错误写法:把这套写法迁移到可能含负数的变体时仍用 num % 2 == 0 之外的写法,例如 num % 2 == 1 判奇数。本题值域非负不受影响,但在负数输入下 -3 % 2 在 Java 和 Go 中都等于 -1,判定失败,所有负奇数会被归入偶数分支。

相似题目

题目 难度 考察点
905. 按奇偶排序数组 简单 只要求偶数在前奇数在后,用对撞双指针即可
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面 简单 与上题镜像,考察就地划分的写法熟练度
283. 移动零 简单 需保持非零元素相对顺序,用快慢指针覆盖式前移
75. 颜色分类 中等 三类元素的一趟原地划分,需要三指针协同
328. 奇偶链表 中等 按位置奇偶拆链表,考察指针重接而非数值判定
86. 分隔链表 中等 按阈值把链表分成两段并保序,最后首尾相接