参考文章:
- 博客园 - skywang12345 - 排序算法
- 力扣 - 排序算法全解析
- 《C语言程序设计 - 现代方法》(第2版·修订版)
一、冒泡排序
1.1 核心思想
- 将数组元素逐个遍历,比较每个元素和其后一位相邻元素的大小,不符合顺序则交换,每遍历一轮都通过不断地与相邻元素交换顺序将最大值放至队尾。
- 名称由来:将数组元素上下排列,下标为0的在最下方。每轮遍历,都将最大值(最小值)逐个交换移到最上方。此过程如同气泡上浮(冒泡)。
过程演示:
(图片来源力扣,若侵权请联系删除 - 图片出处)
1.2 代码实现
1.2.1 初始代码
void bubble_sort0(int* a, int n)
{
for(int i=n-1; i>0; i--){
//将最值放到队尾
for(int j=0; j<i; j++){
if(a[j] > a[j+1]) swap(&a[j], &a[j+1]);
}
}
}
1.2.2 优化1:记录是否发生交换
- 记录某轮遍历是否发生交换,若没有发生交换,意味着排序已经完成,则可直接提前终止此后的遍历。
void bubble_sort1(int* a, int n)
{
for(int i=n-1; i>0; i--){
int flag = 0; //记录是否发生遍交换
for(int j=0; j<i; j++){
if(a[j] > a[j+1]){
swap(&a[j], &a[j+1]);
flag = 1;
}
}
if(flag == 0) break; //某轮遍历未发生交换,意味着排序已完成
}
}
1.2.3 优化2:记录是否交换和交换位置
- 记录是否发生交换,可省去最后可能的不必要的遍历
- 记录上次最后发生交换的位置(此后的数据无需交换),下次遍历则到此处为止。
void bubble_sort2(int* a, int n)
{
int swap_index = n-1;
for(int i=n-1; i>0; i=swap_index){
int flag = 0; //记录是否发生交换
for(int j=0; j<i; j++){
if(a[j] > a[j+1]){
swap(&a[j], &a[j+1]);
swap_index = j; //记录最后的交换位置
flag = 1;
}
}
if(flag == 0) break; //某轮遍历未发生交换,意味着排序已完成
}
}
1.3 说明
- 冒泡排序是稳定的排序,即若两数相等,则排序前和排序后二者的相对位置不会发生改变,因为只有当 a[j] > a[j+1] 的时候才会发生交换,相等时不会交换。
- 冒泡排序的时间复杂度为 O[n2],空间复杂度为 O(1)
二、选择排序
2.1 核心思想
(以从小到大顺序为例)
- 数列左侧已经放入最小值的区域称为有序区,右侧待找到最小值的区域称为无序区,每次选择出无序区中的最小值放入无序区的最左侧,成为有序区,不断扩大有序区,缩小无序区,最终无序区消失,排序完成。关键在于不断地选择最值。
- 每次都选择最小值移到最左边,因此称为选择排序。
过程演示:
(图片来源力扣,若侵权请联系删除 - 图片出处)
2.2 代码实现
2.2.1 初始代码
void select_sort0(int* a, int n)
{
int min_index;
for(int i=0; i<n; i++){
min_index = i;
for(int j=i+1; j<n; j++){
if(a[j] < a[min_index]) min_index = j; //找到最小元素下标
}
swap(&a[i], &a[min_index]); //将最小元素交换至无序区首位
}
}
2.2.2 优化:二元选择排序
- 每次找出最小值的同时也找出最大值,将最小值放到队首,将最大值放到队尾。
void select_sort1(int* a, int n)
{
int min_index, max_index;
for(int i=0; i<n; i++){
min_index = max_index = i;
for(int j=i+1; j<n-i; j++){
if(a[j] < a[min_index]) min_index = j;
if(a[j] > a[max_index]) max_index = j;
}
if(min_index == max_index) break; //最大值等于最小值,必均等于a[i],排序完成
swap(&a[i], &a[min_index]); //最小值移至首位
//交换前a[i]为最大值, 但a[i]已经与a[min_index]交换过了, 因此交换max_index和min_index
if(max_index == i) max_index = min_index;
swap(&a[n-1-i], &a[max_index]); //最大值移至队尾,注意队尾下标
}
}
- 因为同时将最小值和最大值放在队首和队尾,因此只会遍历原来的一半范围,每次遍历 j 的范围都会减2(队首队尾各减1)。
2.3 说明
- 选择排序和二元选择排序都是不稳定的排序,相等元素在排序前后的相对顺序会发生改变。例如 [ 2, 2, 1 ],将最小值与首位元素交换,两个2的相对顺序发生改变。
- 选择排序的时间复杂度为 O(n2),空间复杂度为 O(1)
三、插入排序
3.1 核心思想
- 将元素队列分成有序表和无序表,最开始有序表为队列的首位元素,逐个从无序表中选择一个元素,将其插入到有序表中合适的位置,扩大有序表,缩减无序表,最终无序表消失,排序完成。
- 交换法:
- 新数字插入过程中,不断与前面的数字交换,直到找到自己合适的位置。
- 移动法:
- 在新数字插入过程中,与前面的数字不断比较,前面的数字不断向后挪出位置,然后插入该数字。
3.2 代码实现
3.2.1 交换法
- 通过逐个与前面大于自己的相邻的数交换位置,来逐渐向前移动来实现插入。
void insert_sort1(int* a, int n)
{
for(int i=1; i<n; i++){ //无序区起始位置,i从1开始
int j = i; //向前比较的起始位置
//将待插入元素与有序区逐个比较,不符合排序则交换
while(j>=1 && a[j] < a[j-1]){
swap(&a[j], &a[j-1]);
j--;
}
}
}
3.2.2 移动法
- 将前面大于待插入数字的元素逐个向后移动,找到合适的位置后,直接一次插入。
过程演示:
(图片来源力扣,若侵权请联系删除 - 图片出处)
void insert_sort2(int* a, int n)
{
int j, tmp;
for(int i=1; i<n; i++){ //i表示无序区起始位置
j = i;
tmp = a[i]; //后移会覆盖原数字,将待插入数字备份
//大于它的数字不断向后移动
while(j>=1 && a[j] > tmp){
a[j] = a[j-1];
j--;
}
a[j] = tmp; //找到合适的位置后一次插入
}
}
3.3 说明
- 插入排序是一种稳定的排序,只有当前后相邻的数排序不匹配时才会交换或移动,相等的数不会改变相对位置;
- 算法使用二层嵌套循环,时间复杂度为 O(n2),使用常量级临时变量,空间复杂度为 O(1)
四、快速排序
4.1 核心思想 - 分治法
- 从数组中取出一个数,称为基准数;
- 遍历数组,比基准数小的放前面,大的放后面,遍历完成后,该基准数就位于数列中间位置;
- 将前后两个区域视为两个数组,重复前两个步骤,将这两个数组再次排序,反复递归调用,直到排序完成。
过程演示:
4.2 代码实现
//参数: 待排序数组、数组左右边界
void quick_sort(int* a, int left, int right)
{
int middle; //基准数下标
if(left >= right) return; //判断某分割段是否无法继续分割
middle = split(a, left, right);
quick_sort(a, left, middle-1); //将分割后的数组再次排序
quick_sort(a, middle+1, right);
}
//分割函数,按基准数将数组分为较小和较大的两部分
int split(int* a, int left, int right)
{
int x = a[left]; //取一个基准数并备份,留出空位
for(;;){
//选最左侧值为基准数,则先从右往左遍历,找到第一个小于基准数的数
while(left < right && a[right] > x) right--;
if(left >= right) break;
a[left++] = a[right]; //将小于基准数的移动到左侧空位上,之后left右移
//从左往右,找到第一个大于基准数的数
while(left < right && a[left] < x) left++;
if(left >= right) break; //相遇,表明分割完成
a[right--] = a[left];
}
a[right] = x;
return right;
}
代码说明:
- 快排函数:
- 通过调用分割函数,不断将自身分成两部分,并对这两部分再次分割,反复递归,直至排序完成;
- 总是优先将未排序的部分的最左侧的段排序好,直到其内部排序完成无法分割,再排序与其相邻的右侧的段;
- 分割函数:
- 每次将待分割数组分成较小和较大的左右两段。
五、 库函数qsort
的调用
函数原型:
void qsort(void* base, size_t n, size_t size, int (*compar)(const void*,const void*));
#include <stdlib.h>
...
int* a = {数组元素};
qsort(a, n, compar);
//排序规则函数
int compare(int* a, int* b)
{
return ( *(int*)a - *(int*)b ); //按从小到达排序
}
内容详见:qsort函数 - 数组元素排序
六、完整代码示例
#include <stdio.h>
#include <stdlib.h> //qsort声明所在头文件
#define N 10
void bubble_sort(int* a, int n); //冒泡排序
void select_sort1(int* a, int n); //选择排序
void insert_sort1(int* a, int n); //插入排序 - 交换法
void insert_sort2(int* a, int n); //插入排序 - 移动法
void quick_sort(int* a, int left, int right); //快速排序
int split(int* a, int left, int right); //快速排序 - 分割函数
//qsort比较函数
int compar(int* a, int* b)
{
return ( *(int*)a - *(int*)b );
}
//交换函数
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(){
int array[N] = {0};
printf("请输入%d个数: ", N);
for(int i=0; i<N; i++){
scanf("%d", &array[i]);
}
// bubble_sort(array, N); //冒泡排序
// select_sort1(array, N); //选择排序
// insert_sort1(array, N); //插入排序 - 交换法
// insert_sort2(array, N); //插入排序 - 移动法
// quick_sort(array, 0, N-1); //快速排序
qsort(array, N, sizeof(int), compar); //库函数qsort
printf("排序后: ");
for(int i=0; i<N; i++){
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
//冒泡排序
void bubble_sort(int* a, int n)
{
int swap_index = n-1;
for(int i=n-1; i>0; i=swap_index){
int flag = 0; //记录是否发生交换
for(int j=0; j<i; j++){
if(a[j] > a[j+1]){
swap(&a[j], &a[j+1]);
swap_index = j; //记录最后的交换位置
flag = 1;
}
}
if(flag == 0) break; //某轮遍历未发生交换,意味着排序已完成
}
}
//选择排序
void select_sort1(int* a, int n)
{
int min_index, max_index;
for(int i=0; i<n; i++){
min_index = max_index = i;
for(int j=i+1; j<n-i; j++){
if(a[j] < a[min_index]) min_index = j;
if(a[j] > a[max_index]) max_index = j;
}
if(min_index == max_index) break; //最大值等于最小值,必均等于a[i],排序完成
swap(&a[i], &a[min_index]); //最小值移至首位
//交换前a[i]为最大值, 但a[i]已经与a[min_index]交换过了, 因此交换max_index和min_index
if(max_index == i) max_index = min_index;
swap(&a[n-1-i], &a[max_index]); //最大值移至队尾,注意队尾下标
}
}
//插入排序 - 交换法
void insert_sort1(int* a, int n)
{
for(int i=1; i<n; i++){ //无序区起始位置,i从1开始
int j = i; //向前比较的起始位置
//将待插入元素与有序区逐个比较,不符合排序则交换
while(j>=1 && a[j] < a[j-1]){
swap(&a[j], &a[j-1]);
j--;
}
}
}
//插入排序 - 移动法
void insert_sort2(int* a, int n)
{
int j, tmp;
for(int i=1; i<n; i++){
j = i;
tmp = a[i];
//大于它的数字不断向后移动
while(j>=1 && a[j-1] > tmp){
a[j] = a[j-1];
j--;
}
a[j] = tmp;
}
}
//快速排序
void quick_sort(int* a, int left, int right)
{
int middle;
if(left >= right) return; //判断某分割段是否无法继续分割
middle = split(a, left, right);
quick_sort(a, left, middle-1);
quick_sort(a, middle+1, right);
}
//分割函数,返回分割后的基准数所在下标
int split(int* a, int left, int right)
{
int x = a[left]; //取基准数并备份,空出所在位置
for(;;){
//取a[left]为基准数,则需从最右侧开始遍历
while(left < right && a[right] > x) right--;
if(right <= left) break;
a[left++] = a[right];
while(left < right && a[left] < x) left++;
if(left >= right) break;
a[right--] = a[left];
}
a[right] = x; //循环结束时必定left == right
return right;
}