冒泡、插入、选择、归并、堆排序:从名字由来到Java实现,一篇讲透

本文不涉及工作问题,纯粹是我在准备软考时,发现排序算法的概念已经模糊不清,于是重新系统地复习了一遍。整理成这篇笔记,既是巩固自己,也希望对正在学习算法的你有所帮助。

大家好,我是小杨。最近在复习软件设计师考试,翻到"排序算法"这一章时,突然发现很多原本熟悉的算法,名字还记得,但细节却模模糊糊了。

于是,我决定静下心来,把这五种最经典的排序算法------冒泡排序、插入排序、选择排序、归并排序、堆排序------重新梳理一遍。不仅写了 Java 实现,还加了详细注释,并深入思考了它们"名字"的由来。

希望这篇笔记能帮你理清思路,也欢迎收藏、转发,一起温故知新!


🔍 为什么这些算法叫这个名字?

在看代码之前,我们先来聊聊一个有趣的问题:这些算法的名字是怎么来的?

理解名字的由来,能让我们更直观地记住它们的"行为特征"。

1. 冒泡排序 (Bubble Sort)

  • 名字由来:大的元素像"气泡"一样从底部慢慢"冒"到顶部。
  • 核心动作:每一轮比较相邻元素,较大的"上浮"到末尾。
  • 类比:就像一杯水中的大气泡不断上升到水面。

2. 插入排序 (Insertion Sort)

  • 名字由来:将未排序的元素"插入"到已排序部分的正确位置。
  • 核心动作:像打扑克牌时整理手牌,每次把新牌插入到合适位置。
  • 类比:整理扑克牌,逐个插入。

3. 选择排序 (Selection Sort)

  • 名字由来:每一轮都在未排序部分"选择"最小(或最大)的元素。
  • 核心动作:找到最小值,与未排序部分的第一个元素交换。
  • 类比:从一堆书中找最薄的一本,放到最前面。

4. 归并排序 (Merge Sort)

  • 名字由来:"归并"即"合并",将两个有序子数组合并成一个。
  • 核心动作:分治法 ------ 先分再合,关键在"合并"。
  • 词源:来自英文 "Merge",意为"融合、合并"。

5. 堆排序 (Heap Sort)

  • 名字由来:利用"堆"(Heap)这种数据结构进行排序。
  • 核心动作:构建最大堆,不断将堆顶最大值移到末尾。
  • 注意:"堆"在这里是数据结构,不是内存中的"堆区"。

📌 小结:名字背后的逻辑

算法 名字关键词 核心动作
冒泡排序 气泡上浮 大元素"冒"到末尾
插入排序 插入 将元素插入正确位置
选择排序 选择 选择最小/最大元素
归并排序 归并(合并) 合并两个有序数组
堆排序 堆(数据结构) 利用堆的性质排序

🔧 一、冒泡排序(Bubble Sort)

📌 算法思想

重复遍历数组,比较相邻元素,若顺序错误则交换,直到没有需要交换的元素。

⏱️ 时间复杂度

  • 最坏:O(n²)
  • 最好:O(n)(加优化标志)
  • 平均:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:✅ 稳定

🖼️ 动图演示

java 复制代码
public class BubbleSort {
    /**
     * 冒泡排序主方法
     * 原理:重复遍历数组,比较相邻元素,较大者"冒泡"到末尾
     */
    public static void bubbleSort(int[] arr) {
        int n = arr.length; // 获取数组长度
        boolean swapped;    // 标志位:记录本轮是否发生交换

        // 外层循环:控制排序轮数,共进行 n-1 轮
        for (int i = 0; i < n - 1; i++) {
            swapped = false; // 每轮开始前重置标志位

            // 内层循环:从头到未排序部分的末尾进行比较
            // 每轮后,最大值已确定,所以范围是 n-i-1
            for (int j = 0; j < n - i - 1; j++) {
                // 如果当前元素大于下一个元素,则交换
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1); // 交换相邻元素
                    swapped = true;      // 标记发生了交换
                }
            }

            // 优化:如果某一轮没有发生任何交换,说明数组已经有序,提前结束
            if (!swapped) break;
        }
    }

    /**
     * 交换数组中两个位置的元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        bubbleSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

🔧 二、插入排序(Insertion Sort)

📌 算法思想

将数组分为"已排序"和"未排序"两部分,逐个将未排序元素插入到已排序部分的正确位置。

⏱️ 时间复杂度

  • 最坏:O(n²)
  • 最好:O(n)(接近有序)
  • 平均:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:✅ 稳定

🖼️ 动图演示

java 复制代码
public class InsertionSort {
    /**
     * 插入排序主方法
     * 原理:将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置
     */
    public static void insertionSort(int[] arr) {
        // 从第二个元素开始(索引1),因为第一个元素默认为已排序
        for (int i = 1; i < arr.length; i++) {
            int key = arr[i]; // 当前要插入的元素
            int j = i - 1;    // 已排序部分的最后一个元素索引

            // 在已排序部分从后向前扫描,找到 key 的插入位置
            // 条件:j >= 0 且 arr[j] > key,说明需要将 arr[j] 向后移动
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j]; // 将较大的元素向后移动一位
                j--; // 继续向前比较
            }

            // 找到插入位置,将 key 放入正确位置
            arr[j + 1] = key;
        }
    }

    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        insertionSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

🔧 三、选择排序(Selection Sort)

📌 算法思想

在未排序部分中选择最小元素,与未排序部分的第一个元素交换。

⏱️ 时间复杂度

  • 所有情况:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:❌ 不稳定(可能改变相等元素的相对顺序)

🖼️ 动图演示

java 复制代码
public class SelectionSort {
    /**
     * 选择排序主方法
     * 原理:在未排序部分中找到最小元素,与未排序部分的第一个元素交换
     */
    public static void selectionSort(int[] arr) {
        int n = arr.length;

        // 外层循环:控制已排序部分的边界
        // i 表示当前未排序部分的起始位置
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i; // 假设当前位置的元素是最小的

            // 内层循环:在未排序部分(i+1 到 n-1)中寻找真正的最小值
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j; // 更新最小值的索引
                }
            }

            // 将找到的最小值与未排序部分的第一个元素交换
            swap(arr, i, minIndex);
        }
    }

    /**
     * 交换数组中两个位置的元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11, 90};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        selectionSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

🔧 四、归并排序(Merge Sort)

📌 算法思想

分治法:先递归分割数组,再合并两个有序子数组。

⏱️ 时间复杂度

  • 所有情况:O(n log n)
  • 空间复杂度:O(n)
  • 稳定性:✅ 稳定
  • 适合大数据量 ✅

🖼️ 动图演示

java 复制代码
public class MergeSort {
    /**
     * 归并排序主方法(递归实现)
     * 原理:分治法 ------ 分割、递归排序、合并
     */
    public static void mergeSort(int[] arr, int left, int right) {
        // 递归终止条件:当子数组只有一个或零个元素时停止
        if (left < right) {
            // 计算中点,避免整数溢出
            int mid = left + (right - left) / 2;

            // 递归排序左半部分
            mergeSort(arr, left, mid);
            // 递归排序右半部分
            mergeSort(arr, mid + 1, right);

            // 将两个已排序的子数组合并成一个有序数组
            merge(arr, left, mid, right);
        }
    }

    /**
     * 合并两个已排序的子数组
     * arr[left..mid] 和 arr[mid+1..right]
     */
    private static void merge(int[] arr, int left, int mid, int right) {
        int n1 = mid - left + 1; // 左子数组长度
        int n2 = right - mid;    // 右子数组长度

        // 创建临时数组存储左右子数组
        int[] L = new int[n1];
        int[] R = new int[n2];

        // 复制数据到临时数组
        for (int i = 0; i < n1; i++) {
            L[i] = arr[left + i];
        }
        for (int j = 0; j < n2; j++) {
            R[j] = arr[mid + 1 + j];
        }

        int i = 0, j = 0, k = left; // k 是原数组的指针

        // 合并过程:比较左右数组元素,较小者放入原数组
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k++] = L[i++]; // 取左数组元素
            } else {
                arr[k++] = R[j++]; // 取右数组元素
            }
        }

        // 将剩余元素复制到原数组(如果有的话)
        while (i < n1) arr[k++] = L[i++];
        while (j < n2) arr[k++] = R[j++];
    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 43, 3, 9, 82, 10};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        mergeSort(arr, 0, arr.length - 1);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

🔧 五、堆排序(Heap Sort)

📌 算法思想

  1. 构建最大堆(父节点 ≥ 子节点)
  2. 将堆顶(最大值)与堆尾交换,堆大小减1
  3. 对新堆顶进行"堆化"(heapify)
  4. 重复直到堆大小为1

⏱️ 时间复杂度

  • 所有情况:O(n log n)
  • 空间复杂度:O(1)
  • 稳定性:❌ 不稳定
  • 原地排序 ✅
java 复制代码
public class HeapSort {
    /**
     * 堆排序主方法
     * 原理:构建最大堆,然后逐个将堆顶(最大值)与堆尾交换,并重新堆化
     */
    public static void heapSort(int[] arr) {
        int n = arr.length;

        // 第一步:构建最大堆
        // 从最后一个非叶子节点开始,向上调整每个节点
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }

        // 第二步:逐个提取堆顶元素
        // 将最大值(arr[0])与当前堆的最后一个元素交换
        // 然后对剩余元素重新堆化
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);         // 将最大值移到末尾
            heapify(arr, i, 0);      // 对前 i 个元素重新堆化,堆大小减1
        }
    }

    /**
     * 堆化操作:维护最大堆性质
     * 以索引 i 为根的子树进行堆化
     */
    private static void heapify(int[] arr, int n, int i) {
        int largest = i;        // 假设当前节点是最大的
        int left = 2 * i + 1;   // 左子节点索引
        int right = 2 * i + 2;  // 右子节点索引

        // 如果左子节点存在且大于根节点
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }

        // 如果右子节点存在且大于当前最大值
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }

        // 如果最大值不是根节点,则交换并递归堆化受影响的子树
        if (largest != i) {
            swap(arr, i, largest);
            heapify(arr, n, largest); // 递归堆化新的子树
        }
    }

    /**
     * 交换数组中两个位置的元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        heapSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

📊 五种排序算法对比总结

算法 最坏时间 平均时间 最好时间 空间复杂度 稳定性 适用场景
冒泡排序 O(n²) O(n²) O(n) O(1) 小数据量,教学演示
插入排序 O(n²) O(n²) O(n) O(1) 近乎有序,小数据
选择排序 O(n²) O(n²) O(n²) O(1) 内存敏感,小数据
归并排序 O(n log n) O(n log n) O(n log n) O(n) 大数据,稳定排序
堆排序 O(n log n) O(n log n) O(n log n) O(1) 大数据,原地排序

📌 博主建议与总结

  1. 动手实践 :建议大家把每段代码都亲手敲一遍,甚至用 System.out.println() 打印中间过程,理解会更深刻。
  2. 理解名字:算法的名字往往不是随意取的。记住"冒泡"="上浮","插入"="打牌","归并"="合并","堆"="树结构",能帮助你快速回忆算法逻辑。
  3. 软考重点:归并和堆排序的时间复杂度都是 O(n log n),是效率较高的算法;而冒泡、插入、选择是 O(n²),常用于小数据或教学。
  4. 工作中用吗? 虽然我们很少手写排序,但理解这些算法有助于分析性能、选择合适的数据结构,也能在面试中脱颖而出。

本文为个人软考复习笔记,如有错误,欢迎指正。希望这篇整理对你有帮助!

如果你觉得写得不错,欢迎点赞、收藏、分享,让更多人看到 ❤️


相关推荐
程序员清风12 小时前
推荐几个我常听的AI播客!
java·后端·面试
唐青枫12 小时前
Java Gradle 实战指南:从 Wrapper、Kotlin DSL 到 Spring Boot 多模块构建
java·kotlin·gradle
wuqingshun31415912 小时前
如何理解Spring Boot中的starter?
java·spring boot·后端
带刺的坐椅12 小时前
Solon TeamAgent 协作协议:从 SEQUENTIAL 流水线到 HIERARCHICAL 主管团队
java·ai·llm·agent·solon
wuqingshun31415912 小时前
SpringBoot是如何实现自动配置的
java·spring boot·后端
用户0537040314312 小时前
我的学习记录泛型
java
zhouhui00112 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
java·spring boot·redis·ai编程
JRedisX13 小时前
Java从零手写企业级内存数据库
java
萧瑟余晖13 小时前
JDK 20 新特性详解
java·开发语言
hdsoft_huge13 小时前
SpringBoot系列17:日志体系Logback配置,日志分割、脱敏、异常堆栈收集规范(生产级落地)
java·spring boot·logback