十大经典排序算法汇总(冒泡、选择、插入等)

目录

冒泡排序 - 像气泡一样往上冒

算法步骤:

特点:

选择排序 - 每次都选最小的放前面

算法步骤:

特点:

插入排序 - 像理牌一样插入

算法步骤:

特点:

希尔排序 - 分组插入排序

算法步骤:

特点:

归并排序 - 分而治之的典范

算法步骤:

特点:

快速排序 - 找个基准分两边

算法步骤:

特点:

堆排序 - 用树结构排序

算法步骤:

特点:

计数排序 - 数数排序法

算法步骤:

特点:

桶排序 - 分组再排序

算法步骤:

特点:

基数排序 - 按位排序

算法步骤:

特点:

总结对比

选择建议:

冒泡排序 - 像气泡一样往上冒

一句话理解:就像水里的气泡,小的往上冒,大的往下沉。

算法步骤:

  1. 从第一个元素开始,比较相邻的两个元素

  2. 如果前面的比后面的大,就交换它们的位置

  3. 对每一对相邻元素重复这个过程,这样最大的元素就会"冒"到最后面

  4. 重复上述步骤,每次忽略已经排好序的尾部元素

生动比喻:就像排队时,个子高的人不断往后换,直到最高的人站在最后面。

public static int[] bubbleSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        boolean isSorted = true; // 优化:如果一轮没有交换,说明已经排好序
        for (int j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换位置
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                isSorted = false;
            }
        }
        if (isSorted) break; // 提前结束
    }
    return arr;
}

特点:

  • 稳定排序(相等元素相对位置不变)

  • 简单易懂但效率低

  • 最佳情况:O(n) - 已经排好序时

  • 最差情况:O(n²) - 完全逆序时

选择排序 - 每次都选最小的放前面

一句话理解:就像打牌时,每次都从手里挑最小的牌放到前面。

算法步骤:

  1. 在未排序序列中找到最小元素

  2. 把它放到已排序序列的末尾(也就是与当前位置交换)

  3. 重复这个过程,直到所有元素都排好序

生动比喻:就像整理书架,每次都找最薄的书放到最左边。

public static int[] selectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int minIndex = i; // 假设当前位置是最小的
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j; // 找到更小的
            }
        }
        // 把最小的交换到前面
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

特点:

  • 不稳定排序

  • 无论什么情况都是O(n²)

  • 交换次数少(最多n-1次交换)

插入排序 - 像理牌一样插入

一句话理解:就像打扑克时,每摸一张新牌就把它插入到手牌中的正确位置。

算法步骤:

  1. 从第二个元素开始(第一个元素默认已排序)

  2. 把当前元素与前面已排序的元素比较

  3. 如果当前元素更小,就把前面的元素往后移

  4. 找到合适位置后插入当前元素

  5. 重复直到所有元素都插入正确位置

生动比喻:就像整理扑克牌,每次拿到新牌都插到合适位置。

public static int[] insertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int current = arr[i]; // 当前要插入的牌
        int j = i - 1; // 从已排序部分的最后一个开始比较
        // 把比当前元素大的都往后移
        while (j >= 0 && current < arr[j]) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = current; // 插入到正确位置
    }
    return arr;
}

特点:

  • 稳定排序

  • 对小规模数据或基本有序数据效率很高

  • 最佳情况:O(n) - 已经排好序时

希尔排序 - 分组插入排序

一句话理解:先大步伐分组排序,再小步伐精细排序。

算法步骤:

  1. 选择一个间隔序列(比如长度的一半)

  2. 按间隔分组,对每组进行插入排序

  3. 缩小间隔,重复分组排序

  4. 最后间隔为1时,就是普通的插入排序

生动比喻:就像先粗略整理书架(按大类分),再精细整理每类中的书。

public static int[] shellSort(int[] arr) {
    int n = arr.length;
    // 从大间隔开始,逐渐缩小
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // 对每个间隔分组进行插入排序
        for (int i = gap; i < n; i++) {
            int current = arr[i];
            int j = i;
            // 组内插入排序
            while (j >= gap && arr[j - gap] > current) {
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = current;
        }
    }
    return arr;
}

特点:

  • 不稳定排序

  • 插入排序的改进版

  • 时间复杂度取决于间隔序列的选择

归并排序 - 分而治之的典范

一句话理解:先把大问题拆成小问题,解决后再合并起来。

算法步骤:

  1. 把数组分成两半

  2. 分别对左右两半递归排序

  3. 合并两个已排序的子数组

生动比喻:就像合并两个已经排好序的名单,每次比较两个名单的第一个元素,取较小的。

public static int[] mergeSort(int[] arr) {
    if (arr.length <= 1) return arr; // 基础情况
    int mid = arr.length / 2;
    // 分成两半
    int[] left = Arrays.copyOfRange(arr, 0, mid);
    int[] right = Arrays.copyOfRange(arr, mid, arr.length);
    // 递归排序并合并
    return merge(mergeSort(left), mergeSort(right));
}

// 合并两个有序数组
private static int[] merge(int[] left, int[] right) {
    int[] result = new int[left.length + right.length];
    int i = 0, j = 0, k = 0;
    // 比较两个数组的元素,取较小的
    while (i < left.length && j < right.length) {
        if (left[i] <= right[j]) {
            result[k++] = left[i++];
        } else {
            result[k++] = right[j++];
        }
    }
    // 把剩余的元素加进去
    while (i < left.length) result[k++] = left[i++];
    while (j < right.length) result[k++] = right[j++];
    return result;
}

特点:

  • 稳定排序

  • 总是O(n log n)的时间复杂度

  • 需要额外空间

快速排序 - 找个基准分两边

一句话理解:选个"裁判",比裁判小的站左边,比裁判大的站右边,然后递归处理两边。

算法步骤:

  1. 选择一个基准元素(pivot)

  2. 把所有比基准小的放左边,比基准大的放右边

  3. 递归地对左右两部分快速排序

生动比喻:就像体育老师让学生按身高排队,选个中间身高的同学当基准。

public static void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        // 分区操作,返回基准位置
        int pivotIndex = partition(arr, low, high);
        // 递归排序左右两部分
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[high]; // 选最后一个元素作为基准
    int i = low - 1; // 较小元素的索引
    for (int j = low; j < high; j++) {
        // 当前元素小于等于基准
        if (arr[j] <= pivot) {
            i++;
            // 交换arr[i]和arr[j]
            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;
}

特点:

  • 不稳定排序

  • 平均情况很快,最坏情况较慢

  • 原地排序,不需要额外空间

堆排序 - 用树结构排序

一句话理解:先把数据建成"堆"这种树结构,然后每次取树根(最大或最小值)。

算法步骤:

  1. 构建最大堆(父节点总比子节点大)

  2. 把堆顶元素(最大值)与末尾元素交换

  3. 重新调整堆,使其满足堆性质

  4. 重复步骤2-3,直到堆大小为1

生动比喻:就像从一堆苹果中每次挑出最大的。

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;
        // 调整剩余元素的堆
        heapify(arr, i, 0);
    }
}

// 调整堆
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) {
        int swap = arr[i];
        arr[i] = arr[largest];
        arr[largest] = swap;
        // 递归调整受影响的子树
        heapify(arr, n, largest);
    }
}

特点:

  • 不稳定排序

  • 总是O(n log n)

  • 原地排序

计数排序 - 数数排序法

一句话理解:统计每个数字出现的次数,然后按顺序输出。

算法步骤:

  1. 找出数组中的最大值和最小值

  2. 创建计数数组,统计每个数字出现的次数

  3. 累加计数数组,确定每个数字的最终位置

  4. 从后往前遍历原数组,根据计数数组放到正确位置

生动比喻:就像老师统计每个分数段有多少学生,然后按分数段排队。

public static int[] countingSort(int[] arr) {
    if (arr.length == 0) return arr;
    // 找到最大值和最小值
    int max = arr[0], min = arr[0];
    for (int num : arr) {
        if (num > max) max = num;
        if (num < min) min = num;
    }
    // 创建计数数组
    int[] count = new int[max - min + 1];
    for (int num : arr) {
        count[num - min]++;
    }
    // 累加计数
    for (int i = 1; i < count.length; i++) {
        count[i] += count[i - 1];
    }
    // 构建结果数组
    int[] result = new int[arr.length];
    for (int i = arr.length - 1; i >= 0; i--) {
        result[count[arr[i] - min] - 1] = arr[i];
        count[arr[i] - min]--;
    }
    return result;
}

特点:

  • 稳定排序

  • 只能用于整数

  • 当数据范围不大时很快

桶排序 - 分组再排序

一句话理解:把数据分到多个桶里,每个桶单独排序,然后合并。

算法步骤:

  1. 设置一定数量的空桶

  2. 把数据分布到各个桶中

  3. 对每个非空桶进行排序

  4. 按顺序把各个桶中的元素合并

生动比喻:就像把商品按价格范围分到不同货架上,每个货架单独整理。

public static List<Integer> bucketSort(List<Integer> arr, int bucketSize) {
    if (arr.size() <= 1) return arr;
    // 找到最大值和最小值
    int max = Collections.max(arr);
    int min = Collections.min(arr);
    // 计算桶的数量
    int bucketCount = (max - min) / bucketSize + 1;
    List<List<Integer>> buckets = new ArrayList<>();
    // 初始化桶
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<>());
    }
    // 把元素分配到桶中
    for (int num : arr) {
        int bucketIndex = (num - min) / bucketSize;
        buckets.get(bucketIndex).add(num);
    }
    // 对每个桶排序并合并结果
    List<Integer> result = new ArrayList<>();
    for (List<Integer> bucket : buckets) {
        if (!bucket.isEmpty()) {
            Collections.sort(bucket); // 可以用其他排序算法
            result.addAll(bucket);
        }
    }
    return result;
}

特点:

  • 稳定排序

  • 数据分布均匀时效率高

  • 需要额外空间

基数排序 - 按位排序

一句话理解:先按个位排序,再按十位排序,再按百位排序...

算法步骤:

  1. 找到最大数字,确定需要排序的轮数(位数)

  2. 从最低位开始,对每一位进行稳定的排序(通常用计数排序)

  3. 重复直到最高位排序完成

生动比喻:就像整理学生档案,先按班级排,再按学号排。

public static int[] radixSort(int[] arr) {
    if (arr.length <= 1) return arr;
    // 找到最大值,确定位数
    int max = Arrays.stream(arr).max().getAsInt();
    int exp = 1; // 从个位开始
    while (max / exp > 0) {
        countingSortByDigit(arr, exp);
        exp *= 10; // 处理下一位:十位、百位...
    }
    return arr;
}

// 按指定位数进行计数排序
private static void countingSortByDigit(int[] arr, int exp) {
    int n = arr.length;
    int[] output = new int[n];
    int[] count = new int[10]; // 0-9
    // 统计每个数字出现的次数
    for (int i = 0; i < n; i++) {
        int digit = (arr[i] / exp) % 10;
        count[digit]++;
    }
    // 累加计数
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }
    // 构建输出数组(从后往前保持稳定性)
    for (int i = n - 1; i >= 0; i--) {
        int digit = (arr[i] / exp) % 10;
        output[count[digit] - 1] = arr[i];
        count[digit]--;
    }
    // 复制回原数组
    System.arraycopy(output, 0, arr, 0, n);
}

特点:

  • 稳定排序

  • 只能用于整数

  • 当位数不多时效率很高

总结对比

排序算法

最佳情况

平均情况

最差情况

空间复杂度

稳定性

适用场景

冒泡排序

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²)

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²)

O(n log n)

不稳定

通用、大数据

堆排序

O(n log n)

O(n log n)

O(n log n)

O(1)

不稳定

需要保证最差性能

计数排序

O(n + k)

O(n + k)

O(n + k)

O(k)

稳定

整数、范围小

桶排序

O(n + k)

O(n + k)

O(n²)

O(n + k)

稳定

数据分布均匀

基数排序

O(n × k)

O(n × k)

O(n × k)

O(n + k)

稳定

整数、位数少

选择建议:

  • 小数据:插入排序

  • 通用:快速排序

  • 需要稳定:归并排序

  • 整数且范围小:计数排序

  • 保证最差性能:堆排序

  • 大数据外部排序:归并排序

记住:没有最好的排序算法,只有最适合当前场景的算法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值