值此期末复习阶段,回顾数据结构(C语言)第十章,内部排序汇总
一、直接插入排序
设置哨兵
void InsertSort(RecType 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[0].key>r[j].key
r[j+1]=r[0]; //将r[0]放回找到的位置
}
}
分析:
-
最好情况:原来的n个记录递增有序
比较关键字n-1次
移动关键字2(n-1)次(复制哨兵后又将哨兵复制回来)
-
最坏情况:原来的n个记录递减有序
比较关键字次数 O(n2)
移动关键字次数 O(n2)
-
平均移动记录的次数约为:O(n2)
-
时间复杂度O(n2)
-
空间复杂度O(1)
-
该排序算法是稳定的
二、折半插入排序
折半的前提:在一个有序序列中插入新的记录
折半插入排序的过程中的折半查找的目的是查询插入点
折半插入排序=折半查找+插入
void BInsertSort(){ //对顺序表L做折半查找
for(i=2;i<L.length;i++){
L.r[0]=L.r[i]; //r[0]可以做一个中间寄存量,折半插入排序
low=1;high=i-1; //第i个数是要排的数,所以从1到i-1查找i要插入的位置
while(low<=high){ //结束循环条件,就是low>high(low=high+1)
m=(low+high)/2;
if(L.r[0].key<L.r[m].key)
high=m-1;
else
low=m+1;
}
for(j=i-1;j<=low;--j)//将low到i-1的所有数都向后移动一个长度
L.r[j+1]=L.r[j];
L.r[high+1]=L.r[0]; //最后high+1或者low对应的位置就是要插入的位置
}
}
三、希尔排序
-
缩小增量排序
-
插入排序中效率最高的一种排序方法
void ShellSort(int *a,int len){//希尔排序
int temp=0; //中间值
int i=0;
int j=0;
int k=0;
for(i=(len/2);i>0;i=i/2){ //分组后一个组的长度
for(j=i;j<len;j++){
temp=a[0];
r=j-i;
while(r>=0&&temp<a[r]){
a[r+i]=a[r];
r=r-i;
}
a[r+i]=temp;
}
}
}
分析:
-
空间复杂度:O(1)
只占用了一个暂存单元
-
时间复杂度:O(n(log2n)2)
-
希尔排序是一个不稳定的排序方法(*)
四、冒泡排序
- 改进前的冒泡排序
void bubble1(int a[],int n){
int i,j,temp; //temp作为中间量
for(int i=0;i<j-1;i++){
for(int j=0;j<n-1-i;j++){
if(a[j]>a[j+1]){
temp=a[j]; //交换
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
- 改进后的冒泡排序
void bubblesort2(int a[],int n){
int i,j,flag,temp;
for(int i=0;i<j-1;i++){
flag=1;
for(int j=0;j<n-1-i;j++){
if(a[j]>a[j+1]){
temp=a[j]; //交换
a[j]=a[j+1];
a[j+1]=temp;
flag=0; //如果有交换,那么没有排好序
}
}
if(flag==1) //如果没有交换,说明已经有序,不需要再比较,直接退出
break;
}
}
分析:
- 最好情况:带排序的文件已经是有序文件,只需要进行一趟排序,共计比较关键字的次数为n-1
- 最坏情况:要经过n-1趟排序,所需总的比较关键字的次数为n(n-1)/2
- 空间复杂度O(1)
- 算法是稳定的
五、快速排序
基本思想:首先在r[1…n]中,确定一个r[i],经过比较和移动,使得r[i]左边的所有记录的关键字小于等于r[i].key,r[i]右边所有记录的关键字大于等于r[i].key。以r[i]为界,将文件划分为左右两个子文件,继续下去,使得每个文件只有一个记录为止。
void quksort(int r[],int low,int high){
int x,i,j;
if(low<high){
i=low;j=high;x=r[i]; //把r[i]空了出来
while(i!=j){
while(i<j&&r[i]<x)j--;//从最右端开始找,找到第一个比x小的数
if(i<j){
r[i]=r[j];i++; //这个位置的数已经确定,从下一个继续
while(i<j&&r[i]>x)i++;//从左端开始,找到第一个比x大的数
if(i<j){
r[j]=r[i];j--; //同上
}
}
}// 结束循环时,已经划分好两部分,即找到r[i]的位置,并且此时i=j
r[i]=x;
quksort(r,low,i-1); //处理左边
quksort(r,i+1,high); //处理右边
}
}
void quicksort(){
quksort(r,1.n);
}
分析:
- 平均时间复杂度O(nlog2n)
- 最坏情况下(基本有序),快排需要的比较次数和冒泡的比较次数相同,时间复杂度为O(n2)
- 快排需要一个栈空间来实现递归,若每次将文件均匀的分成两部分,所需要的栈空间为O(log2n),即空间复杂度是O(log2n)
- 快速排序是不稳定的
- 快速排序不适于对原本有序或基本有序的记录进行排序
- ”三者取中“的改进方案
六、选择排序
- 简单排序
void SelectSort(int r[],int n){
int i,j,min;
int x;
for(i=1;i<n;i++){
min=i;
for(j=i+1;j<=n;j++)//找到i+1到n里所有数中,最小的数
if(r[j]<r[min])
min=j;
if(min!=i){ //如果i不是最小的数,则交换,这样略去了多余的交换
x=r[min];
r[min]=r[i];
r[i]=x;
}
}
}
分析:
- 比较次数:O(n2)
- 交换次数:
- 最好情况:不移动记录
- 最坏情况:每次都移动3个,共3(n-1)次
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 该算法是不稳定的(交换时,就不确定交换的顺序了,毕竟不是顺序后移)
- 树形选择排序
树形选择排序又称锦标赛排序,是一种按照锦标赛的思想进行排序的方法,可以用一颗有n个叶子结点的完全二叉树表示
每个内点是它左右孩子中的最小值
分析:
-
需要另外开辟新的空间保存排序结果
-
需要额外的n-1个内部节点,增加了内存开销
-
将最小关键字改为极大数,再于兄弟结点比较属于多余
-
树形选择排序一般不是用来排序,而是证明某些问题
-
空间复杂度:O(n)
-
时间复杂度:
- 第一次选最小值,比较n-1次,以后每一次选最小值要比较n-1次,以后每选一次次小值,要比较log2n次,总的开销O(nlog2n)
-
树形选择排序是不稳定的。
- 堆排序(Heap Sort)
堆的定义(可以看作一个完全二叉树)
ki<=k2i且ki<=k2i+1 (小顶堆)
ki>=k2i且ki>=k2i+1 (大顶堆)
- 两个问题
- 如何初始化一个序列为堆
- 堆顶元素被替换后,调整剩余元素成一个新的堆
堆调整的算法:
void HeapAdjust(int r[],int s,int m){//r中从1开始存放根节点,s是要调整的位置
rc=r[s]; //保存调整的元素,空出s的位置
for(j=2*s;j<=m;j*=2){
if(j<m&&r[j]<r[j+1])//j<m表示s有右孩子j+1
j++; //计算s的具有较大值的孩子的序号
//这步结束时,j指向s的极大值的那个孩子
if(rc>r[j]) //如果rc比极大孩子的值还要大,那么就不能继续下沉
break; //也就是找到了要插入的位置
r[s]=r[j]; //没有找到要插入的位置,就将极大孩子值放到这个位置
s=j;
}
r[s]=rc; //将rc填入到正确的位置,符合堆的定义的位置
}
- 初始化一个大顶堆的过程需要依靠堆调整的过程
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 堆排序是不稳定的排序
- 堆排序对记录较少的文件不提倡,对较大的文件很有效
七、归并排序
把k个有序子文件合在一起,形成一个新的有序文件,同时归并k个有序子文件的过程称为k-路归并排序
2-路归并排序:归并2个有序子文件的排序
注:以下代码中,两个子文件在r中,由mid隔开
同时,这个算法在链表时使用过,当然也可以使用链式存储结构来尝试
void merge(int r[],int y[],int low,int mid,int high){
//归并两个有序子文件
int k=i=low,j=mid+1;
while(i<=mid&&j<=high){
if(r[i]<r[j]){
y[k]=r[i];
i++;
}
else{
y[k]=r[j];
j++;
}
k++;
}
while(j<=high){
y[k]=r[j];
j++;k++;
}
while(i<=mid){
y[k]=r[i];
i++;j++;
}
}
void mergepass(int r[],int y[],int s){
//一趟归并排序
int i=1;
while(i+2*s-1<=n){ //两两归并唱的 为s的子文件
merge(r,y,1,i+2-1,i+2*s-1); //low,mid,high
i=i+2*s;
}
if(i+s-1<n) //如果最后还有大于1个子文件,进行归并
merge(r,y,i,i+s-1,n);
else{
while(i<=n){ //如果最后的长度不够一个s,那么就将后面的直接复制过来,长度<=s时
y[i]=r[i];
i++;
}
}
}
void mergesort(int r,int n){
int y[n+1];
int s=1; //子文件的初始长度为1
while(s<n);{
mergepass(r,y,s);
s=2*s; //将r归并到y中
mergepass(y,y,s);
s=2*s; //再将y归并到r中
}
}
- 比较O(nlogn)次
- 移动O(nlogn)个记录
- 归并排序需要一个大小为n的辅助空间y
- 归并排序是稳定的
八、基数排序
基数排序进分析关键字自身每位的值,通过分配、回收进行处理。
从地位到高位,依次排序,产生有序序列
- 设有效数字位d位,需要d趟分配、回收
- 每趟分配运算时间O(n)
- 收集:基数为rd,即rd个队列,从rd个队列中收集,运算时间O(rd)
- 一趟分配、运算的时间O(n+rd),时间复杂度O(d*(n+rd))
- 基数排序是稳定的
- 辅助空间:每个队列首尾2个指针,共2rd个指针
内部排序的比较
一、时间性能
-
O(nlogn):快速排序、堆排序、归并排序
快排是被认为是最快的排序方法,后两者,比较,在n值较大的情况下,归并排序较堆排序更快
-
O(n2):插入排序、冒泡排序、选择排序
插入排序最为常用,选择排序过程中记录移动次数最少
-
O(n):基数排序
待排序的记录序列安关键字顺序有序时,应尽量避免快速排序
简单排序的三种方法中,冒泡排序效率最低
二、空间性能
- O(1):所有简单排序方法(插入、冒泡、选择)和堆排序
- O(logn):快速排序(递归程序执行过程中栈所需的辅助空间)
- O(n):归并排序和基数排序
三、稳定性
- 不稳定:希尔排序、快速排序、直接选择和堆排序(4个)
- 稳定:除以上排序、以上设计的排序均是稳定的
一般来说,排序的过程中所进行的比较操作和交换数据仅发生在相邻的记录之间,没有大布距的数据调整,则排序方法是稳定的
四、总结
-
排序的记录个数较小时,选择插入排序法,如果一个记录的数据项较多,占用空间大,应该降低移动次数,选用选择排序法,但若有“有序”倾向时,慎用快速排序
-
快速排序和归并排序在n值较小时的性能不及直接插入排序
-
基数排序的时间复杂度是O(d*n),适合n很大,d较小的情况
-
如果安最次位优先进行排序时,必须宣统稳定的排序方法(也就是,含有多个关键字时)