八大排序(使用C语言)

完整代码链接:

诶嘿/DataStructure - 码云 - 开源中国 (gitee.com)

目录

一、排序的概念及应用:

1.排序的概念:

2.排序应用:

二、常见排序算法的实现: 

1 插入排序:

1.1 基本思想:

1.2 直接插入排序:

1.2.1 代码实现: 

1.2.2 测试:

1.2.3 时空复杂度:

1.3 希尔排序(缩小增量排序):

1.3.1 代码实现: 

1.3.2 测试: 

1.3.3 时空复杂度:

2 选择排序:

2.1 基本思想:

2.2 直接选择排序:

2.2.1 代码实现:

2.2.2 测试:

2.2.3 时空复杂度:  

2.3 堆排序: 

3 交换排序:

3.1 基本思想:

3.2 冒泡排序:

3.2.1 代码实现:

3.2.2 测试:

3.2.3 时空复杂度: 

3.3 快速排序:

3.3.1 基本思想:

3.3.2 递归版:

3.3.2.1 递归版基本框架:

3.3.2.2 左右指针法: 

 3.3.2.2.1 代码实现:

3.3.2.2.2 注意事项:

3.3.2.3 挖坑法:

3.3.2.3.1 代码实现:

 3.3.2.4 前后指针法:

 3.3.2.4.1 代码实现:

3.3.2.5 测试:

3.3.3 非递归版: 

3.3.3.1 代码实现:

 3.3.3.2 测试 :

3.3.4 时空复杂度: 

4 归并排序:

4.1 基本思想:

4.2 递归版:

4.2.1 代码实现: 

4.2.2 测试: 

4.3 非递归版:

4.3.1 代码实现: 

4.3.2 测试:

4.4 时空复杂度: 

5 计数排序:

5.1 基本思想:

5.2 代码实现:

5.3 测试:

5.4 时空复杂度: 

三、性能对比:

四、总结: 


一、排序的概念及应用:

1.排序的概念:

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求能在内外存之间移动数据的排序。

2.排序应用:

 3.常见排序算法:

二、常见排序算法的实现: 

Look me我们这里统一排升序

1 插入排序:

1.1 基本思想:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

1.2 直接插入排序:

基本思路:当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

1.2.1 代码实现: 
//直接插入排序
void InsertSort(int* a, int n)
{
	assert(a);
	for (int i = 0;i < n - 1;i++)//注意i的边界
	{
		int end = i;
		int tmp = a[end + 1];
		while (end>=0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
1.2.2 测试:

1.2.3 时空复杂度

①.时间复杂度:

由于第1次要跟0个数比较,第2次要跟1个数比较……第n次要跟n-1个数比较,所以,0+1+2+…n-1=\frac{n*(0+n-1)}{2}=\frac{n^2-n}{2},所以,时间复杂度为 O(N^{2})。

最好的情况:原数据有序或者接近有序,此时,时间复杂度为O(N)(每次进入while循环就break)。

最坏的情况:数组逆序或接近逆序。

②.空间复杂度:O(1)。

1.3 希尔排序(缩小增量排序):

背景:为了解决直接插入排序存在的问题(数组逆序或接近逆序时,时间复杂度为O(N^{2})),所以出现了希尔排序。

基本步骤

①.预排序:将间距为gap的值分为一组,让数组接近有序;

②.直接插入排序:当gap==1时,进行直接插入排序。

1.3.1 代码实现: 
void ShellSort(int* a, int n)
{
	assert(a);
	//1.gap>1相当于预排序,让数组接近有序
	//2.gap==1相当于直接插入排序,保证有序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//+1保证了gap最后一次一定是1,当gap==1时,会先执行一次循环。gap不止这一种取法,但是这个方法比较好
		for (int i = 0;i < n - gap;i++)//多组并排
		{
			//以gap为间距,执行单趟排序
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
		//PrintArray(a, n);//每次打印一下以gap为间距排序后的数组
	}
}
1.3.2 测试: 

1.3.3 时空复杂度:

①.时间复杂度:

希尔排序的时间复杂度不好计算,因为其时间复杂度受到gap的取值的影响,所以导致很难计算。其大概的时间复杂度为 O(N^{1.3}N^{2})。

②.空间复杂度:O(1)。

2 选择排序:

2.1 基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.2 直接选择排序:

在原来的直接插入排序中,我们这里对其做了一个优化,时间效率增长了一倍。每次同时找到最大值与最小值,然后将最大值放在数组最后面,最小值放在数组最前面,缩小待排序区间后,重复上述步骤。

2.2.1 代码实现:
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void SelectSort(int* a, int n)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini, maxi;
		mini = maxi = begin;//存储最小值与最大值的下标
		for (int i = begin + 1;i <= end;i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;//更新最小值下标
			}
			if (a[i] > a[maxi])
			{
				maxi = i;//更新最大值下标
			}
		}
		Swap(&a[begin], &a[mini]);
		//如果maxi和begin在同一位置,则maxi的位置需要更改
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		//缩小区间
		begin++;
		end--;
	}
}

 

2.2.2 测试:

2.2.3 时空复杂度:  

①.时间复杂度:

第一次要比较n-2个数,第二次要比较n-4个数,……,所以其时间复杂度为 O(N^{2})。

②.空间复杂度:O(1)。

2.3 堆排序: 

这个我们之前讲了,可以看下面的链接。

二叉树的实现

3 交换排序:

3.1 基本思想:

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.2 冒泡排序:

基本思路:两两相比,前一个数大于后一个数就交换,第一次将最大的数放在最后,第二次将次大的放在倒数第二个位置,依次类推,直到只有一个数还有排序时。

3.2.1 代码实现:
void BubbleSort(int* a, int n)
{
	int end = n;
	while (end > 0)
	{
		int exchange = 0;
		for (int i = 1;i < end;i++)//单趟排序
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		//如果一趟冒泡的过程中没有发生交换,则前部分已经有序,不需要再冒泡
		if (exchange = 0)
		{
			break;
		}
		end--;
	}
}
3.2.2 测试:

3.2.3 时空复杂度: 

①.时间复杂度:O(N^{2});

②.空间复杂度:O(1)。

3.3 快速排序:

3.3.1 基本思想

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后再在左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

3.3.2 递归版:
3.3.2.1 递归版基本框架:
void QuickSort(int* a, int left, int right)//right指向最后一个元素
{
	assert(a);
	if (left >= right)
		return;
	if ((right - left + 1) > 10)
	{
		int div = PartSort1(a, left, right);//左右指针法
        //int div = PartSort2(a, left, right);//挖坑法
        //int div = PartSort3(a, left, right);//前后指针法
		QuickSort(a, left, div - 1);//递归排左区间
		QuickSort(a, div + 1, right);//递归排右区间
	}
	else
	{
		//小于等于10个以内的区间,不再递归排序(小区间优化)
		InsertSort(a + left, right - left + 1);//小区间使用插入排序去排,不再使用快速排序的递归排,减少整体的递归次数
	}
}
3.3.2.2 左右指针法: 

基本步骤:

将最后一个位置的数据作为key值,用一个begin指针指向数组头,一个end指针指向数组尾,让begin先向右走,找到比key大的值停下来;再让end向左走,找到比key小的值停下来,然后交换begin与end所指向的值。重复begin与end操作,直到begin与end指向同一个位置,将该位置的值与key值交换,完成一次整体排序。接下来就是递归去排左右区间的数据。

 3.3.2.2.1 代码实现:
int GetMidIndex(int* a, int left, int right)//三数取中,保证不取到最大与最小
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])//a[left]<a[mid]<a[right]
			return mid;
		else if (a[left] > a[right])//a[right]<a[left]<a[mid]
			return left;
		else
			return right;//a[left]<a[right]<a[mid]
	}
	else//a[left]>a[mid]
	{
		if (a[mid] > a[right])//a[left]>a[mid]>a[right]
			return mid;
		else if (a[left] < a[right])//a[right]>a[left]>a[mid]
			return left;
		else
			return right;//a[left]>a[right]>a[mid]
	}
}
//前后指针法
int PartSort1(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[end], &a[midIndex]);
	int keyIndex = end;
	while (begin < end)
	{
		while (begin < end && a[begin] <= a[keyIndex])//必须要有 begin < end,不然 begin 与 end 会错位
		{
			begin++;
		}
		while (begin < end && a[end] >= a[keyIndex])
		{
			end--;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyIndex]);
	return begin;
}
3.3.2.2.2 注意事项:

①.不要直接选最左边或最右边的数作为key值。因为在极端情况下(数组有序或接近有序),如果key值每次都是最大(小)的数,快速排序的时间复杂度就会是O(N^{2})。

③.当我们选择最右边值作为key值时,要让begin先走;同样地,当我们选择最左边值作为key值时,要让end先走。因为begin先走一定能保证相遇时所在位置的值一定比key要大(要么begin从左边与end相遇,要么end从右边与begin相遇,相遇点的位置一定在begin位置处,而begin是去找大于key的数),然后与key交换才不会出错(以升序为例)。

3.3.2.3 挖坑法:

基本步骤:

利用GetMidIndex()函数找到key,将其作为坑,然后begin去找比key值大的位置,把原来的坑给填上,将begin位置作为新的坑;然后让end去找比key值小的位置,再把原来的坑给填上,将end位置作为新的坑。重复上述步骤,直到begin与end指向同一个位置,再把key给填进去。接下来就是去递归排左右区间了。

3.3.2.3.1 代码实现:
int PartSort2(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[end], &a[midIndex]);
	int key = a[end];//坑(坑就是指该位置的值被拿走了,可以被新的值覆盖)
	while (begin < end)
	{
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		//左边找到比key大的填到右边的坑,begin位置形成新的坑
		a[end] = a[begin];
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		//右边找到比key小的值填到左边的坑,end位置形成新的坑
		a[begin] = a[end];
	}
	a[begin] = key;
}
 3.3.2.4 前后指针法:

基本步骤:

同样的,先找到key值。让一个cur指针指向数组第一个位置,prev指针指向数组第一个位置的前一个位置,当cur所指向的位置小于key时,prev向后走一步,然后交换cur与prev所指向的值;如果cur所指向的值大于key,就自加一步,prev不动。重复上述步骤,当cur走到最后一个位置,让prev再向后走一步,交换key与prev所指向位置的值,这样就实现了把小的翻到左边,大的推到右边。然后就是递归去排左右区间。

 3.3.2.4.1 代码实现:
int PartSort3(int* a, int left, int right)
{
	int midIndex = GetMidIndex(a, left, right);//7
	Swap(&a[right], &a[midIndex]);
	int keyIndex = right;

	int cur = left;
	int prev = left - 1;
	while (cur < right)
	{
		if (a[cur] < a[keyIndex] && ++prev != cur)
			Swap(&a[cur], &a[prev]);
		
		cur++;
	}
	Swap(&a[++prev], &a[keyIndex]);
	return prev;
}
3.3.2.5 测试:

3.3.3 非递归版: 

对于递归实现快速排序,如果需要排的数很多,可能会存在栈溢出问题(栈帧的深度太深,导致栈里面装不下了)

所以,我们这里需要使用一个栈(数据结构)来实现非递归 ,其本质就是用栈来保存要排序数组所在的区间,再在这个区间里面去排一下序(随便使用递归版里面的三种方法之一)。

3.3.3.1 代码实现:
void QuickSortNonR(int* a, int left, int right)
{
	assert(a);
	Stack st;
	StackInit(&st);
	//将要排序的数组下标压入栈内
	StackPush(&st, right);
	StackPush(&st, left);
	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
		//[begin,end]
		int div = PartSort3(a, begin, end);
		//[begin,div-1] div [div+1,end]
		if (div + 1 < end)//当数组里面值得个数>1
		{
			StackPush(&st, end);
			StackPush(&st, div+1);
		}
		if (begin < div - 1)
		{
			StackPush(&st, div - 1);
			StackPush(&st, begin);
		}
	}
	StackDestory(&st);
}

我们这里也可以加上小区间优化:

void QuickSortNonR(int* a, int left, int right)
{
	assert(a);
	if (right - left + 1 > 10)
	{
		Stack st;
		StackInit(&st);
		//将要排序的数组下标压入栈内
		StackPush(&st, right);
		StackPush(&st, left);
		while (!StackEmpty(&st))
		{
			int begin = StackTop(&st);
			StackPop(&st);
			int end = StackTop(&st);
			StackPop(&st);
			//[begin,end]
			int div = PartSort3(a, begin, end);
			//[begin,div-1] div [div+1,end]
			if (div + 1 < end)//当数组里面值得个数>1
			{
				if (end - div > 10)//end-(div+1)+1
				{
					StackPush(&st, end);
					StackPush(&st, div + 1);
				}
				else//小区间优化
				{
					InsertSort(a, end - div);
				}
			}
			if (begin < div - 1)
			{
				if (div - begin > 10)//div-1-begin+1
				{
					StackPush(&st, div - 1);
					StackPush(&st, begin);
				}
				else//小区间优化
				{
					InsertSort(a, div - begin);
				}

			}
		}
		StackDestory(&st);
	}
	else//小区间优化
	{
		InsertSort(a, right - left + 1);
	}
}
 3.3.3.2 测试 :

3.3.4 时空复杂度: 

①. 时间复杂度:O(NlogN);

②.空间复杂度:

因为在递归期间,使用栈帧也是会消耗空间的(但是递归回来,原来的空间又会被销毁,所以原来的空间可以重复利用),所以其空间复杂度是依据递归的深度决定的,因为这里深度为logn,所以空间复杂度为O(logN)

4 归并排序:

4.1 基本思想:

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

相信屏幕前的你看了这个题会更容易理解归并排序的思想:

合并两个有序数组

4.2 递归版:

基本步骤:

①.将原数组递归分解成一个个元素;

②.利用合并两个有序数组思想,合并分解后的数组。

4.2.1 代码实现: 
//合并
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int left = begin1, right = end2;
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];
	while (begin2 <= end2)
		tmp[index++] = a[begin2++];
	//把归并好的tmp的数据再拷贝到原数组
	for (int i = left;i <= right;i++)
		a[i] = tmp[i];
}
void _MergeSort(int* a, int left, int right, int* tmp)
{
    //分解
	if (left >= right)
		return;
	int mid = (left + right) / 2;
	//[left,mid] [mid+1,right] 有序,则可以合并现在它们没有序,子问题解决
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
	//归并[left,mid] [mid+1,right] 有序
	MergeArr(a, left, mid, mid + 1, right, tmp);
}
void MergeSort(int* a, int n)
{
	assert(a);
	int* tmp = malloc(sizeof(a[0]) * n);//创建辅助空间
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

4.2.2 测试: 

4.3 非递归版:

基本思路:

同样的,我们也要像递归版一样分解与归并数组。我们这里可以用一个gap变量来分解数组,在分解后的数组里面进行归并,利用gap的变化实现不同组的归并,直到gap==n时(可能不会到n,取决于左右子序列所在区间的范围,不能是gap>n/2时结束,因为不能保证左子序列与右子序列的区间长度是一样的),结束归并。

4.3.1 代码实现: 
//合并
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int left = begin1, right = end2;
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];
	while (begin2 <= end2)
		tmp[index++] = a[begin2++];
	//把归并好的tmp的数据再拷贝到原数组
	for (int i = left;i <= right;i++)
		a[i] = tmp[i];
}
void MergeSortNonR(int* a, int n)
{
	assert(a);
	int* tmp = malloc(sizeof(a[0]) * n);
	int gap = 1;
	while (gap < n)//不能是gap<=n/2,左右区间不一定均分
	{
		for (int i = 0;i < n;i += 2 * gap)
		{
			//[i,i+gap) [i+gap,i*2gap)
			//[i,i+gap-1] [i+gap,i*2gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
             //数组的长度不一定都是2^n
			//1.合并时只有第一组,第二组不存在,就不需要合并
			if (begin2 >= n)
				break;
			//2.合并时第二组只有部分数据,需要修正end2边界
			if (end2 >= n)
				end2 = n - 1;
			MergeArr(a, begin1, end1, begin2, end2, tmp);
		}
		gap *= 2;
	}
	free(tmp);
}
4.3.2 测试:

4.4 时空复杂度: 

①.时间复杂度:O(NlogN);

②.空间复杂度:O(N)。

5 计数排序:

5.1 基本思想:

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

基本步骤

①. 统计相同元素出现次数;

②. 根据统计的结果将序列回收到原来的序列中。

5.2 代码实现:

void CountSort(int* a, int n)
{
	assert(a);
	int min = a[0];
	int max = a[0];
	for (int i = 1;i < n;i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	int range = max - min + 1;
	int* countArr = (int*)malloc(sizeof(a[0]) * range);
	memset(countArr, 0, sizeof(a[0]) * range);
	//统计次数
	for (int i = 0;i < n;i++)
	{
		countArr[a[i] - min]++;
	}
	//排序
	int index = 0;
	for (int j = 0;j < range;j++)
	{
		while (countArr[j]--)
		{
			a[index++] = j + min;
		}
	}
	free(countArr);
}

5.3 测试:

5.4 时空复杂度: 

①.时间复杂度:计数排序的时间复杂度与空间复杂度会依据数据范围集中程度而变化,所以其时间复杂度为O(MAX(N,范围));

②.空间复杂度:O(范围)。

三、性能对比:

四、总结: 

  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值