排序与查找.docx
《排序与查找.docx》由会员分享,可在线阅读,更多相关《排序与查找.docx(16页珍藏版)》请在冰豆网上搜索。
![排序与查找.docx](https://file1.bdocx.com/fileroot1/2022-12/7/7c8e362e-5a50-4b62-8ca3-6e18e826549e/7c8e362e-5a50-4b62-8ca3-6e18e826549e1.gif)
排序与查找
第3章排序与查找
在计算机科学中,排序(sorting)是研究得最多的问题之一,许多书籍都深入讨论了这个问题。
本章仅仅是一个介绍,重点放在C语言的实际应用上。
排序
程序员可以使用的基本排序算法有5种:
·插入排序(insertionsort.)
·交换排序(exchangesOrt)
·选择排序(selectionsort)
·归并排序(mergesort)
·分布排序(distributionsort)
为了形象地解释每种排序算法是怎样工作的,让我们来看一看怎样用这些方法对桌上一付乱序的牌进行排序。
牌既要按花色排序(依次为梅花、方块、红桃和黑心),还要按点数排序(从2到A)。
插入排序的过程为:
从一堆牌的上面开始拿牌,每次拿一张牌,按排序原则把牌放到手中正确的位置。
桌上的牌拿完后,手中的牌也就排好序了。
交换排序的过程为:
(1)先拿两张牌放到手中。
如果左边的牌要排在右边的牌的后面,就交换这两张牌的位置。
(2)然后拿下一张牌,并比较最右边两张牌,如果有必要就交换这两张牌的位置。
(3)重复第
(2)步,直到把所有的牌都拿到手中。
(4)如果不再需要交换手中任何两张牌的位置,就说明牌已经排好序了;否则,把手中的牌放到桌上,重复
(1)至(4)步,直到手中的牌排好序。
选择排序的过程为:
在桌上的牌中找出最小的一张牌,拿在手中;重复这种操作,直到把所有牌都拿在手中。
归并排序的过程为:
把桌上的牌分为52堆,每堆为一张牌。
因为每堆牌都是有序的(记住,此时每堆中只有一张牌),所以如果把相邻的两堆牌合并为一堆,并对每堆牌进行排序,就可以得到26堆已排好序的牌,此时每一堆中有两张牌。
重复这种合并操作,就可以依次得到13堆牌(每一堆中有4张牌),7堆牌(有6堆是8张牌,还有一堆是4张牌),最后将得到52张的一堆牌。
分布排序(也被称作radixsort,即基数排序)的过程为:
先将牌按点数分成13堆,然后将这13堆牌按点数顺序叠在一起;再将牌按花色分成4堆,然后将这4堆牌按花色顺序叠在一起,牌就排好序了。
在选用排序算法时,你还需要了解以下几个术语:
(1)自然的(natural)
如果某种排序算法对有序的数据排序速度较快(工作量变小),对无序的数据排序速度却较慢(工作变量大),我们就称这种排序算法是自然的。
如果数据已接近有序,就需要考虑选用自然的排序算法。
(2)稳定的(stable)
如果某种排序算法能保持它认为相等的数据的前后顺序,我们就称这种排序算法是稳定的。
例如,现有以下名单:
MaryJones
MarySmith
TomJones
SusieQueue
如果用稳定的排序算法按姓对上述名单进行排序,那么在排好序后"MaryJones”和"TomJones”将保持原来的Jr顺序,因为它们的姓是相同的。
稳定的排序算法可按主、次关键字对数据进行排序,例如按姓和名排序(换句话说,主要按姓排序,但对姓相同的数据还要按名排序)。
在具体实现时,就是先按次关键字排序,再按主关键字排序。
(3)内部排序(internalsort)和外部排序(externalsort)
待排数据全部在内存中的排序方法被称为内部排序,待排数据在磁盘、磁带和其它外存中的排序方法被称为外部排序。
查找
和排序算法一样,查找(searching)算法也是计算机科学中研究得最多的问题之一。
查找算法和排序算法是有联系的,因为许多查找算法依赖于要查找的数据集的有序程度。
基本的查找算法有以下4种:
·顺序查找(sequentialsearching)。
·比较查找(comparisonsearching)
·基数查找(radixsearching)
·哈希查找(hashing)
下面仍然以一付乱序的牌为例来描述这些算法的工作过程。
顺序查找的过程为:
从第一张开始查看每一张牌,直到找到要找的牌。
比较查找(也被称作binarysearching,即折半查找)要求牌已经排好序,其过程为:
任意抽一张牌,如果这张牌正是要找的牌,则查找过程结束。
如果抽出的这张牌比要找的牌大,则在它前面的牌中重复查找操作;反之,则在它后面的牌中重复查找操作,直到找到要找的牌。
基数查找的过程为:
先将牌按点数分成13堆,或者按花色分成4堆。
然后找出与要找的牌的点数或花色相同的那一堆牌,再在这堆牌中用任意一种查找算法找到要找的牌。
哈希查找的过程为:
(1)在桌面上留出可以放若干堆牌的空间,并构造一个函数,使其能根据点数和花色将牌映射到特定的堆中(这个函数被称为hashfunction,即哈希函数)。
(2)根据哈希函数将牌分成若干堆。
(3)根据哈希函数找到要找的牌所在的堆,然后在这一堆牌中找到要找的牌。
例如,可以构造这样一个哈希函数:
pile=rank+suit
其中,rank是表示牌的点数的一个数值;suit是表示牌的花色的一个数值;pile表示堆值,它将决定一张牌归入到哪一堆中。
如果用1,2,……,13分别表示A,2,…….K,用0,1,2和3分别表示梅花、方块、红桃和黑桃,则pile的值将为1,2,……,16,这样就可以把一付牌分成16堆。
哈希查找虽然看上去有些离谱,但它确实是一种非常实用的查找算法。
各种各样的程序,从压缩程序(如Stacker)到磁盘高速缓存程序(如SmartDrive),几乎都通过这种方法来提高查找速度,
排序或查找的性能
有关排序和查找的一个主要问题就是速度。
这个问题经常被人们忽视,因为与程序的其余部分相比,排序或查找所花费的时间几乎可以被忽略。
然而,对大多数排序或查找应用来说,你不必一开始就花很多精力去编制一段算法程序,而应该先在现成的算法中选用一种最简单的(见3.1和3.4),当你发现所用的算法使程序运行很慢时,再换用一种更好的算法(请参见下文中的介绍)。
下面介绍一种判断排序或查找算法的速度的方法。
首先,引入一个算法的复杂度的概念,它指的是在各种情况(最好的、最差的和平均的)下排序或查找需要完成的操作次数,通过它可以比较不同算法的性能。
算法的复杂度与排序或查找所针对的数据集的数据量有关,因此,引入一个基于数据集数据量的表达式来表示算法的复杂度。
最快的算法的复杂度O
(1),它表示算法的操作次数与数据量无关。
复杂度O(N)(N表示数据集的数据量)表示算法的操作次数与数据量直接相关。
复杂度O(logN)介于上述两者之间,它表示算法的操作次数与数据量的对数有关。
复杂度为O(NlogN)(N乘以logN)的算法比复杂度为O(N)的算法要慢,而复杂度为O(N2)的算法更慢。
注意:
如果两种算法的复杂度都是O(logN),那么logN的基数较大的算法的速度要快些,在本章的例子中,logN的基数均为10。
表3.1 本章所有算法的复杂度
-----------------------------------------------------------------
算 法 最好情况 平均情况 最坏情况
-----------------------------------------------------------------
快速排序 O(NlogN) O(NlogN) O(N2)
归并排序 O(N) O(NlogN) O(NlogN)
基数排序 O(N) O(N) O(N)
线性查找 O(N)
折半查找 O(NlogN)
哈希查找 O(N/M)*
健树查找 O
(1)**
-----------------------------------------------------------------
*M是哈希表项的数目
**实际上相当于有232个哈希表项的哈希查找
表3.1列出了本章所有算法的复杂度。
对于排序算法,表中给出了最好的、平均的和最差的情况下的复杂度,平均情况是指数据随机排列的情况;排序算法的复杂度视数据的初始排列情况而定,它一般介于最好的和最差的两种情况之间。
对于查找算法,表中只给出了平均情况下的复杂度,在最好的情况(即要找的数据恰好在第一次查找的位置)下,查找算法的复杂度显然是O
(1);在最坏的情况(即要找的数据不在数据集中)下,查找算法的复杂度通常与平均情况下的复杂度相同。
需要注意的是,算法的复杂度只表示当N值变大时算法的速度变慢的程度,它并不表示算法应用于给定大小的数据集时的实际速度。
算法的实际速度与多种因素有关,包括数据集的数据类型以及所用的编程语言、编译程序和计算机等。
换句话说,与复杂度高的算法相比,复杂度低的算法并不具备绝对的优越性。
实际上,算法的复杂度的真正意义在于,当N值大于某一数值后,复杂度低的算法就会明显比复杂度高的算法快。
为了说明算法的复杂度和算法的实际执行时间之间的关系,表3.2列出了本章所有例子程序的执行时间。
本章所有例子程序均在一台以Linux为操作系统的90MHz奔腾计算机上由GNUC编译程序编译,在其它操作系统中,这些例子程序的执行时间与表3.2所列的时间是成比例的。
表3.2 本章所有例子程序的执行时间
---------------------------------------------------------------------------
例子程序 算 法 2000 4000 6000 8000 10000
---------------------------------------------------------------------------
例3.1 qsort() 0.02 0.05 0.07 0.11 0,13
例3.2a 快速排序 0.02 0.07 0.13 0.18 0.20
例3.2b 归并排序 0.03 0.08 0.14 0.18 0.26
例3.2c 基数排序 0.07 0.15 0.23 0.30 0.39
例3.4 bsearch() 0.37 0.39 0.39 0.40 0.41
例3.5 折半查找 0.32 0.34 0.34 0.36 0.36
例3.6 线性查找 9.67 20.68 28.71 36.31 45.51
例3.7 键树查找 0.27 0.28 0.29 0.29 0.30
例3.8 哈希查找 0.25 0.26 0.28 0.29 0.28
---------------------------------------------------------------------------
注意:
(1)表中所列的时间以秒为单位。
(2)表中所列的时间经过统一处理,只包括排序或查找所花费的时间。
(3)2000等数值表示数据集的数据量。
(4)数据集中的数据是从文件/usr/man/manl/gcc.1(GNUC编译程序中的一个文件)中随机提取的词。
(5)在查找算法中,要查找的数据是从文件/usr/man/manl/g++.1(GNUC++编译程序中的一个文件)中随机提取的词。
(6)函数qsort()和bseareh()分别是C标准库函数中用于快速排序算法和折半查找算法的函数,其余例子程序是专门为本章编写的。
在阅读完以上内容后,你应该能初步体会到如何根据不同的情况来选用一种合适的排序或查找算法。
在DonaldE.Knuth所著的《TheArtOfComputerProgramming,Volume3,SortingandSearching》一书中,作者对排序和查找算法进行了全面的介绍,在该书中你将读到更多关于复杂度和复杂度理论的内容,并且能见到比本章中所提到的更多的算法。
公用代码
本章中的许多例子程序是可以直接编译运行的。
在这些例子程序中,许多代码是相同的,这些相同的代码将统一在本章的末尾列出。
3.1 哪一种排序方法最方便?
答案是C标准库函数qsort(),理由有以下三点:
(1)该函数是现成的;
(2)该函数是已通过调试的;
(3)该函数通常是已充分优化过的。
qsort()函数通常使用快速排序算法,该算法是由C.A.R.Hoare于1962年提出的。
以下是qsort()函数的原型:
voidqsort(void*buf,size_thum,size_tsize,
int(*comp)(constvoid*ele1,constvoid*ele2));
qsort()函数通过一个指针指向一个数组(buf),该数组的元素为用户定义的数据,数组的元素个数为num,每个元素的字节长度都为size。
数组元素的排序是通过调用指针comp所指向的一个函数来实现的,该函数对数组中由ele1和ele2所指向的两个元素进行比较,并根据前者是小于、等于或大于后者而返回一个小于、等于或大于0的值。
例3.1中给出了一个函数sortStrings(),该函数就是通过qsort()函数对一个以NULL指针结束的字符串数组进行排序的。
将例3.1所示的代码和本章结尾的有关代码一起编译成一个可执行程序后,就能按字母顺序对一个以NULL指针结束的字符串数组进行排序了。
1:
#include
2:
3:
/*
4:
*ThisroutineisusedonlybysortStrings(),toprovidea
5:
*stringcomparison{unctiontopasstoqsort().
6:
*/
7:
staticintcomp(constvoid*elel,constvoid*ele2)
8:
{
9:
returnstrcmp(*(constchar**)ele1,
10:
*(constchar**)ele2);
11:
}
12:
13:
/*Sortstringsusingthelibraryfunctionqsort()*/
14:
voidsortStrings(constchar*array[-'])
15:
{
16, /*First,determinethelengthofthearray*/
17:
intnum;
18:
19:
for(num=O;array[num];num++)
20:
21:
qsort(array,num,sizeof(*array),comp);
22:
}
在例3.1中,第19行和第20行的for循环语句用来计算传递给qsort()函数的数组元素个数,函数comp()的作用是将函数qsort()传递给它的类型(constvoid*)转换为函数strcmp()
所要求的类型(constchar*)。
因为在函数qsort()中,ele1和ele2是指向数组元素的指针,而在例3.1中这些数组元素本身也是指针,因此,应该先将ele1和ele2转换为constchar**类型,然后在转换结果前加上指针运算符“*”,才能得到函数strcmp()所要求的类型。
尽管有qsort()函数,但程序员经常还要自己编写排序算法程序,其原因有这样几点:
第一,在有些异常情况下,qsort()函数的运行速度很慢,而其它算法程序可能会快得多;第二,qsort()函数是为通用的目的编写的,这给它带来了一些不利之处,例如每次比较时都要通过用户提供的一个函数指针间接调用一个函数;第三,由于数组元素的长度在程序运行时才能确定下来,因此用来在数组中移动数组元素的那部分代码没有针对数组元素长度相同的情况进行优化;第四,qsort()函数要求所有数据都在同一个数组中,而在实际应用中,数据的长度和性质千变万化,可能很难甚至无法满足这一要求;第五,qsort()函数通常不是一种稳定的排序方法。
请参见:
3.2哪一种排序方法最快?
3.3当要排序的数据集因太大而无法全部装入内存时,应怎样排序?
3.7怎样对链表进行排序?
7.1什么是间接引用(indirection)?
7,2最多可以使用几层指针?
7.5什么是void指针?
·
7.6什么时候使用void指针?
3.2哪一种排序方法最快?
首先,对大多数包含排序应用的程序来说,排序算法的速度并不重要,因为在程序中排序 的工作量并不是很多,或者,与排序相比,程序中其它操作所花费的时间要多得多。
实际上,没有哪一种排序算法永远是最快的,在运行程序的软硬件环境相同的情况下,不同排序算法的速度还与数据的长度、性质以及数据的初始顺序有关。
在笔者的“工具箱”中,有三种算法在不同的情况下都是最快、最有用的,这三种算法分别是快速排序、归并排序和基数排序。
快速排序
快速排序是一种分割处理式的排序算法,它将一个复杂的排序问题分解为若干较容易处理的排序问题,然后逐一解决。
在快速排序算法中,首先要从数据集的数据中选择一个数据作为分割值,然后将数据分成以下3个子集:
(1)将大于分割值的数据移到分割值前面,组成子集1;
(2)分割值本身为子集2;
(3)将小于分割值的数据移到分割值后面,组成子集3。
等于分割值的数据可以放在任意一个子集中,这对快速排序算法没有任何影响。
由于子集2已经是有序的,所以此后只需对子集1和子集3进行快速排序。
需要注意的是,当数据集很小时,无法进行快速排序,而要使用其它排序算法。
显然,当数据集中的数据只有两个或更少时,就不可能将数据集再分割成三个子集。
实际上,当数据集比
较小时,程序员就应该考虑是否仍然采用快速排序算法,因为在这种情况下另外一些排序算法往往更快。
例3.2a用快速排序算法重写了例3.1中的字符串数组排序程序,你同样可以将它和本章末尾的有关代码一起编译成一个可执行程序。
程序中定义了一个宏,它可使程序更易读,并能加快执行速度。
快速排序算法是由程序中的myQsort()函数实现的,它是按升序对一个字符串数组进行排序的。
函数myQsort()的具体工作过程如下:
(1)首先检查最简单的情况。
在第17行,检查数组中是否没有或只有一个元素——在这种情况下,数组已经是有序的,函数就可以返回了。
在第19行,检查数组中是否只有两个元素——在这种情况下,要么数组已经是按升序排列的,要么交换这两个元素的位置,使它们按升序排列。
(2)在第28行至第53行,将数组分割为两个子集:
第一个子集中的数据大于或等于分割值,第二个子集中的数据小于分割值。
在第28行,选择数组中间的元素作为分割值,并将其和数组中的第一个元素交换位置。
在第37行至第39行,在数组中找到属于第二个子集的第一个元素;在第45行至第47行,在数组中找到属于第一个子集的最后一个元素。
在第49行,检查属于第二个子集的第一个元素是否位于属于第一个子集的最后一个元素的后面,如果是,则第一个子集的所有元素都已在第二个子集的所有元素的前面,数据已经划分好了;否则,交换这两个元素的位置,然后重复上述这种检查。
(3)当两个子集分割完毕后,在第55行,将分割值和第一个子集中的最后一个元素交换位置,排序结束时这个分割值将仍然排在现在这个位置。
在第57行和第58行,分别调用myQsort()函数对分割所得的子集进行排序。
当所有的子集都经过排序后,整个数组也就排好序了。
例3.2a一个不使用qsort()函数的快速排序算法程序
1:
#include
2:
3:
#defineexchange(A,B,T) ((T)=(A),(A)=(B),(B)=(T))
4:
5:
6:
/*Sortsanarrayofstringsusingquicksortalgorithm*/
7:
staticvoidmyQsort(constchar*array[],size_tnum)
8:
{
9:
constchar*temp
10:
size_ti,j;
11:
12:
/*
13:
*Checkthesimplecasesfirst:
14:
*Iffewerthan2elements,alreadysorted
15:
*Ifexactly2elements,justswapthem(ifneeded).
16:
*/
17:
if(num<2)
18:
return;
19:
elseif(num==2)
20:
{
21:
if(strcmp(array[O],array[1])>O)
22:
exchange(array[0],array[1],temp)
23:
}
24:
/*
25:
*Partitionthearrayusingthemiddle(num/2)
26:
elementasthedividingelement.
27:
*/
28:
exchange(array[0]