C语言排序算法

前言

在讲排序前我先将两个数的交换包装成一个函数,以便后面的使用,并且我们今天的结果都是要排升序

以这组数据为例:

int arr[10] = { 3,5,2,7,8,6,1,9,4,0 };

//交换函数
void Swap(int* i, int* j)
{
	int temp = *i;
	*i = *j;
	*j = temp;
}

直接插入排序

时间复杂度O(N^2)

图示讲解

因为是从第二个开始插入的,所以只需要循环n-1次,每一次都会将选中的tmp插入到前面已经排好序的数据中,构成一个新的有序数组

代码实现

//插入排序 时间复杂度 O(N^2)
void InsertSort(int* arr, int n)
{
	for (int i = 0;i < n - 1;i++)
	{
		int end = i;
		int tmp = arr[end + 1];//起点时从第二个元素开始插入
		//如果满足大于起点元素就放到下标为 end+1 的位置
		while (end >= 0)
		{	
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;//两种情况,第一种是插入的值一直小于排好序的值,end为-1;
						   //第二种是找到可以插入的位置,end下标为小于该值的数;
	}
}

希尔排序

时间复杂度为 O(N*logN)

图示讲解

代码实现

//希尔排序 时间复杂度为 O(N*logN)
//1.先进行预排序,让数组接近有序
// 设置间隔gap为一组,先假设 gap=3 
//2.直接插入排序
void ShellSort(int* a, int n)
{	
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		//gap == 1 时就是直接插入排序
		//多组gap的数据同时排
		for (int i = 0;i < n - gap;++i)//默认10个数据的情况
		{	
			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;//同样为两种情况
		}
	}
}

选择排序

图示讲解

代码实现

//直接选择排序(做了优化,头尾开始找)
//但仍然是最差性能的排序,因为无论顺序如何,都会遍历
void SelectSort(int* a, int n)
{	
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{	
		int mini = begin, maxi = begin;
		for (int i = begin;i <= end;++i)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[begin], &a[mini]);//小的放前面
		if (begin == maxi)//没有这个if时,当第一个元素是最大值,交换会篡改maxi下标对应的值为mini下标的值
		{
			maxi = mini;//mini此时下标对应的是最大值,应当将这个值还给a[maxi]
		}
		Swap(&a[end], &a[maxi]);//大的放后面
		++begin;
		--end;
	}
	
}

堆排序

堆排序的使用条件:

如果要使用堆排序,我们首先要有一个“”这个堆分为两种模式:“大顶堆”和“小顶堆”。

  1. 大顶堆(Max Heap):在大顶堆中,父节点的值大于或等于其子节点的值。也就是说,堆中的最大元素位于堆的根部。在大顶堆中,任意节点的值都大于或等于其子节点的值。

  2. 小顶堆(Min Heap):在小顶堆中,父节点的值小于或等于其子节点的值。也就是说,堆中的最小元素位于堆的根部。在小顶堆中,任意节点的值都小于或等于其子节点的值。

  3. 而堆可以看作是一种特殊的二叉树,被称为"堆树"。

图示讲解

这里我们只展示大顶堆的建造过程,建小顶堆方法与之类似

大顶堆的建堆过程:


 

建大堆代码实现:

//建堆
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = root * 2 + 1;//默认先找左孩子
	while (child < n)//右孩子可能越界
	{	
		//找出左右孩子大的一个
		if (child + 1 < n && a[child] < a[child + 1])
		{
			++child;//右孩子
		}
		//满足条件交换
		if (a[child] > a[parent])
		{
			Swap(&a[parent], &a[child]);
			parent = child;//向下调整
		}
		else
		{
			break;
		}
		child = parent * 2 + 1; // 更新 child
	}
}

排序阶段

有了大堆,接下来我们可以进行排 升序 

排序图解

排序代码实现:

//堆排序	时间复杂度为 O(N*logN)
void HeapSort(int* a, int n)
{	
	//从下向上调整
	for (int i = (n - 1 - 1) / 2;i >= 0;--i)
	{
		AdjustDown(a, n, i);//这个i是最后一个非叶子结点的父节点
	}
	//排升序: 建“大”堆!:
	int end = n - 1;//数组的最后一位下标
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);//因为这时的根节点是较小值,需要从上向下调整
		--end;
	}
}

冒泡排序 

因为每一次都会选出一个最大的数,所以只需要循环n-1次即可。

但需要注意的是,如果当前数组已经有序了,就没必要继续进行下去;我们可以加上一点优化:如代码中的 exchange,用来统计是否进行了交换,没有交换即有序。

图示讲解

初始数组:     3, 5, 2, 7, 8, 6, 1, 9, 4, 0

第一次排序: 3, 2, 5, 7, 6, 1, 8, 4, 0, 9

第二次排序: 2, 3, 5, 6, 1, 7, 4, 0, 8, 9

第三次排序: 2, 3, 5, 1, 6, 4, 0, 7, 8, 9

第四次排序: 2, 3, 1, 5, 4, 0, 6, 7, 8, 9

第五次排序: 2, 1, 3, 4, 0, 5, 6, 7, 8, 9

第六次排序: 1, 2, 3, 0, 4, 5, 6, 7, 8, 9

第七次排序: 1, 2, 0, 3, 4, 5, 6, 7, 8, 9

第八次排序: 1, 0, 2, 3, 4, 5, 6, 7, 8, 9

第九次排序: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

代码实现

//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0;i < n - 1;++i)
	{	
		int exchange = 0;
		for (int j = 1;j < n - i;++j)
		{
			if (a[j] < a[j - 1])
			{
				Swap(&a[j - 1], &a[j]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

快速排序

首先我们在讲解快速排序之前先了解什么是基准元素,快速排序之所以快速,是因为它运用到了分治法进行排序,比如:

对班级的同学按照身高大小来排序时,

1. 我们要先找出一个人,比他矮的站在他的左边,比他高的站在他的右边;

2. 接下来对他左边的人群继续找出一个人,根据 (1) 中的方式进行分类。

3. 然后时右边,也同样按照(1 )中的方式。

4. 直到不会分类为止。

但是这个人要怎么寻找呢?

基准元素的选择对快速排序的效率有着重要的影响。一般情况下,可以采用以下几种策略来选择基准元素:

  1. 固定位置:选择数组的第一个元素、最后一个元素或者中间元素作为基准元素。这种策略简单直接,但在某些特殊情况下可能会导致快速排序的性能下降,比如数组已经部分有序或逆序的情况。

  2. 随机选择:在数组中随机选择一个元素作为基准元素。这种策略能够在大多数情况下保证快速排序的平均性能,因为它减少了在特殊情况下(如有序或逆序数组)性能下降的可能性。

  3. 三数取中:在数组的头部、尾部和中间分别取一个元素,然后取这三个元素的中间值作为基准元素。这种策略在某些情况下能够更好地适应不同的输入数据,提高快速排序的效率。

找基准元素

下面是三种找基准元素的方法:

三数取中法

三数取中法:这种方法是从数组的开头、结尾和中间各选择一个元素,然后取这三个元素的中间值作为基准元素。这样做的目的是尽量避免在有序数组或逆序数组等特殊情况下出现快速排序性能下降的情况,从而提高排序的效率。

代码 
//三数取中,适用于接近有序的快速排序
int PartSort1(int* a, int left, int right)
{	
	int mid = (left + right) >> 1;//基准元素
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] < a[left])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

左右指针法

左右指针法:也称为单边扫描法,是一种基于左右指针的基准元素选择方法。该方法通过设置一个左指针和一个右指针,从数组的两端向中间移动,直到两个指针相遇。在移动过程中,左指针寻找大于等于基准元素的值,右指针寻找小于等于基准元素的值,然后交换它们。最终,当两个指针相遇时,基准元素的位置就确定了。这种方法的优点是只需要扫描一遍数组即可完成基准元素的选择,而不需要额外的空间。

图解 

代码
//(左右指针法)
int PartSort2(int* a, int left, int right)
{
	int begin = left + 1;//因为最开始不必自己和自己交换
	int end = right;
	int keyi = begin;
	while (begin < end)
	{	
		//找小
		while (begin < end && a[end]>= a[keyi])
		{
			--end;
		}
		//找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}
		Swap(&a[begin], &a[end]);
	}
	//相遇
	Swap(&a[begin], &a[keyi]);
	
	return begin;
}

前后指针法

前后指针法:是一种常用的基于前后指针的基准元素选择方法。这个方法的核心思想是通过一个循环将小于基准元素的值放到基准元素的左侧,大于等于基准元素的值放到右侧,从而完成分区操作。这种方法的时间复杂度为 O(n),因为只需一次遍历就可以完成分区操作。

图解 

从图中我们可以看出来,如果key是这个区间的最大值,则进行的交换都是没有意义的,我们可以采用判断:

if ((a[cur] < a[keyi] )&&(cur != prev+1)) 

我们同样可以获取正确的答案。 

代码 
//(前后指针法)
int PartSort3(int* a, int left, int right)
{
	//cur找小,每次遇到比 key 小的值就停下来,++prev,交换cur和prev的值
	int keyi = left;
	int	prev = left;
	int cur = left + 1;
	while (cur <= right)
	{	
		//prev ~ cur 之间可能有大于keyi的值 cur++
		if (a[cur] < a[keyi])//prev == cur自身交换没有意义
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

填坑法

图解

代码 
//填坑法
int PartSort4(int* a, int left, int right)
{
	int begin = left;
	int end = right;
	int key = a[begin];
	int pivot = begin;
	while (begin < end)
	{
		//右边找小 放到左边
		while (a[end] >= key && begin < end)
		{
			--end;
		}
		a[pivot] = a[end];//小的放到左边的坑,自己形成新的坑
		pivot = end;
		//左边找大 放到右边
		while (a[begin] <= key && begin < end)
		{
			++begin;
		}
		a[pivot] = a[begin];//大的放到小边的坑,自己形成新的坑
		pivot = begin;
	}
	pivot = begin;//此时重合
	a[pivot] = key;
	return pivot;
}

排序代码实现

//快速排序(递归版本) (挖坑法,三数取中) (时间复杂度为 O(N*logN)数据量大,可能会栈溢出
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int index = PartSort2(a, left, right);//可以用左右指针法,前后指针法......
	//如果分割操作导致了不平衡,即左右两个子数组的大小差别较大,会导致递归树的不平衡,使得其中一个子树的深度较深,而另一个子树的深度较浅。
	
	//QuickSort(a, left, index - 1);
	//QuickSort(a, index + 1, right);

	//小区间优化:比如对 100W 的数据快速排序,但调用函数次数集中在第后面的2^17;2^18;2^19次拆分的小区间
	//但优化效果不是很明显,原因是CPU对于函数栈帧开辟的处理并不耗时,使得函数调用的开销相对较小
	if (index - 1 - left > 10)//对基准元素左边小区间优化
	{
		QuickSort(a, left, index - 1);
	}
	else
	{
		InsertSort(a + left, (index - 1) - left + 1);//插入排序
	}
	if (right - (index - 1) > 10)//对基准元素右边小区间优化
	{
		QuickSort(a, index + 1, right);
	}
	else
	{
		InsertSort(a + index + 1, right - (index + 1) + 1);
	}
}

在这段代码中,我进行了小区间优化

这是因为在数据量很大的情况下,越到后面的小区间会更多,举个例子:

假如我们要排序10W个数据,我们通过分区排序可以得到以下的图例

 

快速排序(非递归)

非递归可以避免栈溢出:下面我用模拟递归调用

图示讲解

代码实现

//快速排序(非递归)
void QuickSortNonR(int* a, int left, int right, int n)
{
	//建立栈的空间
	Stack* st;
	st = createStack(n);
	push(st, right);
	push(st, left);
	//开始,这里我想先对左区间排序,所以先压栈右区间,若左区间仍不有序,继续压栈左区间的分区,直至栈空
	while (!isEmpty(st))
	{	
		left = peek(st);
		pop(st);
		right = peek(st);
		pop(st);
		int index = PartSort3(a, left, right);//分区
		//先存右侧区间
		if (index + 1 < right)
		{
			push(st, right);
			push(st, index + 1);
		}
		//再存左侧区间
		if (left < index - 1)
		{
			push(st, index - 1);
			push(st, left);
		}
	}
	destroyStack(st);
}

归并排序

时间复杂度为 O(N*logN)

图示讲解

1. 3 4 

2. 3 4 7

3. 0 3 4 7 9

4. 2 6

5. 2 5 6

6. 1 8

7. 1 2 5 6 8

8. 0 1 2 3 4 5 6 7 8 9 

 

代码实现

//归并的子函数:
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) >> 1;
	//假设mid左右区间有序,可以进行归并了,以下的递归会将原数组拆分成一个结点返回上一层递归,然后排序这两个数,返回这两个数的数组区间,然后是这一层的另一个区间,1 ~ 2 ~ 4 ~ N最后的tmp是有序的
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
	//归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;//临时数组的下标,从0开始
	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];
	}
}

归并排序(非递归)

时间复杂度为 O(N*logN)

//归并的子函数:
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) >> 1;
	//假设mid左右区间有序,可以进行归并了,以下的递归会将原数组拆分成一个结点返回上一层递归,然后排序这两个数,返回这两个数的数组区间,然后是这一层的另一个区间,1 ~ 2 ~ 4 ~ N最后的tmp是有序的
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
	//归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;//临时数组的下标,从0开始
	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];
	}
}
//归并排序
//左右区间有序,再一次对比取小的放到新的临时数组
//2~4~8~16~N
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

图示讲解

代码实现

//归并排序(非递归)时间复杂度为(N*logN)
void MergeSortNonR(int* a, int n)
{	
	int* tmp = (int*)malloc(sizeof(int) * n);

	int gap = 1;//每个区间的个数
	while (gap < n)
	{
		for (int i = 0;i < n;i += 2 * gap)//两两区间归并
		{
			//两个一组归并排序
			//1.[i, i+gap-1] [i+gap, i+2*gap-1] 2个值排序 一个区间1个值
			//1.[i, i+gap-1] [i+gap, i+2*gap-1]	4个值排序 一个区间2个值
			//1.[i, i+gap-1] [i+gap, i+2*gap-1]	8个值排序 一个区间4个值

			int begin1 = i;
			int end1 = i + gap - 1 < n ? i + gap - 1 : n - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1 < n ? i + 2 * gap - 1 : n - 1;
			int index = i;

			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++];
			}
		}
		//拷贝回去
		for (int j = 0;j < n;++j)
		{
			a[j] = tmp[j];
		}
		gap *= 2;
	}
	
	free(tmp);
}

 到这里就结束了,感谢陪伴o(* ̄▽ ̄*)ブ

  • 23
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值