C语言-排序

十大排序算法

  • 冒泡排序
  • 快速排序
  • 选择排序
  • 直接插入排序
  • 希尔排序
  • 归并排序
  • 堆排序
  • 计数排序
  • 基数排序
  • 桶排序

一、冒泡排序

原理
两两比较相邻元素,如果顺序不对,则进行交换。达到使小数如气泡一样往上浮动(例如在数组中将元素左移),或者使大数如石头一样下沉(在数组中将元素右移)
步骤

  1. 设待排序的n个元素存放在数组r[0…n-1] (注:此处数组下标从0开始)中,首先将第一个元素和第二个元素对比,如果(r[0] > r[1])则交换两个元素顺序。接着比较第二个和第三个元素。依次类推,直至r[n-2]和第r[n-1]进行比较。这个过程就是第一趟起泡排序,结果是最大的元素被放在了r[n-1]的位置(即最后一个位置)
  2. 进行第二趟起泡排序,对前n-1个元素进行同样的操作,使第二大的数放在了r[n-2]的位置
  3. 重复上述步骤,第i趟是从r[0]到r[n-i-1] (注:这里i也是从0开始)依次比较两个元素,并在逆序(前一个元素比后一个元素大)时交换顺序,结果是n-i个元素中值最大的数被放到了r[n-i-1]的位置。直到在某一趟排序过程中没有进行交换操作,说明序列已经全是顺序排列(从左到右元素从小到大排列)

代码实现:

int oriData[8] = {49,38,65,97,76,13,27,49};
void bubbleSprt(int* data, int len)
{
	int flag = 1;						//用于记录某本趟排序是否发生顺序交换
	for(int i = 0; (i < len) && (flag == 1); i < len)
	{
		flag = 0;						//flag置为0,如果本趟Paiute没有发生交换,则说明全部数据已经是顺序排列,不需要再比较
		for(int j = 0; j < len - i - 1; j++)
		{
			if(data[j] > data[j + 1])
			{
				int tmp = data[j + 1];
				data[j + 1] = data[j];
				data[j] = tmp;			//交换顺序
				flag = 1;				//表明本趟排序发生了交换
			}
		}
	} 
	return;
}

在这里插入图片描述
算法分析:

  1. 时间复杂度
    最好情况(初始数据为顺序排列):只需进行一趟排序,在排序过程中进行n-1次元素比较,且不存在交换
    最坏情况(初始数据为逆序排列):需进行n-1趟排序,元素比较次数KCN和移动次数RMN(每次交换移动3次)
    在这里插入图片描述
    在这里插入图片描述
    平均情况下,冒泡排序比较次数和交换测试分别约为在这里插入图片描述在这里插入图片描述,时间复杂度为O(n^2)

  2. 空间复杂度
    冒泡排序只有在交换顺序时用到一个辅助变量做暂存,所以空间复杂度为O(1)

二、快速排序

原理:
快速排序是对冒泡排序的的一种改进。在冒泡排序中,只对相邻的两个数进行比较,每次交换两个相邻数只能消除一个逆序。快速排序是任选一个数(比如数组中第一个数)作为基准数,将所有比其小的数放在它左边,所有比它大的数放在右边,这时,一趟快速排序将要排序的序列分割为两部分,然后再按此方法对两部分数据分别进行快速排序,整个过程递归进行,到每个子序列只有一个数时,排序完成,得到有序序列。
步骤:

  1. 选择待排序序列的第一个数作为基准数,附设两个指针left和right.初始时分别指向数组的第一个数和最后一个数(第一趟:left = 0;right = n - 1)
  2. 从数组最右侧依次向左搜索,找到第一个比基准数小的数,将其移到data[left]处。具体操作为:当left<right时,若data[right] >= 基准数.则向左移动指针(high- -);否则将data[high]和基准数交换。
  3. 然后再从数组最左侧位置,依次向右搜索,找到第一个比基准数大的数,将其移动到data[right]处,具体操作为:当left< right时,若data[left]小于等于基准数,则向右移动指针(left++);否则将data[left]和基准数交换
  4. 重复2和3的操作,直到left和right相等位置。此时left或right的位置即为基准数在祠堂排序中的最终位置,原序列被分为两个子序列。

在上述过程中,数据的交换都是与基准数之间发生,每次交换都要移动3次,排序过程中只移动要和基准数交换的数,也就是制作data[left]和data[right]的单向移动,知道一趟排序结束后再将基准数移到正确的位置上
代码实现:

int oriData[8] = {49,38,65,97,76,13,27,49};
void qSort(int* data, int begin, int end)
{
	int tmp = data[begin];								//记录基准值
    int left = begin;
    int right = end;

    if (right <= left)  return;
    while(left < right)									//从表的两端交替地向中间扫描
    {
        while((left < right) && (data[right] >= tmp))
            right--;
        data[left] = data[right];						//将比基准值小的数移到左边
        while((left < right) && (data[left] <= tmp))
            left++;										//将比基准值大的数移到右边
        data[right] = data[left];
    }
    data[left] = tmp;									//插入基准值
    quickSort(data, begin, left - 1);					//对左子表递归排序
    quickSort(data, left + 1, end);						//对右子表递归排序
    return;
}
void qucikSort(int* data, int len)
{
	qSort(int* data, 0, len - 1);
	return;
}

算法分析:

  1. 时间复杂度
    快速排序的趟数取决于递归树的深度
    最好情况:每一趟排序后都能将序列均匀地分割成两个长度大致相等的子序列,类似折半查找。在n个数据的序列中,对基准值定位所需时间为O(n).如设T(n)是对n个数据进行排序所需的时间,而且每次对基准值正确定位后,正好把序列分为两个长度相等的子序列,那么总的排序时间为

    最坏情况:在待排序序列已经排好序的情况下,其递归树成为单支数,每次划分只得到一个比上次少一个数的子序列。这样,必须经过 n-1 趟才能将所有数定位,而且第i趟需要经过n-1-i次比较。这样,总的关键字比较次数KCN为
    在这里插入图片描述
    这种情况下,快速排序已经退化到简单排序的水平。基准数的合理选择可以避免这种最坏情况的出现。
    理论证明,平均情况下,快速排序时间复杂度为在这里插入图片描述
  2. 空间复杂度
    快速排序是递归的,执行时需要一个栈来存放相应的数据。最大递归调用次数与递归树的深度一直。所以最好情况下的空间复杂度为在这里插入图片描述,最坏情况为O(n).

算法特点:

  1. 排序过程需要定位序列表的上界和下届,适用于顺序存储结构,很难适用于链式结构
  2. 当n比较大时,在平均情况下,快速排序时所有内部排序方法中速度最快的一种,所以其适合初始记录无序、n较大时的情况。

三、选择排序

原理:和冒泡排序类似,只不过是每一趟从待排序的元素中选出最小(或最大)的元素,按顺序排在已经排序的序列最后,直到全部排完为止。可以看作是冒泡排序的优化。
步骤

  1. 设待排序的n个元素存放在数组r[0…n-1] (注:此处数组下标从0开始)中,第一趟从r[0]开始,通过n-1次比较,从n个元素中找出最小的数,记为r[k],交换r[0]和r[k]。
  2. 第二趟从r[1]开始,通过n-2次比较,从n-1个元素中找出最小的数,记为r[k],交换r[1]和r[k]。
  3. 重复上述步骤,第i趟是从r[i]开始(注:这里i从0开始计),通过n-1-i次比较,从n-i个元素中找到最小的数,记为r[k],交换r[i]和r[k]
  4. 经过n-1趟,排序完成

代码实现:

int oriData[8] = {49,38,65,97,76,13,27,49};
void selectSort(int* data, int len)
{
	for(int i = 0; i < len; i++)			//从r[i, len-1]中找出最小值
	{
		int k = 0;
		for(int j = i + 1; j < len; j++)
		{
			if(data[j] < data[k])
				k = j;						//k记录最小值下标
		}
		if(k != i)							//交换r[i]和r[k]
		{
			int tmp = data[i];
			data[i] = data[k];
			data[k] = tmp;
		}
	}
	return;
}

在这里插入图片描述

算法分析:

  1. 时间复杂度
    最好情况(初始序列为顺序):不需要移动
    最坏情况(初始序列为逆序):移动3(n-1)次
    任何情况查找最小值的比较次数相同,为:
    在这里插入图片描述

所以,选择排序的时间复杂度也为O(n^2).
2. 空间复杂度
同冒泡排序一样,只有在交换顺序时需要一个辅助变量,所以空间复杂度也为O(1).

四、直接插入排序

原理
插入排序不是交换位置,而是通过比较,找到合适的位置插入。每次排序将一个待排数据按其大小插入到已经排序好的序列中,直到所有待排记录全部插入为止。
步骤:

  1. 设待排序的n个元素存放在数组r[0…n-1] (注:此处数组下标从0开始)中,认为r[0]为一个有序序列
  2. 循环n-1次,每次是用顺序查找法,查找r[i] (i = 1,2,…,n-1)在已排好序的序列r[0…i-1]中的插入位置,然后将r[i]插入长度为 i 的有序序列r[0…i-1],直到将r[n-1]插入表长为n-1的有序序列r[1…n-2]中,最后得到一个表长为n的有序序列

代码实现:

int oriData[8] = {49,38,65,97,76,13,27,49};
void insertSort(int* data, int len)
{
	for(i = 1; i < len; i++)			//第一个元素默认为已经排好序,所以从第二个元素开始
	{
	    if(data[i]  < data[i - 1])		//<,将data[i]插入有序序列
	    {
	        int tmp = data[i];			//将待插入元素暂存
	        data[i] = data[i - 1];		//r[i -1]后移
	        for(j = i - 2; ((j >= 0) && (tmp < data[j])); j--)//从后往前找插入位置
	        {
	            data[j + 1] = data[j];	//比待插入元素大的数逐个后移,直到找到合适位置
	        }
	        data[j + 1] = tmp;			//将待插入元素插入到正确位置
	    }  
	}
	return;
}

在这里插入图片描述
算法分析:

  1. 时间复杂度
    中间某一趟的排序,内层for循环次数取决于待插入数据与前i个数据之间的关系。
    最好情况(初始序列为顺序),比较1次,不移动。
    最坏情况(初始序列为逆序),比较 i + 1 次(同前面 i 个数比较,另外和暂存数比较一次),移动 i + 2 次(前面 i 个数依次后移,另外开始将待插入数据移到临时变量中,最后从临时变量移到正确的插入位置中)

    所以整个排序过程需要执行 n-1 次
    最好情况下,总的比较次数达 n-1次,不移动
    最坏情况,总的比较次数KCN和移动次数RMN均达到最大值,分别为
    在这里插入图片描述
    在这里插入图片描述
    平均情况下,插入排序的比较次数和移动次数均为在这里插入图片描述
    因此,插入排序的时间复杂度为O(n^2)

  2. 空间复杂度
    只有在交换顺序时需要一个辅助变量,所以空间复杂度为O(1).

算法特点:
适用于初始序列基本有序(顺序)的情况,当初始序列无序,且n较大时,时间复杂度较高,不宜采用。

五、希尔排序

原理:

基于插入排序的快速排序算法。直接插入排序对于n比较大的无序序列效率很低,因为元素只能一点一点地从数组一端移到另一端。希尔排序时为了加快速度简单地改进了直接插入排序,也成为“缩小增量排序”。
希尔排序实际是采用分组插入的方法。先将整个序列分割为几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。这样当经过几次分组排序后,整个序列的记录“基本有序”时,再对全部序列进行依次直接插入排序。

步骤:

  1. 第一趟取增量d1(d1 < n)把全部记录分成d1个组,所有间隔d1的数据分在同一组,在各个组中进行直接插入排序
  2. 第二趟取增量d2(d2 < d1),重复上述的分组和排序
  3. 依次类推,直到所取的增量,所有数据都在同一组中进行直接插入排序。

【补充】
理论上,也就是说只要一个序列是递减的,并且最后一个值是1,都可作为增量序列使用。是否存在一个增量序列使得排序过程中所需的比较和移动次数较少,并且不管n有多大,算法的时间复杂度都能渐进最低,目前在数学上,没法证明某个增量序列是最好的。
【常用增量序列】

  • 希尔增量序列:{n/2, n/4, n/8,…, 1}
  • Hibbard序列:在这里插入图片描述
  • Sedgwick序列:{…, 109, 41, 19, 5,1},表达式为在这里插入图片描述

代码实现:

int oriData[10] = {49,38,65,97,76,13,27,495504};
int dtData[3] = {5,2,1};
//采用希尔增量序列,n = 10,增量序列为{5,2,1}
//第一趟取增量d1=5,所有间隔为5的数据分在同一组,全部数据分为5组,在每个组分别进行直接插入排序
//第二趟取增量d2=2,所有间隔为2的数据分在同一组,全部数据分为2组分别进行直接插入排序
//第三趟取增量d3=1,对整个序列进行一趟直接插入排序,排序完成
void shellInsertSort(int* data, int len, int dt)
{
	for(int i = dt; i < len; i++)
	{
		if(data[i] < data[i-dt])
		{
			int tmp = data[i];
			for(int j = i - dt; (j >= 0) && (tmp < data[j]); j-=dt)
			{
				data[j + dt] = data[j];			//记录后移,直到找到插入的正确位置
			}
			data[j+ dt] = tmp;					//插入到正确的位置
		}
	}
}
void shellSort(int* data, int len, int* dtData, int dtLen)
{//按照增量序列dt,对序列做dtLen趟希尔排序
	for(int i = 0; i < dtLen; i++)
		shellInsertSort(data, len, dtData[i]);	//一趟增量为dtData[i]的希尔插入排序
}

在这里插入图片描述
算法分析:

  1. 时间复杂度
    当增量大于1时,较小的数就不适合直接插入法那样一步一步地挪,而是跳跃式地挪,从而使得在进行最后一趟增量为1地插入排序中,序列基本有序,只要做少量的比较和移动即可完成排序,因此希尔排序的时间复杂度比直接插入排序低。但要具体进行分析,就是一个比较复杂的问题,因为希尔排序的时间复杂度是自己取的增量序列的函数。
    目前为止还没有人求得最好的增量序列,一些结论是,当增量序列为在这里插入图片描述时,希尔排序得时间复杂度为,其中t为排序趟数,在这里插入图片描述.还有实验结论为:当在某个特定范围内,希尔排序所需得比较和移动次数为n^1.3,当在这里插入图片描述
  2. 空间复杂度
    也只需要一个辅助空间,空间复杂度为O(1).

算法特点:

  1. 只能用于顺序存储结构,不能用于链式结构
  2. 元素总的比较和移动次数都比直接插入排序要少,n越大,效果越明显。所以适用于初始记录无序、n较大时的情况。

六、归并排序

原理:
归并排序就是将两个或两个以上的有序表合成一个有序表的过程。将两个有序表合并成一个有序表的过程成为2-路归并,2-路归并最简单和常用,与之对对应的还有多路归并。
归并排序算法的思想为:设初始序列含有n个数据,则可看成n个有序的子序列,每个子序列长度为1,然后两两归并,得到⌈n/2⌉个长度为2或为1的有序子序列;再两两归并,…如此重复,直到得到一个长度为n的有序序列为止。
步骤:
目的:将数组r[0,1,…,n-1]的记录归并排序后放入t[0,1,…,n-1]中,low = 0,high = n-1。当序列长度等于1时,递归结束,否则:

  1. 将当前序列一分为二,求出分裂点mid=⌊(low+high)/2⌋;
  2. 对子序列r[low,…,high]递归,进行归并排序,结果放入s[low…mid]中;
  3. 对子序列r[low+1,…,high]递归,进行归并排序,结果放入s[mid+1,…,high]中;
  4. 将有序的两个子序列s[low, …, mid]和s[mid+1, …, high]归并为一个有序序列r[low, …, high].

代码实现:

int oriData[7] = {49,38,65,97,76,1327};
void merge(int* data, int low, int mid, int high)
 {
    int i = low;
    int j = mid +1;
    int k = low;
	int tmp[high + 1];
	
    for(k = low; k <= high; k++)					//将子序列data[low, ..., mid]和data[mid+1,...,high]暂存到tmp[low, high]中
    {
        tmp[k] = data[k]; 
    }
    k = low;
    while((i <= mid) && (j <= high))				//将tmp中数据由小到大移动到data中
    {
        if(tmp[i] <= tmp[j])    data[k++] = tmp[i++];
        else data[k++] = tmp[j++]; 
    }
    while(i <= mid) data[k++] = tmp[i++];			//将剩余tmp[1, ...,mid]复制到data中
    while(j <= high)data[k++] = tmp[j++];			//将剩余tmp[j, ...,high]复制到data中
 }
void mSort(int* data, int low, int high)
 {
     if(low < high)
     {
       int mid = (low + high) / 2;					//将序列一分为二,求出分裂点mid
       mSort(data, low, mid);						//对子序列data[low,...,mid]递归归并排序,结果放回data[slow,...,mid]
       mSort(data, mid + 1, high);					//对子序列data[mid+1,...,high]递归归并排序,结果放回data[mid+1,...,high]
       merge(data, low, mid, high);					//将顺序子序列data[low, ..., mid]和将顺序子序列data[mid+1,...,high]归并到data[low, high]
     }
     return;
 }
void mergeSort(int* data, int len)
{
	mSort(nums, 0, len -1);
	return;
}

在这里插入图片描述
算法分析:

  1. 时间复杂度
    当序列长度为n时,需进行在这里插入图片描述趟归并排序,每一趟归并,数据比较次数不超过n,数据移动次数都是n,因此归并排序的时间复杂度为在这里插入图片描述

  2. 空间复杂度
    用顺序表实现归并排序时,需要和待排序序列长度相等的辅助空间,所以空间复杂度为O(n).

算法特点:
可用于链式存储结构,且不用附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。

七、堆排序

篇幅较长,另作一篇,见C语言-堆排序

八、计数排序

未完…
【参考资料】
《数据结构》C语言版|第2版

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值