1. 简介

本文将深入讲解原地排序算法(In-Place Sorting Algorithm)的工作原理。我们不仅会从概念上剖析其核心思想,还会通过伪代码对比、Java 实现以及常见算法举例,帮助你真正理解“原地”这一关键特性在实际开发中的意义和优势。

对于有经验的开发者来说,这类知识不仅是面试常客,更是优化性能时绕不开的底层逻辑。


2. 什么是原地算法

原地算法(in-place algorithm) 是指在执行过程中不需要额外辅助数据结构来处理输入数据的算法。换句话说,它直接在原始输入数据上进行操作,用输出结果覆盖原始输入。

⚠️ 注意:这里的“不额外使用空间”是理想化说法。实际上,算法可能会使用少量、非恒定的临时变量(比如循环计数器或交换用的 temp),但这些空间复杂度通常为 O(1) 或最多 O(log n) —— 只要不是线性增长(O(n)),一般仍视为原地操作。

举个例子:

  • ✅ 原地反转数组:只用一个 temp 变量完成元素交换
  • ❌ 非原地反转:新建一个数组复制反向内容,空间开销 O(n)

因此判断是否为原地操作,关键看额外空间是否随输入规模线性增长


3. 伪代码对比:原地 vs 非原地

我们以“反转数组”为例,直观对比两种实现方式的差异。

3.1 原地反转(In-Place)

核心思路:首尾两两交换,中间用一个临时变量暂存值。只需要遍历一半数组即可完成。

reverseInPlace(array A[n])
    for i from 0 to n/2
        temp = A[i]
        A[i] = A[n - 1 - i]
        A[n - 1 - i] = temp

📌 特点:

  • ✅ 额外空间:O(1) —— 仅 tempi
  • ✅ 时间复杂度:O(n)
  • ✅ 空间效率高,适合大数组场景

下图展示了原地反转的过程(原数组被逐步覆盖):

illustration

3.2 非原地反转(Out-of-Place)

做法更直观:创建一个新数组,按反向顺序复制元素,最后返回新数组。

reverseOutOfPlace(array A[n])
    create new array B[n]
    for i from 0 to n - 1
        B[i] = A[n - 1 - i]
    delete A
    return B

📌 特点:

  • ❌ 额外空间:O(n) —— 新建了完整副本
  • ✅ 时间复杂度:O(n)
  • ⚠️ 创建/销毁数组开销大,GC 压力增加

适合对原始数据有保留需求的场景,但一般不推荐用于性能敏感路径。

过程示意如下:

process


4. Java 实现示例

4.1 原地反转实现

public static int[] reverseInPlace(int A[]) {
    int n = A.length;
    for (int i = 0; i < n / 2; i++) {
        int temp = A[i];
        A[i] = A[n - 1 - i];
        A[n - 1 - i] = temp;
    }
    return A;
}

📌 踩坑提醒:该方法会修改原始数组!调用前务必确认是否可变。

测试用例:

@Test
void givenArray_whenInPlaceSort_thenReversed() {
    int[] input = {1, 2, 3, 4, 5, 6, 7};
    int[] expected = {7, 6, 5, 4, 3, 2, 1};
    assertArrayEquals("the two arrays are not equal", expected,
      InOutSort.reverseInPlace(input));
}

4.2 非原地反转实现

public static int[] reverseOutOfPlace(int A[]) {
    int n = A.length;
    int[] B = new int[n];
    for (int i = 0; i < n; i++) {
        B[n - i - 1] = A[i];
    }
    return B;
}

📌 优点:原始数组不受影响,函数更“纯”。

测试用例:

@Test
void givenArray_whenOutOfPlaceSort_thenReversed() {
    int[] input = {1, 2, 3, 4, 5, 6, 7};
    int[] expected = {7, 6, 5, 4, 3, 2, 1};
    assertArrayEquals("the two arrays are not equal", expected,
      InOutSort.reverseOutOfPlace(input));
}

5. 常见原地排序算法举例

以下是一些典型的原地排序算法,它们的空间复杂度均控制在 O(log n) 或 O(1):

算法 是否原地 空间复杂度 备注
✅ 插入排序(Insertion Sort) O(1) 小数据集高效
✅ 冒泡排序(Bubble Sort) O(1) 教学用途为主
✅ 快速排序(Quicksort) O(log n) 平均性能优秀,递归栈开销
✅ 堆排序(Heapsort) O(1) 最坏情况 O(n log n)
✅ 希尔排序(Shell Sort) O(1) 插入排序的改进版
✅ 梳排序(Comb Sort) O(1) 冒泡排序的优化

⚠️ 注意:虽然快速排序使用递归导致栈空间 O(log n),但由于该空间不随输入成线性增长,仍被归类为原地算法。

📌 延伸建议:


6. 总结

本文系统性地介绍了原地算法的核心概念,并通过数组反转这一简单但典型的例子,展示了原地与非原地实现的本质区别:

  • ✅ 原地操作节省内存,适合大规模数据处理
  • ❌ 非原地操作安全但代价高,需权衡使用场景
  • 🔧 实际开发中应优先考虑原地优化,尤其是在资源受限环境

所有示例代码已托管至 GitHub,欢迎参考:

👉 https://github.com/eugenp/tutorials/tree/master/algorithms-modules/algorithms-sorting-2


原始标题:Guide to In-Place Sorting Algorithm Works with a Java Implementation