【数据结构 】八大排序(插入,希尔,选择,堆,归并,快排,冒泡,计数)

一 排序的基本概念

排序啊,一个很有趣的话题,排序就是使得数据能够成为一种有序的操作。(并不严谨,可是这样理解就够了。)准确的定义,朋友们可以看看数据结构的书哈,我就不复制粘贴了哈。

  1. 为什么要有排序这个概念呢,它有啥用呢?

答:其实排序的主要目的就是为了提高查找效率,就比如说,二分查找算法,前提必须有有序的数据才可以使用,极大提高了查找效率。,在我的理解,我的理解上啊,其实从广义上讲:把数据结构分为四大块,线性表,非线性表,排序,查找。前面的三大板块都是为了查找做准备的,都是为了提高查找效率,怎么说呢?假如你在某个浏览器,输入你想要查找的东西,可是它半天都没显示出来,这就说明这个浏览器很菜,为什么菜,很大原因就是底层的代码,如数据结构是按,线性的,非线性的,使用的排序算法安排的不合理导致的呗。所以有必要学习以下排序,并且它非常有用哦。

  1. 排序的稳定与否判断

稳定:如果a没排序之前在b前面,且a = b,在排序之后a仍然在b的前面;
不稳定:如果a排序之前在b前面,且a=b,在排序之后a有可能会出现在b的后面;

  1. 内外排序是啥?

内排序:所有排序操作都在内存中完成;适合小数据的排序
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

二 常见的基本排序

1. 插入排序

思想:插入排序,就是给你一组无序的数据,然后把数据分为 有序序列和无序序列 ,然后通过无序序列的数据和有序序列的数据进行比较,从而达到排序的目的。
在这里插入图片描述


但是我们思路插入排序的时候,首先是思考单趟插入排序是如何操作的,就比如玩扑克牌时候,你每插入一张牌之前,在你手中的排都是有序序列,当我们插入一张牌时候,就相当于对单趟插入排序进行了操作!

在这里插入图片描述


那我们就有办法了:先搞定单趟插入排序
举个例子:
这个是 在 end >= 0的情况下找到的位置要插入值的情况
在这里插入图片描述
end<0情况,插入的值即是数组的第一个位置:
在这里插入图片描述

单趟排序的代码:

int end ; //假设我不知道下标是什么
int x ; //假设我不知道插入的值是什么

//插入单趟的逻辑
while (end >= 0)
{
	if (x < a[end])
	{
		//把前面的往后面挪动
		a[end + 1] = a[end];
		//end往前走,更新指标
		end--;
	}
	else  //如果x >=a[end] 表示要插入到 end的后面,先跳出循环线,因为有两个逻辑可以用同一份代码处理
	{
		break;
	}
}
//退出循环有两种情况:
//一:end<0时候:那么直接插入第一个位置
//二:x>=a[end]时候那么就插入end+1的位置
//三:x = a[end+1],即这一趟的比较是有序的,自己和自己换
a[end + 1] = x;

单趟插入排序写好了,那么就要写出给你一个无须的数组时候,你是如何使用单趟排序的思想了;
无非是从单趟的插入排序,变成多趟的插入排序,那么直接加个for循环就解决了,我们在单趟直接插入排序时候,是假设了数组本身就有序的;对于多趟的插入排序,我们也可以假设数组本身有序,但是实际是无序的,所以我们可以认为单个元素是有序的,然后,紧紧挨着有序数组后面的元素作为要插入的元素,用一个for来控制该段逻辑就可以;


插入排序的最终代码:

void InsertSort(int*a, int n)
{
	
	for (int i = 0; i < n - 1; i++) //注意i的下标【0,n-2】
	{
		int end = i;   //遍历end,从【0,n-2】
		int x = a[i+1]; //把end+1后面的元素往前找位置,找到合适位置插入

		//插入单趟的逻辑
		while (end >= 0)
		{
			if (x < a[end])
			{
				//把前面的往后面挪动
				a[end + 1] = a[end];
				//end往前走,更新指标
				end--;
			}
			else
			{
				break;
			}
		}
		//退出循环有两种情况:
		//一:end<0时候:那么直接插入第一个位置
		//二:x>=a[end]时候那么就插入end+1的位置
		//三:x = a[end+1],即这一趟的比较是有序的,自己和自己换
		a[end + 1] = x;
	}	
}

细节:

上面的代码我们把单趟排序的下标end用 i 来控制,i 遍历往前走:保证每一趟的单趟排序,end都是指向每一趟有序数组的最后一个位置;
为什么 i 最多只能到 n -2 的位置呢
因为i 到 n -2的位置是单趟排序end所在的位置,而我们end+1的位置,也是n -1的位置,已经被要插入的x所占用了,如果 end在 n-1的位置,那么x就会越界!


还有其版本的代码:供你们思路参考

void InsertSort (int *a,int length)
{
	for (int i = 1; i< length;i++) // 控制比较次数
	{	
		if(a[i] < a[i-1]) //升序排序
		{
			swap(a[i],a[i-1]); //交换两个数
			//交换完两个数还不够,还要保证有序序列中也是有序的。
			for (j = i-1;j > 0 && a[j] < a[j-1];j--)
			{	
				swap(a[j],a[j-1]);
			}
		}
	}
}
//交换函数
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

另一种写法:

void InsertSort(int* a,int length)
{
	for (int i = 1;i < length;i++) //控制比较次数
	{
		key = a[i];  //保存要插入的值,为了下面执行a[j+1]  = a[j]时候,a[i]的值还在
		for (j = i-1;j >= 0 && key < a[j];j--) //开始比较插入
			a[j+1] = a[j];
			//退出循环后
		a[j+1] = key; //在执行a[j+1] = a[j]时候,
		//a[i]也就是a[j+1]就被a[j]覆盖了,所以这句话的是把a[i]还会原来的位置 
	}
}

插入排序的时间复杂度

很简单:从原理思考:假设有n个数组;
第一次插入:挪动 1个位置;
第二次插入:挪动 2个位置;
第三次插入:挪动3个位置;



第 N 插入:挪动n个位置;
全部加起来,就是一个等差数列:时间复杂度就是O(n2)


最好时间复杂度是:O(n);也是从原理思考:假如数组本身有序的条件下:那么我们每一趟的单趟排序只需要比较一次,往end+1位置插入即可;
最坏时间复杂度就是:O(n2);也就是逆序对时候嘛!每次都需要挪动位置;


2. 希尔排序

思想:
希尔排序又叫增量缩小排序,就是把一组数据按一定的增量来分成多个小组来插入排序,直到增量缩小到1,则排序结束;


希尔排序怎么想出来的?首先我们知道直接插入排序在对接近有序的数组来说,直接插入排序还是相当快的,因为接近有序的数组使用直接插入排序几乎能达到O(n)的级别,这是什么概念,O(n)级别的排序是排序届的天花板啊;希尔这个人想:既然能够在接近有序的情况下我们的直接插入排序能够如此之快,那么是否可以让一个无序的数组,经过一些操作,然后就搞出接近有序的数组,再使用插入排序,那么就可以降低直接插入排序时间复杂度了吗?


举个例子解释以下希尔排序:
1.首先我们知道希尔排序是对直接插入排序的优化,那么我们需要做的事就是先对无序的数组预排序,排成接近有序的数组;
2.其次再使用直接插入排序,这样就可以完成希尔排序了!


在这里插入图片描述


我只画了排了一组的例子:其他的组你们可以画一画
在这里插入图片描述


按着这个思路:我们也不难写出单组的希尔排序;

int grap = 3; //假设grap = 3;
for (int i = 0; i < n - grap; i += grap) //注意细节:这里我们 i += grap          
                            			//这是控制end的移动的,
                            			//为了控制单组排序完一个后,继续排单组的下一个数
{
	int end = i;
	int x = a[end + grap];
	//排序单趟为grap分组的数据
	while (end >= 0)
	{
		if (x < a[end])
		{
			a[end + grap] = a[end];
			end -= grap;
		}
		else
		{
			break;
		}
	}
	a[end + grap] = x;
}

那么我们就可以写出整趟的希尔排序排序;用一个循环控制即可!有多少组我们就用循环控制就行

int grap = 3; //假设grap = 3;
for(int j = 0;i <grap;j++) //用来控制每一组,比如这里grap等3那么我们控制3组,//每一组排序好再排下一组
{
	
	for (int i = j; i < n - grap; i += grap) //注意细节:这里我们 i += grap
                            			//这里的意思是控制每一组的排序
                            			
	{
		int end = i;
		int x = a[end + grap];
		//排序单趟为grap分组的数据
		while (end >= 0)
		{
			if (x < a[end])
			{
				a[end + grap] = a[end];
				end -= grap;
			}
			else
			{
				break;
			}
		}
		a[end + grap] = x;
	}
}

但是上面的写法我们并不认为很好:我们希望不要分每一组都排序,单独对每一组都排序,我们希望分好组了,但是我们却从左到右直接遍历,从每一组中都可以进行插入排序;
所以我们有这样的代码:

int grap = 3; //假设grap = 3;
for (int i = 0; i < n - grap; i++) //注意这里是i++,这个意思就是从左到右,每个分组都能遍历到,
									//每个分组都一起排序
{
	int end = i;
	int x = a[end + grap];
	//排序单趟为grap分组的数据
	while (end >= 0)
	{
		if (x < a[end])
		{
			a[end + grap] = a[end];
			end -= grap;
		}
		else
		{
			break;
		}
	}
	a[end + grap] = x;
}

接下来我们最重要的是控制grap的增量了,grap到底等于多少好呢?我们一般认为grap每次预排序时候,都是1/2;这样比较好
最终代码:


//希尔排序
void ShellSort(int* a, int n)
{
	//grap每一趟结束后都是预排序完,知道grap等于1就是直接插入排序,	
	int grap = n;
	while (grap > 1) //控制grap的增量,刚开始是n/2,然后每一趟都继续缩减2倍,当grap缩到<=1就结束缩减了
	{
		grap /= 2;
		//控制的是每趟grap/2分组的处理逻辑
		//每一趟的grap/2下来,整个数组就越来越接近有序
		//直到grap == 1时候,排完这一趟后,整个数组就会成为有序的数组
		for (int i = 0; i < n - grap; i++)
		{
			int end = i;
			int x = a[end + grap];
			//排序单趟为grap分组的数据
			while (end >= 0)
			{
				if (x < a[end])
				{
					a[end + grap] = a[end];
					end -= grap;
				}
				else
				{
					break;
				}
			}
			a[end + grap] = x;
		}
	}	
}

还有其他版本的写法,供你参考以下:


void ShellSort(int* a, int length)
{
	int j = 0;
	for (int grap = length / 2; grap > 0; grap /= 2) //控制增量
	{	//每一趟增量的插入排序
		//每一趟都是排了一小组的一部分;
		for (int i = grap; i < length; i++)
		{
			int key = a[i];//保存要插入的值
			
			for ( j = i - grap; j >= 0 && key < a[j]; j -= grap)
				a[j + grap] = a[j]; // 若后面的数 小于 前面, 把 前面的给后面的
			//退出循环后,把前面的数插入要插入的值。
			a[j + grap] = key;
		}
		
	}
}

对于 这个循环的解释 for (int i = grap;i > length;i++)
在这里插入图片描述
在我的理解:希尔排序也是插入排序的一种小变形方式;在插入排序中,增量就是为1的希尔排序;


希尔排序的时间复杂度分析

在这里插入图片描述


3. 选择排序

思想: 选择排序就是,给定一组无序数据,让第一个数据作为最小(最大)的,然后剩下的数据与其比较,找到新的最小(最大)的数,把新的替换旧的数,依次循环认为第二个数据最小…直到排完。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

void SelectSort(int* a, int length)
{
	for (int i = 0;i < length;i++) 
	{
		int min = i; //先假设第一个为最小的
		for(int j = i+1;j < length;j++) //第一个后面开始找新的最小的
		{
			if(a[j] < a[min])
				min = j;
		}
		swap(a[min],a[i]);
	}
}
//交换函数
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

选择排序时间复杂度分析

从原理上分析:每次都是要找最小的下标;
第一次找:n
第二次找:n-1
第N次找:1
分析下来时间复杂度也是O(n2)


即使这个数组本身是有序的,也需要找n2次;


4.堆排序

思想:堆排序,也是叫做优先队列,使用一种“完全二叉树”的模式去存储数据,然而完全二叉树又可以用数组的形式的方式存储。而且,每一个父结点的值大于(小于)左右孩子的节点值,叫做大堆(小堆)。

完全二叉树的性质

  1. 结点 下标 i 父结点的下标:i/2 -1;
  2. 结点下标 i 的左孩子的下标:i *2 +1;
  3. 结点下标 i 的有孩子的下标:i *2 +2;

推荐视频:堆排序(heapsort) 或者这个 排序算法:堆排序【图解+代码】,两个都讲得很清楚;

递归维护堆的性质
维护堆的性质是把不满足堆的排序方式的结点,维护成为堆的排序方式的结点。

// a为堆的数组,n为数组大小,i为要维护堆的下标。
//维护大堆的例子
void Heapify (int* a, int n,int i) 
{
	if (i >=n) //递归的结束条件
		return;
	// 先把维护的下标默认认为是最大值的下标
	int maxIndex = i;
	int left = 2*i+1;
	int right = 2*i+2;
	//开始找真实最大值的下标
	if (left < n && a[left] > a[maxIndex]) //左大于父的话,就换新的maxIndex
		  maxIndex = left;
	if (right < n && a[right] > a[maxIndex]) 
	//右大于父(在左大于父不成立下)或者右大于左,
	//在(左大于父的前提下),把新的maxIndex找出来
		maxIndex= right ;
	if (maxIndex != i) //说明左右结点其中一个大于父结点
	{
		swap(a[maxIndex],a[i]);
		Heapify(a,n,maxIndex);
	}
		
}

这段代码的意思就是,假如要从某个已知不成堆的结点 i 开始 heapify; 假如我要从任意结点开始呢?那可以从最后一个结点的父结点开始逐渐递减就可以 heapify 全部的节点了。这就是建立堆的过程。

这图就是从最后一个结点的父节点开始堆化:
在这里插入图片描述
非递归维护堆的性质----向下调整算法

void heapify(int *a,int n,int parent)
{
	int left = 2*parent + 1;
	while (left < n)
	{	//先找出根左右最大值的下标
		if (left + 1 < n && a[left+1] > a[left]) //交换前看看右结点的值是否大于根节点;
			left++;
		if (a[left] > a[parent]) //右结点的值不大于根节点;左结点和根结点交换
		{						//右结点得值大于根节点,
			swap(a[left],a[parent]);
			//继续对下面的结点堆化
			parent = left;
			left = 2*parent + 1;
		}
		else //假如没有 左右结点比parent大的话就结束堆化;
			break;
	}
}

建立堆
建立堆:就是维护堆的性质,把任意结点 i 的位置定为 最后一个结点;对最后一个结点的父节点进行维护堆;

void BulidHeap(int *a,int n)
{
	int lastNode = n - 1; //先找出最后一个结点
	for (int i = lastNode/2 -1; i > 0;i--) // 从最后一个结点的父节点开始向上堆化构建,
										//到根节点时候结束堆化
	{
		heapify(a,n,i);
	}
}

堆排序
建立起的对我们结构我们还没开始排序呢。现在开始排序。
因为刚刚建立了大堆,所以我们可以知道根结点一定是堆树中最大的值,我们而要做就是

  1. 把根节点和最后一个结点做一次交换,然后取出交换后的最后一个结点,这样就可以把最大的值取出来了,取出来的目的也是为了排序,然后由于交换了就会破坏堆的结构。
  2. 然后对交换后的根结点再进行一次堆化就可以了。继续完成上诉操作,依次把堆树中最大的值取出来,这样就可以排序啦。

在这里插入图片描述

void HeapSort(int* a,int n)
{
	int  lastNode= n -1;
	for (int i = lastNode; i >= 0; i--) //从最后一个结点开始和根交换
	{
		swap(a[0],a[i]);
		//交换后,根又不满足堆的性质了,所以重新堆化
		heapify(a,i,0); //对根堆化,根的下标为 0;数组大小每次会减一
	}
}

堆排序的时间复杂度

建堆的时间复杂度
在这里插入图片描述


对于排序时候
在这里插入图片描述
所以总的时间复杂度是O(n logn)


5 归并排序

思想:归并排序就是将一个无序得序列,先分开,直到分到只有一个元素,然后再合并排序


归并排序的操作就有点像二叉树的后序遍历一样:先分开左边数组,再分开右边数组,当两边数组都是有序时候,就可以处理了,做的处理就是归并起来!
在这里插入图片描述


void merge_sort(int a[], int temp[], int left, int right)
{
	if (left >= right) // left > right 不用分了, left = right 就是分到一个元素了,也不用分了
		return;
	//先把数组分开 ,在 left < right 的前提下 
	int mid = (left + right) / 2;
	//分为左边的数组
	merge_sort(a, temp, left, mid );
	// 分为右边的数组
	merge_sort(a, temp, mid + 1, right);
	//开始归并
	// 定义一个 左边数组的第一个元素的下标
	int l_pos = left;
	//定义一个 右边数组的第一个元素的下标
	int r_pos = mid + 1;
	// 定义一个存放归并结果的数组下标,用来后面归并时候的操作
	int t_pos = left;
	//开始归并,到临时数组
	//在满足左边数组的第一个元素下标<=最后一个元素的下标 
	//和右边数组第一个元素小于等于最后一个元素的下标前提下
	while (l_pos <= mid && r_pos <= right)
	{
		if (a[l_pos]<a[r_pos])
			temp[t_pos++] = a[l_pos++];
		else
			temp[t_pos++] = a[r_pos++];
	}
	//退出循环后
	//假如左边数组还有元素,直接将左边数组的元素丢进去临时数组
	while(l_pos <= mid)
	{
		temp[t_pos++] = a[l_pos++];
	}
	//假如右边数组还有元素,直接将右边数组的元素丢进去临时数组
	while(r_pos <= right)
	{
		temp[t_pos++] = a[r_pos++];
	}
	// 将临时数组拷贝到 原数组
	while (left <= right)
	{
		a[left] = temp[left];
		left++;
	}
}
void MergeSort(int a[], int n)
{	//给 归并的结果存放的数组开辟一个空间
	int *temp = (int*)malloc(n*sizeof(int));
	if (temp)
	{	//开辟成功;
		//开始归并排序,先分开再归并
		merge_sort(a, temp, 0, n - 1);
		free(temp);
	}		
}

6 快速排序

思想:就是任意选定一个数为 pivotKey(中心轴关键字),为了方便,通常这个pivotKet 为第一个元素,把剩余的元素和 pivotKey 比较,比它大放到右边,比它小放到左边;然后重复操作,直到分到左右的序列只有一个就排好序了。

举个例子:在单趟的快速排序中:假如排如下数组
在这里插入图片描述


开始单趟的快速排序:
在这里插入图片描述


注意细节:
假如我们选最左边的作为key值,那么我们就需要右边的right先走;这样可以保证,当到相遇点时候,相遇点的值肯定是比key要小的,此时交换就不会出错;
假如我们选最右边的最为key值,那么我嫩就需要左边的left先走;这样可以保证,当到相遇点时候,相遇点的值肯定是比key要大的,此时交换就不会出错;


基于上面的操作我们可以写出单趟快速排序的算法:

//快速排序的单趟排序
//选左边的第一个值做key,那么就需要右边的值先走
//需要的是:找左边的比key大,找右边比key小的,找到之后就交换两个位置

int Partion(int* a, int left, int right)//[left,right]
{
	int keyI = left; //定左边的第一个值为关键字
	while (left < right) //只要没相遇,就一直走下去
	{
		//右边的先走,因为我们选了左的为key值
		while (a[right] >= a[keyI] && right > left) //而相等也作为条件的原因是:
												//相等需要一直往前走,假如不走那么如果数组都是相等的值,
												//就会陷入一直交换的死循环
												//right > left 的原因防止:
												//right一直都没有找到比key小的,会一直往前走直到越界
		{
			right--;
		}
		//右边走完左边再走
		while (a[left] <= a[keyI] && left < right) //left < right的原因防止:
		{										//left一直没有找到比key大的,会一直往右走直到越界
			left++;
		}
		//当右边找到比key要小的,左边找到比key要大的时候,就交换
		Swap(&a[left], &a[right]);
	}
	//当上面循环结束:表示到了相遇点,那么我们用相遇点和key交换
	//这样就会完成左边的都比key的小,右边的都比key的大
	Swap(&a[left], &a[keyI]);

	//最后返回相遇点就行:相遇点就是新的keyI
	return left;
}

单趟的快速排序有了,那么我们就可以写出整趟的快速排序:
我们只要继续递归key左边的数组:做单趟的快速排序;继续递归key右边的数组的单谈快速排序即可了;

void QuickSort(int* a, int left,int right)
{
	if (left >= right) //递归出口
		return;
	//拿到中间的keyI值
	int keyI = Partion(a,left,right);	
	//递归排序左边的
	QuickSort(a, left, keyI-1);
	//递归排序右边的
	QuickSort(a, keyI+1, right);
}

快速排序的时间复杂度

在这里插入图片描述

我们从原理出发:
我们从left找大,right找小,走了两个步骤加起来走了n个步骤;
然后以key分区,左边的走n/2,右边走n/2;继续分下去,每一步都是除2;
但是我们从递归二叉树图看,它们尽管分开,但是每次递归都是走了N步骤;
而二叉树的高度是logn,每一层是n,总共有logn层,所以时间复杂度为O(n logn)


快速排序有什么缺陷吗?
对于有序的数组,那么快速排序的时间复杂度会直接降到O(n2)
从原理出发:
第一次:right往前找,都找不到比key小的值,那么right就会走n步;
第二次:right往前走,也找不到比key小的,right走了n-1步;

第n次:right走了1步,就可以确认是有序了;
此时时间复杂度直接到O(n2);


如何避免这个问题呢?
关键是key的选择,因为当数组有序的时候,我们key还选择最左边作为key值,那么就会使得时间复杂度降低;
我们可以使用三数取中法选出合理的key值,也就是选择最左边和最右边和中间的值,取它们三个数中第二大的数,这样作为key值,我们就可以避免right一直往左走走n步骤了;

//获取三个数中第二大的值的下标
int GetMidIndex(int*a, int left, int right)
{
	//int mid = (left + right) / 2;
	//int mid = left + ((right - left) / 2);
	int mid = left + ((right - left) >>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 //a[left] >= a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		return right;
	}
}
//快速排序的单趟排序
//选左边的第一个值做key,那么就需要右边的值先走
//需要的是:找左边的比key大,找右边比key小的,找到之后就交换两个位置

int Partion(int* a, int left, int right)//[left,right]
{
	int mid = GetMidIndex(a,left.right);
	Swap(&a[mid],&a[left]);
	
	int keyI = left; //定左边的第一个值为关键字
	while (left < right) //只要没相遇,就一直走下去
	{
		//右边的先走,因为我们选了左的为key值
		while (a[right] >= a[keyI] && right > left) 
		{
			right--;
		}
		//右边走完左边再走
		while (a[left] <= a[keyI] && left < right) 
		{										
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyI]);
	return left;
}


快速排序–挖坑法

在这里插入图片描述


快速排序–非递归栈模拟实现

用栈模拟实现,也是模拟递归调用栈帧开辟的情况!
放入总区间,再取出来处理总区间,处理完,继续放左区间和右区间;
大致思路是把数组的,左右指针分别入栈;然后出栈处理,如何处理?以关键值得位置来分区左右处理;

//非递归版本
//相当于用栈模拟递归的实现
void QuickSortR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);

	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackIsEmpty(&st))
	{
		//获取【left,right】区间处理
		int end = StackTop(&st);
		StackPop(&st);
		int begin = StacTop(&st);
		StackPop(&st);

		//以keyI为基准分割左右区间
		int keyI = Partion(a,  begin, end);
		//分开的区间【begin,keyi-1】keyi【keyi+1,end】
		//入栈keyI的右边区间
		if (keyI + 1 < end)//【keyi+1,end】至少有两个值才可以放入栈
		{
			StackPush(&st, keyI + 1);
			StackPush(&st, end);
		}
		//入栈keyI的左边区间
		if (begin < keyI - 1)
		{
			stackPush(&st, begin);
			stackPush(&st, keyI - 1);
		}		
	}
	StackDestroy(&st);
}

7 冒泡排序

思想:冒泡排序就是相邻的两个元素比较,前面大于后面的就交换,重复这个步骤;

普通的冒泡排序相信大家都会,这里我提供一种优化版的;我们都知道假如你给的序列就是有序的,就不需要排序了,可是,在普通冒泡排序中,并不是这样,及时给你是有序的还是会一直找很多遍,优化版的只需要找一遍就可以;
在这里插入图片描述

void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
void bubble_sort(int arr[], int n)
{
	bool flag = false;
	for (int i = 0; i < n-1; i++)
	{	//没交换保持flag = false;
		flag = false;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				flag = true;
				swap(&arr[j], &arr[j + 1]);
			}				
		}
		//若一趟下来,都没有交换,说明 已经是有顺序的了,直接退出外循环
		if (flag == false)
			break;
	}
}

8 计数排序

思想:计数排序就是统计一个无序序列的范围大小,把里面相同的元素个数统计出来,把每组相同元素的个数存放到一个数组中,用数组下标对应元素的值。然后再取出其中的值。
在这里插入图片描述
计数排序适合数据比较集中,范围不大的数排序!

类似一种哈希表的映射:用元素的值映射到数组下标。

void counting_sort(int a[], int n)
{
	//统计元素的范围大小,先找出元素的最大和最小值
	int min = a[0]; //假定最小值为第一个元素
	int max = a[0];//假定最大值为第一个元素
	for (int i = 0; i< n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	//开辟一个数组,大小为元素的范围大小即可
	int range = max - min + 1;
	int	*temp = (int*)malloc(range*sizeof(int));
	assert(temp);
	//初始化数组
	memset(temp, 0, range*sizeof(int));
	//往数组temp添加相同元素的个数,于此同时,下标对应着元素的映射值
	for (int j = 0; j < n; j++)
		temp[a[j] - min]++;
	//开始把值赋给 a 数组
	int i = 0; //存放数组 a 的下标
	for (int j = 0; j < range; j++)
	{
		while (temp[j]--)
		{
			a[i++] = j + min;
		}
	}
	free(temp);
}

注意:记得开辟内存时候大小别赋值错了,要不然改bug,改到你头疼;


三 总结

在这里插入图片描述


  • 97
    点赞
  • 504
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 30
    评论
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

呋喃吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值