目录

题目描述

给定一个元素互不相同的整数数组 nums,求将数组排成递增顺序所需的最小交换次数。

面试时必须先确认交换规则:

  1. 可以交换任意两个位置的元素。
  2. 只能交换相邻位置的元素。

题意分析

两种规则只差“相邻”两个字,解法却完全不同:

  • 任意交换:将位置关系拆成置换环。长度为 L 的环需要 L - 1 次交换。
  • 相邻交换:每次交换只能消除一个逆序对,答案就是逆序对数。

例如 nums = [4, 3, 2, 1]:任意交换只需 2 次,相邻交换需要 6 次。

本题按元素互不相同处理。如果允许重复元素,相邻交换的结论仍然成立,但任意交换时需要额外确定相同值与目标位置的映射。

解法一:任意交换——置换环

核心思路

将下标按元素值排序。排序后 idx[i] 表示:最终位于位置 i 的元素,当前在位置 idx[i]

这个映射会形成若干个置换环。对于长度为 L 的环,每次把一个元素换到正确位置,恰好需要 L - 1 次。

解题步骤

  1. 创建下标数组 idx = [0, 1, ..., n - 1],按对应元素值升序排序。
  2. 使用 visited 标记已访问位置。
  3. 从每个未访问位置出发,沿 idx 统计环长 size
  4. 每个环对答案贡献 size - 1

代码实现

class Solution {
    public int minSwaps(int[] nums) {
        int n = nums.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; i++) {
            idx[i] = i;
        }
        Arrays.sort(idx, (a, b) -> Integer.compare(nums[a], nums[b]));

        boolean[] visited = new boolean[n];
        int swaps = 0;
        for (int i = 0; i < n; i++) {
            if (visited[i] || idx[i] == i) {
                continue;
            }

            int size = 0;
            for (int j = i; !visited[j]; j = idx[j]) {
                visited[j] = true;
                size++;
            }
            swaps += size - 1;
        }
        return swaps;
    }
}
func minSwaps(nums []int) int {
    idx := make([]int, len(nums))
    for i := range idx {
        idx[i] = i
    }
    sort.Slice(idx, func(i, j int) bool {
        return nums[idx[i]] < nums[idx[j]]
    })

    visited := make([]bool, len(nums))
    swaps := 0
    for i := range idx {
        if visited[i] || idx[i] == i {
            continue
        }

        size := 0
        for j := i; !visited[j]; j = idx[j] {
            visited[j] = true
            size++
        }
        swaps += size - 1
    }
    return swaps
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,排序占主导。
  • 空间复杂度:$O(n)$。

关键点总结

  • 长度为 L 的置换环最少需要 L - 1 次交换。
  • 已经归位的元素是长度为 1 的环,贡献 0。
  • 如果数组本身是 0..n-1 的排列,可以省去排序,时间降为 $O(n)$。

解法二:相邻交换——逆序对

核心思路

逆序对是满足 i < jnums[i] > nums[j] 的下标对。

一次相邻交换只会改变这两个元素的前后关系,最多消除一个逆序对,所以至少需要“逆序对数”次交换。冒泡排序又能恰好用这么多次完成排序。

因此,最小相邻交换次数 = 逆序对数。用归并排序可在 $O(n \log n)$ 时间内统计。

解题步骤

  1. 递归排序左右两半,并统计各自内部的逆序对。
  2. 合并两个有序区间。
  3. arr[i] > arr[j] 时,左区间从 imid 的元素都与 arr[j] 构成逆序对,累加 mid - i + 1

代码实现

class Solution {
    public long minSwaps(int[] nums) {
        int[] arr = nums.clone();
        return mergeCount(arr, new int[arr.length], 0, arr.length - 1);
    }

    private long mergeCount(int[] arr, int[] tmp, int left, int right) {
        if (left >= right) {
            return 0L;
        }

        int mid = left + (right - left) / 2;
        long count = mergeCount(arr, tmp, left, mid)
                + mergeCount(arr, tmp, mid + 1, right);
        int i = left, j = mid + 1, k = left;

        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                tmp[k++] = arr[i++];
            } else {
                count += mid - i + 1;
                tmp[k++] = arr[j++];
            }
        }
        while (i <= mid) {
            tmp[k++] = arr[i++];
        }
        while (j <= right) {
            tmp[k++] = arr[j++];
        }
        System.arraycopy(tmp, left, arr, left, right - left + 1);
        return count;
    }
}
func minSwaps(nums []int) int64 {
    arr := append([]int(nil), nums...)
    tmp := make([]int, len(arr))
    return mergeCount(arr, tmp, 0, len(arr)-1)
}

func mergeCount(arr, tmp []int, left, right int) int64 {
    if left >= right {
        return 0
    }

    mid := left + (right-left)/2
    count := mergeCount(arr, tmp, left, mid) + mergeCount(arr, tmp, mid+1, right)
    i, j, k := left, mid+1, left

    for i <= mid && j <= right {
        if arr[i] <= arr[j] {
            tmp[k] = arr[i]
            i++
        } else {
            count += int64(mid - i + 1)
            tmp[k] = arr[j]
            j++
        }
        k++
    }
    for i <= mid {
        tmp[k] = arr[i]
        i++
        k++
    }
    for j <= right {
        tmp[k] = arr[j]
        j++
        k++
    }
    copy(arr[left:right+1], tmp[left:right+1])
    return count
}

复杂度分析

  • 时间复杂度:$O(n \log n)$。
  • 空间复杂度:$O(n)$,递归栈为 $O(\log n)$。

关键点总结

  • 最小相邻交换次数等于逆序对数。
  • mid - i + 1 是归并阶段批量统计逆序对的关键。
  • 相等元素不构成逆序对,合并时要使用 arr[i] <= arr[j]
  • 逆序对最多有 $\frac{n(n-1)}{2}$ 个,计数结果要用 64 位整数。

解法对比

交换规则 核心模型 答案 时间复杂度
任意两个位置 置换环 所有环的 size - 1 之和 $O(n \log n)$
只能交换相邻元素 逆序对 逆序对总数 $O(n \log n)$

易错点总结

  • 不确认交换规则,直接套错模型。
  • 任意交换时忘记元素互不相同的前提。
  • 相邻交换用冒泡排序模拟,结果正确,但时间复杂度为 $O(n^2)$。
  • 归并时把相等元素误算成逆序对。
  • 使用 int 保存逆序对数,数组较长时可能溢出。

相似题目

题目 难度 考察点
315. 计算右侧小于当前元素的个数 困难 归并统计逆序对
493. 翻转对 困难 归并计数
765. 情侣牵手 困难 置换环
2471. 逐层排序二叉树所需的最少操作数 中等 置换环