【C补充】常用排序算法

20 篇文章 1 订阅

参考文章:

一、冒泡排序

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值