概述
-
按主关键字,排序结果唯一;按此关键字,不唯一。
-
排序算法的稳定和不稳定:相同元素排序前后相对位置的一致性?
-
分类:内排序和外排序 (看再内存中还是需要访问外存储器进行)
- 基于数据元素之间的比较操作
- 插入/选择/交换/归并/计数排序
- 不基于…(看策略)
- 基数排序
- 基于数据元素之间的比较操作
-
按排序工作量
- 简单的排序方法 O(n*n)
- 先进排序 O(n log n)
- 基数排序 O(d.n)
-
时间效率:
- 比较的次数
- 数据移动的次数
-
待排序数据元素存放方式(数组)
-
typedef struct { elemType r[MAX]; int length; }SqList L;
-
-
排序算法的评价标准
-
时间性能,空间性能
- 最好/最坏/平均情况
-
稳定性
-
插入排序
基本思想
分成有序和无序两部分,多趟插入排序将无序的放到有序的适当位置上
直接插入排序
哨兵(下标为零处)的作用很关键,在排序中存储将要插入的元素;
前边是有序的,待插入元素从后往前比较
平均性能
O(n*n/4) 当n<=16时,性能优于先进排序
void insertSort(SqList &L)
{ int i,j; //0是哨兵,1是第一个元素(看成有序)
for(i=2; i<=L.length; i++)//从第2个数据开始插入,n=L.length
if(L.r[i].key<L.r[i-1].key)//第i个数据比前面已经有序的i-1个数据最大的小
{ L.r[0]=L.r[i]; //将第i个数据放入哨兵位置
L[i]=L.r[i-1];//前一个元素后移一位,这里也可以不移动,但是后边整体移动的时候要改为 j 从 i-1 开始
for( j=i-2;L.r[0].key<L.r[j].key; --j )//L.r[0]存放的是此次要插入的第i个数据,将前边有序的元素与哨兵项比较,找到它的位置
L.r[j+1]=L.r[j];
L.r[j+1]=L.r[0]; }
}
评价
- 稳定的排序方法
- 额外的辅助空间
- n个待排序元素进行n-1次插入排序
- 最好:n-1次数据比较,0次数据移动
- 最坏:(n+2)(n-1)/2次数据比较,(n+4)(n-1)/2数据移动
- 平均时间复杂度:O(n*n);最好情况,基本有序时:O(n)
- **可证平均时间复杂度约为:**n2/4
- 当n<16时,n2/4<nlogn,理论值当n<16时,直接插入比O(nlogn)的排序方法快!**
二分插入排序
- n个元素n-1趟二分插入排序完成排序
- 第 i-1 趟二分插入排序时,前 i-1 个元素有序,待插入第 i 个元素,将大于第i个元素的数据后移一位
- 稳定的排序方法
算法实现
void BinsertSort(SqList &L)
{ int i,low,high,mid;
for(i=2;i<=L.length;i++)
if(L.r[i].key<L.r[i-1].key)
{
L.r[0]=L.r[i];low=1; high=i-1;//每趟在1-i-1的元素区间进行操作
while(low<=high)
{//二分的方法找到当前元素(即第i个元素在前边有序序列的位置)
mid=(low+high)/2;
if (L.r[0].key<L.r[mid].key) high=mid-1;
else low=mid+1;
}
//找到后进行整体的移动,要放入的位置是low或者high+1
for(j=i-1;j>=high+1;j−−)
L.r[j+1]=L.r[j];
L.r[high+1]=L.r[0];
}
}
同直接插入排序相比:
- 时间复杂度没变 n*n
- 减少了关键字的比较次数,移动次数没变
- 排序数据必须放在数组中,而直接排序还可以放在链表中
希尔排序(缩小增量法)
- 大问题分割成小问题,小问题采用直接插入排序,待小问题基本有序后再一起进行插入排序
- 增量序列应没有除1之外的公因子,并且最后一个增量必须等于1
- 增量序列做直接插入排序
- 不稳定
- 性能分析
- 时间性能
算法实现
void shell(SqList &L,int d)//增量为d的一趟希尔排序
{ //遍历分的每一组,对每一组进行直接插入排序
for(i=d+1;i<=L.length;i++)
if(L.r[i].key<L.r[i-d].key)
{ L.r[0]=L.r[i];L.r[i]=L.r[i-d];
for(j=i-2d;j>0&&(L.r[0].key<L.r[j].key);j=j-d )
L.r[j+d]=L.r[j];
L.r[j+d]=L.r[0];
}
}
交换排序
快速排序
基本思想
-
每趟快速排序选择一个基准,通过比较找到该基准再待排序序列中的位置,
-
一趟快速排序将待排序序列分成两个部分,大于和小于的
-
基准的选择:
- 第一个数据元素,最后一个,或者中间
-
一趟快速排序的步骤(每次快排在 l 和 h 之间进行)
-
不稳定的排序算法
-
平均时间复杂度 O(n log 2 n),最坏情况的快速排序的时间复杂度为 O(n*n)
-
一趟快速排序的算法实现
int partition( SqList L, int l, int h )//一趟快排 { L.r[0]=L.r[l];//取待排序的第一个数据元素为基准放到L.r[0] while(l<h)//一趟快排结束的条件是左右边界重合 l=h { //找到待排序列(l-h之间)右边部分(放比基准大的),比基准小的元素 while((l<h)&&(L.r[h].key >=L.r[0].key) ) h--; //把上边找到的小的元素移到左边 if(l<h){ L.r[l]=L.r[h]; l++;} //找到待排序列(l-h之间)左边部分(放比基准小的),比基准大的元素 while((l<h)&&(L.r[l]].key <L.r[0].key )) l++; //把上边找到的大的元素移到右边 if(l<h){ L.r[h]=L.r[l]; h--;} } //找到基准应放入的位置 l 或者 h,将哨兵里边的基准移到该处 L.r[l]=L.r[0]; return l; //返回基准位置用于后续排序 } //递归实现排序,结束的条件是待排序的数据元素个数小于等于1 void QSort(SqList &L,int l,int h)//快速排序算法 { int t; if(l<h)//待排序的数据有2个或2个以上才进行排序操作 { t = partition(L,l,h);//调用一趟快排,t为返回的基准的位置 QSort (L, l, t-1);//对比基准小的继续进行快速排序 QSort (L, t+1, h);//对比基准大的继续进行快速排序 } }
冒泡排序
基本思想:
- 待排序元素的关键字顺次两两比较,若为逆序则将两个元素交换
- 按照此方法从头到尾处理一遍称作一趟冒泡排序
- 一趟排序之后:关键字最大的元素交换到排序的最终位置
- 若一趟冒泡排序没发生任何数据元素的交换,则排序过程结束
- n个元素最多需要n-1趟冒泡排序,每趟待排序元素少一个,每次确定待排序中最大的那个
算法实现
void qppx(SqList &L)
{
int i,j,k;
j=1;k=1;
while((j<L.length) && (k>0))
{ k=0;
for(i=1;i<=L.length-j; i++)
if(L.r[i+1].key<L.r[i].key)//稳定的排序方法
{ L.r[0]=L.r[i];
L.r[i]=L.r[i+1];
L.r[i+1]=L.r[0];//需要一个额外的辅助空间
k++;}
j++;}
}
算法分析(n个数据元素)
- 最好情况:1趟冒泡排序,0次数据移动,n-1次比较
- 最坏情况:n-1趟冒泡排序,比较n(n-1)/2次,移动3n(n-1)/2次
- 平均时间复杂度:O(n*n)
- 一个额外的辅助空间
- 稳定的排序算法
选择排序
基本思想
每次从待排序选出最小的元素,顺序放在已排序的最后
堆排序
堆:键值序列
基本步骤:
- 使用筛选法(为例)或者插入法建堆:
- 第一次建堆,从中间元素开始,按照从下到上,从左到右的顺序将每个非叶节点调整为以该节点为根的子树为大顶堆
- 后续进行二次建堆只需要从根节点开始就行
- 进行多趟堆排序,每趟堆排序是待排元素减一,
- 每趟堆排序完成后要对剩余元素重新进行建堆
评价:
- 时间复杂度:O(nlog n)
- 适用于待排序元素较多
- 一个额外的辅助空间 L.r[0]
- 不稳定
算法实现
- 从 i = n/2 个数据建堆,按照从下到上,从右到左对非叶节点进行调整
- 辅助空间先存入第 i 个 结点
- 2i <= n有左孩子, 2i + 1<= n有右孩子,取左右孩子最大的跟第 i 个结点比较
void HeapAdjust(SqList &L,int s,int m)
//调整以s为根结点的子树为一个大顶堆 ,堆中最大的数据元素编号为m,且以s为根的子树中除根结点s外,均满足大顶堆的定义
// s 当前元素; m 元素总数。
{ int j;
L.r[0]=L.r[s];
for(j=2*s;j<=m;j=j*2)//j s的左孩子
{ if(j<m && L.r[j].key< L.r[j+1].key) ++j; //选出左右孩子中的最大者
if(L.r[0].key>=L.r[j].key) break; //与 s 进行比较
L.r[s]=L.r[j]; 0
s=j;
}
L.r[s]=L.r[0];
}
void HeapSort(SqList &L){
int i,j,k;
for(i=L.Length/2;i>0;--i) //筛选法建堆,从n/2处开始调整
HeapAdjust(L,i,L.Length); //调整以i为根结点的子树为一个大顶堆
for(i=L.Length;i>1;--i)
// n-1趟堆 排序,当前大顶堆中的数据元素i个,L.r[1]中是i个数据元素中的最大值
{ L.r[0]=L.r[i];
L.r[i]=L.r[1];
L.r[1]=L.r[0];
// 将堆中最大的数据元素L.r[1]交换到第i个位置,也是它最终排序后的位置
HeapAdjust(L,1,i-1);//这里从根节点开始调整
// 堆中数据元素个数为i-1,将i-1个数据元素重新调整为大顶堆
}
}
简单选择排序
- n个元素进行n-1趟选择,每次找出待排序的最小元素与第一个交换位置
- 不管输出的待排序数据是什么顺序,每一趟简单选择排序的比较次数不变都是 n(n-1)/2次
- 最好情况:移动零次
- 最坏情况:移动每趟进行元素位置交换,移动三次,进行n-1趟,共移动3(n-1)次
- 时间复杂度:O(n*n)
- 适用于待排序元素较少的情况
- 不稳定的算法
树排序?
归并排序
基本思想
待排序序列分成几个有序序列,拼接有序序列成为一个有序序列
分治策略
常采用二-路归并排序,即将两个相邻的有序序列合并成为一个有序序列
主要操作
-
已知待排序元素为SR[s,t],分成两路有序序列
-
对两个序列使用二路归并排序Msort算法
void Msort(ElemType SR[],ElemType &TR1[],int s,int t ) { // 将SR[s..t] 归并排序为 TR1[s..t] if(s==t) TR1[s]=SR[s];//序列中只有一个数据元素,序列自然有序 else//序列中包含2个或2个以上数据元素 { m=(s+t)/2;//计算序列的中间位置,以此为界划分为2个序列 Msort(SR,TR2,s,m);//对第一个子序列递归调用归并排序算法,使其有序 Msort(SR,TR2,m+1,t); //对第二个子序列递归调用归并排序算法,使其有序 Merge(TR2,TR1,s,m,t); //将2个有序子序列合并为一个有序序列 } } // Msort
-
申请 O(n)量级的辅助空间,使用Merge算法,二路归并到辅助空间中成为一个有序序列
//二路归并到辅助空间中成为一个有序序列 void Merge(ElemType SR[ ],ElemType &TR[ ],int s,int m,int t) { // 将有序的序列 SR[s..m] 和 SR[m+1..t]归并为有序的序列 TR[s..t] for(i=s,j=m+1,k=s;i<=m&&j<=t;++k) if(SR[i].key<=SR[j].key) TR[k]= SR[i++]; // i为第一个有序序列 SR[s..m] 当前正在查看的数据,该序列的第一个数据元素在s处; // j为第二个有序序列 SR[m+1..t]当前正在查看的数据,该序列的第一个数据元素在m+1处; // k为合并后的有序序列 TR[s..t]的存放位置,第一个位置为s else TR[k]=SR[j++]; if (i<=m) // 第一个有序序列 还有数据没有比较,将其复制到合并后的序列; for(;i<=m;) TR[k++]=SR[i++]; if(j<=t) // 第二个有序序列 还有数据没有比较,将其复制到合并后的序列 for(;j<=t;) TR[k++]=SR[j++]; } // Merge
算法分析
- 一趟时间复杂度为:O(n),共进行[log2 n ]趟
- 总时间复杂度为O(nlogn)
- 稳定的算法
基数排序
- 不通过比较排序元素实现排序
- 需要已知待排序元素值的取值范围
- 基本思想:
- 创建容器 “桶”(个数由数据元素的取值范围决定)
- 遍历数据元素将其放入对应的“桶”中
- 依据“桶”进行收集,完成排序
- 稳定的算法
- 局限性:适合关键字值集合不大
桶排序
- 不能像比较排序那样统一的数据元素之间的比较次数衡量工作量
- 分析每一步骤的工作进行的时间复杂度
- 多关键字:从优先级低的开始,通过对每个桶一次性收集形成的序列即时有序的
- 若从高优先级开始分配,通过对每个桶进行一次性收集完时并不能完成排序,需要通过对所有桶进行收集。
- “桶”使用队列模拟
- 收集时按照队列的先进先出进行收集
示例(52张牌排序)
- 先按面值,再分花色(优先级低的先开始)
- 13个桶对应13个面值,每个桶有四张牌——第一次分配
- 将每个桶放在一起——第一次收集(每一桶顺序收集即可)
- 对第一次收集的结果,准备四个桶对应四个花色——第二次分配
- 第二次收集,所有桶放一起就是最终排序
- 先按花色,再分面值(优先级高的先开始)
- 四个桶——第一次分配
- 四个桶放在一起——第一次收集
- 13个桶——第二次分配
- 第二次分配不能一起收集
- 正确的策略:
- 对四个桶优先级高的单独进行第二次分配到13个桶
- 在对13桶进行第二次一起收集可以得到最终序列
基数排序的方法
- 最高位优先MSD算法
- 最低位优先LSD算法
- 通常采用低位优先——简单方便
基数排序的实现
-
原始数据使用链表存储,队列实现桶(实际上就是两个指针限定每个桶的队首和队尾,其中的元素通过修改原始数据的每个元素的指针将其放入桶的头尾指针之间)
-
分配/收集(操作仅通过修改链表中的指针和设置队列的头尾指针实现)
- 收集:将前面桶(低位)的尾部和后面桶的头部相连
-
第n次基数排序的定义:第n次分配+第n次收集
-
时间复杂度:O(d(n+rd))
- n个数据元素分配到相应队列为O(n);收集为O(rd);
- d 分配的趟数;rd为基,关键字取值的个数;
-
空间复杂度:O(rd)
算法实现
#define MAX_NUM_OF_KEY 8//关键字个数最大值
#define radix 10//队列个数
#define MAX_SPACE 1000
typedef struct{
Keystype keys[MAX_NUM_OF_KEY];
……
int next;
}SLCell;
typedef struct{
SLCell R[MAX_SPACE];
int keynum;//关键字个数
int recnum;//待排序数据元素个数
}SLList;
typedef int ArrType[radix];
void Distribute(SLCell &R,int i,ArrType &f,Arrtype &r,int head)
//静态链表的数据元素从数组下标为0处存放,-1代表是尾结点
{ for(j=0;j<radix;j++) f[j]=-1;
for(p=head;p!=-1;p=R[p].next)
{ j=ord(R[p].keys[i]);//示意性操作,取R[p]的第i个关键字
if(f[j]==-1)f[j]=p;
else R[r[j]].next=p;
r[j]=p;
}
}
void collect(SLCell &R, int i, ArrType f, ArrType r, int &head)
//静态链表的数据元素从数组下标为0处存放,-1代表是尾结点
{ for(j=0;j<Radix&&f[j]==-1;j++);
head=f[j]; t=r[j];
while(j<Radix)
{ for(++j;j<Radix-1&&f[j]==-1;j++);
if(f[j]!=-1)
{ R[t].next=f[j];t=r[j]; }
}
R[t].next=-1;
}
void RadixSort(SLList &L)
{ //建立静态链表,数据元素从数组下标为0处存放,
//next成员为-1代表是尾结点
//head存放链表的头指针
for(j=0;j<L.recnum-1;j++) L.R[j].next=j+1;
L.R[L.recnum-1].next=-1;
head=0;
for(i=0;i<L.keynum;i++);
{ Distribute(L.R,i,f,r;head);
Collect(L.R,i,f,r,head);}
}
小结
时间性能
平均时间性能==(如何分析)==
- O(nlogn): 快速排序,堆排序,归并排序
- O(n*n): 直接插入排序,冒泡和简单选择排序
- O(n): 基数排序
当待排序序列按关键字顺序有序时
- 直接插入和冒泡排序:O(n)
- 快速排序: O(n*n)
简单选择排序,堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变
空间性能
排序所申请的辅助空间大小
所有简单排序方法(直接插入,冒泡,简单选择)和堆排序O(1)
快速排序O(logn),为递归过程栈需要的辅助空间
归并辅助空间需要最多,O(n)
链式基数排序需要设置队列首尾指针,O(rd)
比较次数/排序趟数
比较次数
- 无关:二路归并,简单选择,基数
- 有关:快速,直接插入,冒泡,堆,希尔
排序趟数
- 无关:直接插入,值班插入,希尔,简单选择,归并,基数
- 有关:冒泡,快速
其他
排序算法 | 快排、冒泡、堆排、归并、基数、递归、希尔、计数_基数排序、归并排序、堆排序-CSDN博客
排序序列按关键字顺序有序时
- 直接插入和冒泡排序:O(n)
- 快速排序: O(n*n)
简单选择排序,堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变
空间性能
排序所申请的辅助空间大小
所有简单排序方法(直接插入,冒泡,简单选择)和堆排序O(1)
快速排序O(logn),为递归过程栈需要的辅助空间
归并辅助空间需要最多,O(n)
链式基数排序需要设置队列首尾指针,O(rd)
比较次数/排序趟数
比较次数
- 无关:二路归并,简单选择,基数
- 有关:快速,直接插入,冒泡,堆,希尔
排序趟数
- 无关:直接插入,值班插入,希尔,简单选择,归并,基数
- 有关:冒泡,快速