排序讲解(图解)

文章目录

插入排序

插入排序就跟我们玩扑克一下,当我们码牌的时候,不断选大往后 插,选小的前插

代码如何实现呢?

我们可以 看做 第一个数看做有序的, 直接枚 举 (end作为枚举变量) 第 二个数(下标为1),同时让一个tmp变量存储它下一个位置的值,判断此时a[end]与tmp的大小 ,如果 a[end] > tmp 那么此时就说明 tmp该放前面 ,所 以 a[end]后 移 , 同 时 end-- 往 前 移 动 , 然 后 再 判 断 此 时a[end]与tmp的 大小,这时要注意, 如果此时我们 的tmp是最小值,那么end比下标为0的都要小 ,那 么end有可能会小于0,所以我们每次放tmp都是a[end+1]= tmp,因为此时我们把a[end]往后移, end--,如果停下来了,就说明此时a[end] < tmp,那么a[end+1]不就是我们应该放的位置吗。


代码实现:

c 复制代码
//1.插入排序
//时间复杂度: O(N^2)
void InsertSort(int* a, int n)
{
	int end = 0;
	int tmp = 0;
	for (end = 1; end < n-1; end++)
	{
		tmp = a[end + 1];
		while (end >= 0 && a[end] > tmp)
		{
			a[end + 1] = a[end];
			end--;
		}
		a[end + 1] = tmp;
	}
}

希尔排序

希尔排序可以看做是插入排序的优化版,插入排序如果排的是有序数组,那将非常快,我们这次先对原数组进行预排序,先让数组部分有序,怎么做呢?

我 们 可 以 定 义 一 个 gap ,gap初始化 n/2 ,每次比较的时候,tmp 这时候不是a[end+1]了,而是a[end+gap] ,如果 a[end] > tmp , 就让 a[end]向后移动到gap , 同时 end -= gap ,然后再让 tmp 去到end位 置 ,这时候就相当 于对gap进行了预排序 ,当gap == 1 的时候就是 插入排序 ,不过经过预排序后,此时插入排序的速度会有很大的优化
时间复杂度: O(N*lgN)

这里其实跟插入排序一样,只不过1变成了gap,为了保证end+gap小于n,所以我们的end只能枚举到n-gap-1

代码实现:

c 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
	int end = 0;
	int tmp = 0;
	int i = 0;
	while (gap > 1)
	{
		gap /= 2;
		for (i = 0; i < n - gap; i++)
		{
			end = i;
			tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

测试排序消耗时间

那究竟有没有优化呢?

这里有个函数clock,他可以返回程序消耗的时间,单位是毫秒

如图定义一个大小为10万的数组,每个元素都是rand随机出来的,再分别对他们进行插入和希尔排序,记录所需要的时间。

这里我们测试可以调到release版本,可以看到希尔排序对于插入排序来说是一个很大的优化

快速排序

核心思路 :快排的总体思路就是,在数组中选一个key 出来,它可以是数组中的任意元素 ,但是一般 我们都选首元素尾元素 ,第一趟 的结果就是让数组以key为界限 ,key左边的元素 一定小于 它,key右边的元素 一定大于 它,然后再不断的缩小左区间右区间 ,直到排到一个元素为止

时间复杂度: O(N*lgN)

快速排序作为最快的排序,我们这里分为递归和不递归版本

根据核心思路,我们这里有三种做法

  1. 挖坑法
  2. 左右指针法
  3. 前后指针法

不管哪种方法,目的都是核心思路

挖坑法

首先我们先定义一个key ,表示我们要依据它划分 ,下图中我们选择第一个数作为key ,用pivot(坑)记录此时的下标,从end往前找 只要找到比他小 的我们就交换 两个数的位置,同时pivot指向end

找到比key还小的数后,start 从头开始找比key大的 ,找到了就交换 ,同时pivot变为start此时的位置了 ,一直找直到start >= end 为止

当找完一遍过后,此时数组元素如下图:

我们可以看以看到 ,此时 红色49左边 都 是比他小的数字 ,红色49右边 的 都比它大 ,这个步骤就是我们快排的核心步骤 ,此时红色49的下标为pivot,我们只需要再按照上面的步骤遍历 **[0.pivot-1][pivot+1,rihgt]**就可以得到一个有序的数组了。

代码实现:

c 复制代码
//挖坑法
这里的right注意我们调用的时候要传入闭区间
void PartSort1(int* a, int left, int right)
{	
	if (left >= right)//如果left == right就说明只有一个元素了,那就不用比了,直接return返回
		return;
	int begin = left;
	int end = right;
	int pivot = left;
	int key = a[pivot];
	while (begin < end)
{
	//从后找小去前面
	while (end > begin && a[end] >= key)
	{
		end--;
	}
	//此时a[end] < a[pivot]
	//1.交换
	swap(&a[end], &a[pivot]);
	//2.把end给Pivot
	pivot = end;
	while (end > begin && a[begin] <= key)
	{
		begin++;
	}
	swap(&a[begin], &a[pivot]);
	pivot = begin;
}
	PartSort1(a, left, pivot - 1);
	PartSort1(a, pivot + 1, right);
}

但是快排还有个缺陷,就是当此时如果我们用现在写的快排去排一个有序的数组时,效率会很低,为什么呢?

如果数组有序 的话,那么我们的end就会找到start为止,此时Pivot指向0 ,pivot-1肯定执行不了了,所以我们只能执行**[pivot+1,right]** ,那么end还是会找到头,指向不断找,最后的执行次数就是 1+2+3+ ...+ n-1 ,那最后的时间复杂度就为O(N^2)

怎么解决呢?

三数取中

这里有个方法叫三数取中,既然数组有序的情况下,我们如果取第一个那就会去到最小值 ,这样就会造成O(N^2)的时间复杂度,那么我们就取中间的值就好了 ,

如何取呢,当我们拿到一个数组的时候,我们可以判断在**[l,r]这个区间呢** , nums[l],nums[mid],nums[r] 的大小,我们把**nums[mid]这个和nums[l]**的值一交换,那么无论数组是有序还是无序,我们都不会取到极端情况,这样就能保证快排的效率。

代码实现:

c 复制代码
int TreeNumMid(int* a, int left, int right)
{
	int mid = left + ((right - left) >> 1);
	if (a[left] < a[right])
	{
		if (a[right] < a[mid])//left < right < mid
			return right;
		else if (a[left] > a[mid]) // mid < left < right
			return left;
		else //right >= mid    left <= mid
			return mid;
	}
	else // a[left] > right
	{
		if (a[left] < a[mid]) // right < left <mid
			return left;
		else if (a[right] > mid)
			return right;
		else
			return mid;
	}
	return -1;
}

小区间优化

由于我们是用递归实现的,如果数据很大,比如100万,1000万,这么多数要进行排序,那么我们会有大量的小区间排序,这里我们以10个数排序为例,如果数据量在1000万

那么在最后在开辟大约10个元素这样的小区间,就会开辟大量的栈帧 ,就会消耗时间 ,所以我们当数据元素在10个或者15个的时候 ,我们就不使用用快排,直接使用插入排序 ,为什么使用插入排序呢?因为我们此时快排已经排了一点序了 ,所以相对来说是比较有序 的,插入排序 此时排已经进过预排序的数组会很快 ,所以我们使用插入排序

代码实现:

c 复制代码
void PartSort1(int* a, int left, int right)
{	
	if (left >= right)
		return;
	//1.优化2,小区间优化
	if (right - left + 1 < 14)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		//优化1.三数取中
		int Minindex = TreeNumMid(a, left, right);
		swap(&a[left], &a[Minindex]);

		int begin = left;
		int end = right;
		int pivot = left;
		int key = a[pivot];
		while (begin < end)
		{
			//从后找小去前面
			while (end > begin && a[end] >= key)
			{
				end--;
			}
			//此时a[end] < a[pivot]
			//1.交换
			swap(&a[end], &a[pivot]);
			//2.把end给Pivot
			pivot = end;
			while (end > begin && a[begin] <= key)
			{
				begin++;
			}
			swap(&a[begin], &a[pivot]);
			pivot = begin;
		}
		PartSort1(a, left, pivot - 1);
		PartSort1(a, pivot + 1, right);
	}

}

左右指针

这里借用一下佬的动图

动图来源<-

以上只是第一趟的结果,我们还需要不断的调整,[left,keyi-1][keyi+1,right]的区间的顺序,最终才能有序,就是我提到的核心思路

快排这里我们都选第一个值当做key

不过这里我们除了记录key值以外,还要记录此时的keyi(key值下标),然后end往前遍历找小,start往后遍历找大,只有都找到了,才能交换,当start >= end的时候,还有与keyi交换,因为我们是以key作为分界点的。

**注意:**这里我们要先让end先找

我们是找严格大于或小于key值的数,就是 > 或 < 而不是 >=,<=,所以如果我们先让start开始找的话,那么最后start和keyi交换的值就是比key要大的值,如下图:

那么此时这个76比key还要大的值就会跑到key值的左边。

代码实现:

c 复制代码
//左右指针法
void PartSort2(int* a, int left, int right)
{
	if (left >= right)
		return;
	if (right - left + 1 < 14)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		//优化1.三数取中
		int Minindex = TreeNumMid(a, left, right);
		swap(&a[left], &a[Minindex]);

		int begin = left;
		int end = right;
		int keyi = begin;
		//如果先从后面找大,就让keyi等于left

		//错误原因:
		/* keyi = begin, 如果我们先从Begin的位置找小, 那么最后begin停下来的位置一定是大于keyi位置的元素的, 此时只要交换就把
		比keyi位置元素大的元素交换过去了,而我们是要begin左边比keyi小,右边比他大,所以出错了*/

		//否则就让keyi等于right
		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]);

		PartSort2(a, left, begin - 1);
		PartSort2(a, begin + 1, right);
	}

}

前后指针法

网上找的动图 [doeg]

以上只是第一趟的结果,我们还需要不断的调整,[left,keyi-1][keyi+1,right]的区间的顺序,最终才能有序,就是我提到的核心思路

这里定义一个prev 指向left ,而cur 指向prev+1 ,同时选第一个为key ,并用keyi记录此时key值的下标 ,cur不断移动取找比key要小 的,放在key的左边 ,然后prev首先要++ ,然后才交换,因为最终我们的keyi位置是要当做分界点的 ,直到cur > right,然后交换keyi和prev ,再继续遍历**[left,prev-1],[prev+1,right]**的子区间

代码实现:

c 复制代码
//前后指针法
void PartSort3(int* a, int left, int right)
{
	if (left >= right)
		return;
	//1.优化2,小区间优化
	if (right - left + 1 < 14)
	{
		InsertSort(a + left, right-left + 1);
	}
	else
	{
		//优化1.三数取中
		int Minindex = TreeNumMid(a, left, right);
		swap(&a[left], &a[Minindex]);

		int begin = left;
		int end = right;
		int keyi = begin;
		int prev = left;
		int cur = prev + 1;
		while (cur <= end)
		{
			if (a[cur] < a[keyi])
			{
				++prev;
				swap(&a[prev], &a[cur]);
			}
			cur++;
		}
		swap(&a[prev], &a[keyi]);
		PartSort1(a, left, prev - 1);
		PartSort1(a, prev + 1, right);
	}
}
//快速排序
//核心思想: 取一个值当做中间的值,不断地比较,最终使得mid 左边的值一定比mid这个位置的值小,右边一定大于它

快排-非递归版

上述中我们使用的都是递归,也可以不适用递归,其实我们递归主要是用来划分我们要调整的子区间,那么我们也可以使用 来存储我们每次需要调整的区间 ,因为递归所需要的数据结构其实就是栈 ,特点是: 后入先出

如图我们只需要往栈里面存储我们需要调整区间的下标即可,当我们用完之后直接Pop掉就可以了

如上图: 这里我选则先调整左区间,那么我们就要先把有区间给弄进去,然后再把左区间给压进去,红色序号代表压栈的顺序。

以左区间为例子:

只要我们此时的left < pivot-1 ,那我们就先把pivot-1 压栈然后再把left 压栈,这样取出来的就是left,pivot-1

右区间就是只要 pivot+1 < right,就依次压入,right,pivot+1

代码实现:

这里我们以挖坑法作为调整的方法,但是此时我们就要让挖坑法返回pivot的下标

c 复制代码
//快排-非递归


int PartSort1_NonR(int* a, int left, int right)
{
	//优化1.三数取中
	int Minindex = TreeNumMid(a, left, right);
	swap(&a[left], &a[Minindex]);

	int begin = left;
	int end = right;
	int pivot = left;
	int key = a[pivot];
	while (begin < end)
	{
		//从后找小去前面
		while (end > begin && a[end] >= key)
		{
			end--;
		}
		//此时a[end] < a[pivot]
		//1.交换
		swap(&a[end], &a[pivot]);
		//2.把end给Pivot
		pivot = end;
		while (end > begin && a[begin] <= key)
		{
			begin++;
		}
		swap(&a[begin], &a[pivot]);
		pivot = begin;
	}
	return pivot;
}

void QuickSortNonR(int* a, int n)
{
	Stack st;
	StackInit(&st);

	StackPush(&st,n-1);
	StackPush(&st,0);
	int left = 0;
	int right = 0;
	while (!StackEmpty(&st))
	{
		left = StackTop(&st);
		StackPop(&st);
		right = StackTop(&st);
		StackPop(&st);
		int mid = PartSort1_NonR(a,left,right);

		if (mid + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st,mid+1);
		}
		if (mid - 1 > left)
		{
			StackPush(&st,mid-1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);
}

归并排序

归并排序 就是排序两个有序数组 ,只要两个区间有序 ,那我把他俩按照顺序 排列起来不就是有序数组了吗?

怎么让两个区间有序呢? ,把每个区间再划分为两个子区间 ,再让两个子区间分别有序 ,然后再排列,一直分分分直到子区间只有一个元素 时,那他肯定有序

如图,我们把数组一半一半 分为两个子区间,再分到只有一个元素,然后排序,保证每个子区间都有序,最后一合并就为一个有序数组,所以这里我们会创建一个数组 来存储合并后的有序数组 ,然后再把数据拷回去

归并递归版

我们直到了应该划分子区间直到不可划分之后再合并,那如何划分呢?

我们可以求出mid 下标,然后再不断递归**[0,mid]和[mid+1,right]** ,只要此时区间只有一个元素的时候我们就直接return,此时都不用归并。这样我们只需要写一个合并两个有序数组就可以了。

代码实现:

c 复制代码
//归并排序
void MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;
	int mid = left + ((right - left) >> 1);
	MergeSort(a,left,mid,tmp);
	MergeSort(a,mid+1,right,tmp);
	
	int begin1 = left;
	int begin2 = mid + 1;
	int begin3 = left;
	int j = 0;
	while (begin1 <= mid && begin2 <= right)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[begin3++] = a[begin1++];
		}
		else
		{
			tmp[begin3++] = a[begin2++];
		}
	}

	while (begin1 <= mid)
	{
		tmp[begin3++] = a[begin1++];
	}
	while (begin2 <= right)
	{
		tmp[begin3++] = a[begin2++];
	}

	for (j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}

}

归并非递归版

归并非递归版我们要一个一个开始合并,然后再依次增大合并的范围,定义一个gap表示每次归并的范围,这个gap应该每次都要*2

如下图:

至于i怎么取,可以看到我们是有一个end2 ,他作为第二个数组的边界 ,他是不能大于等于数组长度的 ,而且每次我们的i应该要跨越2*gap,2*gap就我们合并的两个有序数组的长度

**注意:**我们的gap每次*2那就会是偶数,那如果我们在这个的基础上多一个元素呢?

如下图:

当我们此时begin1来到新的结尾后,此时begin2都超出数组长度 了,那么此时我们就不应该再继续 了,直接break跳出循环 ,同时gap*2 就可以了

当我们的gap此时等于原数组长度时(8) ,那么此时begin2此时指向了3 ,但是此时的end2就已经超了 ,那这个时候我们就要把end2修正了 ,只要begin2还在,end2超出数组长度,我们就要给他修正一下,直接赋值为n-1

总结一下:

  • 只要begin2此时大于等于数组长度直接break,不要再调整了
  • begin2还没有超出,但是end2已经超出了,及时修复end2,给end2赋值为n-1

代码实现:

c 复制代码
void MergeSortNonR(int* a,int n, int* tmp)
{
	int gap = 1;
	int i = 0;
	int j = 0;
	while (gap < n)
	{
		for (i = 0; i < n; i += (2 * gap))
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int begin3 = begin1;
			if (begin2 >= n)
				break;
			if (end2 >= n)
				end2 = n - 1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[begin3++] = a[begin1++];
				}
				else
				{
					tmp[begin3++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[begin3++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[begin3++] = a[begin2++];
			}

			for (j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}

		}
		

		gap *= 2;
	}
}

结言:

到这里我要说的排序差不多说完了,后期学完二叉树的时候会把堆排序写在二叉树章节,如果有问题的欢迎指出来~我们下期再见~

相关推荐
小码农<^_^>22 分钟前
优选算法精品课--滑动窗口算法(一)
算法
羊小猪~~24 分钟前
神经网络基础--什么是正向传播??什么是方向传播??
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
软工菜鸡1 小时前
预训练语言模型BERT——PaddleNLP中的预训练模型
大数据·人工智能·深度学习·算法·语言模型·自然语言处理·bert
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
AI视觉网奇1 小时前
sklearn 安装使用笔记
人工智能·算法·sklearn
JingHongB2 小时前
代码随想录算法训练营Day55 | 图论理论基础、深度优先搜索理论基础、卡玛网 98.所有可达路径、797. 所有可能的路径、广度优先搜索理论基础
算法·深度优先·图论
weixin_432702262 小时前
代码随想录算法训练营第五十五天|图论理论基础
数据结构·python·算法·深度优先·图论
小冉在学习2 小时前
day52 图论章节刷题Part04(110.字符串接龙、105.有向图的完全可达性、106.岛屿的周长 )
算法·深度优先·图论
Repeat7152 小时前
图论基础--孤岛系列
算法·深度优先·广度优先·图论基础
小冉在学习2 小时前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论