第十章 内部排序
文章目录
线性表操作
无序线性表的并集:
如:A = (17,55,9,30,28)
B = (30,68,5,17)
时间复杂度O(LA*LB)
有序线性表的并集:
如:A = (7,15,29,38)
B = (3,7,46,57)
时间复杂度O(LA+LB)
查找
无序表的顺序查找:O(n)
有序表的顺序查找:O(logn)
以及构造二叉排序树以及B树的过程中均涉及排序的思想。
排序概述
排序相关基本概念:
记录/元素:排序的基本单位
序列:所有待排序的记录
排序:将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。
关键字:排序的依据 主关键字/次关键字/若干数据项的组合
排序的严格定义:
如果待排序的序列正好符合排序要求,则称其为"正序"序列
如果待排序的序列逆转过来,正好符合排序要求,则称其为"逆序"序列
排序的稳定性:
假设Ki = Kj ,且在排序前的序列中Ri领先于Rj(即i<j)
若在排序之后的序列中Ri仍领先于Rj,则称所用的排序方法是稳定的。
反之,若可能使排序后的序列Rj领先于Ri,则称所用的排序方法是不稳定的。
多关键字排序:由多个关键字共同决定记录排列顺序。(除第一趟排序外,后继的排序都必须选用稳定的排序方法)
排序的分类:
内部排序:待排序记录存放在计算机随机存储器中进行的排序过程。
外部排序:待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问排序过程。
内部排序算法的性能:
-
时间性能
原操作:关键字比较和记录移动
三种情况下的代价:最好情况(最小时间代价),最差情况(最大时间代价)以及平均情况。
-
辅助空间
除了存放待排序记录之外所需的其他存储空间
-
算法本身的复杂程度
内部排序的分类:
按依据的原则不同来分:插入排序,交换排序,选择排序,归并排序,基数排序。
按所需的工作量来分:
- 简单的排序方法:O(n*n) 冒泡排序 简单选择排序 直接插入排序
- 先进的排序算法:O(nlogn) 希尔排序 堆排序 归并排序 快速排序
- 基数排序:O(d*n)
序列的三种存储方式:
记录移动可以通过改变记录的存储方式来减少或避免
待排序序列的存储方式:
- 存放在地址连续的一组存储单元中,记录之间的次序关系由存储位置决定,排序必须移动元素。
- 存在静态链表中,记录之间的次序关系由指针指示,排序不用移动记录,仅需修改指针(链表排序)。
- 待排序记录本身存储在一组地址连续的存储单元内,同时附设一个指示记录存储位置的地址向量,排序过程不移动记录,而移动地址向量中这些记录的地址,在排序结束后,再按照地址向量中的值调整记录的存储位置(地址排序)。
以第一种存储方式为例:
#define MAXSIZE 20 //一个用作实例的小顺序表的最大长度。
typedef int KeyType; //定义关键字类型为整数类型
typedef struct{
KeyType key; //关键字项
InfoType otherinfo; //其它数据项
}RedType;
typedef struct{
RedType r[MAXSIZE+1]; //r[0]闲置或用作哨兵单元
int length; //顺序表长度
}SqList; //顺序表类型
插入排序
插入排序的基本思想:
将待排序序列分成两部分:前部有序,后部无序。
初始:前部只含第一个记录,后部包含其余n-1个记录。
每趟将后部种的第一个记录,按关键字大小插入到前部的有序表中。
最终:前部包含所有记录,后部为空。
不同的插入排序算法:
依据不同的查找方法:
- 直接插入排序
- 折半插入排序
- 希尔排序
减少移动次数:
- 2-路插入排序
- 表插入排序
直接插入排序
- 初始化:将序列中第1个记录r[1]看成是一个有序的子序列。
- 一趟插入:采用直接查找方法,在含有i-1个记录的有序子序列r[1…i-1]中为第i(i=2,3,4…n)个记录r[i]查找到合适的插入位置,以便得到含有i个记录的有序子序列r[1…i]
- 重复步骤2,n-1趟,便可以将整个序列变成有序序列。
void InsertSort(SqList &L){
for(int i=2;i<=L.length;++i)
if(L.r[i].key<L.r[i-1].key){
L.r[0]=L.r[i];
L.r[i]=L.r[i-1];
for(int j=i-2;L.r[0].key<L.r[j].key;--j)
L.r[j+1] = L.r[j]; //后移
L.r[j+1]=L.r[0];
}
}
例如:(稳定)
直接插入排序算法性能分析:
只需一个记录的辅助空间
时间复杂度:取决于比较和移动次数 O(n*n)
折半插入排序
void BInsertSort (SqList &L){
for(int i=2;i<=L.length;++i){
L.r[0] = L.r[i];
low = 1;
high = i -1;
while(low<=high){
m = (low+high)/2;
if(L.r[0].key<L.r[m].key)
high = m-1;
else
low = m+1;
}
for(int j=i-1;j>=high+1;--j)
L.r[j+1] = L.r[j];
L.r[high+1]=L.r[0];
}
}//稳定
折半查找排序虽然可以将关键字中的比较次数降低为O(nlogn),但是记录的移动次数和辅助空间都没有改变,因此时间复杂度仍然为O(n*n)。
希尔排序
改进插入排序的两个特性
- 待排序列"正序"时,时间复杂度为O(n)
- 当n值很小时,效率也较高
希尔排序,又称为"缩小增量排序"(不稳定)
基本思想:
- 先将整个序列按某个增量分割成若干子序列,分别进行直接插入排序
- 减少增量,将序列分割为更大,更有序的子序列,再分别进行直接插入排序
- 如此重复下去,直到整个序列"基本有序"时,再对全体记录(增量1)进行一次插入排序
void ShellInsert(SqList &L,int dk){
for(int i=dk+1;i<=L.length;++i)
if(L.r[i].key<L.r[i-dk].key){
L.r[0]=L.r[i];
for(int j=i-dk;j>0&&L.r[0].key<L.r[j].key;j-=dk)
L.r[j+dk] = L.r[j]; //后移
L.r[j+dk]=L.r[0];
}
}
void ShellSort(SqList &L,int dlta[],int t){
//按增量序列dlta[0...t-1]对顺序表L作希尔排序。
for(int k = 0;K<t;++k){
ShellInsert(L,dlta[k])
}
}
希尔排序性能分析:当n趋向无穷大时,时间复杂度可减少到O(以2为底n的平方的对数)
交换排序
起泡排序
基本思想:两两比较相邻记录的关键字,如果反序则交换,直到没有反序位置。
void bubble_Sort(int a[],int n){
for(int i = 1;i<n;i++){
for(int j = n-1;j>=i;j--) //从后往前起泡
if(a[j]>a[j+1]){
int temp = a[j];
a[j] = a[j+1];
a[j+1] = a[j];
}
}
}
算法优化:
void bubble_Sort(int a[],int n){
//对顺序表a作起泡排序
for(int i = 1,change = TURE;i<=n&&change;i++){
change = FALSE;
for(j=n-1;j>=i;--j)
if(a[j]>a[j+1]){
int temp = a[j];
a[j] = a[j+1];
a[j+1] = a[j];
change = TRUE;
}
}
}
起泡排序性能分析:
只需一个记录的辅助空间
时间复杂度:O(n*n) 简单排序中最慢的
快速排序
分治策略:
分:将待排序列分割成独立的两部分
治:对这两部分递归地进行快速排序
合:整个序列有序
快速排序思想:
-
任意选取一个记录K作为枢纽(或支点,prvot)
-
一趟快速排序/划分:通过一系列交换,将其余记录分割成两个子序列L和R,使得L中的所有记录的关键字都不大于K,R中的所有记录的关键字都不小于K
-
递归地对L和R进行快速排序。
快速划分方法:
附设两个指针low和high,初值风别指向序列的第一个和最后一个记录。
设枢轴记录的关键字为pivotKey,则:
- high向前搜索,找到第一个关键字小于pivotKey的记录,和枢轴交换
- low向后搜索,找到第一个关键字大于pivotKey的记录,和枢轴交换
- 重复1,2两步,直到low==high为止
快速划分方法的优化
外移:将枢轴记录暂存到0号位置
扫描:
- high向前搜索,找到第一个关键字小于pivotKey的记录,移到low所指位置。
- low向后搜索,找到第一个关键字大于pivotKey的记录,移到high所指位置。
- 重复1,2两步,直到low==high为止。
归为:将枢轴记录移动到low所指位置
快速划分算法:
int Partition(SqList &L,int low,int high){
//交换顺序表L中子表r[low...high]的记录,枢轴记录到位
//并返回其所在位置,此时在它之前(后)的记录均不大(小)于它
L.r[0]=L.r[low]; //用子表的第一个记录作为枢轴记录
pivotkey = L.r[low].key; //枢轴记录关键字
while(low < high){
while(low<high&&L.r[high].key>=pivotkey)
--high;
L.r[low]=L.r[high]; //将比枢轴记录小的记录移到低端
while(low<high&&L.r[high].key<=pivotkey)
++low;
L.r[high]=L.r[low];//将比枢轴记录大的记录移到高端
}
L.r[low]=L.r[0]; //枢轴记录到位
return low; //返回枢轴位置
}
例:快速排序全过程
快速排序算法:
void QSort(SqList &L,int low,int high){
//对顺序表L中的子序列L.r[low..high]作快速排序
if(low < high){
//长度大于1
pivotloc = Partition(L,low,high);//将L.r[low...high]一分为二
QSort(L,low,pivotloc-1);//对低子表递归排序,pivotloc是枢轴位置
QSort(L,pivotloc+1,high);//对高子表递归排序
}
}
void Quicksort(SqList &L){
//对顺序表L作快速排序
QSort(L,1,L.length)
}
快速排序性能分析:
在所有O(nlogn)级别的排序方法中,快速排序是常数因子最小的,目前被认为是平均性能最好的一种内部排序算法。
快速排序算法的改进:
改变枢轴记录选取策略,降低最坏情况发生概率
-
随机法:随机选一个记录
-
三者取中法:
(1)low ,high,mid三者取中间值
(2)随机选三者,取中间值
-
混合排序策略
选择排序
选择排序的基本思想:
-
第i趟:在n-i+1(i=1,2,3…,n-1)个记录中选取关键字最小(大)的记录作为有序序列中第i(n-i+1)个记录。
-
将待排序序列分成两部分:前部有序,后部无序
初始:前部无记录,后部包含全部n个记录
每趟从后部选出关键字最小的记录,扩充到前部
简单选择排序
又称直接选择排序,是一种原地排序。
第i趟:通过n-i(i=1,2,3…n-1)次关键字比较,从n-i+1个记录中选出关键字最小的记录,并与第i个记录交换。
void SersctSort(SqList &L){
int i,j,min;
for(i=1;i<L.length;i++){
min = i;
for(j=i+1;j<=L.length;j++){
if(L.r[min]>L.r[j])
min=j;
}
}
if(i!=min){
int temp = l.[i];
L.[i] = L.[min];
L.[min] = L.[i];
}
}
快速排序性能分析:O(n*n)
简单选择排序的不足:
选择排序的主要操作:关键字间的比较
简单选择排序没能充分利用上一趟比较结果
树形选择排序
树形排序的基本思想:
又称锦标赛排序
首先,对n个记录的关键字两两进行比较;
然后,在[n/2]个较小者之间再进行两两比较;
如此重复,直到选出最小关键字的记录为止。
胜者树
- n个叶子结点的完全二叉树
- 叶子(外部结点)——所有n个待排序记录
- 内部结点——两两比较中的胜者
- 根——关键字最小的记录
将叶子结点中的最小关键字替换成"最大值",并修改从叶子结点到根路径上的个结点。
依次类推…
树形选择排序性能分析:
时间复杂度 O(nlogn)
空间复杂度 O(n)
堆排序
树形选择排序的缺点:
- 需要较多的辅助空间
- 和"最大值"进行多余的比较
堆的定义:
n个元素的序列{K1,K2,…,Kn}当且仅当满足如下关系时,成之为堆
若将此序列对应的一维数组看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左右孩子结点的值。
堆顶元素必为序列中n个元素的最小值(最大值)
堆排序的基本思想
-
将待排序列构造成一个大顶堆,对顶就是整个序列的最大值;
-
第i趟:在移走堆顶后,将剩余的n-i个元素重新调整成一个大顶堆,就可得到整个i个最大值;
(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)
-
反复执行步骤2,便能得到一个有序序列。
问题1:
移走堆顶(即将其与第n-i+1个元素交换)之后,如何将剩余的n-i个元素重新调整成一个大顶堆?
注意:只有新顶堆元素不符合堆定义。
解决方案:“筛选”,即从堆顶到叶子的一个调整过程:比较左右孩子,选大者再与该结点比较,如果该结点小,则将大的孩子与该结点交换,接着做下一层比较。
筛选算法:
typedef SqList HeapType; //堆采用顺序表存储表示
void HeapAdjust(HeapType &H,int s,int m){
//已知H.r[s...m]中记录的关键字除H.r[s].key之外均满足堆的定义
//本函数调整H.r[s]的位置,使H.r[s...m]成为一个大顶堆
rc = H.r[s]
for(j=2*s;j<=m;j*=2){ //沿key较大的孩子结点向下筛选
if(j<m&&H.r[j].key<H.r[j+1].key)
++j; //j为key较大的记录的下标
if(rc.key>=H.r[j].key) //rc应插入在位置s上
break;
H.r[s] = H.r[j];
s=j;
}
H.r[s]=rc; //插入
}
问题2:
如何将一个无序的序列构造成一个堆?
注意:叶子一定符合堆定义
解决方案:从最后一个非终端结点(第[n/2]个元素)开始逆序,逐个"筛选"
堆排序算法:
void HeapSort(HeapType &H){
//对顺序表H进行堆排序
for(i=H.length/2;i>0;--i)
HeapAdjust(H,i,H.length);
for(i=H.length;i>1;--i){
temp = H.r[1];
H.r[1]=H.r[i];
H.r[i]=H.r[1];
HeapAdjust(H,1,i-1);
}
}
堆排序性能分析:时间复杂度O(nlogn)
- 对初始序列不敏感,最好,最坏和平均时间复杂度均为O(nlogn)
- 只需要一个辅助存储空间
- 不稳定
- 初始建堆所需的比较次数较多,不适合序列长度较小的情况
归并排序
归并排序基本思想:
归并:将两个或两个以上的有序表组合成新的有序表
归并排序:利用归并思想来实现的排序方法
2-路归并排序:
算法:归并相邻的两个有序序列
void Merge(RcdType SR[],RcdTypest &TR[],int i,int m,int n){
//将有序的SR[i...m]和SR[m+1...n]归并为有序的TR[i..n]
for(j=m+1,k=i;i<=m&&j<=n;++k){
//将SR中的记录由小到大依次并入到TR
if(SR[i].key<SR[i].key)
TR[k]=SR[i++];
else
TR[k]=SR[j++];
if(i<=m)
{
for(l=0;l<m-i;l++)
TR[k+l]=SR[i+l]; //将剩余的SR[i...m]复制到TR
}
if(j<=n)
{
for(l=0;l<n-j;l++)
TR[k+l]=SR[j+l]; //将剩余的SR[j...n]复制到TR
}
}
}
递归实现2-路归并排序:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UecJFhHi-1640026290706)(C:\Users\1\Desktop\23.jpg)]
void MSort(RcdType SR[],RcdTypest &TR1[],int s,int t){
//将SR[s...t]归并成TR1[s...t]
if(s==t)
TR1[s]=SR[s];
else{
m = (s+t)/2;
MSort(SR,TR2,s,m);
MSort(SR,TR2,m+1,t);
Merge(TR2,TR1,s,m,t);
}
}
void MergeSort(SqList &L){
MSort(L.r,L.r,1,L.length);
}
2-路归并排序性能分析:
- 每趟归并的时间复杂度为O(n)
- 需进行[log(2)n]趟排序
- 时间复杂度为O(nlogn)
- 空间复杂度为O(n)
- 稳定
基数排序
(学会原理不做实现)
前四大类排序的共同之处:基于"关键字比较"和"记录移动"这两种操作。
基数排序:借助于多关键字排序的思想对单关键字进行排序。
多关键字排序:
最高位优先法(MSD)
最低位优先法(LSD)
基数排序:将多关键字排序中的LSD思想用于单逻辑关键字的排序。
将单逻辑关键字看成是由若干个关键字复合而成的
例如:
数值关键字,可以把每个数位看成一个关键字
字符串关键字,可以把每个位置上的字符看成一个关键字
若分解得到的每个关键字取值范围均相同,则按LSD进行排序更方便。
基数排序的主要思想:
基数排序所需的存储空间:
假设共N个记录,关键字K可拆分成d个关键字,每个都有RADIX中取值
顺序存储实现共需要RADIX*N个记录的辅助空间!
RADIX个队列
每个队列需预留N个记录的存储空间
链式存储实现只需要N+RADIX*2个指针的辅助空间!
RADIX条单链表(队列)
每条均设有头指针和尾指针
分配时只需要修改指针,不需要额外的存储空间
示例:链式存储实现基数排序
基数排序性能分析:
各种内部排序方法比较
按时间复杂度分类:
-
简单的排序方法(平均O(n*n)):直接插入,折半插入,简单选择,起泡排序
直接插入最常用,尤其是已基本有序的序列
-
先进的排序方法(平均O(nlogn)):堆排序,快速排序,归并排序,树形选择
快速排序被认为是目前平均性能最好的一种
在序列长度较大时,归并排序比堆排序要快
-
介于两者之间:希尔排序
-
基数排序
最好情况,最坏情况下的时间复杂度:
-
最好情况:直接插入和起泡排序最好
-
最坏情况:堆排序和归并排序最快
-
平均情况:快速排序
按空间复杂度分类:
- O(n):归并排序,基数排序
- O(log(2)n-n):快速排序
- O(1):直接插入,折半插入,简单选择,起泡排序,希尔排序,堆排序
按稳定性分类:
- 稳定:直接插入,折半插入,起泡插入,归并插入,基数插入,树形选择
- 不稳定:简单选择,希尔排序,堆排序,快速排序
当n不大(<10*1024)时,宜采用简单排序方法
当n较大时,宜采用先进的排序方法
终于把排序整理完了!!!!!!
个别算法实现难度较大,所以没有写。大家见谅,期末考试不会考太难的,学懂原理最重要。
祝大家期末高过哦!!!