【数据结构】插入排序、希尔排序、冒泡排序、选择排序

文章目录

一、直接插入排序

思想

程序代码

时间复杂度

二、希尔排序

思想

程序代码

时间复杂度

三、冒泡排序

思想

程序代码

时间复杂度

四、选择排序

思想

程序代码

时间复杂度


一、直接插入排序

思想

        直接插入排序有些类似于我们玩扑克牌时的整理牌序动作,比如有整理好的手牌是 3,3,3,4,6,7,8,9,10,J,J,K;还差一张手牌 5 没有整理好,那么一般而言是会把 5 放在 4 的后面。而直接插入排序也类似,在一个有序序列中插入若干个数字,使最后的序列也是有序的。可以使用下图的做法:

        这里可能会比较疑惑,为什么不从左边开始比较,而从右边开始比较?一方面,从左或者从右比较两者没有什么差别,只是在上图的具体例子中,从左边插入确实比较的次数要少一些。另一方面,如果从左边插入,就只能找到 “3” 该在的位置,然后一次将后面的数据全部移走,这样子写代码比较麻烦,不如比较一次,移动一个数据

       那么如果给一个乱序的序列,如何将其排好序呢?比如[ 5,1,6,2,9,4,5,7,10 ] ,其实也是类似的原理,只需要把第一个数据看作是有序的(因为它本来就只有一个数据),然后将第二个数据按上面的算法插入进去,这样前两个数据就有序。然后将第三个数据插入……直到最后,所有数据就都有序了。

程序代码

        如下,只需要套一层循环,用 i 来控制,将第1个数据看作是有序的,从第二个数据开始插入,每一次插入一个数据。因为有 n 个数据,所以下标最多是n-1 。

void InsertSort(int* p, int n)
{
	for (int i = 0;i < n - 1;i++)
	{
		int end = i;
		int temp = p[i + 1];
		while (end >= 0)
		{
			if (p[end] > temp)
			{
				p[end + 1] = p[end];
				end--;
			}
			else
				break;
		}
		p[end + 1] = temp;//这一句不能写在else里面,是因为如果end=-1,那么不会进入while循环,也就白比较和后移了
	}                     
}

时间复杂度

        直接插入排序的时间复杂度还是很好分析的,按最坏的情况来看,一轮排序中,每次都要和所有数据比较完才停下来。比如有[  3,4,5,6,8] ,然后本次要插入的是数据 "2" ,需要比较到从右往左最后一个数据 “3” ,才发现也不满足,此时由于end 小于0,才跳出循环。

        这样子导致每次都要比较 i 次,所以时间复杂度= 1+2+3+……+ N = (N*N+N)/ 2,所以时间复杂度为O(N^2)。

二、希尔排序

        上面说到,直接插入排序可能会面临最坏的情况,就是原本升序的数据,要排成降序,这样每一个数据都要移动最多次。为了优化这个问题,D.L.Shell 于1959年发明了希尔排序。

思想

        此排序算法主要是将无序数组分割为若干个子序列,子序列不是逐段分割的,而是相隔特定的增量(记为gap)的子序列,对各个子序列进行插入排序然后再选择一个更小的增量,再将数组分割为多个子序列进行排序......最后选择增量为1,即使用直接插入排序,使最终数组成为有序

        如下图,假设此时 gap 为3,也就是下标间隔为3的数据是一个子序列。当进行第一组排序时,如图红色数据就是第一组,使用直接插入排序的算法。进行第二组排序时,第一组的数据已经排好了,下图也可以看出,然后进行蓝色的第二组数据排序。进行第三组排序时,前两组都排好了,进行绿色的第三组数据排序,得到最后的结果。

        最后得出的数据,虽然不是一个有序序列,但是相较之前的数据,已经相对有序了:值大的已经相对而言移到后面了,小的相对而言移到前面,这样子最后使用直接插入算法升序排序的时候,就会大大减少出现最坏的情况

        然后再对该组数据,进行 gap=1 的同样操作,gap=1就是直接插入排序,此时的数据相较于之前的数据,已经好排序多了,数据要移动的次数大大减少,时间复杂度得到了优化。

        但是此时,我们能不能把这个方法优化一下呢?这样一组一组比过去是否过于麻烦?我们可以化整为零,将一组一组分散成一对一对。如下图,gap也是39和5,1和7,2和4,分别是上面三组的第一次比较内容,这里我们按顺序第一次、第二次、第三次分别排序这三对数据,而不是之前一组一组来。

        三次完成之后,到了第四次,即原本第一组数据第二对数据 9和8 ,在之前的方法里,这对数据应该是第二次就排序了,但是在这里是第四次。
        再后面的某一次,也会到原本第一组数据的最后一对,如下图第三行(这里不仔细画出其中间排序的结果)。这样就将原本集中排序的第一组数据,分散为多次排序,每次排一对数据。另外两组也是同理。

        优化后的排序算法,其代码如下,可以对比上文的直接插入排序算法进行更好的理解,其实就是原本的gap=1,变成了gap=3 :

        gap = 3;
		for (int i = 0;i < n - gap;i++)
		{
			int end = i;
			int temp = p[i + gap];
			while (end >= 0)
			{
				if (p[end] > temp)
				{
					p[end + gap] = p[end];
					end -= gap;
				}
				else
					break;
			}
			p[end + gap] = temp;

程序代码

        代码如下,gap是自己设置的,数据多的时候,可能一开始是一个比较大的数字,比如15,并不是gap=15 排完之后就立刻排 gap =1 的情况,要逐层来,比较好的方法就是 gap/=2。第一次gap=15,第二次gap=7,第三次为3,第四次为1,这样子第四次排完之后,就完成排序了。控制条件就是在最外面加一个while循环,gap=1 的情况排完序之后,就跳出循环了。

void ShellSort(int *p,int n)
{
	int gap=n;
	while (gap > 1)
	{
		gap /= 2;//这样的好处是gap最后可以得到1
		for (int i = 0;i < n - gap;i++)
		{
			int end = i;
			int temp = p[i + gap];
			while (end >= 0)
			{
				if (p[end] > temp)
				{
					p[end + gap] = p[end];
					end -= gap;
				}
				else
					break;
			}
			p[end + gap] = temp;
		}
	}
}

时间复杂度

        希尔排序时间复杂度约为O(N^1.3),这个推导过程属实比较繁琐(其实是目前还未掌握..),所以就先不写啦!!

三、冒泡排序

思想

        冒泡排序重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

        假设有N个数据,需要升序排序,那么就要冒泡N-1次,每一次都把当前序列的最大值放到最后的位置,最后剩下的那个数据不需要冒泡,因为它就是最小的。示意图如下:

程序代码

void BubbleSort(int* p, int n)
{
	for (int i = 0;i < n - 1;i++)//冒泡次数,假设有4个数据,实际上冒泡好三个数据,剩下的就是最小的
	{
		int exchange = 0; // 标记一下,如果本轮冒泡没有数据交换,就是排好序了,退出循环

		for (int j = 0;j < n - i - 1;j++) //看第一次冒泡,假设有4个数据,那么只需要到第三个即可,因为下面的逻辑是当前数据和下一个数据比较  
		{
			if (p[j] > p[j + 1])
			{
				Swap(&p[j], &p[j + 1]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			return;
	}
	return;
}

时间复杂度

        假设有N个数据,需要冒泡N-1次,如果每次冒泡都是最坏的情况,需要交换数据的次数都是拉满,那么总共需要交换数据的次数是: (N-1) + (N-2) + (N-3) + (N-4) + …… + 1 。最后算出来的结果是:N*(N-1) / 2 ,所以时间复杂度为 O(N^2)。

四、选择排序

思想

        选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
        思想是,首先在未排序序列中找到最小(大)元素,分别存放到序列的起始位置。再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。重复第二步,直到所有元素均排序完毕。

        但是,可不可以优化一下子算法呢?一次找两个如何?在未排序序列中,寻找到最大的和最小的数据,记下来,然后最小的和当前序列起始位置互换最大的和当前序列末尾位置互换。重复此过程,最后得到的也是一个升序序列。

        如下图,第一次交换最大和最小的两个数据之后,首位两端算是排好了,第二次就要对中间未排的数据进行排序,重复此过程直到排完。

        但是,由于是要交换两个数据,难免会出现如下状况,避免错误的机制就是,交换完最小的数据和当前序列首位的数据之后,改变记录最大的数据下标的变量,让它变成正确的。具体可见下面的代码。

程序代码

        如下代码:

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

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

			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}

		Swap(&a[begin], &a[mini]);
		if (maxi == begin)
			maxi = mini;

		Swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}
}

时间复杂度

        任何情况都是O(N^2) 包括有序或接近有序。因为要一个一个比过去的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力努力再努力.xx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值