排序讲解(图解)

插入排序

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

在这里插入图片描述
代码如何实现呢?

我们可以把第一个数看做有序的, 直接枚举 (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]不就是我们应该放的位置吗。

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

//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;
	}
}

在这里插入图片描述

希尔排序

 希尔排序可以看做是插入排序的优化版,希尔排序可以分为两个步骤:

  1. 预排序
  2. 插入排序

预排序:

预排序就是对数组进行分组排序,先让数组稍微有序,然后再插入排序。

在这里插入图片描述
如上图: ,插入排序我们上文说过时间复杂度最坏情况下就是O(N^2),我们交换的次数此时就是一个等差
数列

如果我们能让数组中的元素部分有序的话,是不是就可以减少交换的次数?,这也是为什么我们需
要对数组进行预排序的原因。

如何进行预排序呢:

在这里插入图片描述

    如上图:我们把数组分为gap(假设为3)组,我们依次排完每一组,图中不同颜色代表不同的组,这样排完之后数组就比原来有序了。

    当gap > 1的时候就是预排序

    当gap == 1的时候此时,数组已经接近有序,所以插入排序会排的很快,这样就对插入排序进行了一个优化。

代码实现:

这里我们先看一下我们上图的思路:

这里分为两组写法::

第一种: 一次排一组
在这里插入图片描述
由于每组我们要找到下一个位置,所以我们i += gap,又因为是gap组,所以我们的 j < gap。
在这里插入图片描述
第二种: 一次排多组

在这里插入图片描述

当i = n-gap-1的时候,n+gap恰好等于n-1指向最后一个元素
在这里插入图片描述

这两种效率并没有差异,只不过一个需要三层 循环,一个需要两层循环。

总代码:

//一次排多组
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//排多组
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

至于希尔排序的时间复杂度: 这里我们先记住希尔排序的时间复杂度大概O(N^1.3),希尔排序的时间复杂度非常难算。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

测试排序消耗时间

那究竟有没有优化呢?
这里有个函数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] 就可以得到一个有序的数组了。

代码实现:

//挖坑法
这里的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]的值一交换。那么无论数组是有序还是无序,我们都不会取到极端情况,这样就能保证快排的效率。

代码实现:

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个的时候,我们就不使用用快排,直接使用插入排序,为什么使用插入排序呢?因为我们此时快排排完之后已经较为有序了,所以使用插入排序效率会高一点。

代码实现:

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值的左边。

代码实现:

//左右指针法
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]的子区间

代码实现:

//前后指针法
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的下标

//快排-非递归


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。 此时都不用归并。这样我们只需要写一个合并两个有序数组就可以了。

代码实现:

//归并排序
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

代码实现:

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;
	}
}

结言:

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

在这里插入图片描述

  • 30
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值