题目描述

✅ 补充题 15. 自然数数组的排序

给定长度为 n 的数组 arr,它恰好包含 1 到 n 的所有整数,且每个整数只出现一次。

请将数组原地排列为升序并返回。要求时间复杂度为 O(n),额外空间复杂度为 O(1)。

示例 1:

输入:arr = [3,1,2,5,4]
输出:[1,2,3,4,5]

提示:

  • n = arr.length。
  • 数组是 1 到 n 的一个排列,不包含重复值或范围外的值。
  • 不能直接给每个位置赋值 arr[i] = i + 1 来构造答案,需要重新排列原有元素。
  • 需要直接修改原数组,不能依赖额外的长度为 n 的数组。

题意分析

输入数组恰好是 1 到 n 的一个排列,每个值出现一次,要求用线性时间、常数额外空间把它按升序排列。这里利用的是取值与位置的一一对应,不是对任意整数数组进行通用比较排序。

排序完成后,值 v 必须位于下标 v - 1。因此读到一个错位值时,可以直接知道它的最终位置,通过交换将它送回去,而无需继续比较大小。

解法:值归位的原地交换

核心思路

[!blue]

从左到右检查下标 i。若 arr[i] == i + 1,这个位置已经正确,直接继续;否则当前值应位于 arr[i] - 1,先保存这个目标下标,再交换当前位置与目标位置。

交换后,被送出的值已经到达正确位置,但换入 i 的值可能仍然错位,因此需要在同一个下标持续交换,直到当前位置也正确。只交换一次不能保证完成这一位置的处理。

每次有效交换都会新增至少一个正确位置。因为值互不相同,另一个位置不会再拿着相同的值来交换这个已归位位置;它以后被外层检查时,也会直接跳过。所以已经归位的值不会再次被移走。

正确位置的数量只增加,最多增加到 n,因此所有内层交换的总次数为线性,不是每个位置都要重新做一遍线性搜索。外层结束时每个下标都对应应有的值,数组自然已经有序。

目标下标要在改写当前值之前保存。Java 先备份目标位置的旧值再完成交换,Go 的多重赋值会先求右侧值,两种方式都不会丢失换入数据。

解题步骤

  1. 长度不足二时直接返回;否则依次检查每个下标。
  2. 当前值未归位时,保存它的目标下标 arr[i] - 1。
  3. 交换当前值与目标位置的值,继续检查换入当前位置的元素。
  4. 当前值正确后进入下一个下标,最后返回原数组。

代码实现

class Solution {
    public int[] sortArray(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }

        for (int i = 0; i < arr.length; i++) {
            // 换入当前位置的值仍可能错位,必须反复交换直到归位。
            while (arr[i] != i + 1) {
                // 先固定目标下标,避免交换过程中 arr[i] 被改写后再取值。
                int target = arr[i] - 1;
                int tmp = arr[target];

                arr[target] = arr[i];
                arr[i] = tmp;
            }
        }

        return arr;
    }
}
func sortArray(arr []int) []int {
    if len(arr) < 2 {
        return arr
    }

    for i := 0; i < len(arr); i++ {
        // 换入当前位置的值仍可能错位,必须反复交换直到归位。
        for arr[i] != i+1 {
            t := arr[i] - 1
            // 右侧先整体求值,交换是安全的。
            arr[i], arr[t] = arr[t], arr[i]
        }
    }

    return arr
}

复杂度分析

  • 时间复杂度:$O(n)$,每次交换至少永久归位一个元素,总交换次数不超过 n。
  • 空间复杂度:$O(1)$,只使用下标和交换临时变量。

关键点总结

[!green]

  • 值与下标一一对应,直接确定交换目标。
  • 用 while 持续处理换入的值,而不是只交换一次。
  • 目标下标先保存,避免改写 arr[i] 后再计算到错误位置。
  • 有序输入无需交换,但仍会完成一遍线性扫描。

易错点总结

[!yellow]

  • 目标下标不减一:值从一开始、下标从零开始,最大值应放在最后一个下标。
  • 只用一次判断交换:换入的值还可能错位,需要持续处理当前下标。
  • 覆盖当前值后再计算目标:会把新值当成原值,写入错误位置,应先固定目标下标。
  • 用于存在重复值的数组:可能反复交换相同值而无法推进,本方法依赖输入恰好是一个排列。

相似题目

题目 难度 关联与区别
41. 缺失的第一个正数 困难 都可把合法值交换到对应下标,本题每值唯一且都在1到n,缺失正数题还需防越界和重复循环。
补充题 101. 数组排序的最少交换次数 困难 位置映射形成置换环,本题直接归位排序,原题进一步统计最少交换次数。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/53157769
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!