数据结构自学笔记(C语言)十大排序

这节讲排序,排序有十大经典算法,如图所示.(概念参考https://www.cnblogs.com/onepixel/p/7674659.html)
在这里插入图片描述
十种常见排序算法可以分为两大类:
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 (由于非比较排序需要一些对数据的要求,通用性较差,所以只讲一下概念就好了,程序没写)
在这里插入图片描述
先介绍几个概念:
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序:所有排序操作都在需要排列的数组中完成;
外排序:需要外部再借助一段空间才能完成
时间复杂度: 一个算法执行所耗费的时间。
空间复杂度:运行完一个程序所需内存的大小。
其实稳定不稳定对于排序好不好用意义不大,我们更注重的是时间复杂度和空间复杂度,时间复杂度决定了排序的时间,空间复杂度决定了占用的内存。从表中可以看出归并排序、快速排序、堆排序、希尔排序的时间复杂度低,程序也复杂一点。下面从简单到难开始讲了。(全部以从小到大排序讲)

冒泡排序:冒泡排序是我们学C语言接触的第一个排序方法,原理很简单,就是每次从后先前推,两两比较,如果后面的比前面的小就交换,一直推到最前面,这样可以保证每次推过去的都是整个数列的最小值,第二次推就是第二小。。。。。。下面图是排了两趟的冒泡,大家看看就懂了。每次冒泡都能冒出来一个排好的,一共要冒n次,每次需要(n-1,n-2…)可以看成n,所以平均时间复杂度是O(n2).因为没有用其他空间,所以空间复杂度O(1).
4ubmV0L3dlaXhpbl80MzM4OTE3OQ==,size_16,color_FFFFFF,t_70)
程序很简单,两重循环就好了,一个循环从0到倒数第二个,一个循环从倒数第二个到第一个循环的变量

void BubbleSort(int *a, int size)
{
	int i,j,swap;
	for(i=0;i<size-1;i++)
		for(j=size-1;j>i;j--)
		{
			if(a[j]<a[j-1])
			{
				swap = a[j];
				a[j] = a[j-1];
				a[j-1] = swap; 
			}
		}
}

选择排序:选择排序顾名思义,数组分成两部分,前半部分是排好的,后半部分是没拍的。默认没排好的部分的第一个是最小的,逐个比较,如果有比他还小的就交换一下,所以保证了他是后半部分中最小的,然后把它划到排好的前半部分。用选择排序排两趟,第一趟5比2大,交换,然后2是最小的,所以2是排好的;第二趟8比7大,交换,7比6大,交换,6比5大,交换,5比3大,交换,3是最小的,23是排好的,后面没排好。
和冒泡一样,每次选择出一个排好的,选择n次,每次可以看成n次选择,所以时间复杂度O(n2).因为没有用其他空间,所以同理,空间复杂度O(1).
9nLmNzZG4ubmV0L3dlaXhpbl80MzM4OTE3OQ==,size_16,color_FFFFFF,t_70)
程序

void SelectionSort(int *a, int size)
{
	int i,j,swap;
	for(i=0;i<size;i++)
		for(j=i;j<size;j++)
		{
			if(a[j]<a[i])
			{
				swap = a[j];
				a[j] = a[i];
				a[i] = swap;
			}
		}
}

插入排序:插入排序和前面两个排序是反过来的,前面两个是在没排好的部分花时间找出来最小的。而插入排序是把没排好的直接插到排好的里面,然后通过在排好的里面通过冒泡的思维把它冒到合适的位置。所以他也要排n-1次,但是每次比较的n次是在排好的里面比较,所以也是O(n2),空间复杂度是O(1).画图拍两趟,第一趟8比5大,不用调整位置了。第二趟7比8小,往前换一次,然后就结束了。
在这里插入图片描述
程序也简单,两层循环,就不讲了。

void InsertionSort(int a[],int size)//插入排序
{
	int i,j,temp;
	for(i=1;i<size;i++)
	{
		temp = a[i];
		for(j=i;j-1>=0 && temp<a[j-1];j--)
			a[j]=a[j-1];
		a[j] = temp;
	}
}

希尔排序:前三种排序都比较简单,平均时间复杂度都是O(n2),理解起来也比较好理解,程序也好写,都是常规思路。接下来的这几种就需要好好想一下了。希尔排序又叫做缩小增量排序,它通过比较相距一定间隔的元素来工作,间隔随着算法的进行而减小,知道只比较相邻元素的最后一趟为止。所以希尔排序不是唯一的,他会根据增量序列的不同而不同。希尔排序有一个性质:使用大增量序列排好的顺序,在变成小增量序列时不会改变之前的有序性。希尔排序其实是在上面三种排序上进行了改进,每一趟的排序方式我们选择插入排序,选择h=N/2的增量序列来讲。一共6个元素,第一次h = 3,每隔三个比较一次;第二次h=1,其实就是普通的插入排序了。所以说不管增量序列怎么选,最后都会变成有一个最基本的插入排序,最后都能排出正确结果,前面所做的都是为最后这趟排序省事了,让最坏时间复杂度没那么坏。增量序列的选择的证明太复杂了,大家感兴趣的话自己查查资料把
在这里插入图片描述

程序和插入排序有点像,因为是插入排序的升级版,当然你也可以携程冒泡排序的升级版,都可以。

void ShellSort(int data[],int n)
{
	for(int inc = n/2;inc>0;inc/=2)
	{
		for(int i=1;i<n;i++)
		{
			for(int j=i;j>0 && data[j]<data[j-inc];j-=inc)
			{
				int temp = data[j];
				data[j] = data[j-inc];
				data[j-inc] = temp;
			}
		}
	}
}

堆排序:在前面我们介绍过堆的数据结构,但是一点点不同,之前写堆的程序的时候是在插入的时候就保证了堆的性质。但是用堆来排序的时候要把数据进行改变位置,让它具有堆的性质,这个过程叫作初建堆,建完之后数据就有堆的性质了,之后排序就很简单了,因为堆的性质是最小的元素(或者最大的元素)在堆顶,分别叫作小顶堆和大顶堆。之后一次一次的把堆顶取出来,就排好序了。有一个小技巧:为了不额外开辟空间,所以我们可以每次取完堆顶之后不放在别的新数组里面,可以把它放在原数组的最后面,但是它不算做堆的一部分了,这样等到堆顶都取完了,数组还是满的,但是已经排好序了。因为要从小到大排序,而取堆要放在数组最后面,所以我们要构造的是大顶堆。
初建堆:第一件事是把不是堆的数组变成堆,大家肯定能想到是通过循环不断把双亲和他的孩子比较,把大的放在双亲的位置。第一个问题来了:是从上到下过滤呢还是从下到上过滤呢。答案是都可以的,但是不管从上到下还是从下到上,都要想到堆结构的改变会使得前面已经建好的堆会出问题,需要检查。如果是从上到下过滤,那么每次改堆顶的时候要不断看他的双亲是否还是比他大,不是的话就一层一层向上检查,5和9交换完之后要检测9是不是比8大,大继续换,换完再看换完的位置和他的双亲,已经到顶了不用换了。从下到上同理,要检查他的孩子是否还是都比他小,一层一层向下检查。
在这里插入图片描述
排序:不知道大家还记不记得前面讲堆的时候拿出元素的时候特别麻烦,因为是没有去替的,堆的结构会被改变了。但是用堆排序就不用考虑这么多了,每次取出来其实是和最后一个元素换个位置,换完之后再把堆重建一下就可以了。这么想想其实之前可以每次取完顶之后也把最后一个放到堆顶,然后重建一下,有兴趣的同学可以回头去试试。

程序:

void HeapSort(int *a, int size)//堆排序
{
	int temp,child=0;
	int i,j;
	//初建堆
	for(i=(size-2)/2;i>=0;i--)
	{
		child = 2*i+1;
		if(child+1<size && a[child+1]>a[child] )
			child = 2*i+2;
		if(a[child] > a[i])//孩子比双亲小 
		{
			temp = a[child];
			a[child] = a[i];
			a[i] = temp;
			for(j=child;2*j+1<size;j = child)
			{
				child = 2*j+1;
				if(child+1<size && a[child+1]>a[child])
					child = 2*j+2;
				if(a[child]>a[j])
				{
					temp = a[child];
					a[child] = a[j];
					a[j] = temp;
				}
				else
					break;
			}
		}
	}
	//排序
	for(i=size-1;i>0;i--)
	{
		temp = a[i];
		a[i] = a[0];
		a[0] = temp;
		size--;
		for(j=0;2*j+1<size;j = child)
		{
			child = 2*j+1;
			if(child+1<size && a[child+1]>a[child])
				child = 2*j+2;
			if(a[child]>a[j])
			{
				temp = a[child];
				a[child] = a[j];
				a[j] = temp;
			}
			else
				break;
		}
	}
}

归并排序:归并排序是分治的思想,把一件事拆成两件事去做,两件事拆成四件,一直拆,知道最后拆不下去了再一层一层往上的排序。可能会有个误区:第一种图式就是错误的理解方式,确实最后是从一个比较到两个比较,一直往上回,但分的时候不是这么分的。两个图对比一下,感受一下什么是分治。
在这里插入图片描述在这里插入图片描述
归并排序的程序写的时候实在是恶心到自己了,因为一直有自己没想好的地方,然后通过一步一步的修正才最后改好。讲讲思路把:既然是分治就应该很容易想到递归,因为是分成好多人去做一件事,这件事是什么呢,是把两个数组从小到达拼合。既然是拼合,那肯定要引入一个新的数组了,不然是拼合不了的,不信的话可以自己试试,所以说归并排序的空间复杂度是O(n),因为要开辟一个一模一样大的空间去做拼合用。每次拼的时候先放到开辟的空间上去,之后在一模一样复制回来。这步很关键,我就是忘了这一步所以卡了好久,程序里原数组是a,新的是b。你每次分治后往回传递的都是a数组,虽然b数组是排好的,但是a不是啊,所以没有意义。还是那句话,自己不写程序出问题是看不懂我在说什么的,还是自己动手写写,这个程序还是挺烧脑的,大家加油。

//归并排序采用递归的方式 核心思想是分治
int* Merge(int *a,int*b,int left,int middle,int right)
{
	int i,j,k,num;
	k=left;
	num = right-left+1;
	for(i=left,j=middle+1;i<=middle && j<=right;)
	{
		if(a[i]<a[j])
			b[k++] = a[i++];
		else
			b[k++] = a[j++];
	}
	while(i<=middle)
		b[k++]=a[i++];
	while(j<=right)
		b[k++]=a[j++];
	for(i=0;i<num;i++)
		a[left+i] = b[left+i];
}

void Msort(int *a,int*b,int left,int right)
{
	int middle = (left+right)/2;
	if(left<right)
	{
		Msort(a,b,left,middle);
		Msort(a,b,middle+1,right);
		Merge(a,b,left,middle,right);
	}
}

void MergeSort(int *a,int size)
{
	int *b = (int*)malloc(size*sizeof(int));
	Msort(a,b,0,size-1);
	free(b);
}

快排:快拍的思想是,先选一个中枢,比他大的放在一侧,比他小的放在另一侧。然后两侧再各自选中枢,再分,一直分下去。其实也是基于分治理念的,但是不是平均的分治。讲道理是平均的分治更快的,但是为什么快排要比归并排序更快一点呢,因为在分的过程中,那个中枢的位置其实已经排好了,可以在一定程度上弥补大小不等的分治缺陷。但是快排对于中枢的选择的好会让算法更好,最好选到中间,但是不能花那么多时间去找最中间的。比较好的做法是每次去头尾中间三个,取中间值那个。我为了图省事,每次还是取左边第一个作为中枢来讲了。第一次中枢是5,分成左右两部分的同时5也排好了,坐标的中枢是8,右边的中枢是2,依次排下去就好了。快排应该是使用的最多的一种排序方式了,因为时间复杂度低,又不用额外开辟空间。
4ubmV0L3dlaXhpbl80MzM4OTE3OQ==,size_16,color_FFFFFF,t_70)

程序也是不好写,只要跟递归联系上的程序都不好写。第一步是取中枢后的左右交换,理论上是有好几种想法的,我就讲一下我的想法吧。如下图所示,选好中枢之后把它取出来,然后在另一侧设一个指针,一直减减,找到一个小于中枢的值3,就把左右指针的元素互换,因为左边的是空的,所以就是把右边的元素3放过来,然后右边变成空的。之后左边指针加加,找到一个大于中枢的值8,放到右边,一直下去,知道两个指针相遇,结束,把中枢放进去。然后左右两部分继续递归,知道只剩一个元素的时候停止递归。
在这里插入图片描述

void Qsort(int *a,int left,int right)
{
	int pivot,swap;
	int i,j;
	if(left==right)return;
	i = left;
	j = right;
	pivot = a[left];
	while(i<j)
	{
		while(i<j && a[j]>=pivot)
			j--;
		a[i] = a[j];//当j=i时也没问题
		while(i<j && a[i]<pivot )
			i++;
		a[j] = a[i];
	}
	a[i] = pivot;
	Qsort(a,left,i-1);
	Qsort(a,i+1,right);
}
void QuickSort(int *a, int size)
{
	Qsort(a,0,size-1);
}

程序虽然短,但是想起来不是太好想。之前写的是while(a[j–]>=pivot),有问题,其实我想要的效果是while(a[j]>=pivot)j–;但是这两句话是不等同的,虽然是后置减减,看起来没什么问题,区别在于,前面那句话如果判断失败了j也会–,后面那句话只有判断成功了才会执行–。虽然只差了一点点,但是对于程序来说直接影响了最后结果。

计数排序、桶排序、基数排序这三种因为太局限了,不太实用。所以也没详细写,我在刚开始挂的那篇文章写的挺好的,大家可以点进去看。懒得点进去的,我就抄了点哈

计数排序:计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
如下图所示,知道了整数是1-9之间的,就在外部申请一个1到9的数组,然后把所有元素都放到对应的数组里,因为数组是有序的,把数组从小到大把所有元素都取出来就是有顺序的了。
在这里插入图片描述
桶排序:是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
在这里插入图片描述

基数排序:是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。可以理解为双重计数排序。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值