• (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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    冒泡排序的特性总结:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    完整快速排序思路分析:

    • 这样的一趟排序返回的这个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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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;
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    类比二叉树
    我们仔细想一下假设最后某一层的每一个待排序列只剩下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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    前后指针版本

    下图为前后指针版本的单趟排序动图:
    在这里插入图片描述
    解释:通过观察动图我们凑丝剥茧,最明显的就是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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.3快速排序完整代码链接

    快速排序完整代码链接

    2.4快速排序的特性总结:

    在这里插入图片描述

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

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

  • 相关阅读:
    【生成模型】Diffusion Models:概率扩散模型
    lua入门(3) - 变量
    数据在线迁移
    首都博物京韵展,监测系统实现文物科技保护
    Mybatis-Plus--update(), updateById()将字段更新为null
    ByteTrack阅读思考笔记
    【Unity】如何查看源码
    点击旋转箭头样式
    Java代码Demo——Map根据key或value排序
    【力扣刷题】Day30——DP专题
  • 原文地址:https://blog.csdn.net/qq_55712347/article/details/126414887