从基础到进阶——常见排序算法详解

引言

​ 排序(Sorting)是计算机科学中最基础、最重要的算法之一,广泛应用于数据处理、搜索优化等领域。本文将详细讲解几种常见排序算法的原理、实现,并通过可视化方式帮助理解排序过程。

排序算法分类

排序算法可以根据不同的方式进行分类:

  • 比较排序:冒泡排序、快速排序、插入排序、Shell排序、选择排序、堆排序、归并排序。
  • 非比较排序:桶排序、基数排序。

图片来源

img

image-20250313151655462

基础比较排序算法

1. 冒泡排序(Bubble Sort)

1.1 原理

冒泡排序是一种简单的交换排序,通过重复遍历数组,每次比较相邻元素并进行交换,使较大的元素逐步上浮。

1.2 代码实现(Java)

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        boolean swapped;
        for (int i = 0; i < n - 1; i++) {
            swapped = false;
            for (int j = 0; j < n - i - 1; 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;
        }
    }
}
算法原理:

image-20250313152813122

冒泡排序(Bubble Sort) 中,排序的趟数取决于数组的长度 n,一般情况下,需要 (n-1) 趟 排序才能完全有序。

适用场景:教学示例、小数据集验证

计算方式:

  • 第一趟:将最大的数“冒泡”到最后一位。
  • 第二趟:将第二大的数“冒泡”到倒数第二位。
  • 以此类推,经过 n−1n-1n−1 趟后,整个数组变得有序。

2. 快速排序(Quick Sort)

2.1 原理

快速排序采用分治策略,每次选择一个基准元素,将数组分成两部分,使得左边小于基准,右边大于基准,然后递归排序。

public class QuickSort {
    /**
     * 对数组进行快速排序
     * @param arr 要排序的数组
     * @param low 数组的起始索引
     * @param high 数组的结束索引
     */
    public 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++;
                // 交换 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;
    }
}

快速排序是不稳定的算法,它不满足稳定算法的定义。

算法稳定性 – 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

优化策略
1. 三数取中法选择基准
2. 尾递归优化
3. 小数组切换插入排序
4. 双轴快排(Dual-Pivot)

3. 插入排序(Insertion Sort)

3.1 原理

插入排序的核心思想是将数组分为已排序部分和未排序部分,每次从未排序部分取出一个元素,插入到正确位置。

3.2 代码实现(Java)

public class InsertionSort {
    // 插入排序方法,对整型数组进行排序
    public static void insertionSort(int[] arr) {
        // 从数组的第二个元素开始遍历数组
        for (int i = 1; i < arr.length; i++) {
            // 将当前元素存储在变量key中
            int key = arr[i];
            // 初始化j为当前元素的前一个元素的索引
            int j = i - 1;
            // 当j不小于0且前一个元素大于当前元素时,进行循环
            while (j >= 0 && arr[j] > key) {
                // 将前一个元素向后移动一位
                arr[j + 1] = arr[j];
                // 将j减1,继续向前比较
                j--;
            }
            // 将当前元素插入到正确的位置
            arr[j + 1] = key;
        }
    }
}

image-20250313155328863

4. Shell排序(Shell Sort)

4.1 原理

Shell排序是插入排序的改进版,使用**步长(gap)**对数组进行分组排序,逐步缩小步长,最终变为插入排序。

4.2 代码实现(Java)

public class ShellSort {
    // 希尔排序方法,对整型数组进行排序
    public static void shellSort(int[] arr) {
        // 获取数组长度
        int n = arr.length;
        // 初始化间隔 gap 为数组长度的一半,每次循环后 gap 减半,直到 gap 为 0
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 从间隔 gap 开始,对每个元素进行排序
            for (int i = gap; i < n; i++) {
                // 将当前元素存储在临时变量 temp 中
                int temp = arr[i];
                // 初始化 j 为当前元素的索引
                int j;
                // 当 j 不小于 gap 且 j-gap 位置的元素大于当前元素时,进行循环
                for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                    // 将 j-gap 位置的元素移动到 j 位置
                    arr[j] = arr[j - gap];
                }
                // 将当前元素插入到正确的位置
                arr[j] = temp;
            }
        }
    }
}

image-20250313160116507

5. 选择排序(Selection Sort)

5.1 原理

选择排序每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。

5.2 代码实现(Java)

public class SelectionSort {
    // 选择排序方法,对整型数组进行排序
    public static void selectionSort(int[] arr) {
        // 获取数组长度
        int n = arr.length;
        // 遍历数组,i表示当前要排序的位置
        for (int i = 0; i < n - 1; i++) {
            // 初始化最小元素的索引为当前位置i
            int minIndex = i;
            // 从i+1位置开始,寻找剩余数组中的最小元素
            for (int j = i + 1; j < n; j++) {
                // 如果找到比当前最小元素还小的元素,更新最小元素的索引
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 交换当前位置i的元素与找到的最小元素
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }
}

image-20250313160026866

适合闪存等写入成本高的场景

6. 堆排序(Heap Sort)

6.1 原理

堆排序使用堆数据结构,首先建立最大堆,然后依次取出堆顶元素进行排序。

6.2 代码实现(Java)

public class SelectionSort {
    // 选择排序方法,对整型数组进行排序
    public static void selectionSort(int[] arr) {
        // 获取数组长度
        int n = arr.length;
        // 遍历数组,i表示当前要排序的位置
        for (int i = 0; i < n - 1; i++) {
            // 初始化最小元素的索引为当前位置i
            int minIndex = i;
            // 从i+1位置开始,寻找剩余数组中的最小元素
            for (int j = i + 1; j < n; j++) {
                // 如果找到比当前最小元素还小的元素,更新最小元素的索引
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 交换当前位置i的元素与找到的最小元素
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }
}

image-20250313162304733

适用场景

  • 选择排序(需要快速寻找最大/最小元素)
  • 任务线程排序(尽量减少元素之间的交换)

7. 归并排序 (Merge Sort)

7.1 原理

归并排序是一种基于分治思想的排序算法,将数组分为更小的部分,分别排序后合并。

7.2 代码实现(Java)

public class MergeSort {
    // 归并排序的主方法,对数组arr的从left到right的部分进行排序
    public void sort(int arr[], int left, int right) {
        // 如果左索引小于右索引,说明至少有两个元素,可以进行排序
        if (left < right) {
            // 找到中间索引,避免直接使用(left + right) / 2可能导致的整数溢出
            int mid = left + (right - left) / 2;
            // 递归地对左半部分进行排序
            sort(arr, left, mid);
            // 递归地对右半部分进行排序
            sort(arr, mid + 1, right);
            // 将排序好的左右两半部分合并
            merge(arr, left, mid, right);
        }
    }

    // 合并两个已排序的子数组的方法
    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];
        // 复制数据到临时数组L
        for (int i = 0; i < n1; ++i) L[i] = arr[left + i];
        // 复制数据到临时数组R
        for (int j = 0; j < n2; ++j) R[j] = arr[mid + 1 + j];
        // 初始化左子数组的索引
        int i = 0;
        // 初始化右子数组的索引
        int j = 0;
        // 初始化归并后数组的索引
        int k = left;
        // 当两个子数组都没有遍历完时,进行比较合并
        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++];
    }
}

适用场景

  • 需要稳定排序
  • 数据规模较大,适用于链表排序

8. 桶排序 (Bucket Sort)

8.1 原理

​ 假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。

桶排序是一种利用哈希映射的排序算法,将数据分布到不同的桶中,然后对每个桶进行排序。

8.2 代码实现(Java)

import java.util.*; // 导入java.util包下的所有类,包括List, ArrayList和Collections等

public class BucketSort {
    /**
     * 对浮点数数组进行桶排序
     * @param arr 待排序的浮点数数组
     */
    public void sort(float arr[]) {
        int n = arr.length; // 获取数组长度
        // 创建一个List类型的数组,作为桶,每个桶是一个ArrayList
        List<Float>[] buckets = new List[n];
        // 初始化每个桶
        for (int i = 0; i < n; i++) {
            buckets[i] = new ArrayList<>(); // 为每个桶分配一个新的ArrayList
        }
        // 将数组中的元素分配到各个桶中
        for (float num : arr) {
            // 计算元素应该放入的桶的索引,假设数组中的元素都在0到1之间
            int bucketIndex = (int) (n * num);
            // 将元素添加到对应的桶中
            buckets[bucketIndex].add(num);
        }
        // 对每个桶中的元素进行排序
        for (List<Float> bucket : buckets) {
            Collections.sort(bucket); // 使用Collections的sort方法对桶中的元素进行排序
        }
        // 将排序后的元素放回原数组
        int index = 0; // 用于追踪原数组的索引
        for (List<Float> bucket : buckets) {
            for (float num : bucket) {
                arr[index++] = num; // 将桶中的元素按顺序放回原数组
            }
        }
    }
}

适用场景

  • 数据分布均匀的情况下
  • 适用于浮点数排序

9. 基数排序 (Radix Sort)

9.1 原理

​ 它的基本思想是: 将整数按位数切割成不同的数字,然后按每个位数分别比较。 具体做法是: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.

9.2 代码实现(Java)

import java.util.*; // 导入java.util包下的所有类

public class RadixSort {
    /**
     * 对数组进行基数排序
     * @param arr 待排序的数组
     */
    public void sort(int[] arr) {
        // 找到数组中的最大值
        int max = Arrays.stream(arr).max().getAsInt();
        // 对每一位进行计数排序,exp表示当前位数的权重(1, 10, 100, ...)
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countSort(arr, exp); // 对数组按照当前位数进行计数排序
        }
    }

    /**
     * 根据指定位数进行计数排序
     * @param arr 待排序的数组
     * @param exp 当前位数的权重
     */
    void countSort(int[] arr, int exp) {
        int output[] = new int[arr.length]; // 输出数组,存储排序后的结果
        int count[] = new int[10]; // 计数数组,存储每个数字出现的次数

        // 计算每个数字在当前位数下出现的次数
        for (int num : arr) {
            count[(num / exp) % 10]++;
        }

        // 将计数数组转换为累加数组
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 构建输出数组
        for (int i = arr.length - 1; i >= 0; i--) {
            output[--count[(arr[i] / exp) % 10]] = arr[i];
        }

        // 将排序后的数组复制回原数组
        System.arraycopy(output, 0, arr, 0, arr.length);
    }
}

结论

个数字在当前位数下出现的次数
for (int num : arr) {
count[(num / exp) % 10]++;
}

    // 将计数数组转换为累加数组
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }

    // 构建输出数组
    for (int i = arr.length - 1; i >= 0; i--) {
        output[--count[(arr[i] / exp) % 10]] = arr[i];
    }

    // 将排序后的数组复制回原数组
    System.arraycopy(output, 0, arr, 0, arr.length);
}

}


## 结论

不同排序算法在**时间复杂度**、**空间复杂度**和**适用场景**上各有优劣,开发时需要根据具体需求选择合适的排序方法。
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值