(C语言)数据结构——冒泡排序和快速排序(超详解)

交换排序

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

1.冒泡排序

冒泡排序(Bubble Sort)是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。一趟冒泡排序就可以把一个最大或者最小的挑出来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

算法步骤:

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  • 针对所有的元素重复以上的步骤,除了最后一个。

  • 针对所有的元素重复以上的步骤,除了最后两个。

  • 重复持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

在这里插入图片描述
实现代码:

// 最坏情况:O(N^2)
// 最好情况:O(N)
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; ++j)
	{
		int flag = 0;
		for (int i = 1; i < n - j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O( N 2 N^2 N2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

2.快速排序

在这里插入图片描述

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。将区间按照基准值划分为左右两半部分的常见方式有以下三种:

hoare版本

下图为hoare版本单趟排序的动图:

在这里插入图片描述单趟排序
1、选一个key(数组下标)。(一般是第一个或者是最后一个)
2、单趟排序,要求小在的key的左边,大的在key的右边

实现代码:

// hoare
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		// 6 6 6 6 6
		// R找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}

		// L找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		if (left < right)
			Swap(&a[left], &a[right]);
	}
	//left==right
	int meeti = left;

	Swap(&a[meeti], &a[keyi]);

	return meeti;
}

完整快速排序思路分析:

  • 这样的一趟排序返回的这个meeti其实就是a[key]在数组中的正确位置,而且a[key]的左边都是小于a[key]的数了,a[key]的右边都是大于a[key]的数了,相当于这样的一种状态[begin, keyi-1] keyi [keyi+1, end],
  • 下一次就是再把[begin, keyi-1]这个区间和 [keyi+1, end]放入单趟排序函数里再排序,直到这个区间最后只剩下0个或者一个元素的时候递归就可以停止了。

实现代码:

// [begin, end]
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort1(a, begin, end);
	//[begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.1对快速排序算法优化

上述方法其实还有缺陷,由于我们每次选定key的都是待排序列的最左边第一个,然后经过一次排序就把它放到了它的最终位置。下面是理想情况的递归展开图,这样的前提条件就是整个序列处于一种混乱无序的状态。
在这里插入图片描述

那么假如待排序列是有序或者接近有序的递归展开图是怎样的呢?如下图,我们从最左边选一个数最后经过排序还是放到了最左边。
在这里插入图片描述
这样的情况我们把它想象成二叉树的话就是一颗没有左子树只有右子树的二叉树,执行次数是从N到1递减的,递归深度由理想状态的logN变为N,这样的递归深度数据量大的情况下可能会导致栈溢出。
所以我们要对选key进行优化:

下面是三个优化选key逻辑:
1、随机选一个位置做key
2、针对有序,选定中间值做key
3、 三数取中。在待排序数列的第一个位置和中间位置以及最后一个位置 选出中间值,然后把这个中间值与第一个位置交换。

这里我们就选择第三种方法的三数取中
实现代码:


int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] >= a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

类比二叉树
我们仔细想一下假设最后某一层的每一个待排序列只剩下8个数的时候,这一个序列就要递归调用7到8次左右才能完成排序,这样做很不划算,而数据量大的时候等到这一层有很多个这样的序列在等着我们呢。
在这里插入图片描述
怎么解决呢?
其实方法很简单
当待排序列小于或者等于8时我们就采用插入排序的算法。(这个方法也称小区间优化)
实现代码:

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end - begin <= 8)//待排序列小于或者等于8时我们就采用插入排序
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
	int keyi = PartSort3(a, begin, end);
	//[begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
	}
}

2.2其他快速排序单趟排序方法

挖坑法

下图为挖坑法单趟排序的动图:
在这里插入图片描述
解释:把左边第一个数定义为key并且从L开始先将其定义为坑位,然后R - - 找小(小于key的数),放入左边的坑位,更新坑位到R的位置。
然后L++找大(大于key的数)放入右边的坑位,再更新坑位到L的位置,这样循环往复。
直到L和R相遇,再把key填到L和R相遇的位置,然后返回这个相遇位置,为下一次递归做准备。
实现代码:

// 挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		// 右边找小,填到左边坑
		while (left < right && a[right] >= key)
		{
			--right;
		}

		a[hole] = a[right];
		hole = right;

		// 左边找大,填到右边坑
		while (left < right && a[left] <= key)
		{
			++left;
		}

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;
	return hole;
}

前后指针版本

下图为前后指针版本的单趟排序动图:
在这里插入图片描述
解释:通过观察动图我们凑丝剥茧,最明显的就是cur在一直找小

  • 当a[cur] < a[keyi]的时候就我们就让prev++然后交换cur的和prev的值,只不过前面他俩处在同一个位置交换之后也没有效果,
  • 当a[cur] >= a[keyi]的时候就只++cur
  • cur越界了就停止然后交换prev和key的值返回prev的位置即可。

这样key也找到了他自己正确的位置并且key的左边也是小于key的值,右边都是大于key的值。
实现代码:

int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		// 找小
		if (a[cur] < a[keyi])
		{
			++prev;
			Swap(&a[cur], &a[prev]);
		}
		
		++cur;
	}

	Swap(&a[keyi], &a[prev]);

	return prev;
}

2.3快速排序完整代码链接

快速排序完整代码链接

2.4快速排序的特性总结:

在这里插入图片描述

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:最好:O(N*logN),最坏:N * N
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
在这里插入图片描述

好了今天的分享就到此为止了
最后:如果你觉得对你有用就一键三连吧,哪里有没看懂的地方或者哪里有错误可以在评论区留言欢迎批评指正,作者看到的话会第一时间回复。
end

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
数据结构是计算机科学中一个重要的分支,涉及各种数据的组织、存储和管理方法。排序算法数据结构中常用的算法之一,在实际应用中也具有重要的作用。其中,冒泡排序快速排序是两种常见的排序算法冒泡排序是一种简单且易于理解的排序算法,其基本思想是将相邻的元素进行比较并互换位置,从而将最大的元素逐渐冒泡到序列的最后。冒泡排序的时间复杂度为O(n^2),虽然实现简单,但在大量数据的情况下,其效率较低,不能满足实际应用的需求。 快速排序是一种更加高效的排序算法,其基本思想是通过选取一个基准元素,将序列分成左右两个子序列,左子序列的元素均小于基准元素,右子序列的元素均大于基准元素,然后对左右子序列分别进行递归处理,直到子序列中只包含一个元素为止。快速排序的时间复杂度为O(nlogn),具有较快的速度,在大规模数据的情况下,其性能优于冒泡排序。 在实际应用中,选择哪种排序算法取决于排序的对象数量和性质。如果排序的数据较少,且要求排序过程简单、易于实现,可以选择冒泡排序;如果排序的数据较多,且要求排序速度快、效率高,可以选择快速排序。 在课设中,可以通过编程实现冒泡排序快速排序,并通过测试数据来比较两种排序算法的性能。可以使用同一组测试数据,在相同时间内比较两种排序算法所需花费的时间,从而得出两种算法的性能差异。此外,还可以比较两种算法所需的空间复杂度、稳定性等方面的性能差异,从而深入了解两种排序算法的优劣。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

有效的放假者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值