数据结构—排序

谁说我不能喝 提交于 2020-03-01 02:54:30

排序:假设含有 n 个记录的序列为

,其相应的关键字分别为

,需确定

的一种排列

,使其相应的关键字满足

( 非递减或非递增 )关系,即使得序列成为 一个按关键字有序的序列

,这样的操作就称为排序

相信大家都有着极其丰富的网上购物的经验,现在网上购物已经非常成熟,对用户来说带来了很大的方便。

假如我想买一台 iPhone4 的手机,于是上了某电子商务网站去搜索。可搜索后发现(如下图所示),有 8863 个相关的物品,如此之多,这叫我如何选择。我其实是想买便宜一点的,但是又怕遇到骗子,想找信誉好的商家,如何做?

下面的有些购物达人给我出主意了,排序呀。对呀,排序就行了(如下图所示)。我完全可以根据自己的需要对搜索到的商品进行排序,比如按信用从高到低、再按价格从低到高,将最符合我预期的商品列在前面,最终找到我愿意购买的商家,非常的方便。

网站是如何做到快速地将商品按某种规则有序的呢?——排序

1. 排序的基本概念与分类

排序是我们生活中经常会面对的问题。同学们做操时会按照从矮到高排列;老师查看上课出勤情况时,会按学生学号顺序点名;高考录取时,会按成绩总分降序依次录取等。那排序的严格定义是什么呢?

假设含有 n 个记录的序列为

,其相应的关键字分别为

,需确定 1,2,…… ,n 的一种排列

,使其相应的关键字满足 

( 非递减或非递增 )关系,即使得序列成为一个按关键字有序的序列

,这样的操作就称为排序

注意我们在排序问题中,通常将数据元素称为记录。显然我们输入的是一个记录集合,输出的也是一个记录集合,所以说,可以将排序看成是线性表的一种操作。

排序的依据是关键字之间的大小关系,那么,对同一个记录集合,针对不同的关键字进行排序,可以得到不同序列。

这里关键字

可以是记录 r 的主关键字,也可以是次关键字,甚至是若干数据项的组合。比如我们某些大学为了选拔在主科上更优秀的学生,要求对所有学生的所有科目总分降序排名,并且在同样总分的情况下将语数外总分做降序排名。这就是对总分和语数外总分两个次关键字的组合排序。如下图所示,对于组合排序的问题, 当然可以先排序总分,若总分相等的情况下,再排序语数外总分,但这是比较土的办法。我们还可以应用一个技巧来实现一次排序即完成组合排序问题,例如,把总分与语数外都当成字符串首尾连接在一起( 注意语数外总分如果位数不够三位,需要在前面补零 ),很容易可以得到令狐冲的 “753229” 要小于张无忌的 “753236” ,于是张无忌就排在了令狐冲的前面。

从这个例子也可看出,多个关键字的排序最终都可以转化为单个关键字的排序, 因此,我们这里主要讨论的是单个关键字的排序。

1.1 排序的稳定性

也正是由于排序不仅是针对主关键字,那么对于次关键字,因为待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,我们给出了稳定与不稳定排序的定义。

假设

,且在排序前的序列中

领先于

,( 即 i<j )。如果排序后

仍领先于

,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中

领先

,则称所用的排序方法是不稳定的。

如下图所示,经过对总分的降序排序后,总分高的排在前列。此时对于令狐冲和张无忌而言,未排序时是令狐冲在前,那么它们总分排序后,分数相等的令狐冲依然应该在前,这样才算是稳定的排序,如果他们二者颠倒了,则此排序是不稳定的了。只要有一组关键字实例发生类似情况, 就可认为此排序方法是不稳定的。排序算法是否稳定的,要通过分析后才能得出。

1.2 内排序与外排序

根据在排序过程中待排序的记录是否全部被放置在内存中排序分为:内排序外排序

内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。

外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

我们这里主要就介绍内排序的多种方法。

对于内排序来说,排序算法的性能主要是受 3 个方面影响:

1.  时间性能

排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标志。在内排序中,主要进行两种操作:比较移动比较指关键字之间的比较,这是要做排序最起码的操作移动指记录从一个位置移动到另一个位置,事实上,移动可以通过改变记录的存储方式来予以避免。总之,高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数

2.  辅助空间

评价排序算法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间

3.  算法的复杂性

注意这里指的是算法本身的复杂度而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能

根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选 择排序和归并排序。可以说,这些都是比较成熟的排序技术,已经被广泛地应用于许 许多多的程序语言或数据库当中,甚至它们都已经封装了关于排序算法的实现代码。 因此,我们学习这些排序算法的目的更多并不是为了去在现实中编程排序算法,而是 通过学习来提高我们编写算法的能力,以便于去解决更多复杂和灵活的应用性问题。

接下来一共要讲解七种排序的算法,按照算法的复杂度分为两大类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。后面将依次讲解。

1.3 排序用到的结构与函数

为了讲清楚排序算法的代码,我先提供一个用于排序用的顺序表结构,此结构也将用于之后我们要讲的所有排序算法。

#define MAXSIZE 10	/* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct 
{ 
	int r[MAXSIZE + 1]; /* 用于存储要排序数组,r[0] 用作哨兵或临时变量 */
	int length;	    /* 用于记录顺序表的长度 */
}SqList;

另外,由于排序最最常用到的操作是数组两元素的交换,我们将它写成函数,在之后的讲解中会大量的用到。

/* 交换 L 中数组 r 的下标为 i 和 j 的值 */ 
void swap(SqList *L, int i, int j)
{
	int temp = L->r[i];
	L->r[i] = L->r[j];
	L->r[j] = temp;
}

好了,说了这么多,我们来看第一个排序算法。

2. 冒泡排序

无论你学习哪种编程语言,在学到循环和数组时,通常都会介绍一种排序算法来作为例子,而这个算法一般就是冒泡排序。并不是它的名称很好听,而是说这个算法的思路最简单,最容易理解。因此,哪怕大家可能都已经学过冒泡排序了,我们还是从这个算法开始我们的排序之旅。

2.1 最简单排序实现

冒泡排序( Bubble Sort )一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

冒泡的实现在细节上可以有很多种变化,我们将分别就 3 种不同的冒泡实现代码,来讲解冒泡排序的思想。这里,我们就先来看看比较容易理解的一段。

/* 对顺序表 L 作交换排序(冒泡排序初级版)*/ 
void BubbleSort0(SqList *L)
{
	int i, j;
	for (i = 1; i<L->length; i++)
	{
		for (j = i + 1; j <= L->length; j++)
		{
			if (L->r[i] > L->r[j])
			{
				swap(L, i, j); /* 交換 L->r[i] 与 L->r[j] 的值 */
			}
		}
	}
}

这段代码严格意义上说,不算是标准的冒泡排序算法,因为它不满足 “两两比较相邻记录” 的冒泡排序思想,它更应该是最最简单的交换排序而已。它的思路就是让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值。如下图所示,假设我们待排序的关键字序列是

,当 i=1 时,91 交换后,在第一位置的 1 与后面的关键字比较都小,因此它就是最小值。当 i=2 时,第二位置先后由 9 换成 5 ,换成 3 ,换成 2 ,完成了第二小的数字交换。后面的数字变换类似,不再介绍。

它应该算是最最容易写出的排序代码了,不过这个简单易懂的代码,却是有缺陷的。观察后发现,在排序好 12 的位置后,对其余关键字的排序没有什么帮助( 数字 3 反而还被换到了最后一位 )。也就是说,这个算法的效率是非常低的。

2.2 冒泡排序算法

我们来看看正宗的冒泡算法,有没有什么改进的地方。

/* 对顺序表 L 作冒泡排序 */ 
void BubbleSort(SqList *L)
{
	int i, j;
	for (i = 1; i<L->length; i++)
	{
		for (j = L->length - 1; j >= i; j--)	/* 注意 j 是从后往前循环 */
		{
			if (L->r[j] > L->r[j + 1])	/* 若前者大于后者(注意这里与上一算法差异)*/
			{
				swap(L, j, j + 1); /* 交换 L->r[j] 与 L->r[j+l] 的值 */
			}
		}
	}
}

依然假设我们待排序的关键字序列是 { 9, 1, 5, 8, 3, 7, 4, 6, 2} ,当 i=1 时,变量 j 8 反向循环到 1 ,逐个比较,将较小值交换到前面,直到最后找到最小值放置在了第 1 的位置。如下图所示,当 i=1、j=8 时,我们发现 6>2 ,因此交换了它们的位置,j=7 时,4>2 ,所以交换 …… 直到 j=2 时,因为 1<2 ,所以不交换。j=1 时,9>1 ,交换, 最终得到最小值 1 放置第一的位置。事实上,在不断循环的过程中,除了将关键字 1 放到第一的位置,我们还将关键字 2 从第九位置提到了第三的位置,显然这一算法比前面的要有进步,在上十万条数据的排序过程中,这种差异会体现出来。图中较小的数字如同气泡般慢慢浮到上面,因此就将此算法命名为冒泡算法

i=2 时,变量 j 8 反向循环到 2 ,逐个比较,在将关键字 2 交换到第二位置的同时,也将关键字 4 3 有所提升。

后面的数字变换很简单,这里就不在详述了。

2.3 冒泡排序优化

这样的冒泡程序是否还可以优化呢?答案是肯定的。试想一下,如果我们待排序的序列是 { 2, 1, 3, 4, 5, 6, 7, 8, 9 } ,也就是说,除了第一和第二的关键字需要交换外,别的都已经是正常的顺序。当 i=1 时,交换了 21 ,此时序列已经有序,但是算法仍然不依不饶地将 i=2 9 以及每个循环中的 j 循环都执行了一遍,尽管并没有交换数据,但是之后的大量比较还是大大地多余了,如下图所示。

i=2 时,我们已经对 9 8 8 7 ,…… ,3 2 作了比较,没有任何数据交换,这就说明此序列已经有序,不需要再继续后面的循环判断工作了。为了实现这个想法,我们需要改进一下代码,增加一个标记变量 flag 来实现这一算法的改进。

/* 对顺序表 L 作改进冒泡算法 */
void BubbleSort2(SqList *L)
{
	int i, j;
	Status flag = TRUE;	/* flag 用来作为标记 */
	for (i = 1; i<L->length && flag; i++) /* 若 flag 为 true 则退出循环 */
	{
		flag = FALSE;	/* 初始为 false */
		for (j = L->length - 1; j >= i; j--)
		{
			if (L->r[j]>L->r[j + 1])
			{
				swap(L, j, j + 1); /* 交换 L->r[j] 与 L->r[j + l] 的值 */
				flag = TRUE;	/* 如果有数据交换,则 flag 为 true */
			}
		}
	}
}

代码改动的关键就是在 i 变量的 for 循环中,增加了对 flag 是否为 true 的判断。 经过这样的改进,冒泡排序在性能上就有了一些提升,可以避免因已经有序的情况下的无意义循环判断。

2.4 冒泡排序复杂度分析

分析一下它的时间复杂度。当最好的情况,也就是要排序的表本身就是有序的, 那么我们比较次数,根据最后改进的代码,可以推断出就是 n-1 次的比较,没有数据交换,时间复杂度为

。当最坏的情况,即待排序表是逆序的情况,此时需要比

次,并作等数量级的记录移动。因此,总的时间复杂度为

3. 简单选择排序

 

4.

 

 

 

 

 

 

 

 

 

 

 

未完待续。。。。。。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!