冒泡排序、选择排序、插入排序、快速排序

目录

[1. 冒泡排序 (Bubble Sort)](#1. 冒泡排序 (Bubble Sort))

算法思路分析

代码实现

复杂度分析

[2. 选择排序 (Selection Sort)](#2. 选择排序 (Selection Sort))

算法思路分析

代码实现

复杂度分析

[3. 插入排序 (Insertion Sort)](#3. 插入排序 (Insertion Sort))

算法思路分析

代码实现

复杂度分析

[4. 快速排序 (Quick Sort)](#4. 快速排序 (Quick Sort))

算法思路分析

代码实现

复杂度分析

[5. 归并排序 (Merge Sort)](#5. 归并排序 (Merge Sort))

算法思路分析

代码实现

复杂度分析

[6. 堆排序 (Heap Sort)](#6. 堆排序 (Heap Sort))

算法思路分析

代码实现

复杂度分析

算法性能对比总结


排序算法稳定性解释:

  • 稳定排序:相等的元素在排序后保持原有的相对顺序
  • 不稳定排序:相等的元素在排序后可能改变原有的相对顺序

比如:

原始顺序是 4₁, 2, 4₂, 1, 3,排序后变成了 1, 2, 3, 4₁, 4₂

  • 原来:4₁ 在 4₂ 前面
  • 现在:4₁ 仍然在 4₂ 前面

这就是稳定排序

1. 冒泡排序 (Bubble Sort)

算法思路分析

冒泡排序是最简单的排序算法之一,其核心思想是:

  1. 相邻比较:比较相邻的两个元素,如果第一个比第二个大,则交换它们
  2. 逐步冒泡:每一轮比较后,最大的元素会"冒泡"到数组的末尾
  3. 重复执行:重复n-1轮,直到所有元素都排好序

代码实现

java 复制代码
/**
 * 冒泡排序算法
 * 
 * 算法思路:
 * 1. 比较相邻的两个元素,如果第一个比第二个大,则交换它们
 * 2. 对每一对相邻元素执行同样的操作,从开始第一对到结尾的最后一对
 * 3. 重复以上步骤,每次都能将当前最大的元素"冒泡"到数组末尾
 * 4. 重复n-1轮,直到没有任何一对数字需要比较
 */
public static void bubbleSort(int[] arr) {
    int n = arr.length;
    
    // 外层循环控制排序轮数,总共需要n-1轮
    for (int i = 0; i < n - 1; i++) {
        // 设置一个标志位,用于优化算法
        // 如果某一轮没有发生交换,说明数组已经有序,可以提前退出
        boolean swapped = false;
        
        // 内层循环进行相邻元素比较和交换
        // 每轮排序后,最大的元素会"冒泡"到数组末尾
        // 因此内层循环的范围可以逐渐缩小
        for (int j = 0; j < n - 1 - i; j++) {
            // 如果前一个元素大于后一个元素,则交换它们
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        
        // 如果这一轮没有发生交换,说明数组已经有序,可以提前退出
        if (!swapped) {
            break;
        }
    }
}

复杂度分析

  • 时间复杂度:O(n²) - 最坏和平均情况都是O(n²)
  • 空间复杂度:O(1) - 只需要一个临时变量
  • 稳定性:稳定排序

2. 选择排序 (Selection Sort)

算法思路分析

选择排序的核心思想是:

  1. 找最小值:在未排序序列中找到最小元素
  2. 交换位置:将找到的最小元素与未排序序列的第一个元素交换位置
  3. 重复执行:重复上述过程,直到所有元素都排好序

代码实现

java 复制代码
/**
 * 选择排序算法
 * 
 * 算法思路:
 * 1. 在未排序序列中找到最小元素,存放到排序序列的起始位置
 * 2. 然后,再从剩余未排序元素中继续寻找最小元素
 * 3. 重复第二步,直到所有元素均排序完毕
 */
public static void selectionSort(int[] arr) {
    int n = arr.length;
    
    // 外层循环控制排序轮数
    for (int i = 0; i < n - 1; i++) {
        // 假设当前位置的元素是最小的
        int minIndex = i;
        
        // 在未排序序列中寻找最小元素
        for (int j = i + 1; j < n; j++) {
            // 如果找到更小的元素,更新最小元素的索引
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        
        // 将找到的最小元素与当前位置的元素交换
        if (minIndex != i) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

复杂度分析

  • 时间复杂度:O(n²) - 无论最好、最坏、平均情况都是O(n²)
  • 空间复杂度:O(1) - 只需要一个临时变量
  • 稳定性:不稳定排序

3. 插入排序 (Insertion Sort)

算法思路分析

插入排序的核心思想是:

  1. 构建有序序列:将第一个元素看作已排序序列
  2. 逐个插入:对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
  3. 重复执行:重复上述过程,直到所有元素都插入到有序序列中

代码实现

java 复制代码
/**
 * 插入排序算法
 * 
 * 算法思路:
 * 1. 从第一个元素开始,该元素可以认为已经被排序
 * 2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
 * 3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
 * 4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
 * 5. 将新元素插入到该位置后
 * 6. 重复步骤2~5
 */
public static void insertionSort(int[] arr) {
    int n = arr.length;
    
    // 从第二个元素开始,逐个插入到已排序序列中
    for (int i = 1; i < n; i++) {
        // 保存当前要插入的元素
        int key = arr[i];
        
        // j指向已排序序列的最后一个元素
        int j = i - 1;
        
        // 从后向前扫描已排序序列,寻找插入位置
        // 如果已排序序列中的元素大于key,则将其向后移动
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        // 找到插入位置,将key插入
        arr[j + 1] = key;
    }
}

复杂度分析

  • 时间复杂度:O(n²) - 最坏和平均情况,最好情况O(n)(已排序数组)
  • 空间复杂度:O(1) - 只需要一个临时变量
  • 稳定性:稳定排序

4. 快速排序 (Quick Sort)

算法思路分析

快速排序是一种高效的排序算法,使用分治策略:

  1. 选择基准:从数组中选择一个基准元素(通常是最后一个元素)
  2. 分区操作:将数组分为两部分,左边都小于基准,右边都大于基准
  3. 递归排序:对左右两部分分别进行快速排序
  4. 合并结果:由于分区操作,合并时数组已经有序

代码实现

java 复制代码
/**
 * 快速排序算法
 * 
 * 算法思路:
 * 1. 选择一个基准元素(通常是最后一个元素)
 * 2. 将数组分为两部分:左边都小于基准,右边都大于基准
 * 3. 对左右两部分分别递归执行快速排序
 * 4. 由于分区操作,合并时数组已经有序
 */
public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

/**
 * 快速排序的递归实现
 * @param arr 待排序数组
 * @param low 起始索引
 * @param high 结束索引
 */
private static void quickSort(int[] arr, int low, int high) {
    // 递归终止条件:当起始索引小于结束索引时
    if (low < high) {
        // 进行分区操作,返回基准元素的最终位置
        int pi = partition(arr, low, high);
        
        // 递归排序基准元素左边的子数组
        quickSort(arr, low, pi - 1);
        // 递归排序基准元素右边的子数组
        quickSort(arr, pi + 1, high);
    }
}

/**
 * 分区操作:将数组分为两部分
 * @param arr 待分区数组
 * @param low 起始索引
 * @param high 结束索引
 * @return 基准元素的最终位置
 */
private static int partition(int[] arr, int low, int high) {
    // 选择最后一个元素作为基准
    int pivot = arr[high];
    
    // i指向小于基准元素的最后一个位置
    int i = low - 1;
    
    // 遍历数组,将小于基准的元素移到左边
    for (int j = low; j < high; j++) {
        // 如果当前元素小于基准
        if (arr[j] < pivot) {
            i++;
            // 交换元素
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    
    // 将基准元素放到正确的位置
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    // 返回基准元素的最终位置
    return i + 1;
}

复杂度分析

  • 时间复杂度:O(n log n) - 平均情况,最坏情况O(n²)
  • 空间复杂度:O(log n) - 递归调用栈的深度
  • 稳定性:不稳定排序

5. 归并排序 (Merge Sort)

算法思路分析

归并排序是一种稳定的排序算法,使用分治策略:

  1. 分解:将数组分成两个子数组,递归地对子数组进行排序
  2. 合并:将两个已排序的子数组合并成一个有序数组
  3. 递归执行:重复上述过程,直到所有元素都排好序

代码实现

java 复制代码
/**
 * 归并排序算法
 * 
 * 算法思路:
 * 1. 将数组分成两个子数组,递归地对子数组进行排序
 * 2. 将两个已排序的子数组合并成一个有序数组
 * 3. 重复上述过程,直到所有元素都排好序
 */
public static void mergeSort(int[] arr) {
    mergeSort(arr, 0, arr.length - 1);
}

/**
 * 归并排序的递归实现
 * @param arr 待排序数组
 * @param left 左边界
 * @param right 右边界
 */
private 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);
    }
}

/**
 * 合并两个已排序的子数组
 * @param arr 原数组
 * @param left 左边界
 * @param mid 中间位置
 * @param right 右边界
 */
private static void merge(int[] arr, int left, int mid, int right) {
    // 计算两个子数组的长度
    int n1 = mid - left + 1;
    int n2 = right - mid;
    
    // 创建临时数组来存储两个子数组
    int[] leftArray = new int[n1];
    int[] rightArray = new int[n2];
    
    // 复制数据到临时数组
    for (int i = 0; i < n1; i++) {
        leftArray[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        rightArray[j] = arr[mid + 1 + j];
    }
    
    // 合并两个子数组
    int i = 0, j = 0, k = left;
    
    // 比较两个子数组的元素,将较小的元素放入原数组
    while (i < n1 && j < n2) {
        if (leftArray[i] <= rightArray[j]) {
            arr[k] = leftArray[i];
            i++;
        } else {
            arr[k] = rightArray[j];
            j++;
        }
        k++;
    }
    
    // 将剩余的元素复制到原数组
    while (i < n1) {
        arr[k] = leftArray[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = rightArray[j];
        j++;
        k++;
    }
}

复杂度分析

  • 时间复杂度:O(n log n) - 无论最好、最坏、平均情况都是O(n log n)
  • 空间复杂度:O(n) - 需要额外的数组来存储合并结果
  • 稳定性:稳定排序

6. 堆排序 (Heap Sort)

算法思路分析

堆排序是一种基于堆数据结构的排序算法:

  1. 构建堆:将数组构建成最大堆(或最小堆)
  2. 提取根节点:将堆的根节点(最大值)与最后一个元素交换
  3. 调整堆:对剩余的n-1个元素重新构建堆
  4. 重复执行:重复上述过程,直到所有元素都排好序

代码实现

java 复制代码
/**
 * 堆排序算法
 * 
 * 算法思路:
 * 1. 将数组构建成最大堆
 * 2. 将堆的根节点(最大值)与最后一个元素交换
 * 3. 对剩余的n-1个元素重新构建堆
 * 4. 重复上述过程,直到所有元素都排好序
 */
public static void heapSort(int[] arr) {
    int n = arr.length;
    
    // 构建最大堆
    // 从最后一个非叶子节点开始,自底向上构建堆
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
    
    // 逐个提取堆顶元素(最大值)
    for (int i = n - 1; i > 0; i--) {
        // 将堆顶元素(最大值)与最后一个元素交换
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        
        // 对剩余的i个元素重新构建最大堆
        heapify(arr, i, 0);
    }
}

/**
 * 将以root为根的子树调整为最大堆
 * @param arr 数组
 * @param n 堆的大小
 * @param root 根节点的索引
 */
private static void heapify(int[] arr, int n, int root) {
    // 假设根节点是最大的
    int largest = root;
    
    // 计算左子节点的索引
    int left = 2 * root + 1;
    // 计算右子节点的索引
    int right = 2 * root + 2;
    
    // 如果左子节点存在且大于根节点
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    
    // 如果右子节点存在且大于当前最大值
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    
    // 如果最大值不是根节点
    if (largest != root) {
        // 交换根节点和最大值
        int temp = arr[root];
        arr[root] = arr[largest];
        arr[largest] = temp;
        
        // 递归调整被交换的子树
        heapify(arr, n, largest);
    }
}

复杂度分析

  • 时间复杂度:O(n log n) - 无论最好、最坏、平均情况都是O(n log n)
  • 空间复杂度:O(1) - 原地排序
  • 稳定性:不稳定排序

堆排序原理参考:堆排序步骤推演-CSDN博客


算法性能对比总结

排序算法 时间复杂度 空间复杂度 稳定性 适用场景
冒泡排序 O(n²) O(1) 稳定 小数据量
选择排序 O(n²) O(1) 不稳定 小数据量,交换次数少
插入排序 O(n²) O(1) 稳定 小数据量,基本有序
快速排序 O(n log n) O(log n) 不稳定 大数据量,实际应用
归并排序 O(n log n) O(n) 稳定 大数据量,要求稳定
堆排序 O(n log n) O(1) 不稳定 大数据量,原地排序

使用建议:小数据量(n < 50):使用插入排序,代码简单且效率较高;大数据量:优先使用快速排序,平均性能最好;要求稳定性:使用归并排序;内存受限:使用堆排序,原地排序;

推荐:Java的Arrays.sort(),此方法已经为我们提供了高效的排序实现