数据结构第四部分——常见排序算法总结(C语言版)

一、插入类排序

直接插入排序

  • 思想:
    • 将一个序列看成左右两个部分,左边表示已经排好序,右边表示待排序。
    • 然后,将右边待排序中的数逐个插入到左边排好序部分的适当位置,直到所有数字都被插入到左边有序部分。
  • 步骤:
    • 1、该数组的第一个元素被认为已经排好序
    • 2、从下一个元素(新元素)开始,在已经排序的元素序列中从后向前扫描
    • 3、如果这个已排序元素大于新元素,将这个元素后移
    • 4、重复3,直到找到已排序的元素小于或者等于新元素的位置
    • 5、将新元素插入到该位置后
    • 6、重复2~5
  • 时间复杂度:
    • 最坏:O(n²),最好:O(n),平均复杂度:O(n²)
  • 空间复杂度:
    • O(1)
void InsertSort(int R[],int n)
{
	int i,j,tmp;
	//右边,从第一个数字开始
	for(i=1,i<n,++i)
	{
		tmp = R[i]; //将待插入关键字暂存与temp中
		j = i-1;
		//和左边从右到左比较,如果R[j]>tmp,将它向后移
		//直到找到小于自己的数,将自己放到这个数的后边
		while(j>=0 && tmp<R[j])
		{
			R[j+1] = R[j];
			--j;
		}
		R[j+1] = tmp;//找到插入位置
	}
}

希尔排序

  • 思想:

    • 又叫缩小增量排序,本质还是插入排序,只不过将待排序序列按照某种规则分成了几个子序列,分别对几个子序列直接插入排序。
  • 步骤:

    • 1、选择一个增量序列 t1,t2,t3…tn,该序列递减且最后一个tn=1
    • 2、对序列进行 n 趟排序
    • 3、每趟排序,根据对应增量,将待排序数组分割成若干子序列
    • 4、对每个子序列进行直接插入排序
    • 5、最后一个增量为1,其实就是对整个数组进行一次直接插入排序
  • 时间复杂度:

    • 最坏:O(nlog2n),最好:O(nlog2n),平均复杂度:O(nlog2n)
  • 空间复杂度:

    • O(1)
int main() {
	int len ;
	scanf("%d", &len);
	int* nums = (int*)malloc(sizeof(int) * len);
 
	for (int i = 0; i < len; i++) {
		scanf("%d", &nums[i]);
	}
 
	int gap = len / 2;
	while (gap > 0) { //每完成一次while循环,代表完成了一次分组后的插入排序
		for (int i = 0; i <= gap; i++) {
			
			for (int j = i+gap; j < len; j += gap) { //对间隔为gap的数组成员进行插入排序
				if (nums[j] < nums[j - gap]) {
					int t = nums[j];
					nums[j] = nums[j - gap];
					nums[j - gap] =t;
				}
			}
		}
		gap /= 2;
	}
	
	for (int i = 0; i < len; i++) {
		printf("%d\n", nums[i]);
	}
	return 0;
}

二、交换类排序

冒泡排序

  • 思想:

    • 它是通过一系列的“交换”动作完成的
  • 步骤:

    • 1、比较相邻的元素
    • 2、如果第一个比第二个大,就交换它们两个,否则不交换
    • 3、如果第二个比第三个大,就交换它们两个,否则不交换…
    • 4、一直按照上边的方式交换,最终最大的关键字将被移动到最后,一趟冒泡排序完成
    • 5、针对所有的元素重复以上的步骤,除了最后一个
  • 时间复杂度:

    • 最坏:O(n²),最好:O(n),平均复杂度:O(n²)
  • 空间复杂度:

    • O(1)
void BubbleSort(int R[], int n){
	int i,j,flag;
	int temp;
	for(i = n-1;i >=1;--i){
		flag = 0;
		for(j = 1;j <= i;++j){
			if(R[j-1] > R[j]){
				temp = R[j];
				R[j] = R[j-1];
				R[j-1] = temp;
				flag = 1;
			}
			if(flag == 0)
				return;
		}
	}
}

快速排序

  • 思想:
    • 快速排序也是“交换”类的排序,它通过多次划分操作实现排序。
  • 步骤:
    • 1、每一趟选择当前所有子序列中的一个关键字作为枢轴

    • 2、将子序列中比枢轴小的移到枢轴前边,比枢轴大的移到枢轴后边

    • 3、以上述规则划分完毕后会得到新的一组更短的子序列

    • 4、继续以上述规则划分子序列

    • 注意:快速排序中对每一个子序列的一次划分算作一趟排序,每一趟结束之后有一个关键字到达最终位置

  • 时间复杂度:
    • 最坏:O(n²) ,待排序列接近有序;最好:O(nlog2n),待排序列接近无序,平均复杂度:O(nlog2n)
  • 空间复杂度:
    • O(log2n)
void QuickSort(int R[],int low,int high)
{
	int tmp;
	int i=low,j=high;

	if(low<high)
	{
		temp=R[low];
		//小于temp的关键字放在左边,大于temp的关键字放在右边
		while(i<j)
		{
			while(j>i&&R[j]>=temp) --j;
			if(i<j)
			{
				R[i]=R[j];
				++i;
			}
			while(i<j&&R[i]<temp) ++i;
			if(i<j)
			{
				R[j]=R[i];
				--j;
			}
		}
		R[i]=temp;
		QuickSort(R,low,i-1);
		QuickSort(R,i+1,high);
	}
}

三、选择类排序

简单选择排序

  • 思想:

    • 简单选择排序主要的动作就是 选择。
  • 步骤:

    • 1、从头至尾顺序扫描序列,找出最小的一个关键字
    • 2、和第一个关键字交换
    • 3、从剩下的关键字中继续这种选择和交换
    • 4、最终使序列有序
  • 时间复杂度:

    • O(n²)
  • 空间复杂度:

    • O(1)
void SelectSort(int R[],int n)
{
	int i,j,k;
	int temp;
	for(i=0;i<n;++i)
	{
		k=i;
		//从无序序列中挑出一个最下的关键字
		for(j=i+1;j<n;++j)
			if(R[k]>R[j])
				k=j;

		//交换
		temp = R[i];
		R[i] = R[k];
		R[k] = temp;
	}
}

堆排序

  • 思想:
    • 堆是一种数据结构,可以把堆看成一棵完全二叉树,这棵完全二叉树满足:任何一个非叶结点的值都不大于(或不小于)其左右孩子结点的值。若父亲大孩子小,则这样的堆叫作大顶堆;若父亲小孩子大,则这样的堆叫作小顶堆。
    • 代表堆的这棵完全二叉树的根结点的值是最大(或最小)的,因此将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样,有序序列关键字增加1个,无序序列中关键字少1个,对新的无序序列重复这样的操作,就实现了排序。

举个例子,原始序列为: 49 38 65 97 76 13 27 49

  • 1、下边为原始序列对应的完全二叉树,第一步先将这个树调整为一个大顶堆
    在这里插入图片描述

    • 在上边的完全二叉树中,76、13、27、49 是叶子节点,满足堆的定义。
    • 调整97,97 > 49,不需要调整
    • 调整65,65>13,65>27,不需要调整
    • 调整38,38小于他的两个子节点,和子节点中较大的一个交换,38和97交换。交换后38还小于子节点49,继续交换。
    • 调整49,49小于两个子节点,和较大的97交换,交换后49还小于76,继续与76交换。
      在这里插入图片描述
  • 2、排序

    • 第一步:根据上边调整好的大顶堆,先将97和38交换,第一趟堆排序完成,97到达最终位置。
    • 第二步:将除堆顶97外的其他结点重新调整为大顶堆,再将堆顶和最后一个结点交换,第二趟排序完成。
    • 重复第二步,知道树中只剩一个结点,排序完成。

步骤总结:

  • 1)从无序序列所确定的完全二叉树的最后一个非叶子结点开始,从右至左,从下至上,对每个结点
    进行调整,最终将得到一个大顶堆。

  • 对结点的调整方法:将当前结点(假设为a)的值与其孩子结点进行比较,如果存在大于a值的孩
    子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子结点值
    都小于a的值为止。

  • 2)将当前无序序列中的第一个关键字,反映在树中是根结点(假设为)与无序序列中最后一个关
    键字交换(假设为b)。进入有序序列,到达最终位置。无序序列中关键字减少1个,有序序列中关键
    字增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。

  • 3)重复第2步,直到无序序列中的关键字剩下1个时排序结束。

  • 时间复杂度:

    • O(nlog2n)
  • 空间复杂度:

    • O(1)
void Sift(intR[],int low,int high)//这里关键字的存储设定为从数组下标1开始
{
	int i=low,j=2*i;  //R[j]是R[i]的左孩子结点
	int temp=R[i];
	while(j<=high)
	{
		if(j<high&&R[j]<R[j+1]) //若右孩子较大,则把j指向右孩子
			++j;  //行变为2*i+1
		if (temp<R[j])
		{
			R[i]=R[j];//将R[5]调整到双亲结点的位置上
			i=j;//修改1和j的值,以便继续向下调整
			j=2*i;
		}
		else
			break;  //调整结束
	}
	R[i]=temp;  //被调整结点的值放入最终位置
}

/*堆排序函数*/
void heapsort(int R[],int n)
{
	int i;
	int temp;
	for(i=n/2;i>=1;--1)  //建立初始堆
		Sift (R,i,n);
		
	for(i=n;i>=2;--1) //进行n-1次循环,完成堆排序
	{
		/*以下3句换出了根结点中的关键字,将其放入最终位置*/
		temp=R[1];
		R[1]=R[i]:
		R[i]=temp;
		Sift(R,1,i-1);  //在减少了1个关键字的无序序列中进行调整
	}
}

四、二路归并排序

  • 思想:

    • 归并排序可以看作一个分而治之的过程。
  • 步骤:

    • 1、将原始序列看成只含有一个关键字的子序列,显然这些子序列都是有序的。
    • 2、两两归并,形成若干有序二元组。
    • 3、再将这个序列看成若干二元组子序列。
    • 4、继续两两归并,形成若干四元组。
    • 5、知道将整个子序列合并成一个序列。
      在这里插入图片描述
  • 时间复杂度:

    • 最坏,最好,平均复杂度:O(nlog2n)
  • 空间复杂度:

    • O(n)
//int A[size],B[size]
void mergeSort(int A[],int low,int high)
{
	if(low<high)
	{
		int mid = (low+high)/2;
		mergeSort(A,low,mid); //前部分
		mergeSort(A,mid+1,high); //后部分
		merge(A,low,mid,high); //合并
	}
}

void merge(int A[],int low,int mid,int high){
	//B里暂存A的数据 
	for(int k = low ; k < high + 1 ; k++){
		B[k] = A[k]; 
	} 

	int i = low , j = mid + 1 , k = low; 	
	while(i < mid + 1 && j < high + 1) {
		//A的元素暂存在B里,因为不能再A上原地操作,会打乱数据
		//这也是为什么二路归并排序(合并排序)空间复杂度是O(n)的原因 
		//我们这里把值小的放在前面,最后排序结果就是从小到大 
		if(B[i] > B[j]){
			A[k++] = B[j++]; 
		}else{
			A[k++] = B[i++]; 
		} 	 
	} 
	//循环结束后,会有一个没有遍历结束的数组段。
	while(i < mid + 1) 
		A[k++] = B[i++]; 
	while(j < high + 1) 
		A[k++] = B[j++]; 
}

五、基数类排序

  • 思想:
    • 将待排序的元素拆分为k个关键字,其中k为最大值的位数,从低位开始进行稳定排序。
    • 适合的场景是序列中的关键字很多,但组成关键字的取值范围较小。
  • 步骤:(278、109、063、930、589、184、505、269、008、083)
    • 1、每一个元素的每一位都是 0~9 数字,所以准备10个桶

    • 2、进行第一趟分配,按照最后一位
      在这里插入图片描述

    • 3、按桶 0~9顺序收集,从桶下边出:930 063 083 184 505 278 008 109 589 269

    • 4、进行第二趟分配,按照中间位
      在这里插入图片描述

    • 5、收集:505 008 109 930 063 269 278 083 184 589

    • 6、进行第三趟分配,按照最高位
      在这里插入图片描述

    • 7、收集:008 063 083 109 184 269 278 505 589 930,最终整个序列有序。

各种排序算法比较总结

在这里插入图片描述


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
冒泡排序是一种简单的排序算法,它重复地遍历待排序的元素,比较相邻元素的大小,如果顺序错误就交换它们,直到没有需要交换的元素为止。下面是冒泡排序C语言实现: ```c void bubbleSort(int arr[], int n) { for(int i = 0; i < n-1; i++) { int flag = 0; //是否冒泡 for(int j = 0; j < n-1; j++) { if(arr[j > arr[j+1]) { swap(&arr[j], &arr[j+1]); flag = 1; } } if(flag == 0) { //如果没有发生交换,说明数组已经有序,可以提前结束循环 break; } } } ``` 在这个实现中,冒泡排序函数`bubbleSort`接受一个整型数组`arr`和数组长度`n`作为参数。它使用两层循环来遍历数组,并通过比较相邻元素的大小来进行交换。内层循环每次将最大的元素冒泡到数组的末尾。外层循环控制了冒泡的轮数,每一轮都会将当前未排序的最大元素放在正确的位置上。如果在某一轮的冒泡过程中没有发生交换,说明数组已经有序,可以提前结束循环,从而提高算法的效率。 需要注意的是,上述代码中使用了一个`swap`函数来交换两个元素的值,你可以根据需要自行实现该函数。此外,为了减少冒泡排序的时间复杂度,可以在内层循环中添加一个标志位`flag`,用于标记是否发生了交换。如果某一轮的冒泡过程中没有发生交换,说明数组已经有序,可以提前结束循环。这样可以避免不必要的比较和交换操作,提高排序的效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anton丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值