冒泡、插入、选择、归并、堆排序:从名字由来到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. 工作中用吗? 虽然我们很少手写排序,但理解这些算法有助于分析性能、选择合适的数据结构,也能在面试中脱颖而出。

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

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


相关推荐
yinke小琪3 小时前
面试官:谈谈为什么要拆分数据库?有哪些方法?
java·后端·面试
自由的疯3 小时前
java DWG文件转图片
java·后端·架构
小兔崽子去哪了3 小时前
EasyExcel 使用
java·excel
青云交3 小时前
Java 大视界 -- Java 大数据机器学习模型的对抗攻击与防御技术研究
java·机器学习模型·对抗攻击·java 大数据·防御技术·对抗训练·i - fgsm
程序员小假3 小时前
请介绍类加载过程,什么是双亲委派模型?
java·后端
汤姆yu3 小时前
基于springboot的家具商城销售系统
java·spring boot·后端
红尘客栈23 小时前
K8s-kubeadmin 1.28安装
java·网络·kubernetes
Larry_Yanan4 小时前
QML学习笔记(三十一)QML的Flow定位器
java·前端·javascript·笔记·qt·学习·ui
灰灰老师4 小时前
七种排序算法比较与选择[Python ]
java·算法·排序算法