
🌸雨落在了我的手上:个人主页
🐟个人仓库:Gitee仓库
❄️个人专栏:<<JaveSe>> <<C语言>> <<C语言数据结构>>**
🔥🔥🔥 人生格言:无人扶我青云志,我自踏雪至山巅
🎬 博主简介:

目录
内容大纲:
本文系统介绍了排序的基本概念,包括排序定义、稳定性、内部/外部排序分类及应用场景。重点阐述了六种常见排序算法:插入排序(稳定,O(n²))、希尔排序(优化插入)、选择排序(不稳定,O(n²))、堆排序(不稳定,O(nlogn))、冒泡排序(稳定,O(n²))和快速排序(不稳定,平均O(nlogn))。通过代码实现与过程分析,对比了各算法的时间复杂度、空间复杂度及稳定性特性,为算法选型提供依据。
一:排序的概念及引⽤
1.排序的概念
排序:所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的 操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的 相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,⽽在排序后的序列中,ri仍在rj之 前,则称这种排序算法是稳定的;否则称为不稳定的

内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断的在内外存之间移动数据的 排序。
2.排序运⽤

3.常⻅的排序算法

二:常⻅排序算法的实现
1.插⼊排序
想象你在打扑克牌,手里已经有一些排好序的牌,现在摸到一张新牌,你会把它插入到合适的位置,使得手里的牌依然有序。插入排序就是模拟这个过程:
-
把数组分成两部分:已排序区 和未排序区。
-
初始时,已排序区只有第一个元素(因为一个元素本身是有序的)。
-
然后依次从未排序区取出第一个元素,在已排序区从后往前扫描,找到合适的位置插入。
-
重复直到未排序区为空。
代码:
java
//插入排序(这里我们默认是升序)
public class InsertionSort {
//插入方法的实现
public static void insertionSort(int[] arr){
//默认从第二个元素开始,即下标为1的位置
for (int i =1;i< arr.length;i++){
//先把我们要插入的元素保存起来
int cur = arr[i];
//用j 来表示已经排序的最后一个元素
int j =i - 1;
//用j去和插入元素比较大小,如果j大,就把j往后移动一格
while (j>=0&&arr[j]>cur){
arr[j+1]=arr[j];
j--;
}
//此时j+1 就是我们需要插入的位置cur
arr[j+1]= cur;
}
}
}
以数组 {5, 2, 9, 1, 5, 6} 为例,排序过程如下:
| 轮次 | 已排序区 | 未排序区 | 操作 |
|---|---|---|---|
| 初始 | 5 | 2, 9, 1, 5, 6 | - |
| 1 | 2, 5 | 9, 1, 5, 6 | 2 插入到 5 前面 |
| 2 | 2, 5, 9 | 1, 5, 6 | 9 比前面都大,直接放最后 |
| 3 | 1, 2, 5, 9 | 5, 6 | 1 插入到最前面 |
| 4 | 1, 2, 5, 5, 9 | 6 | 第二个 5 插入到 5 后面 |
| 5 | 1, 2, 5, 5, 6, 9 | \[\] | 6 插入到 5 和 9 之间 |
-
时间复杂度:
-
最好情况:数组已经有序,只需要比较
n-1次,时间复杂度为 O(n)。 -
最坏情况:数组完全逆序,需要比较和移动
n(n-1)/2次,时间复杂度为 O(n²)。 -
平均情况:O(n²)。
-
-
空间复杂度 :只用了几个临时变量,O(1),属于原地排序。
-
稳定性 :插入排序是稳定 的,因为当
arr[j] == current时不会交换,保持原有顺序。
2.希尔排序(缩⼩增量排序)
基本思想:
普通插入排序每次只能把元素往前移动一位,如果元素离正确位置很远,就要移动很多次。
希尔排序的做法是:
-
选择一个增量 gap,把数组分成 gap 个组。
-
每组内进行插入排序。
-
然后缩小 gap,再分组、再排序。
-
当 gap 缩小到 1 时,就是普通插入排序。但此时数组已经基本有序,所以最后一步很快。
常用的增量序列是:gap = 数组长度 / 2,然后每次 gap = gap / 2,直到 gap = 1。
代码:
java
//希尔排序(升序)
public class ShellSort {
//希尔排序方法的实现
public static void shellSort(int[]arr){
//设置一个gap来作为划分组的中间值,初始gap为数组的/2
//之后每次都/2,一直到gap=1
for (int gap = arr.length/2;gap>0;gap/=2){
//内层循环对每个分组插入排序
// 为什么从 i = gap 开始?因为每个分组的前 gap 个元素是各组的第一个元素,默认有序
for (int i =gap;i<arr.length;i++){
//当前要插入的元素
int cur = arr[i];
//同一分组中,前一个元素的下标
int j =i - gap;
// 在同一个分组内,从后往前找插入位置
while (j>=0&&arr[j]>cur){
//先把大的元素往后移动gap个位置
arr[j+gap] = arr[j];
//继续往前找
j-=gap;
}
//此时要插入的位置找到了
arr[j+gap] =cur;
}
}
}
}
希尔排序的特性总结:
-
希尔排序是对直接插⼊排序的优化。
-
当gap>1时都是预排序,⽬的是让数组更接近于有序。当gap==1时,数组已经接近有序的了,这 样就会很快。这样整体⽽⾔,可以达到优化的效果。我们实现后可以进⾏性能测试的对⽐。
3.选择排序
基本思想:
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待 排序的数据元素排完
把数组分成两部分:
-
已排序区:在左边,一开始为空。
-
未排序区:在右边,一开始是整个数组。
每一轮:
-
在未排序区中找到最小的元素。
-
把它和未排序区的第一个元素交换。
-
这样已排序区就多了一个元素,未排序区少了一个元素。
重复 n - 1 轮,整个数组就有序了。
代码:
java
//选择排序
public class SelectionSort {
public static void selectionSort(int[]arr){
// 外层循环:i 表示当前已排序区的末尾,也是未排序区的第一个位置
for (int i =0;i< arr.length-1;i++){
//先设置一个未排序最小元素
int min = i;
//通过循环找到最小元素
for (int j =i+1;j< arr.length;j++){
if (arr[j]<arr[min]){
min =j;
}
}
//如果最小元素不在i上,就交换
if (min!=i){
int temp = arr[i];
arr[i]=arr[min];
arr[min]=temp;
}
}
}
}
-
时间复杂度:
-
比较次数是固定的:
n(n-1)/2,所以最好、最坏、平均都是 O(n²)。 -
交换次数最多
n-1次,比冒泡排序少。
-
-
空间复杂度 :O(1),只需要几个临时变量。
-
稳定性 :选择排序是不稳定的。
- 例如
{5a, 5b, 2},第一轮最小的是2,和5a交换后变成{2, 5b, 5a},两个5的相对顺序改变了。
- 例如
4.堆排序
基本思想:
堆是一种特殊的完全二叉树,通常用数组来存储。
-
大顶堆 :每个父节点的值都 大于等于 它的左右子节点。
-
小顶堆 :每个父节点的值都 小于等于 它的左右子节点。
堆排序(升序)的做法:
-
先把整个数组调整成一个大顶堆。
-
此时堆顶(数组第 0 个元素)就是最大值。
-
把堆顶和数组末尾元素交换,最大值就放到了最终位置。
-
然后把剩下的元素重新调整成大顶堆。
-
重复以上步骤,直到所有元素有序。
用数组表示完全二叉树时,下标关系很重要:
-
父节点下标
i -
左孩子下标
2 * i + 1 -
右孩子下标
2 * i + 2
代码:
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, arr.length, i);
}
//此时建堆完成
//排序:每次把堆顶(最大值)交换到末尾,然后调整剩余部分
for (int i=n-1;i>0;i--){
//交换堆顶和当前末尾元素
swap(arr,0,i);
// 对剩下的 i 个元素重新调整为大顶堆
heapify(arr, i, 0);
}
}
//建立大堆的辅助方法
private static void heapify(int[] arr, int heapSize, int root) {
int largest = root; // 先假设根节点最大
int left = 2 * root + 1; // 左孩子
int right = 2 * root + 2; // 右孩子
// 如果左孩子比当前最大还大,更新 largest
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
// 如果右孩子比当前最大还大,更新 largest
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点,交换,并继续向下调整
if (largest != root) {
//交换
heapify(arr, heapSize, largest);
}
}
//交换元素辅助方法
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
-
时间复杂度:
-
建堆:O(n)
-
每次调整堆:O(log n) ,共
n - 1次 -
总时间复杂度:O(n log n),最好、最坏、平均都是这个量级。
-
-
空间复杂度 :O(1) (递归栈是
O(log n),但通常忽略)。 -
稳定性 :堆排序是不稳定的。交换时可能改变相同元素的相对顺序。
5.冒泡排序
想象水里的气泡,越大的气泡越先浮到水面。
冒泡排序也是这样:
-
从数组第一个元素开始,相邻两个元素两两比较。
-
如果前面比后面大,就交换它们。
-
这样一轮下来,最大的元素就"冒"到了数组末尾。
-
然后对剩下的元素重复这个过程,直到整个数组有序
代码:
java
//冒泡排序
public class BubbleSort {
//冒泡排序方法的实现
public static void bubbleSort(int[] arr){
int n = arr.length;
// 外层循环:控制一共需要几轮
for (int i= 0;i<n-1;i++){
boolean swapped = false; // 标记本轮是否发生了交换
// 内层循环:比较相邻的两个元素
// 每轮结束后,末尾已经有 i 个元素排好了,所以是 n - 1 - i
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(n²)。
-
-
空间复杂度 :O(1) ,只用了一个临时变量
temp。 -
稳定性 :冒泡排序是稳定 的。因为只有
arr[j] > arr[j + 1]时才交换,相等时不交换,不会改变相同元素的相对顺序
6.快速排序
基本思想:
-
从数组中选一个元素作为基准(pivot)。
-
把数组重新排列:小于等于基准的放左边,大于基准的放右边 。这个过程叫分区(partition)。
-
分区后,基准元素就处于它的最终位置。
-
然后对基准左边和右边的子数组分别重复以上步骤,直到子数组长度为 0 或 1。
这样整个数组就有序了。
代码:
java
//快速排序
public class QuickSort {
/**
* 快速排序方法
* @param arr 待排序的数组
* @param low 当前子数组的起始下标
* @param high 当前子数组的结束下标
*/
//快速排序方法的实现
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);
}
}
/**
* 分区方法:把小于等于基准的放左边,大于基准的放右边
* @param arr 数组
* @param low 起始下标
* @param high 结束下标
* @return 基准元素的最终位置
*/
//分区方法
public static int partition(int[]arr,int low,int high){
// 选择最后一个元素作为基准
int pivot = arr[high];
// i 指向小于等于基准的最后一个元素
int i = low - 1;
// 遍历 low 到 high-1
for (int j = low; j < high; j++) {
// 如果当前元素小于等于基准,就把它换到左边
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
// 最后把基准放到正确的位置(i+1)
swap(arr, i + 1, high);
return i + 1; // 返回基准的下标
}
//交换辅助方法
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
-
时间复杂度:
-
最好情况:每次分区都很均匀,时间复杂度 O(n log n)。
-
平均情况:O(n log n)。
-
最坏情况:数组已经有序,且每次选最后一个元素为基准,分区极不均匀,退化为 O(n²)。
-
-
空间复杂度 :递归调用需要栈空间,平均 O(log n) ,最坏 O(n)。
-
稳定性 :快速排序是不稳定的。分区交换时可能改变相同元素的相对顺序。
以上就是我们的全部内容了!!!