排序算法分析1--冒泡排序、插入排序、选择排序

0.综述

所谓排序,就是将原本无序的序列重新排列成有序序列的过程。排序算法对任何一个程序员来说都是一项基本功,在实际的项目中我们也经常需要使用排序。排序算法的选择和使用不仅要针对实际数据的特点,有时候排序的稳定性也必须考虑。这样可以提高程序的运行效率,让开发工作事半功倍。

学习排序算法不能仅仅停留在了解算法原理和使用代码实现这两步,我们应该在此基础上学会去分析和评价一个算法。我们可以从以下几各方面去衡量一个排序算法:

  • 对于排序算法执行效率的分析,包括最好、最坏、平均情况下的时间复杂度,算法运行过程中数据比较和交换的次数等。
  • 对于排序算法的内存消耗分析。算法的内存消耗通过空间复杂度来衡量。
  • 排序算法的稳定性。所谓稳定性,就是待排序的序列中的值相同的元素经过排序算法处理后是否保持原先的位置关系,若保持,则称这个排序算法为稳定的排序算法,反之则称为不稳定的排序算法。

1.冒泡排序(Bubble sort)

1.1冒泡排序算法原理

冒泡排序算法只操作相邻的两个元素,每次冒泡操作都会比较相邻的两个元素,看着两个元素的大小是否满足要求,要是不满足就交换它们的位置。每进行一次冒泡就会至少会有一个元素被放到正确的位置上,这样重复n次就可以完成n个元素的排序工作。

例如对于这样一组数据:4,5,6,3,2,1,需要进行从小到大的排序。
进行第一次冒泡操作的时候

  • 首先比较4和5,满足条件 此时元素序列为 4,5,6,3,2,1;
  • 然后比较5和6,满足条件 此时元素序列为 4,5,6,3,2,1;
  • 比较6和3,6比3大,所以交换6和3 此时元素序列为 4,5,3,6,2,1;
  • 比较6和2,6比2大,所以交换6和2 此时元素序列为 4,5,3,2,6,1;
  • 比较6和1,6比1大,所以交换6和1 此时元素序列为 4,5,3,2,1,6;

这样经过一次冒泡后元素6就被移动到了正确的位置。由于冒泡过程中元素的移动就像气泡网上冒一样,这也是冒泡算法名称的由来。可以看得出,要想对着6个数据进行正确的排序,需要进行6次冒泡操作。

下面是针对上面的数据每一次冒泡后得到的序列状态:

  • 初始状态:4,5,6,3,2,1
  • 第一次冒泡后:4,5,3,2,1,6
  • 第二次冒泡后:4,3,2,1,5,6
  • 第三次冒泡后:3,2,1,4,5,6
  • 第四次冒泡后:2,1,3,4,5,6
  • 第五次冒泡后:1,2,3,4,5,6
  • 第六次冒泡后:1,2,3,4,5,6

其中加粗的是每一次冒泡操作往上冒,也就是到达正确位置的元素。

1.2冒泡排序算法优化与代码实现

针对上面一节的内容,我们理解了冒泡排序的基本原理与操作步骤。上述的算法是可以优化的,例如针对这样的一组数据:3,4,5,1,2,6。这样的六个数是不是也需要进行六次冒泡操作呢?我们来走一遍冒泡操作看看。

  • 初始状态:3,4,5,1,2,6
  • 第一次冒泡:3,4,1,2,5,6
  • 第二次冒泡:3,1,2,4,5,6
  • 第三次冒泡:1,2,3,4,5,6
  • 第四次冒泡:1,2,3,4,5,6
  • 第五次冒泡:1,2,3,4,5,6

可以看得出来进行到第三次冒泡操作的时候整个序列就已经是有序的了,所以第四次和四五次都没有进行数据交换。有上述分析可知,此处的六个数据只需进行四次冒泡操作就可以了。

实际上当没有数据交换的时候,序列就是完全有序的了,此时我们也可以认为排序已经完成,不用在继续执行后面的冒泡操作了。

最终冒泡排序C语言代码如下:

//a是数组,n是数组中元素的个数
void bubble_sort(int a[],int n)
{
	if(n<=1)
		return;
	for(int i=0;i<n;++i)
	{
		int flag = 0;//设定是否提前退出冒泡排序操作的flag
		for(int j=0;j<n-1;++j)
		{
			if(a[j]>a[j+1])
			{
				int temp = a[j];
				a[j] = a[j+1];
				a[j+1] = temp;
				flag = 1; //flag = 1表示有数据交换
			}					
		}
		if(!flag)
			break;
	}
}

1.3冒泡排序算法分析

  • 由上述分析可以看出,冒泡排序只涉及相邻两个元素的交换操作,它所使用的临时空间是常量级的,所以冒泡排序算法的空间复杂度为O(1),因此它也是一个原地算法(所谓原地算法,就特指空间复杂度为O(1)的算法)。
  • 从时间复杂度上看,冒泡排序最好情况是原序列已经有序,这样只需要进行一次冒泡操作即可判断出不需要继续冒泡了,所以最好的情况下时间复杂度为O(n)。而最坏的情况下需要进行n次冒泡操作,每次冒泡又要进行n次比较操作,所以最坏情况复杂度为O(n²)。于是总的时间复杂度为O(n²)。
  • 由于可以在程序中设定当两个值相等时不交换两个元素的位置,所以冒泡排序是稳定的排序算法。

2.插入排序(Insertion sort)

2.1插入排序算法原理

首先思考这样一个问题,如何在一个有序序列中插入一个新元素?假设有需要在这样一个有序序列中插入6:1,7,8,15。很容易想到只需要遍历当前这个有序序列,然后找到应该插入新元素的合适位置插入该元素即可。这是一个动态的插入过程,需要搬移插入位置之后的元素,插入过程如下图:
在这里插入图片描述

由上面的思想就可以引出这部分的主题:插入排序算法。插入排序的算法思想就是:将数组中的数据分为已排序区间和未排序区间两个区间。一开始的时候已排序区间只有一个元素,一般就是数组中的第一个元素。则剩下的数组中的其他元素所组成区间就是未排序区间。插入算法每次选取一个未排序区间内的元素按照上面插入6的方法插入到前面的已排序区间中。一直重复插入操作至未排序区间中的元素个数为0算法结束。

2.2直接插入排序代码实现

与上面的分析不难写出插入排序代码:

void inseration_sort(int a[],int n)
{
	if(n<=1)
		return;
	for(int i=1;i<n;++i)
	{
		int value = a[i];	//待插入的元素
		int j = i-1;	//从后往前找
		//查找插入位置
		for(;j>=0;--j)
		{
			if(a[j]>value)
			{
				a[j+1] = a[j];	//移动数据
			}
			else
				break;
		}
		a[j+1]=value;
	}
}

2.3直接插入排序算法分析

  • 由上述分析可知,插入排序空间复杂度为O(1),即插入排序算法是一个原地排序算法。
  • 在插入排序中我们可以设定对于值相同的两个元素,将后出现的元素插入到前面出现元素的后面,这样就可以保证排序后两个元素的位置关系不变,所以插入排序是一个稳定的排序算法。
  • 在最好的情况下,也就是数组已经有序的时候,比较次数为n-1,所以时间复杂度为O(n)。在最坏情况下,数组刚好严格逆序的时候,比较次数和移动次数都是n(n-1)/2,时间复杂度为 O ( n 2 ) O(n^2) O(n2)所以最终得出,插入排序算法的时间复杂度为 O ( n ² ) O(n²) O(n²)

2.4折半插入排序算法

折半插入排序算法的是对直接插入排序算法的优化,它们的区别是查找插入位置的方法不同,折半插入采用折半查找法来查找插入位置。我们知道,对于折半查找法来说,它的一个基本要求是待查找的序列已经有序,而先前直接插入法中的有序区间内的元素显然符合这样的条件,因此针对于直接插入法中的遍历查找待插入位置的方法,我们可以用折半法来进行优化,折半查找法可以在这个有序序列中查找插入位置。

折半插入代码实现:

void binary_inseration_sort(int a[], int n)
{
	for (int i = 1; i<n; ++i)
	{
		int tmp = a[i];
		//查找区间 0~i-1
		int low = 0, high = i - 1;
		int mid;
		while (low <= high)
		{
			mid = (low + high) / 2;
			if (a[mid]>tmp)
			{
				high = mid-1;
			}
			else 
			{
				low = mid+1;
			}
		}

		int j;
		for (j = i - 1; j >= low; --j)
		{
			a[j + 1] = a[j];
		}
		a[j + 1] = tmp;
	}
}

3.选择排序(Slection sort)

3.1选择排序算法原理

选择排序的算法思路有点类似插入排序,它也是分为了已排序区间和未排序区间,并且每次从未排序区间的选取一个元素插入到已排序区间。不同于插入排序的是,选择排序算法每次从未排序区间中找到最小的元素,将其插入到已排序区间的末尾。

3.2选择排序代码实现

void slection_sort(int a[],int n)
{
	int temp = 0;
	for(int i=0;i<n-1;++i)
		for(int j=i+1;j<n;++j)
		{
			if(a[i]>a[j])
			{
				temp = a[i];
				a[i]=a[j];
				a[j]=temp;
			}
		}
}

选择排序算法实现较为简单,但是要注意区别,很多人不仔细看会误认为跟冒泡算法实现代码一样。冒泡排序算法是挨个比较两个相邻元素,而选择插入排序是每次将未排序区间内的最小值提取出来插入到已排序区间的末尾。具体的过程就是挨个比较未排序区间内的元素和已排序区间的末元素,若已排序区间末元素大则替换掉,否则继续比较未排序区间内的下一个元素,直到都比较完。

例如对于下面的序列,第一次选择的时候挨个比较a[0]与后面的元素,10比6大,所以交换它们,序列就成了6,10,7,8,15。注意接下来比较的不是10和7,而是6和7,6和8,6和15!这是和冒泡排序的不同的地方。
在这里插入图片描述

3.3选择排序算法分析

  • 选择排序的空间复杂度为O(1),所以它是一个原地排序算法。
  • 选择排序不是一个稳定算法,因为选择排序每次从未排序区间中找到一个最小值,并且和前面的元素交换位置,这样就破坏了稳定性。
  • 选择排序的时间复杂度为O(n²)。

4.总结

上面的三种算法都是O(n²)的,但是相对而言,插入排序又比冒泡排序优秀一些,因为插入排序过程中每次只需要赋值一次,而冒泡排序因为要交换两个数,因此要经过三次赋值,在数据规模比较大的时候就会有明显的性能差异。

在实际工作中O(n²)的排序算法并不常用,但是插入排序还是挺有用的,因为它在某些情况下的性能还是比较好的。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值