6 内部排序Word格式.docx
《6 内部排序Word格式.docx》由会员分享,可在线阅读,更多相关《6 内部排序Word格式.docx(21页珍藏版)》请在冰豆网上搜索。
⑤基数排序:
是一种多关键字排序。
3.排序算法的效率
评价排序算法的效率主要有两点:
一是在数据量规模一定的条件下,算法执行所消耗的平均时间,对于排序操作,时间主要消耗在关键字之间的比较和数据元素的移动上,因此我们认为,高效率的排序算法应该是尽可能少的比较次数和尽可能少的数据元素移动次数;
二是执行算法所需要的辅助存储空间,辅助存储空间是指在数据量规模一定的条件下,除了存放待排序数据元素占用的存储空间之外,执行算法所需要的其他存储空间,理想的空间效率是算法执行期间所需要的辅助空间与待排序的数据量无关。
4.待排序序列的参考存储结构
待排序记录序列可以用顺序存储结构和链式存储结构表示。
我们将待排序的记录序列用顺序存储结构表示,即用一维数组实现。
大部分算法,都可按照以下定义的数据类型设定。
#defineMAXSIZE20
typedefintKeyType;
//定义关键字类型为整数类型(也可以为其它类型)
typedefstruct{
KeyTypekey;
//关键字项
InfoTypeotherinfo;
//其它数据项
}RecordType;
RecordTyper[MAXSIZE+1];
//r[0]闲置或者作为哨兵单元
intlength;
//顺序表长度
}SqList;
//顺序表类型
『真题解析』
1.某内排序方法的稳定性是指()。
A.该排序算法不允许有相同的关键字记录
B.该排序算法允许有相同的关键字记录
C.平均时间为0(nlogn)的排序方法
D.以上都不对
【答案】D。
【解析】待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,则该排序方法是稳定的;
若具有相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。
注意:
排序算法的稳定性是针对所有输入实例而言的。
即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。
(二)插入排序
1.直接插入排序
直接插入排序是一种最基本的排序算法,基本操作为:
将一个记录插入到一个已经排好序的有序表中,从而得到一个新的、长度增1的有序表。
一般情况下,第i趟的操作为:
在含有i-1个记录的有序子序列r[1..i-1]中插入一个新记录r[i],变成含有i个记录的有序序列r[1..i]。
设置r[0]为空值,从r[1]开始保存信息,可首先将待插入的记录r[i]复制到r[0]中,如下所示:
r[0]
r[1]
r[2]
r[3]
...
r[i-1]
r[i]
可把r[0]看成是r[i]的备份,以后从r[1]~r[i-1]查找插入位置时可直接同r[0]比较,而且r[i]也可被覆盖了。
因为r[i]复制到r[0]后,可认为已经空出了r[i]。
考虑从后向前比较,只要r[i-1]≤r[i],则r[i]的位置不必改变,否则(即r[i-1]>
r[i]),则将r[i-1]移动到r[i]处,然后再比较r[i-2]和r[0],依次等等。
当最后找到一个比r[0]小的关键字时,将r[0]复制到此关键字的后面一个位置,结束。
具体算法为:
voidInsertSort(SqList&
L)
{for(i=2;
i<
=L.length;
++i)//第一个元素认为本身有序,所以从第二个元素开始即可
if(L.r[i].key<
L.r[i-1].key)//当r[i]小于r[i-1]时,需排序,否则无需排序
{L.r[0]=L.r[i];
//复制L.r[i]至L.r[0],保证下面的for循环肯定能正常结束
for(j=i-1;
L.r[0].key<
L.r[j].key;
--j)
L.r[j+1]=L.r[j];
//记录后移
L.r[j+1]=L.r[0];
//插入到第一个小于L.r[0]的元素L.r[j]的后面
}
举例:
排序以下序列4938659776132749
过程:
(49)38659776132749
(38)(3849)659776132749
(65)(384965)9776132749
(97)(38496597)76132749
(76)(3849657697)132749
(13)(133849657697)2749
(27)(13273849657697)49
(49)(1327384949657697)
该算法的时间复杂度是O(n2),属稳定排序。
直接插入排序算法简单、容易实现。
当待排序记录较少时,排序速度较快,但是,当待排序的记录数量较大时,大量的比较和移动操作将使直接插入排序算法的效率降低;
然而,当待排序的数据元素基本有序时,直接插入排序过程中的移动次数大大减少,从而效率会有所提高。
2.折半插入排序
由于直接插入排序的基本操作是向一个有序表中进行查找和插入,因此,其中的“查找”操作可以利用“折半查找”来实现。
由此改进的直接插入排序称为“折半插入排序”。
由于查找是找不到需插入的记录的,因此有一个固定的结论:
待插入位置是查找之后high指示的坐标之后的位置,即high+1,则从high+1到i-1的记录需向后整体平移一个单位,变成high+2~i。
算法入下:
voidBInsertSort(SqList&
L)
i<
=L.length;
++i)
{L.r[0]=L.r[i];
low=1;
high=i-1;
while(low<
=high)
{mid=(low+high)/2;
if(L.r[0].key<
L.r[mid].key)high=mid-1;
elselow=mid+1;
}//查找合适的插入位置
for(j=i-1;
j>
=high+1;
j--)
L.r[high+1]=L.r[0];
//high之后的第一个位置就是合适的位置
该算法的时间复杂度是O(n2),属稳定排序。
折半插入排序比直接插入排序明显地减少了关键字间的“比较”次数,但记录“移动”的次数不变。
(三)起泡排序
1.基本思想
起泡排序是交换排序中一种简单的排序方法。
它的基本思想是对所有相邻记录的关键字值进行比效,如果是逆序(a[j]>
a[j+1]),则将其交换,最终达到有序化。
其处理过程为:
1>
将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记录。
2>
对无序区从前向后依次将相邻记录的关键字进行比较,若逆序则将其交换,从而使得关键字值小的记录向上“飘浮”(左移),关键字值大的记录好像石块,向下“堕落”(右移)。
每经过一趟冒泡排序,都使无序区中关键字值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可以将这n个记录重新按关键字顺序排列。
2.起泡排序算法
1)原始的冒泡排序算法:
对由n个记录组成的记录序列,最多经过(n-1)趟冒泡排序,就可以使记录序列成为有序序列,第一趟定位第n个记录,此时有序区只有一个记录;
第二趟定位第n-1个记录,此时有序区有两个记录;
以此类推,算法框架为:
(假设用数组a存储待排序记录)
voidBubbleSort1(DataTypea,intn)
{
for(i=n;
i>
1;
i--)
{
for(j=1;
j<
=i-1;
j++)
if(a[j].key>
a.[j+1].key)
temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
2)改进的冒泡排序算法:
在冒泡排序过程中,一旦发现某一趟没有进行交换操作,就表明此时待排序记录序列已经成为有序序列,冒泡排序再进行下去已经没有必要,应立即结束排序过程。
voidBubbleSort2(DataTypea,intn)
{
for(i=n;
1;
exchange=0;
if(a[j].key>
{temp=a[j];
exchange=1;
if(exchange==0)break;
3)进一步地改进冒泡排序算法:
在算法2)给出的冒泡排序算法的基础上,如果我们同时记录第i趟冒泡排序中最后一次发生交换操作的位置m(m<
=n-i),就会发现从此位置以后的记录均已经有序,即无序区范围缩小在a[1]~a[m]之间,所以在进行下一趟排序操作时,就不必考虑a[m+1]~a[n]范围内的记录了,而只在a[1]~a[m]范围内进行。
voidBubbleSort3(DataTypea,intn)
last=n-1;
i--)
{exchange=0;
m=last;
//初始将最后进行记录交换的位置设置成i-1
=m;
last=j;
//记录每一次发生记录交换的位置
if(exchange==0)break;
冒泡排序比较简单,当初始序列基本有序时,冒泡排序有较高的效率,反之效率较低;
其次冒泡排序只需要一个记录的辅助空间,用来作为记录交换的中间暂存单元;
冒泡排序是一种稳定的排序方法。
利用冒泡法对以下序列进行排序。
(49,38,65,97,76,13,27,49)n=8
第1趟:
3849657613274997
第2趟:
3849651327497697
第3趟:
3849132749657697
第4趟:
3813274949657697
第5趟:
1327384949657697
第6趟:
1327384949657697
第7趟:
没有必要再比了,因为上一趟(第6趟)没有发生交换,这说明已经排序完毕。
该算法的时间复杂度为O(n2),属于稳定排序。
1.在执行某个排序算法过程中,出现了排序关键字朝着最终排序序列相反的方向的移动,从而认为该算法是不稳定的。
这种说法对么?
为什么?
【解析】这种看法不对。
本题的叙述与稳定性的定义无关,不能据此来判断排序中的稳定性。
例如5,4,1,2,3在第一趟冒泡排序后得到4,1,2,3,5。
其中4向前移动,与其最终位置相反。
但冒泡排序是稳定排序。
快速排序中无这种现象。
(四)简单选择排序
简单选择排序的基本思想是:
每一趟在n-i+1(i=1,2,3,...,n-1)个记录中选取关键字最小的记录作为有序序列中的第i个记录。
它的具体实现过程为:
将整个记录序列划分为有序区域和无序区域,有序区域位于最左端,无序区域位于右端,初始状态有序区域为空,无序区域含有待排序的所有n个记录。
设置一个整型变量index,用于记录在一趟的比较过程中,当前关键字值最小的记录位置。
开始将它设定为当前无序区域的第一个位置,即假设这个位置的关键字最小,然后用它与无序区域中其他记录进行比较,若发现有比它的关键字还小的记录,就将index改为这个新的最小记录位置,随后再用a[index].key与后面的记录进行比较,并根据比较结果,随时修改index的值,一趟结束后index中保留的就是本趟选择的关键字最小的记录位置。
3>
将index位置的记录交换到无序区域的第一个位置,使得有序区域扩展了一个记录,而无序区域减少了一个记录。
不断重复2>
、3>
,直到无序区域剩下一个记录为止。
此时所有的记录已经按关键字从小到大的顺序排列就位。
2.简单选择排序算法
算法如下:
voidselecsort(DataTypea,intn)
for(i=1;
i<
n;
i++)//对n个记录进行n-1趟的简单选择排序
index=i;
//初始化第i趟简单选择排序的最小记录指针
for(j=i+1;
=n;
j++)//搜索关键字最小的记录位置
if(a[j].key<
a[i].key)index=j;
if(index!
=i)
{temp=a[i];
a[i]=a[index];
a[index]=temp;
}
简单选择排序算法简单,但是速度较慢,并且是一种不稳定的排序方法,但在排序过程中只需要一个用来交换记录的暂存单元。
其时间复杂度为O(n2)。
利用简单选择排序算法对以下序列排序:
(72,73,71,23,94,16,5,68)
过程:
第一趟:
5,73,71,23,94,16,72,68
第二趟:
5,16,71,23,94,73,72,68
第三趟:
5,16,23,71,94,73,72,68
第四趟:
5,16,23,68,94,73,72,71
第五趟:
5,16,23,68,71,73,72,94
第六趟:
5,16,23,68,71,72,73,94
第七趟:
5,16,23,68,71,72,73,94完成
(五)希尔排序
希尔排序又称“缩小增量排序”,是一种改进的插入排序,在时间效率上有了较大的改进。
对直接插入排序来说,比较的步长为1。
这种情况下,如果较小的数在序列的较后面部分,则需要一步一步的向前移动,无疑是比较慢的。
如果采用步长>
1的方法,则可以使较小的数向前推进是“跳跃式”进行,故可以提高排序效率。
方法:
将整个序列分成若干子序列,对各个子序列进行直接插入排序,得到一趟希尔排序序列;
然后缩短步长,重复以上动作,直到步长为1。
具体步骤如下:
①先取一正整数d(d<
n,一般可取d=n/2),把所有距离为d的倍数的记录编在一组,组成一个子序列,这样将整个待排序序列分成若干组;
②在各个子序列中进行直接插入排序;
③取一个新的d(比原来的要小,一般取原来的1/2),重复执行1),2),直到d=1为止(此时,整个序列变成直接插入排序)。
例如:
利用希尔排序对下序列进行排序。
(72,73,71,23,94,16,15,68),n=8
第一次分组,取d=n/2=4,则对序列分组为:
对每个子序列直接插入排序得(4组):
7216152394737168
第二次分组,取d=2(原来的一半),对序列分组为:
对每个子序列直接插入排序得(2组):
1516712372689473
3>
第三次分组,取d=1(原来的一半),序列为一组,直接插入排序得:
1516236871727394
排序完成。
步长的选择是迄今为止还没有完全解决的。
该算法的时间复杂度O(n1.5),是不稳定排序(因为存在着跳跃)。
1.给出一组关键字T=(12,2,16,30,8,28,4,10,20,6,18),写出用下列算法从小到大排序时第一趟结束时的序列;
(1)希尔排序(第一趟排序的增量为5)
(2)快速排序(选第一个记录为枢轴(分隔))
(3)链式基数排序(基数为10)
【解析】
(1)一趟希尔排序:
12,2,10,20,6,18,4,16,30,8,28(D=5)
(2)一趟快速排序:
6,2,10,4,8,12,28,30,20,16,18
(3)链式基数排序LSD[0][1][2][3][4][5][6][7][8][9]
↓↓↓↓↓
分配30124168
↓↓↓↓
102628
↓↓
2018
收集:
→30→10→20→12→2→4→16→6→8→28→18
(六)快速排序
快速排序是对冒泡排序的一种改进,其基本思想是:
通过一趟排序将待排序的记录分成独立的两部分,其中一部分记录的关键字比另一部分的关键字小。
然后对这两部分再继续排序,一直达到整个序列有序。
设待排序序列为{L.r[1],L.r[2],...,L.r[n]},首先任意选取一个记录(通常选择第一个记录L.r[1])作为支点(pivot),然后按照下述原则排列其余记录:
将所有关键字比L.r[1].key小的记录都安排在它的位置之前,将所有关键字比L.r[1].key大的记录都安排在它的位置之后。
可以发现,在安置的过程中,L.r[1]的确切位置将被最终确定。
设该支点(pivot)最后确定的位置为i,则将序列分割为左右两部分。
这个过程称为一趟快速排序。
2.具体步骤
设待排序序列用数组e[low..high]保存。
设置两个指针low和high,分别指向数组的开始位置和终止位置。
设支点记录为e[low],并将之暂存于t。
首先,从high的位置向前搜索,找到第一个小于t的记录,将这个记录和e[low]的值交换;
然后,从low所指向的位置向后搜索,找到第一个值大于t的记录,将这个记录和e[high]的值交换。
重复以上步骤,直到low==high。
完成一趟排序,low(或者high)指向的位置就是第一个元素的确切位置。
(从两边向中间“夹挤”)。
第一趟完成后,确定了第一个元素的确切位置,同时生成了两个子序列,然后再对这两个序列使用同样的办法,最终实现整个序列的有序。
利用快速排序法对以下序列进行排序:
(49,38,65,97,76,13,27,49)
过程如下:
初始状态:
high向左移动(high--),直到找到小于t(49)的关键字27,将27的值赋给e[low],如下:
接着low开始向右移动(low++),直到找到大于t(49)的关键字65,将65的值赋给e[high],如下:
high继续左移(high--),直到一个小于t的数13,将之赋给e[low],如下:
low继续右移(low++),直到找到大于t(49)的关键字97,将之赋给e[high],如下:
high继续左移(high--),没有找到比t(49)还小的数,但是由于出现了high==low的情况,结束左移。
如下:
此时low(或者high)指向的位置就是第一个元素的确定位置:
e[low]=t;
经过以上一趟快速排序,可将原序列分为了两个序列,同时确定关键字49的确切位置,如下:
{27,38,13}49{76,97,65,49}
下面再分别对两个子需类进行快速排序,得最终结果:
{13}27{38}49{49,65}76{97}
49{65}
则最终得有序序列:
(13,27,38,49,49,6576,97)
在一趟排序中,支点最后才确定位置,故只需最后一步赋值即可,中间不必交换。
即,虽然快速排序是交换排序的一种,但是可以不用交换操作即可实现)
该算法由于有不相邻元素交换,故是不稳定排序,其时间复杂度为O(nlog2n),是内排序中最好的方法之一。
1.我们知道,对于n个元素组成的线性表进行快速排序时,所需进行的比较次数与这n个元素的初始排序有关。
问:
(1)当n=7时,在最好情况下需进行多少次比较?
请说明理由。
(2)当n=7时,给出一个最好情况的初始排序的实例。
(3)当n=7时,在最坏情况下需进行多少次比较?
(4)当n=7时,给出一个最坏情况的初始排序的实例。
(1)在最好情况下,假设每次划分能得到两个长度相等的子文件,文件的长度n=2k-1,那么第一遍划分得到两个长度均为n/2的子文件,第二遍划分得到4个长度均为n/4的子文件,以此类推,总共进行k=log2(n+1)遍划分,各子文件的长度均为1,排序完毕。
当n=7时,k=3,在最好情况下,第一遍需比较6次,第二遍分别对两个子文件(长度均为3,k=2)进行排序,各需2次,共10次即可。
(2)在最好情况下快速排序的原始序列实例:
4,1,3,2,6,5,7。
(3)在最坏情况下,若每次用来划