草庐IT

【数据结构与算法】万字剖析八大排序(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序)

匿名者Unit 2023-06-02 原文


目录

一.直接插入排序

初窥直接插入排序我们先来看一张动图:

由动图我们可以分析出直接插入排序是从第二个数据开始遍历,与前面的数据进行比较如果小于 则让前面的数据向前移动 自己接着向前面的数据比较 直到比较到大于等于自己的数据或者没有数据能进行比较时停止 插入当前的位置。

直接插入排序性能分析:
时间复杂度:O(N^2),最好情况当数据本来就有序时可以做到O(N)
空间复杂度:直接插入排序并没再开过一段连续的空间所以为O(1)

完整代码如下,我们看到while循环中的判断即是让tmp小于的数据 不断向前覆盖 直到找到小于tmp的数据 或者 end小于0(前面没有可比较的数据)时 跳出循环将数据插入。

void InsertSort(int* a, int size)
{
	assert(a);

	for (int i = 0; i < size - 1; i++)
	{
		int tmp = a[i + 1];
		int end = i;

		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp;
	}
}

二.希尔排序

希尔排序又称缩小增量排序,它选定了一个整数gap将待排数据分组,然后对组内的数据进行直接插入排序,接着减小gap 改变分组的区间 再进行排序。此过程称为预排序最后gap减小到1时,数据已经接近有序,再对数据进行直接插入的排序效率则较高。动图演示如下:

希尔排序性能分析:
时间复杂度:最坏O(N^ 2) ,平均在<数据结构>严蔚敏中给出O(N^1.3);
空间复杂度:O(1);

完整代码如下,如有疑问随时私信或者在评论区提出。

void ShellSort(int* a, int size)
{
	assert(a);
	int gap = size;

	while (gap > 1)
	{
		gap /= 2;//使得gap最后能减到1 成为直接插入排序
		//gap /= 3 + 1;
		for (int i = 0; i < size - gap; i += gap)
		{
			int end = i;
			int tmp = a[i + gap];

			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}

	}
}

三.选择排序

选择排序在八大排序中属于是最“老实的”一个排序,其基本思想 首先选出一个关键值 一般是数据的第一位 接着遍历剩下的数据 记录剩下数据中最小值和最大值的下标。接着将最小值放到左边 最大值放到右边 接着减小需要排序的区间(因为已经排一个最大值和一个最小值了

当前代码逻辑实现如下:

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;

	while (left < right)
	{
		int mini = left, maxi = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}

			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]);
		Swap(&a[right], &a[maxi]);
		--right;
		++left;
	}
}

将我测试代码时发现,有些情况,上面的代码无法处理。

这是为什么了,我们调试观察到:

此时关键值为a[3] 6 ,在下标区间4到6遍历后max仍然是6min是3。接着min与a[left]交换即:

后续最大值与a[right]交换时,原本应该放着最大值的a[left]已经被换成了最小值,所以交换会发生错误。也就是left与maxi重叠时,需要将maxi赋为mini才不会发生错误。

选择排序性能:
时间复杂度:O(N^2);
空间复杂度:O(1);

正确代码如下:

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;

	while (left < right)
	{
		int mini = left, maxi = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}

			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]);
		if (left == maxi)//防止left与maxi重叠
		{
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]);

		--right;
		++left;
	}
}

四.堆排序

堆排序在之前我就写过文章讲解,这里就不赘述了。需要注意的是:排升序、建大堆,排降序、建小堆文章堆和堆排序的链接

五.冒泡排序


冒泡排序在学习C语言时就已经学过,所以实现它轻而易举,代码如下:

选择排序性能:
时间复杂度:O(N^2);
空间复杂度:O(1);

void BubbleSort(int* a, int size)
{
	for (int i = 0; i < size - 1; i++)
	{
		for (int j = 1; j < size  - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
			}
		}
	}
}

六.快速排序

1.hoare版

快速排序顾名思义它的效率较高,是由Hoare于1962年提出的一种二叉树结构的交换排序方法。它的思想是选定一个基准值,将小于基准值的数据放到左边大于的放到右边。接着在分别递归左右区间。

快速排序性能:
时间复杂度:O(N*logN)最坏O(N^2)
空间复杂度:O(1);

此版本需要注意的是如果将左边第一个作为基准值就需要让右边先走,需要小于基准值的数据,相反就让左边先走

void QuickSort11(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left;
	int end = right;

	int keyi = left;
	while (left < right)
	{
		while (left < right && a[keyi] <= a[right])
			--right;

		while (left < right && a[keyi] >= a[left])
			++left;

		Swap(&a[left], &a[right]);
	}

	Swap(&a[keyi], &a[left]);
	keyi = left;


	QuickSort11(a, begin, keyi - 1);
	QuickSort11(a, keyi + 1, end);
}

2.挖坑法

挖坑法的优化不明显,排序思想也与原先的差不多。

代码如下,供大家理解:

void QuickSort22(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left;
	int end = right;

	int hole = left;
	int key = a[left];
	while (left < right)
	{
		while (left < right && key <= a[right])
			--right;

		a[hole] = a[right];
		hole = right;

		while (left < right && key >= a[left])
			++left;

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;
	QuickSort22(a, begin, hole - 1);
	QuickSort22(a, hole + 1, end);
}

3.前后指针

前后指针是快速排序的第三个版本,它的做法就非常的妙:

完整代码如下:

void QuickSort33(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left;
	int end = right;
	
	int keyi = left;
	
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (a[cur] <= a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	QuickSort33(a, begin, prev - 1);
	QuickSort33(a, prev + 1, end);
}

4.选取基准值的优化

我们知道快速排序的效率是很高的,特别是处理无序的数据时,但是当数组为有序时,时间复杂度就会下降到O(N^2),这是因为我们总是将第一个数据作为基准值导致的。这里就有个优化的方法———三数取中———它将比较mid left right的数据,取中值作为基准值,使得即使数据有序,效率也不会太差。

int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 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
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[right] < a[left])
			return left;
		else
			return right;
	}
}

(1)快速排序非递归

上文快速排序的三个版本都使用了递归,当数据量过大,建立的栈帧空间过多时,容易产生栈溢出,导致错误。所以我们要学习 将代码从递归改为非递归避免错误 是以后我们工作的必备技能。
快速排序的非递归需要使用到栈 使用栈辅助改为循环。
如何利用栈将快排改为循环呢?我们将数组左右下标压入栈,为了出栈顺序为先左后右,我们则要先压右下标,再压左下标。然后当栈非空时就 继续弹出区间给快排函数排序,当左右区间数据少于1时停止压栈。

void QuickSortNon(int* a, int left, int right)
{
	ST QS;
	InitST(&QS);
	Push(&QS, right);
	Push(&QS, left);

	while (!STEmpty(&QS))
	{
		int begin = STTop(&QS);
		Pop(&QS);

		int end = STTop(&QS);
		Pop(&QS);

		int key = QuickSort3(a, begin, end);
		if (begin < key - 1)
		{
			Push(&QS, key - 1);
			Push(&QS, begin);
		}
		if (key + 1 < end)
		{
			Push(&QS, end);
			Push(&QS, key + 1);
		}

	}
	DestroyST(&QS);
}

七.归并排序

归并排序采用了分治的算法思想,将数据分成两个区间不断递归到两个区间有序后,将两个区间内的数据排到新malloc的辅助空间中
演示动图如下:

归并排序性能:
时间复杂度:O(N*logN)
空间复杂度:O(N);归并排序需要的辅助空间较多,所以常用于解决磁盘的外排序问题

完整代码如下:

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;
	int mid = (left + right) / 2;

	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] > a[begin2])
		{
			tmp[i++] = a[begin2++];
		}
		else
		{
			tmp[i++] = a[begin1++];
		}
	}

	while(begin1 <= end1)//前面两个区间中还有多余没有拿下来的数据,一并排进去
		tmp[i++] = a[begin1++];
	while (begin2 <= end1)
		tmp[i++] = a[begin2++];

	memcpy(a + left, tmp+left, sizeof(int)*(right-left+1));
}

(2)归并排序非递归

由上面的归并排序代码我们可知,排序开始先找到mid 将数据分为两个区间,接着不断递归到区间内只有一个数据也就是有序了然后与右区间的数据进行比较排进辅助空间中。
那我们可不可以不通过递归 控制数据区间有序呢?可以直接定义gap控制一区间内的数据个数,gap初值为1,也就达到了区间有序

控制如下:

	int gap=1;
	while (gap < size)
	{	
		gap *= 2;
	}

而且非递归版本不想递归有mid,便于控制区间,其区间的控制需要理解:

	int gap = 1;
	while (gap < size)
	{
		for (int i = 0; i < size; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;//区间控制
			int begin2 = i + gap, end2 = i + 2 * gap - 1;//好好体会
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] > a[begin2])
				{
					tmp[j++] = a[begin2++];
				}
				else
				{
					tmp[j++] = a[begin1++];
				}
			}

			while (begin1 <= end1)
				tmp[j++] = a[begin1++];
			while (begin2 <= end2)
				tmp[j++] = a[begin2++];

			memcpy(a, tmp, sizeof(int) * (end2-1+1));

		}

到这里我们就不免有个疑问,不同于上图所示,如果数据为奇数,不能正好的分区间,那该怎么办呢?
我们则需要在区间控制,再判断一下区间是否越界以及相关的处理。

	int begin1 = i, end1 = i + gap - 1;//区间控制
	int begin2 = i + gap, end2 = i + 2 * gap - 1;

区间如上所示,又因为i是0到size-1的所以end1 begin2 end2都有越界的可能,我们分类讨论解决一下。

共有三种情况:

  1. end1就越界了,begin2和end2肯定也就越界了,这时只有左区间的部分数据有效所以无法进行比较+排入,所以这时的gap是错误了,此次循环不进行直接break
  2. begin2和end2越界时,这时只有左区间有效,无法进行比较+排入与第一种情况相同直接break
  3. 最后一种情况只有end2越界了,我们只需将end2赋为正常区间之内即可
    调整代码如下:
		int begin1 = i, end1 = i + gap - 1;//区间控制
		int begin2 = i + gap, end2 = i + 2 * gap - 1;
		if (end1 >= size || begin2 >= size)
		{
			break;
		}
		else if (end2 >= size)
		{
			end2 = size - 1;
		}

完整代码如下:

void _MergeSortNon6(int* a, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);

	if (tmp == NULL)
	{
		perror("malloc fail");
	}
	int gap = 1;
	while (gap < size)
	{
		for (int i = 0; i < size; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;//区间控制
			int begin2 = i + gap, end2 = i + 2 * gap - 1;//好好体会
			if (end1 >= size || begin2 >= size)
			{
				break;
			}
			else if (end2 >= size)
			{
				end2 = size - 1;
			}

			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] > a[begin2])
				{
					tmp[j++] = a[begin2++];
				}
				else
				{
					tmp[j++] = a[begin1++];
				}
			}

			while (begin1 <= end1)
				tmp[j++] = a[begin1++];
			while (begin2 <= end2)
				tmp[j++] = a[begin2++];

			memcpy(a, tmp, sizeof(int) * (end2-1+1));

		}

		gap *= 2;
	}
	free(tmp);
}

上面的代码是归并一部分就拷贝一部分,也可以在for循环外一次性拷贝进去,一次性拷贝对区间的控制更为麻烦,这里就不过多赘述。感兴趣的可以在我的gitee中找到代码匿名者Unit码云

八.计数排序

计数排序属于非比较排序,适合范围集中,并且范围不大的整形数组排序。

计数排序性能:
时间复杂度:O(N+range)
空间复杂度:O(range);

完整代码如下:

void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];

	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if(a[i]<min)
		{
			min = a[i];
		}
	}

	int range = max - min + 1;
	int* countA = (int*)malloc(sizeof(int) * range);
	assert(countA);

	memset(countA, 0, sizeof(int) * range);

	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = i + min;
		}
	}
}

九.八大排序稳定性分析

在排序中稳定性的含义如下:

冒泡排序每一趟将一个数排到最终位置,可以控制相等条件使得冒泡稳定。
选择排序不稳定的情况就难以想到了:

1小于基准值2,则将1与2交换,导致了两个2的相对顺序发生改变
直接插入排序同样可以控制相等交换条件,做到稳定排序。
希尔排序有可能会出现相同的值分到不同组中,导致不稳定的情况。
堆排序在堆调整时可能会更改相同值的相对次序,所以不稳定。
归并排序在插入辅助空间时,是按照原来的顺序 不会改变次序,所以是稳定排序。
快速排序与选择排序的情况相同,也是交换了基准值导致了不稳定的情况。

文章到此结束,有疑问的随时私信或者评论区留言!

有关【数据结构与算法】万字剖析八大排序(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序)的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby-on-rails - Ruby on Rails 计数器缓存错误 - 2

    尝试在我的RoR应用程序中实现计数器缓存列时出现错误Unknownkey(s):counter_cache。我在这个问题中实现了模型关联:Modelassociationquestion这是我的迁移:classAddVideoVotesCountToVideos0Video.reset_column_informationVideo.find(:all).eachdo|p|p.update_attributes:videos_votes_count,p.video_votes.lengthendenddefself.downremove_column:videos,:video_vot

  3. ruby - 使用多个数组创建计数 - 2

    我正在尝试按0-9和a-z的顺序创建数字和字母列表。我有一组值value_array=['0','1','2','3','4','5','6','7','8','9','a','b','光盘','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','','u','v','w','x','y','z']和一个组合列表的数组,按顺序,这些数字可以产生x个字符,比方说三个list_array=[]和一个当前字母和数字组合的数组(在将它插入列表数组之前我会把它变成一个字符串,]current_combo['0','0','0']

  4. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  5. ruby - Rails 3 的 RGB 颜色选择器 - 2

    状态:我正在构建一个应用程序,其中需要一个可供用户选择颜色的字段,该字段将包含RGB颜色代码字符串。我已经测试了一个看起来很漂亮但效果不佳的。它是“挑剔的颜色”,并托管在此存储库中:https://github.com/Astorsoft/picky-color.在这里我打开一个关于它的一些问题的问题。问题:请建议我在Rails3应用程序中使用一些颜色选择器。 最佳答案 也许页面上的列表jQueryUIDevelopment:ColorPicker为您提供开箱即用的产品。原因是jQuery现在包含在Rails3应用程序中,因此使用基

  6. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  7. ruby-on-rails - 一般建议和推荐的文件夹结构 - Sinatra - 2

    您将如何构建一个简单的Sinatra应用程序?我正在制作,我希望该应用具有以下功能:“应用程序”更像是一个包含所有信息的管理仪表板。然后另一个应用程序将通过REST访问信息。我还没有创建仪表板,只是从数据库中获取东西session和身份验证(尚未实现)您可以上传图片,其他应用可以显示这些图片我已经使用RSpec创建了一个测试文件通过Prawn生成报告目前的设置是这样的:app.rbtest_app.rb因为我实际上只有应用程序和测试文件。到目前为止,我已经将Datamapper用于ORM,将SQLite用于数据库。这是我的第一个Ruby/Sinatra项目,所以欢迎任何和所有建议-我应

  8. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  9. ruby-on-rails - Rails 单选按钮 - 模型中多列的一种选择 - 2

    我希望用户从一个模型的三个选项中选择一个。即我有一个模型视频,可以被评为正面/负面/未知目前我有三列bool值(pos/neg/unknown)。这是处理这种情况的最佳方式吗?为此,表单应该是什么样的?目前我有类似的东西但显然它允许多项选择,而我试图将它限制为只有一个..怎么办? 最佳答案 如果要使用字符串列,让我们说rating。然后在你的表单中:#...#...它只允许一个选择编辑完全相同但使用radio_button_tag: 关于ruby-on-rails-Rails单选按钮-模

  10. ruby-on-rails - 需要帮助最大化多个相似对象中的 3 个因素并适当排序 - 2

    我需要用任何语言编写一个算法,根据3个因素对数组进行排序。我以度假村为例(如Hipmunk)。假设我想去度假。我想要最便宜的地方、最好的评论和最多的景点。但是,显然我找不到在所有3个中都排名第一的方法。Example(assumingthereare20importantattractions):ResortA:$150/night...98/100infavorablereviews...18of20attractionsResortB:$99/night...85/100infavorablereviews...12of20attractionsResortC:$120/night

随机推荐