1.31k likes | 1.42k Views
第 9 章 排序. 9.1 概述. 9.2 插入排序. 9.3 交换排序. 9.4 选择排序. 9.5 归并排序. 9.6 基数排序. 9.7 各种排序方法的综合比较. 9.8 外排序简介. 9.1 概述. 9.1.1 什么是排序. 9.1.2 内部排序和外部排序. 9.1.3 内部排序的方法. 9.1.1 什么是排序. 排序 (Sorting) 是计算机程序设计中的一种重要操作,其目的是将一组 “ 无序 ” 的记录序列调整为 “ 有序 ” 的记录序列。.
E N D
9.1 概述 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 归并排序 9.6 基数排序 9.7 各种排序方法的综合比较 9.8 外排序简介
9.1概述 9.1.1什么是排序 9.1.2内部排序和外部排序 9.1.3内部排序的方法
9.1.1什么是排序 排序(Sorting)是计算机程序设计中的一种重要操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。 例如:将下列关键字序列52,49,80,36,14,58,61,23,97,75,调整为14,23,36,49,52,58,61,75,80,97。
一般情况下, 假设含n个记录的序列为{ R1, R2, …, Rn } 其相应的关键字序列为 { K1, K2, …,Kn } 这些关键字相互之间可以进行比较,即在 它们之间存在着这样一个关系 Kp1≤Kp2≤…≤Kpn 按此固有关系将上式记录序列重新排列为 { Rp1, Rp2,…,Rpn } 的操作称作排序。
关键字Ki可以是记录的主关键字,也可以是次关键字。若Ki是主关键字,则任何一个记录的无序序列经排序后得到的结果是惟一的;若Ki是次关键字,则排序的结果不惟一。关键字Ki可以是记录的主关键字,也可以是次关键字。若Ki是主关键字,则任何一个记录的无序序列经排序后得到的结果是惟一的;若Ki是次关键字,则排序的结果不惟一。 假设Ki=Kj(1≤i≤j≤n),即排序前记录Ri领先于Rj。若排序后的序列中Ri仍领先于Rj,则称所用的排序方法是稳定的;反之,若排序后的序列中Rj领先于Ri,则称所用的排序方法是不稳定的。
9.1.2内部排序和外部排序 内部排序,是指待排序列完全存放在内存中进行的排序过程,适合不太大的元素序列。 外部排序,是指排序过程中还需要访问外存,对于待排序列记录数量很大,因不能完全放入内存,排序过程中数据在内存和外存之间需要多次移动。
9.1.3内部排序的方法 内部排序的过程是一个逐步扩大 记录的有序序列长度的过程。 有序序列区 无 序 序 列 区 经过一趟排序 有序序列区 无 序 序 列 区
基于不同的“扩大”有序序列长度的方法,内部排序方法大致可分下列几种类型:基于不同的“扩大”有序序列长度的方法,内部排序方法大致可分下列几种类型: 插入类 交换类 选择类 归并类 基数排序
(1)插入类 将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。 Return
(2)交换类 通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。 Return
(3)选择类 从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。 Return
(4)归并类 通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。 Return
(5)基数排序 通过借助于多关键字排序的思想对单逻辑关键字进行排序。 Return
内排序过程有两种基本操作:比较两个关键字的大小;将记录从一个位置移到另一个位置。第一种操作对大多数排序方法都是必要的,第二种操作则要看记录序列的存储方式。内排序过程有两种基本操作:比较两个关键字的大小;将记录从一个位置移到另一个位置。第一种操作对大多数排序方法都是必要的,第二种操作则要看记录序列的存储方式。 (1)若待排序记录存放在一组连续的存储地址上,类似于线性表的顺序存储结构,即记录之间的次序关系是由其存储时的相对位置决定的,那么排序过程中记录的移动显然是必须的。
(2)若待排序记录是采用链式存储结构,则记录之间的次序关系是由链表指针指示的,那么排序过程中仅需修改指针,而不需要记录的移动。
(3)若待排序记录存放在一组连续的存储地址上,同时另外增加一组存储记录地址的向量,使用这些地址向量来指示记录在序列中的位置,那么排序过程中只需要调整地址向量中这些记录的地址,而不必进行记录的移动。在排序结束后再按照地址向量中的值调整记录序列。
评价排序算法优劣的标准主要有两条:一是算法的运算量,这主要通过记录关键字的比较次数和移动记录的次数来反映;另一个是执行算法所需要的辅助存储单元空间。另外,算法本身的稳定性也是要考虑的一个因素。
#define MAXSIZE 1000// 待排顺序表最大长度 待排记录的数据类型定义如下: typedef int KeyType;// 关键字类型为整数类型 typedef struct { KeyType key; // 关键字项 InfoType otherinfo; // 其它数据项 } RcdType; // 记录类型 typedef struct { RcdType r[MAXSIZE+1]; // r[0]闲置 int length; // 顺序表长度 } SqList; // 顺序表类型
9.2插入排序 一趟直接插入排序的基本思想: 有序序列R[1..i-1] 无序序列 R[i..n] R[i] 有序序列R[1..i] 无序序列 R[i+1..n]
实现“一趟插入排序”可分三步进行: (1)在R[1..i-1]中查找R[i]的插入位置 j ; R[1..j].key R[i].key < R[j+1..i-1].key (2)将R[j+1..i-1]中的所有记录均后移 一个位置; (3)将R[i] 插入(复制)到R[j+1]的位置上。
不同的具体实现方法导致不同的算法描述 9.2.1直接插入排序(基于顺序查找) 9.2.2 折半插入排序(基于折半查找) 9.2.3 二路插入排序(折半查找的改进) 9.2.4 表插入排序(基于链表存储) 9.2.5 希尔排序(基于逐趟缩小增量)
9.2.1直接插入排序 直接插入排序是利用“顺序查找”实现“在R[1i-1]中查找R[i]的插入位置”的一种比较简单的排序算法。其实现要点是: (1)从R[i-1]起向前进行顺序查找。为在查找插入位置的过程中避免数组下标出界,监视哨设置在R[0],即R[0]=R[i]。执行循环语句“for(j=i-1;R[0].key<R[j].key;--j);”,循环结束表明R[i]的插入位置为j+1。
(2)对于在查找过程中找到的那些关键字不小于R[i].key的记录,并在查找的同时实现记录向后移动,即 for(j=i-1;R[0].key<R[j].key;--j) R[j+1]=R[j]; (3)上述循环结束后可以直接进行“插入”,即执行“R[j+1]=R[i];”。
void InsertionSort ( SqList &L ) { // 对顺序表 L 作直接插入排序。 for ( i=2; i<=L.length; ++i ) if (L.r[i].key < L.r[i-1].key) { } } // InsertSort L.r[0] = L.r[i]; // 复制为监视哨 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]; // 插入到正确位置
内部排序的时间分析: 实现内部排序的基本操作有两个: (1)“比较”序列中两个关键字的 大小; (2)“移动”记录。
对于直接插入排序: 最好的情况(关键字在记录序列中顺序有序): “移动”的次数: “比较”的次数: 0 最坏的情况(关键字在记录序列中逆序有序): “比较”的次数: “移动”的次数:
9.2.2 折半插入排序 因为R[1i-1]是一个按关键字有序的有序序列,为了减少关键字的比较次数,则可利用折半查找实现在有序序列R[1i-1]中查找R[i]的插入位置。如此实现的插入排序称为折半插入排序。 例2有4个待排序记录,其关键字分别为:51,33,62,51。
void BiInsertionSort ( SqList &L ) { } // BInsertSort for ( i=2; i<=L.length; ++i ) { } // for L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0] 在 L.r[1..i-1]中折半查找插入位置; for ( j=i-1; j>=low; --j ) L.r[j+1] = L.r[j]; // 记录后移 L.r[low] = L.r[0]; // 插入
low = 1; high = i-1; while(low<=high){ } m = (low+high)/2; // 折半 if(L.r[0].key < L.r[m].key) high = m-1; // 插入点在低半区 elselow = m+1; // 插入点在高半区
折半插入排序算法与直接插入排序算法相比,仅减少了关键字比较次数,而记录移动次数不变,所以时间复杂度为O(n2)。 由于L.r[i].key=L.r[m].key时,L.r[i]插入到L.r[m]之后,所以折半插入排序是一个稳定的排序方法。折半插入排序算法只适合于顺序存储的有序表。
9.2.3二路插入排序 二路插入排序是在折半排序的基础上再改进之,其目的是减少排序过程中移动记录的次数,但它需要n个记录的辅助空间。 其基本思想是另设一个数组d,将R[1]赋给d[1],并将d[1]看成是排序后的中间记录,从第二个记录开始一次将关键字小于d[1]的记录插入到d[1]之前的有序序列,将关键字大于d[1]的记录插入到d[1]之后的有序序列。
在实现算法时,可将d看成一个循环向量,并设两个指针first和final分别指示排序过程中得到的有序序列的第一个记录和最后一个记录在中的位置。在实现算法时,可将d看成一个循环向量,并设两个指针first和final分别指示排序过程中得到的有序序列的第一个记录和最后一个记录在中的位置。
void BiInsSort(SqList &L){//二路插入排序 } RcdType d[MAXSIZE+1];//辅助存储 n=L.length; d[1]=L.r[1];first=1;final=1; for(i=2;i<=n;++i){ } if(L.r[i].key>=d[1].key入){…//插入后部} else {…//插入前部} L.r[1]=d[first]; for(i=first%n+1,j=2;i!=first;i=i%n+1,j++) L.r[j]=d[i]; //将序列复制回去
low=1;high=final; while(low<=high){//折半查找插入位置; }//while m=(low+high)/2;//折半 if(L.r[i].key<d[m].key)high=m-1;//插入点在低半区 else low=m+1;//插入点在高半区 for(j=final;j>=high;--j)d[j+1]=d[j];//移动记录 d[high+1]=L.r[i];//插入 final++; Return
if(first==1){first=n;d[n]=L.r[i]; else{ } low=first;high=n; while(low<=high){ }//while m=(low+high)/2;//折半 if(L.r[i].key<d[m].key) high=m-1;//插入点在低半区 else low=m+1;//插入点在高半区 for(j=first;j<=high;j++)d[j-1]=d[j];//移动记录 d[high]=L.r[i];//插入 Return first--;
在二路插入排序中,移动记录的次数为n2/8,故二路插入排序并不能绝对避免记录移动,并且当记录的关键字是最小或最大时,二路插入排序就完全失去了它的优越性。在二路插入排序中,移动记录的次数为n2/8,故二路插入排序并不能绝对避免记录移动,并且当记录的关键字是最小或最大时,二路插入排序就完全失去了它的优越性。
9.2.4 表插入排序 为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。
静态表类型定义如下: typedef struct{ RcdType rc;//记录项 int next;//指针域 }SLNode;//表结点类型 typedef struct{ SLNode r[SIZE];//Size为静态表容量 int length; //链表当前长度 }SLinkList; //静态链表类型
void LInsertionSort (Elem SL[ ], int n){ // 对记录序列SL[1..n]作表插入排序。 SL[0].key = MAXINT ; SL[0].next = 1; SL[1].next = 0; for ( i=2; i<=n; ++i ) for ( j=0, k = SL[0].next;SL[k].key<= SL[i].key ; j=k, k=SL[k].next ); { SL[j].next = i; SL[i].next = k;} // 结点i插入在结点j和结点k之间 }// LinsertionSort
从表插入排序的过程可知,表插入排序的基本操作是将一个记录插入到已排好序的有序表中。和直接插入排序相比,不同之处仅是以修改2n次指针代替移动记录,排序过程中所需进行的关键字的比较次数相同。因此,表插入排序的时间复杂度仍为O(n2)。从表插入排序的过程可知,表插入排序的基本操作是将一个记录插入到已排好序的有序表中。和直接插入排序相比,不同之处仅是以修改2n次指针代替移动记录,排序过程中所需进行的关键字的比较次数相同。因此,表插入排序的时间复杂度仍为O(n2)。 另一方面,表插入排序的结果只是求得一个有序链表,则只能对它进行顺序查找,不能进行随机查找,为了能实现有序表的折半查找,尚需对记录进行重新排列。 。
重排记录的做法是:顺序扫描有序链表,将链表中第i个结点移动到数组的第i个分量中。算法中使用了三个指针:其中p指示第i个记录的当前位置;i指示第i个记录应在的位置;q指示第i+1个记录的当前位置。示例如图9-6所示:重排记录的做法是:顺序扫描有序链表,将链表中第i个结点移动到数组的第i个分量中。算法中使用了三个指针:其中p指示第i个记录的当前位置;i指示第i个记录应在的位置;q指示第i+1个记录的当前位置。示例如图9-6所示:
如何在排序之后调整记录序列? 算法中使用了三个指针: 其中:p指示第i个记录的当前位置; i指示第i个记录应在的位置; q指示第i+1个记录的当前位置
void Arrange ( Elem SL[ ], int n ) { p = SL[0].next; // p指示第一个记录的当前位置 for ( i=1; i<n; ++i ) { while (p<i) p = SL[p].next; q = SL[p].next; // q指示尚未调整的表尾 if ( p!= i ) { SL[p]←→SL[i]; // 交换记录,使第i个记录到位 SL[i].next = p; // 指向被移走的记录, }//if p = q; // p指示尚未调整的表尾,准备找第i+1个记录 }//for } // Arrange
表插入排序的基本操作是将一个记录i插入到已排好序的有序链表中,若有序表长度为i,则需要比较最多i+1次,修改指针2次。因此,总比较次数与直接插入排序相同,其时间复杂度仍为O(n2)。表插入排序的基本操作是将一个记录i插入到已排好序的有序链表中,若有序表长度为i,则需要比较最多i+1次,修改指针2次。因此,总比较次数与直接插入排序相同,其时间复杂度仍为O(n2)。