第八章 排序
·排序定义——将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序
列叫~
·排序分类
按待排序记录所在位置
内部排序:待排序记录存放在内存
外部排序:排序过程中需对外存进行访问的排序
按排序依据原则
插入排序:直接插入排序、折半插入排序、希尔排序
交换排序:冒泡排序、快速排序
选择排序:简单选择排序、堆排序
归并排序:2-路归并排序
基数排序
按排序所需工作量
简单的排序方法:T(n)=O(n²)
先进的排序方法:T(n)=O(logn)
基数排序:T(n)=O(d.n)
·排序基本操作
比较两个关键字大小
将记录从一个位置移动到另一个位置
8.1 插入排序
直接插入排序
排序过程:整个排序过程为n-1趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序
算法描述
typedef struct
{ int key;
float info;
}JD;
void straisort(JD r[],int n)
{ int i,j;
for(i=2;i<=n;i++)
{ r[0]=r[i];
j=i-1;
while(r[0].key<r[j].key)
{ r[j+1]=r[j];
j--;
}
r[j+1]=r[0];
}
}
算法评价
时间复杂度
若待排序记录按关键字从小到大排列(正序)
关键字比较次数:
记录移动次数:
若待排序记录按关键字从大到小排列(逆序)
关键字比较次数:
记录移动次数:
若待排序记录是随机的,取平均值
关键字比较次数: T(n)=O(n²)
记录移动次数:
空间复杂度:S(n)=O(1)
折半插入排序
排序过程:用折半查找方法确定插入位置的排序叫~
算法描述
void binsort(JD r[],int n)
{ int i,j,x,s,m,k;
for(i=2;i<=n;i++)
{ r[0]=r[i];
x=r[i].key;
s=1; j=i-1;
while(s<=j)
{ m=(s+j)/2;
if(x<r[m].key) j=m-1;
else s=m+1;
}
for(k=i-1;k>=s;k--)
r[k+1]=r[k];
r[s]=r[0];
}
}
算法评价
时间复杂度:T(n)=O(n²)
空间复杂度:S(n)=O(1)
希尔排序(缩小增量法)
排序过程:先取一个正整数d1<n,把所有相隔d1的记录放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止
算法描述
void shellsort(JD r[],int n,int d[T])
{ int i,j,k;
JD x;
k=0;
while(k<T)
{ for(i=d[k]+1;i<=n;i++)
{ x=r[i];
j=i-d[k];
while((j>0)&&(x.key<r[j].key))
{ r[j+d[k]]=r[j];
j=j-d[k];
}
r[j+d[k]]=x;
}
k++;
}
}
#define T 3
int d[]={5,3,1};
希尔排序特点
· 子序列的构成不是简单的“逐段分割”,而是将相隔某个增量的记录组成一个子序列
· 希尔排序可提高排序速度,因为
分组后n值减小,n²更小,而T(n)=O(n²),所以T(n)从总体上看是减小了
关键字较小的记录跳跃式前移,在进行最后一趟增量为1的插入排序时,序列已基本有
序
·增量序列取法
无除1以外的公因子
最后一个增量值必须为1
8.2 交换排序
·冒泡排序
排序过程
将第一个记录的关键字与第二个记录的关键字进行比较,若为逆序r[1].key>r[2].key,
则交换;然后比较第二个记录与第三个记录;依次类推,直至第n-1个记录和第n个
记录比较为止——第一趟冒泡排序,结果关键字最大的记录被安置在最后一个记录上
对前n-1个记录进行第二趟冒泡排序,结果使关键字次大的记录被安置在第n-1个记
录位置
重复上述过程,直到“在一趟排序过程中没有进行过交换记录的操作”为止
算法描述
void bubble_sort(JD r[],int n)
{ int m,i,j,flag=1;
JD x;
m=n-1;
while((m>0)&&(flag==1))
{ flag=0;
for(j=1;j<=m;j++)
if(r[j].key>r[j+1].key)
{ flag=1;
x=r[j];
r[j]=r[j+1];
r[j+1]=x;
}
m--;
}
}
算法评价
时间复杂度
最好情况(正序)
比较次数:n-1
移动次数:0
最坏情况(逆序)
比较次数:
移动次数:
T(n)=O(n²)
空间复杂度:S(n)=O(1)
快速排序
基本思想:通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序
排序过程:对r[s……t]中记录进行一趟快速排序,附设两个指针i和j,设枢轴记录rp=r[s],x=rp.key
·初始时令i=s,j=t
·首先从j所指位置向前搜索第一个关键字小于x的记录,并和rp交换
·再从i所指位置起向后搜索,找到第一个关键字大于x的记录,和rp交换
·重复上述两步,直至i==j为止
·再分别对两个子序列进行快速排序,直到每个子序列只含有一个记录为止
算法描述
void qksort(JD r[],int t,int w)
{ int i,j,k;
JD x;
if(t>=w) return;
i=t; j=w; x=r[i];
while(i<j)
{ while((i<j)&&(r[j].key>=x.key)) j--;
if(i<j) { r[i]=r[j]; i++; }
while((i<j)&&(r[i].key<=x.key)) i++;
if(i<j) { r[j]=r[i]; j--; }
}
r[i]=x;
qksort(r,t,j-1);
qksort(r,j+1,w);
}
算法评价
时间复杂度
最好情况(每次总是选到中间值作枢轴)T(n)=O(nlog2n)
最坏情况(每次总是选到最小或最大元素作枢轴)T(n)=O(n²)
T(n)=O(n²)
空间复杂度:需栈空间以实现递归
最坏情况:S(n)=O(n)
一般情况:S(n)=O(log2n)
8.3 选择排序
简单选择排序
排序过程
·首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记
录交换
·再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记
录交换
·重复上述操作,共进行n-1趟排序后,排序结束
算法描述
void smp_selesort(JD r[],int n)
{ int i,j,k;
JD x;
for(i=1;i<n;i++)
{ k=i;
for(j=i+1;j<=n;j++)
if(r[j].key<r[k].key) k=j;
if(i!=k)
{ x=r[i];
r[i]=r[k];
r[k]=x;
}
}
}
算法评价
时间复杂度
记录移动次数
最好情况:0
最坏情况:3(n-1)
比较次数:
T(n)=O(n²)
空间复杂度:S(n)=O(1)
堆排序
堆的定义:n个元素的序列(k1,k2,……kn),当且仅当满足下列关系时,称之为堆
堆排序:将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最小(大)值后,使剩余的n-1个元素重又建成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列,这个过程叫~
堆排序需解决的两个问题:
如何由一个无序序列建成一个堆?
如何在输出堆顶元素之后,调整剩余元素,使之成为一个新的堆?
第二个问题解决方法——筛选
方法:输出堆顶元素之后,以堆中最后一个元素替代之;然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”
算法描述
int sift(JD r[],int k,int m)
{ int i,j;
JD x;
i=k; x=r[i]; j=2*i;
while(j<=m)
{ if((j<m)&&{r[j].key>r[j+1].key)) j++;
if(x.key>r[j].key)
{ r[i]=r[j];
i=j;
j*=2;
}
else j=m+1;
}
r[i]=x;
}
第一个问题解决方法
方法:从无序序列的第ën/2û个元素(即此无序序列对应的完全二叉树的最后一个非终端
结点)起,至第一个元素止,进行反复筛选
算法描述
void heapsort(JD r[],int n)
{ int i;
JD x;
for(i=n/2;i>=1;i--)
sift(r,i,n);
for(i=n;i>=2;i--)
{ x=r[1];
r[1]=r[i];
r[i]=x;
sift(r,1,i-1);
}
}
int sift(JD r[],int k,int m)
{ int i,j;
JD x;
i=k; x=r[i]; j=2*i;
while(j<=m)
{ if((j<m)&&{r[j].key>r[j+1].key)) j++;
if(x.key>r[j].key)
{ r[i]=r[j];
i=j;
j*=2;
}
else j=m+1;
}
r[i]=x;
}
算法评价
时间复杂度:最坏情况下T(n)=O(nlogn)
空间复杂度:S(n)=O(1)
8.4 归并排序
归并——将两个或两个以上的有序表组合成一个新的有序表,叫
2-路归并排序
排序过程
·设初始序列含有n个记录,则可看成n个有序的子序列,每个子序列长度为1
·两两合并,得到ën/2û个长度为2或1的有序子序列
·再两两合并,……如此重复,直至得到一个长度为n的有序序列为止
算法描述
void mergesort(JD r[],int n)
{ int i,s=1;
JD t[M];
while(s<n)
{ tgb(s,n,r,t);
s*=2;
if(s<n) { tgb(s,n,t,r); s*=2; }
else { i=1;
while(i<=n) r[i]=t[i++];
}
}
}
void tgb(int s,int n,JD r[],JD t[])
{ int i=1;
while(i<=(n-2*s+1))
{ merge(r,i,i+s-1,i+2*s-1,t);
i=i+2*s;
}
if(i<(n-s+1)) merge(r,i,i+s-1,n,t);
else
while(i<=n) t[i]=r[i++];
}
算法评价
时间复杂度:T(n)=O(nlog2n)
空间复杂度:S(n)=O(n)
8.5 基数排序
多关键字排序方法
·最高位优先法(MSD):先对最高位关键字k1(如花色)排序,将序列分成若干子序列,每个子序列有相同的k1值;然后让每个子序列对次关键字k2(如面值)排序,又分成若干更小的子序列;依次重复,直至就每个子序列对最低位关键字kd排序;最后将所有子序列依次连接在一起成为一个有序序列
·最低位优先法(LSD):从最低位关键字kd起进行排序,然后再对高一位的关键字排序,……依次重复,直至对最高位关键字k1排序后,便成为一个有序序列
MSD与LSD不同特点
·按MSD排序,必须将序列逐层分割成若干子序列,然后对各子序列分别排序
·按LSD排序,不必分成子序列,对每个关键字都是整个序列参加排序;并且可不通过关键字比较,而通过若干次分配与收集实现排序
链式基数排序
·基数排序:借助“分配”和“收集”对单逻辑关键字进行排序的一种方法
·链式基数排序:用链表作存储结构的基数排序
链式基数排序步骤
·设置10个队列,f[i]和e[i]分别为第i个队列的头指针和尾指针
·第一趟分配对最低位关键字(个位)进行,改变记录的指针值,将链表中记录分配至10个链队列中,每个队列记录的关键字的个位相同
·第一趟收集是改变所有非空队列的队尾记录的指针域,令其指向下一个非空队列的队头记录,重新将10个队列链成一个链表
·重复上述两步,进行第二趟、第三趟分配和收集,分别对十位、百位进行,最后得到一个有序序列
算法描述
#define D 3
typedef struct node
{ int key;
float info;
int link;
}JD;
int radixsort(JD r[],int n)
{ int i,j,k,t,p,rd,rg,f[10],e[10];
for(i=1;i<n;i++) r[i].link=i+1;
r[n].link=0;
p=1; rd=1; rg=10;
for(i=1;i<=D;i++)
{ for(j=0;j<10;j++)
{ f[j]=0;
e[j]=0;
}
do{
k=(r[p].key%rg)/rd;
if(f[k]==0)
f[k]=p;
else
r[e[k]].link=p;
e[k]=p;
p=r[p].link;
}while(p>0);
j=0;
while(f[j]==0) j++;
p=f[j]; t=e[j];
for(k=j+1;k<10;k++)
if(f[k]>0)
{ r[t].link=f[k];
t=e[k];
}
r[t].link=0;
rg*=10;
rd*=10;
}
return(p);
}
算法评价
时间复杂度:
分配:T(n)=O(n)
收集:T(n)=O(rd)
T(n)=O(d(n+rd))
其中:n——记录数
d——关键字数
rd——关键字取值范围
空间复杂度:S(n)=2rd个队列指针+n个指针域空间