八大基本排序(详解)

前言
博主将在本文讲述选择,插入,希尔,快速,冒泡,堆排,归并排序,记数排序.
在这里插入图片描述

排序相关概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递
减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.插入排序

1.1 直接插入排序

动图演示:
请添加图片描述

思路:

就像我们打牌时,一个一个插入一样
在这里插入图片描述
具体思想就是,我们在一个有序的数组中插入一个数,那么我们是将这个数在有序数组中依次比较,那么我们怎么将这个数据插入其中呢? 我们采用覆盖的方法,就是将后一个数等于前一个数这样依次覆盖,这样就会给我们要插入的数据留下一个空位,然后插入即可,可能大家会有疑问,一个无序的数组我们怎么插入呢??? 其实我们将第一个数看成一个数组,那么他不就是有序的吗,然后将第二个数插入,再插入第三个数. . . . . . .,这样就完成了排序

代码实现

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n-1; i++)//可以分为n-1次插入,第一次就认为是有序,
	{
		int end = i;//记录每次插入的有序数组最后一个位置
		int tmp = a[end + 1];//记录插入的数据
		while (end >= 0)//一直比较到0下标位置
		{
			if (tmp< a[end])
			{
				a[end + 1] = a[end];//往后覆盖
				--end;
			}
			else
			{
				break;
			}
			a[end + 1] = tmp;//插入,不能放到else中,因为可能是else中break出来 也有可能是end>=0为假出来,也就是说tmp是在有序数组中是最小的数
		}	
		// 放这也行  a[end + 1] = tmp;//插入,不能放到else中,因为可能是else中break出来 也有可能是end>=0为假出来,也就是说tmp是在有序数组中是最小的数
	}
}

复杂度

空间复杂度:

O(1),未开辟额外空间

时间复杂度:

最好的情况:已经有序的情况下,只需遍历一次,即O(N)
最坏的情况:每插入一个数都要遍历,即F(N)=1+2+3+4+…+N=N(N-1)/2=O(N^2).

1.2希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

动图演示:

请添加图片描述

画图讲解:

在这里插入图片描述
从图中可知gap越大,分成的小数组越多,希尔排序就是通过这种小区间预排,让数组似有序化,从而减轻最后一次直接插入排序(gap=1)的遍历次数,从而提升速度!

代码

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//避免gap=0,导致最后一次不为1,所以加上1,当为1的时候就是插入排序
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)//拆分多组数组进行插入排序
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				a[end + gap] = tmp;
			}
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算.但是通过大量实验证明O(N^1.3)左右

2.选择排序

2.1.1单向选择排序

思路

就是再数组中找出最值,然后放入最左边,依次排放

动图演示:

请添加图片描述

代码:

void SelectSort(int* a, int n)
{

	for (int i = 0; i < n; i++)
	{
		int index = i;
		for (int j = i + 1; j < n; j++)//找到最值的下标
		{
			if (a[index] < a[j])
			{
				index = j;
			}
		}
		Swap(&a[index], &a[i]);
	}
}

2.1.2 双向选择排序(对单向选择排序的优化)

代码

void SelectSort2(int* a, int n)
{
	int begin = 0;
	int end = n-1;
	while(begin<end)
	{

		int maxi = begin;
		int mini = begin;
		for (int j = begin; j <= end; j++)
		{
			if (a[maxi] < a[j])
			{
				maxi = j;
			}
			if (a[mini] > a[j])
			{
				mini = j;
			}
		}
		Swap(&a[maxi], &a[begin]);
		if (mini == begin)//因为上面的交换,maxi和begin交换了,当mini==begin时,说明begin是最小值,但是和maxi交换了,所以maxi现在是最小值
		{
			mini = maxi;	
		}
		int temp = 0;
		Swap(&a[mini], &a[end]);

		begin++;
		end--;
	}
}

复杂度

时间复杂度:O(N^2)
空间复杂度:O(1)

2.2 堆排序

看我这篇文章:二叉堆的实现(含堆排序讲解),点击直达

3.交换排序

3.1冒泡排序

思路:

相邻数据依次比较交换

代码:

void BubbleSort(int* a, int n)
{

	for (int j = 0; j < n - 1; ++j)
	{
		int exchange = 0;
		for (int i = 1; i < n - j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}

		if (exchange == 0)//exchange==0说明数据已经有序
		{
			break;
		}
	}
}

3.2 快速排序

思想;

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
重点解决,单趟key的位置

代码模板:

void QuickSort(int* a, int begin, int end)
{

	while (begin >= end)
		return;
	int keyi = PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}

和二叉树的前序遍历很像!就是将数组分成最小单元,然后最小单元有序,那么上一层key值两边均有序,以此类推,完成排序!!

3.2.1 hoare版本

思路

先从key的反方向找小,假设key在左边,我们先让右边的right找到比key小的暂停,然后让左边的left找大,找到之后和right交换,然后right继续找,left然后也找,找到就交换,知道相遇之后,相遇位置的左边就是都比key小的,右边都是比key大的,我们让key的值和相遇点交换,然后key的位置到相遇点,这就完成了单趟交换,然后以key的左边,右边分别为一个数组,再完成交换!!

动图

在这里插入图片描述

代码:
int PartSort(int* a, int begin, int end)//hoare版本  这种就像是先走完再找key位置
{
	int left = begin;
	int keyi = left;
	int right = end;
	while (left < right)
	{

		while (left < right && a[right] >= a[keyi])//找小
			//这里为什么从keyi的反方向开始呢??  因为在left和right相遇的时候,相遇的值的比a[keyi]小或者相等,即使right一路畅通无阻跑到keyi位置也没关系
			//这就是因为right就是找小的,最极端也就是相等,因为找小的先走的,所以相遇的时候一定是小的,如果从keyi的正方向走的话,相遇一定是大的啊,大的和a[keyi]交换肯定不对啊
			//我们就是需要找小的交换, 你找个大的算怎么回事呢?
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])//找大
		{
			left++;
		}
		Swap(&a[right], &a[left]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	return keyi;
}

3.2.2 挖坑法

思路

将key的位置当做一个坑,然后right指针开始找小,找到小的就放入坑中(就是交换),然后left找大,找到就放入坑中,直到两个指针相遇停止,那么相遇的这个位置一定是坑,将key原始数据放入坑中,key的位置变成改坑的位置,和hoare版本很像,但是更容易理解!这就完成了单趟排序!

动图

在这里插入图片描述

代码
int PartSort(int* a, int begin, int end)//挖坑法,有效的帮助你理解  这种就像边走边找key位置
{
	int key = a[begin];
	int piti = begin;//坑位
	while (begin < end)
	{
		//从左找大,找到之后把它放坑位里,然后这里成为新的坑位
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[piti] = a[end];//将大值放入坑位
		piti = end;//坑位到原来大值的位置
		//从左找小,找到之后把它放坑位里,然后这里成为新的坑位
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[piti] = a[begin];//将小值放入坑位
		piti = begin;//坑位到原来小值的位置
	}
	a[piti] = key;
	return piti;//最后的坑位就是我们的key值
}

3.2.3 前后指针法

思路

前指针cur,后指针prev,前指针在前面依次找比key值小的,找到后就和prev交换位置,然后prev+1,重复执行,最后prev的位置就是key的分界点,所以让那个key和prev交换

动图

在这里插入图片描述

代码
int PartSort3(int* a, int begin, int end)//前后指针法  代码最简洁
{
	int cur = begin+1;
	int prev = begin;
	int keyi = begin;
	for (int i = begin; i < end; i++)
	{
		if (a[cur] <= a[keyi]&& prev++!=cur)//找大,找到最后prev往后腾一个位置和cur交换位置,就是cur去找大值,找到就往prev那边放,放完prev就跑一个
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);//将keyi对应的数值和prev交换,让keyi变成分界线!
	keyi = prev;
	return keyi;
}

快速排序优化

优化1:

如果我们学过二叉树的话,我们应该key在数组中间的话,这个遍历的深度就越小,而且如果每次选到的key是这组数的最大值或者最小值的话,快排的效率就会变得非常低,几乎接近于O(N²),还可能因为递归深度太深导致栈溢出。,所以我们采取三数取中来进行优化!

代码
int GetMidIndex(int* a, int begin, int end)//三值取中
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] < a[end])
			return end;
		else
			return begin;
	}
	else //(a[begin] >= a[mid])
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}
优化2

要知道,快排算法是不断递归来排一个数的左右两边,所以当快排算法越接近与结束的时候,左右两边数组越接近有序。
而快排算法对于接近有序的序列是没有任何效率上的提高的,但是我们知道插入排序时,如果一段序列越接近于有序,插入排序的效率就越高。
所以我们可以采用插入排序进行优化!!

代码
void QuickSort(int* a, int begin, int end)
{

	while (begin >= end)
		return;
	while (end - begin > 20)//优化,因为越小递归次数越多,就得不偿失了
	{
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	InsertSort(a, end - begin+1);//数组下标差+1等于数组个数

}

快速排序非递归版

思路:

在设计单趟排序的时候,我们要知道改数组的头和尾,所以我们通过栈来实现非递归,将头和尾依次压入栈中,然后在单趟排序之前出栈,得到一个keyi值,这样我们又知道左右子区间的头和尾,继续进行压栈,这类似于二叉树的层序遍历

代码

void QuickSortNonR(int* a, int begin, int end)//快排非递归方法
{
	Stack q;
	StackInit(&q);
	StackPush(&q, end);
	StackPush(&q, begin);
	while (!StackEmpty(&q))
	{
		int left = StackTop(&q);
		StackPop(&q);
		int right = StackTop(&q);
		StackPop(&q);
		int keyi = PartSort3(a, left, right);
		//区间[keyi-1,left] keyi [keyi+1,right]
		if (keyi + 1 < right)
		{
			StackPush(&q, right);//因为先top的是left,所以left边最后放
			StackPush(&q, keyi+1);
		}
		if (keyi - 1 > left)
		{
			StackPush(&q, keyi-1);//因为先top的是left,所以left边最后放
			StackPush(&q, left);
		}
	}
	StackDestroy(&q);
}

4.归并排序

思路

归并排序的思路就是先将所有元素分成最小单元(一个),然后再通过指针在每个有序单元数组中挑选元素比较,将较小的元素放到辅助数组中即可,递归版就是通过递归得到最小元素单元(类似后序遍历),非递归就是直接找最小元素进行比较放入辅助数组

动图

请添加图片描述

4.1归并排序递归法

void _MargeSort(int* a, int begin, int end, int* tmp)//递归法
{
	if (begin >= end)//截止条件
		return;
	int mid = (begin + end) / 2;
	_MargeSort(a, begin, mid,tmp);
	_MargeSort(a, mid + 1, end, tmp);
	//左右区间[begin,mid] [mid+1,end]
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin1;//记录归并的两个数组的头下标
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin+1));
	//归并数组长度整体拷贝到原数组中,注意下标开始位置
}
void MargeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail!");
		exit(-1);
	}
	_MargeSort(a, 0, n - 1, tmp);
	//MargeSortNone1(a, n, tmp);
	//MargeSortNone2(a, n, tmp);
	free(tmp);
}

4.2归并排序非递归版(1)

void MargeSortNone1(int* a, int n,int* tmp)//非递归法
{
	int gap = 1;//gap是设置归并数组的长度
	while (gap <= n)//gap>n的时候就已经排序好了
	{
		for (int i = 0; i < n; i += gap * 2)//gap*2的意思是每次加上两个数组长度,因为归并是两个数组之间的
		{
			//左右数组的范围[i,i+gap-1]  [i+gap,i+gap*2-1]  i是归并的两个数组初始位置,数组的第一个下标
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = begin1;
			//这里是修正边界,因为我们通过非递归法,边界一定是2的次方倍,所以会导致end1越界,或者begin2越界,或者end2越界
			if (end1 >= n)//end1越界
			{
				end1 = n - 1;//修正边界
				//将右区间变成一个不存在的区间,这就不会进入下面的第一个循环
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				//将右区间变成一个不存在的区间,这就不会进入下面的第一个循环
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;//修正边界
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

		}
		memcpy(a, tmp, sizeof(int) * n);//每次进行一个完整的归并结束后将结果cpy到原数组中
		gap *= 2;//gap数组(归并数组)长度翻倍
	}
}
void MargeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail!");
		exit(-1);
	}
	//_MargeSort(a, 0, n - 1, tmp);
	MargeSortNone1(a, n, tmp);
	//MargeSortNone2(a, n, tmp);
	free(tmp);
}

方法一就是通过修正边界和设置一个不存在的右区间
然后每次完整的归并结束复制的原数组
在这里插入图片描述

4.3归并排序非递归版(2)

void MargeSortNone2(int* a, int n, int* tmp)
{
	int gap = 1;
	while (gap <= n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = begin1;
			if (end1 >= n || begin2 >= n)//和1的区别就是这里不修正边界,这里是越界的区间直接不归并
			{
				break;
			}
			else if (end2 >= n)//右区间越界就修正一下
			{
				end2 = n - 1;
			}
			int m = end2 - begin1 + 1;//这里是计算cpy到原数组的长度
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a+i, tmp+i, sizeof(int) * m);//每归并比较一次就cpy到原数组,最容易出错的地方
		}
		gap *= 2;
	}
}
void MargeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail!");
		exit(-1);
	}
	//_MargeSort(a, 0, n - 1, tmp);
	//MargeSortNone1(a, n, tmp);
	MargeSortNone2(a, n, tmp);
	free(tmp);
}

方法二是之间让越界的部分直接不去归并
但是这里就得每一次小归并就得复制到原数组
在这里插入图片描述

5.记数排序

思想

源于哈希的思想,通过数据的波动差值创建一个数组count,然后在原数组中找到最大值和最小值,将每个数组元素-min(最小值)存到对应的count数组下标中,然后在遍历count取的排序的数组,取出的元素+min,因为我们存的时候就是下标=元素-min,所以我们取的下标元素下标=元素-min,所以元素=下标+min

5.1记数排序

不适用于上下波动较大的数组
只是用整型排序

void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)//找到最大值和最小值
	{
		if (max < a[i])
			max = a[i];
		if (min > a[i])
			min = a[i];
	}
	int range = max - min + 1;//求出数据的波动范围
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	memset(count, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

总结

排序方法时间复杂度平均情况时间复杂度最好情况时间复杂度最坏情况空间复杂度稳定性
直接插入排序O(N^2)O(N)O(N^2)O(1)稳定
希尔排序O(N*logN)-O(N^2)O(N^1.3)O(N^2)O(1)不稳定
选择法排序O(N^2)O(N^2)O(N^2)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
冒泡排序O(N^2)O(N)O(N^2)O(1)稳定
快排O(N*logN)O(N*logN)O(N^2)O(1)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
计数排序O(N)O(N)O(N)O(countsize)不稳定
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

相知-

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

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

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

打赏作者

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

抵扣说明:

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

余额充值