题目描述

✅ 剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

image-20261001230752545

题意分析

调整整数数组,使所有奇数位于所有偶数之前,返回调整后的数组。每个元素都要保留,划分依据是元素值的奇偶,不是下标的奇偶,也不要求前后两部分长度相等。

题目不要求同类元素保持原来的相对顺序,因此可以通过交换原地划分。零属于偶数,负数也按自身奇偶处理;题面要求线性时间和常量额外空间。

解法:双指针原地交换

核心思路

[!blue]

用 left、right 包围尚未确定归属的区间。始终保证 left 左边已经全是奇数,right 右边已经全是偶数,只有中间部分还需要处理。

左指针遇到奇数,可以直接向右跳过,因为它已经位于正确一侧;直到遇到偶数才停下。右指针同理,跳过偶数,直到遇到奇数。如果两指针还没相遇,停下的两个元素恰好都在错误的一侧,一次交换就能同时修复两处位置,再各自向中间推进。

扫描和交换只扩大已经确认的前缀、后缀,不会破坏它们。两指针相遇或交错时,中间至多剩一个元素:它无论是奇数还是偶数,都可以接在对应分区边缘,不会出现偶数位于奇数之前的情况,因此可以结束。

奇偶使用最低位判断,x & 1 为 1 表示奇数,为 0 表示偶数,负数也适用。内层扫描同样要检查 left < right,避免全奇或全偶时继续越过待处理区间。由于不保证稳定顺序,首尾交换符合本题要求。

解题步骤

  1. 初始化 left = 0、right = nums.length - 1。
  2. 当 left < right 时,让左指针连续跳过奇数,右指针连续跳过偶数;每次读取元素前都检查指针尚未相遇。
  3. 如果扫描后仍有 left < right,交换左侧偶数与右侧奇数,再令 left++、right--。
  4. 指针相遇或交错后返回原数组。空数组和单元素数组会直接结束。

代码实现

class Solution {
    public int[] exchange(int[] nums) {
        int left = 0;
        int right = nums.length - 1;

        while (left < right) {
            while (left < right && (nums[left] & 1) == 1) {
                left++;
            }

            while (left < right && (nums[right] & 1) == 0) {
                right--;
            }

            // 两端分别停在错位的偶数和奇数,交换后两侧各确认一个位置。
            if (left < right) {
                int value = nums[left];

                nums[left] = nums[right];
                nums[right] = value;
                left++;
                right--;
            }
        }

        return nums;
    }
}
func exchange(nums []int) []int {
    left := 0
    right := len(nums) - 1

    for left < right {
        for left < right && nums[left]&1 == 1 {
            left++
        }
        for left < right && nums[right]&1 == 0 {
            right--
        }
        // 两端分别停在错位的偶数和奇数,交换后两侧各确认一个位置。
        if left < right {
            nums[left], nums[right] = nums[right], nums[left]
            left++
            right--
        }
    }
    return nums
}

复杂度分析

  • 时间复杂度:$O(n)$,两个指针始终向中间移动,不会回退,合计移动次数为线性数量。
  • 空间复杂度:$O(1)$,只维护边界和交换所需的常量变量,原地完成分区。

关键点总结

[!green]

  • 左侧已确认全奇,右侧已确认全偶,所有操作都围绕未确定的中间区间进行。
  • 两端分别寻找错位元素,一次交换同时修复两个位置。
  • 题目不要求稳定顺序,是可以直接首尾交换的前提。

易错点总结

[!yellow]

  • 内层扫描漏掉边界判断,全奇或全偶数组可能一直推进到越界位置。
  • 外层使用 left <= right,而内部操作仅在严格小于时推进,会在两指针相遇后无法结束。
  • 将奇数判断写成 x % 2 == 1,在 Java、Go 中会错判负奇数;可使用最低位或判断余数不等于零。
  • 内层扫描后不重新判断 left < right 就交换,可能干扰已经确认的分区。
  • 若要求同类元素保持原顺序,这段首尾交换代码不满足稳定性,不能把两种题型的要求混为一谈。

相似题目

题目 难度 关联与区别
905. 按奇偶排序数组 简单 划分方法相同,但原题偶数在前,本题奇数在前,判定方向相反。
补充题 174. 数组的稳定奇偶划分 中等 变形题要求同类元素保持原顺序,本题没有稳定性要求,可使用两端交换。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/87028700
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!