数据结构四大简单排序算法详解:直接插入排序、选择排序、基数排序和冒泡排序
一、引言
排序算法是计算机科学中最基础也是最重要的内容之一。在实际开发和面试中,四大简单排序算法(直接插入排序、选择排序、基数排序、冒泡排序)是必须掌握的基础知识。本文将全面详细地讲解这四种排序算法的原理、实现和适用场景。
二、直接插入排序
2.1 排序思路
直接插入排序的基本思想是:将待排序的元素插入到已经排好序的有序序列中,从而得到一个新的、记录数增加1的有序序列。
2.2 排序过程
从第一个元素开始,该元素可以认为已经被排序
取出下一个元素,在已经排序的元素序列中从后向前扫描
如果该元素(已排序)大于新元素,将该元素移到下一位置
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
将新元素插入到该位置后
重复步骤2~5
2.3 复杂度分析
时间复杂度:最好情况:O(n) - 数组已经有序 最坏情况:O(n²) - 数组逆序 平均情况:O(n²)
- 空间复杂度:O(1) - 原地排序
稳定性:稳定排序
2.4 排序过程图解
初始序列:[5, 2, 4, 6, 1, 3] 第1趟: [2, 5, 4, 6, 1, 3] 第2趟: [2, 4, 5, 6, 1, 3] 第3趟: [2, 4, 5, 6, 1, 3] 第4趟: [1, 2, 4, 5, 6, 3] 第5趟: [1, 2, 3, 4, 5, 6]
2.5 代码实现
#include <stdio.h>
void insertSort(int arr[], int n) {int i, j, key;for (i = 1; i < n; i++) {key = arr[i];j = i - 1;while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;}arr[j + 1] = key;}
}
void printArray(int arr[], int n) {for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}
int main() {int arr[] = {5, 2, 4, 6, 1, 3};int n = sizeof(arr) / sizeof(arr[0]);insertSort(arr, n);printArray(arr, n);return 0;
}
三、选择排序
3.1 排序思路
选择排序的基本思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3.2 排序过程
初始状态:无序区为R[0..n-1],有序区为空
第i趟排序(i=0,1,2,...,n-2)开始时,当前有序区和无序区分别为R[0..i-1]和R[i..n-1]
该趟排序从当前无序区中选出关键字最小的记录R[k]
将R[k]与无序区的第1个记录R[i]交换
重复n-1趟,排序完成
3.3 复杂度分析
时间复杂度:最好情况:O(n²) 最坏情况:O(n²) 平均情况:O(n²)
空间复杂度:O(1) - 原地排序
稳定性:不稳定排序
3.4 排序过程图解
初始序列:[64, 25, 12, 22, 11] 第1趟: [11, 25, 12, 22, 64] 第2趟: [11, 12, 25, 22, 64] 第3趟: [11, 12, 22, 25, 64] 第4趟: [11, 12, 22, 25, 64]
3.5 代码实现
#include <stdio.h>
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}
void selectSort(int arr[], int n) {int i, j, minIndex;for (i = 0; i < n - 1; i++) {minIndex = i;for (j = i + 1; j < n; j++) {if (arr[j] < arr[minIndex]) {minIndex = j;}}swap(&arr[minIndex], &arr[i]);}
}
void printArray(int arr[], int n) {for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}
int main() {int arr[] = {64, 25, 12, 22, 11};int n = sizeof(arr) / sizeof(arr[0]);selectSort(arr, n);printArray(arr, n);return 0;
}
四、基数排序
4.1 排序思路
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
4.2 排序过程
取得数组中的最大数,并取得位数
从最低位开始,依次进行一次稳定的排序(如计数排序)
从最低位排序一直到最高位排序完成以后,数组就变成一个有序序列
4.3 复杂度分析
时间复杂度:O(d*(n+k)) - d为位数,k为基数范围
空间复杂度:O(n+k) - 需要额外的计数数组和输出数组
稳定性:稳定排序
4.4 排序过程图解
初始序列:[170, 45, 75, 90, 2, 802, 24, 66] 按个位排序:[170, 90, 2, 802, 24, 45, 75, 66] 按十位排序:[2, 802, 24, 45, 66, 170, 75, 90] 按百位排序:[2, 24, 45, 66, 75, 90, 170, 802]
4.5 代码实现
#include <stdio.h>
#include <stdlib.h>
int getMax(int arr[], int n) {int max = arr[0];for (int i = 1; i < n; i++) {if (arr[i] > max) {max = arr[i];}}return max;
}
void countSort(int arr[], int n, int exp) {int output[n];int count[10] = {0};for (int i = 0; i < n; i++) {count[(arr[i] / exp) % 10]++;}for (int i = 1; i < 10; i++) {count[i] += count[i - 1];}for (int i = n - 1; i >= 0; i--) {output[count[(arr[i] / exp) % 10] - 1] = arr[i];count[(arr[i] / exp) % 10]--;}for (int i = 0; i < n; i++) {arr[i] = output[i];}
}
void radixSort(int arr[], int n) {int max = getMax(arr, n);for (int exp = 1; max / exp > 0; exp *= 10) {countSort(arr, n, exp);}
}
void printArray(int arr[], int n) {for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}
int main() {int arr[] = {170, 45, 75, 90, 2, 802, 24, 66};int n = sizeof(arr) / sizeof(arr[0]);radixSort(arr, n);printArray(arr, n);return 0;
}
五、冒泡排序及其优化
5.1 基本冒泡排序思路
重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。
5.2 优化策略
设置标志位:如果某一趟排序中没有发生交换,说明已经有序
记录最后交换位置:下一趟排序只需比较到该位置
5.3 复杂度分析
时间复杂度:最好情况:O(n) - 数组已经有序(优化后) 最坏情况:O(n²) - 数组逆序 平均情况:O(n²)
空间复杂度:O(1) - 原地排序
稳定性:稳定排序
5.4 排序过程图解
初始序列:[5, 3, 8, 6, 4] 第1趟: [3, 5, 6, 4, 8] 第2趟: [3, 5, 4, 6, 8] 第3趟: [3, 4, 5, 6, 8] 第4趟: [3, 4, 5, 6, 8](无交换,提前结束)
5.5 代码实现
#include <stdio.h>
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}
void bubbleSort(int arr[], int n) {int i, j;int swapped;for (i = 0; i < n - 1; i++) {swapped = 0;for (j = 0; j < n - i - 1; j++) {if (arr[j] > arr[j + 1]) {swap(&arr[j], &arr[j + 1]);swapped = 1;}}if (swapped == 0) {break;}}
}
void printArray(int arr[], int n) {for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}
int main() {int arr[] = {5, 3, 8, 6, 4};int n = sizeof(arr) / sizeof(arr[0]);bubbleSort(arr, n);printArray(arr, n);return 0;
}
六、四种排序算法的比较和选择
6.1 性能对比表
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
基数排序 | O(d*(n+k)) | O(d*(n+k)) | O(d*(n+k)) | O(n+k) | 稳定 |
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
6.2 适用场景分析
直接插入排序适用场景:
数据量较小(n < 50)
数据基本有序
作为快速排序的补充(小区间优化)
选择排序适用场景:
数据量较小
对稳定性没有要求
交换次数最少的需求
基数排序适用场景:
数据为整数或字符串
数据范围不大
需要稳定排序且数据量较大
冒泡排序适用场景:
数据量很小
教学演示用途
对稳定性有要求的小数据量排序
七、实际应用代码示例
7.1 学生成绩排序(直接插入排序)
#include <stdio.h>
typedef struct {int id;int score;
} Student;
void sortStudents(Student students[], int n) {int i, j;Student key;for (i = 1; i < n; i++) {key = students[i];j = i - 1;while (j >= 0 && students[j].score > key.score) {students[j + 1] = students[j];j = j - 1;}students[j + 1] = key;}
}
void printStudents(Student students[], int n) {for (int i = 0; i < n; i++) {printf("学号:%d 成绩:%d\n", students[i].id, students[i].score);}
}
int main() {Student students[] = {{1, 85}, {2, 92}, {3, 78}, {4, 90}, {5, 82}};int n = sizeof(students) / sizeof(students[0]);sortStudents(students, n);printStudents(students, n);return 0;
}
7.2 手机号排序(基数排序)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void phoneRadixSort(char phones[][12], int n) {char output[n][12];for (int digit = 10; digit >= 0; digit--) {int count[11] = {0};for (int i = 0; i < n; i++) {int index = (phones[i][digit] == '\0') ? 0 : (phones[i][digit] - '0' + 1);count[index]++;}for (int i = 1; i < 11; i++) {count[i] += count[i - 1];}for (int i = n - 1; i >= 0; i--) {int index = (phones[i][digit] == '\0') ? 0 : (phones[i][digit] - '0' + 1);strcpy(output[count[index] - 1], phones[i]);count[index]--;}for (int i = 0; i < n; i++) {strcpy(phones[i], output[i]);}}
}
void printPhones(char phones[][12], int n) {for (int i = 0; i < n; i++) {printf("%s\n", phones[i]);}
}
int main() {char phones[][12] = {"13812345678", "13987654321", "13711112222", "13655556666", "13599998888"};int n = sizeof(phones) / sizeof(phones[0]);phoneRadixSort(phones, n);printPhones(phones, n);return 0;
}
八、常见面试题
8.1 基础概念题
直接插入排序和冒泡排序在最好情况下的时间复杂度各是多少?
直接插入排序最好情况O(n),冒泡排序最好情况O(n)选择排序为什么是不稳定的?举例说明
因为交换可能改变相同元素的相对位置,如[5, 5, 2]排序后第一个5可能到第二个5后面基数排序适用于什么类型的数据?
适用于整数、字符串等可以按位分割的数据类型冒泡排序的优化方法有哪些?
设置交换标志位、记录最后交换位置
8.2 代码实现题
手写直接插入排序算法
实现一个稳定的选择排序版本
用基数排序对字符串数组进行排序
优化冒泡排序,使其在最好情况下达到O(n)
8.3 综合应用题
如果数据基本有序,应该选择哪种排序算法?为什么?
选择直接插入排序,因为其在数据基本有序时接近O(n)时间复杂度如果需要排序的数据量很小(n<10),推荐使用哪种算法?
推荐使用直接插入排序或冒泡排序如何选择合适的排序算法?考虑哪些因素?
数据量大小、数据分布情况、稳定性要求、空间限制等解释为什么快速排序在实际应用中比这些简单排序更常用
快速排序平均时间复杂度O(nlogn),对于大数据量更高效,且缓存友好
九、总结
四大简单排序算法是学习更复杂算法的基础,并且4种简单排序都是以双重for循环为核心,每种算法都有其独特的适用场景:
直接插入排序:小数据量、基本有序数据
选择排序:小数据量、交换次数少的场景
基数排序:整数、字符串排序,稳定且高效
冒泡排序:教学演示、小数据量稳定排序
掌握这些基础排序算法不仅有助于理解算法设计思想,也为大家学习更高级的排序算法打下坚实基础。在实际应用中,应根据具体需求选择合适的排序算法。