什么是排序?
排序方法的分类
按存储介质可分为:
内部排序:数据量不大、数据在内存,无需内外存交换数据
外部排序:数据量较大、数据在外存(文件排序)
外部排序时,要将数据分批调入内存在排序,中间结果还要及时放入外存,显然外部排序要复杂得多。
按比较器个数可分为:
按主要操作可分为:
比较排序:用比较的方法
插入排序、交换排序、选择排序、归并排序
基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。
按辅助空间可分为:
原地排序:辅助空间用量为O(1)的排序方法。
(所占的辅助存储空间与参加排序的数据量大小无关)
非原地排序:辅助空间用量超过O(1)的排序方法。
按稳定性可分为:
按自然性可分为:
存储结构——记录序列以顺序表存储
#define MAXSIZE 20
typedef int KeyType;//设关键字为整型量(int型)
Typedef struct{//定义每个记录(数据元素)的结构
KeyType key;//关键字
InfoType otherinfo;//其他数据项
}RedType;
Typedef struct{//定义顺序表的结构
RedType r[MAXSIZE+1];//存储顺序表的向量
int length;//r[0]一般作哨兵或缓冲区
}SqList;
基本思想:每一步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
即边插遍排序,保证子序列中随时都是排好序的
基本操作:有序插入
有序插入方法:
插入排序的种类:
顺序法定位插入位置————直接插入排序
二分法定位插入位置————二分插入排序
缩小增量多遍插入排序————希尔排序
x=a[i];
for(j=i-1;j>=0&&x<a[j];j--)
a[j+1]=a[j];
a[j-1]=x;
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];
void InsertSort(SqList &L){
int i,j;
for(i=2;i<=L.length;++i){
if(L.r[i].key<L.r[i-1].key){//若“<”,需将L.r[i]插入有序子表
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];//插入到正确位置
}
}
}
实现排序的基本操作有两个:
直接插入排序在什么情况下效率比较高?
直接插入排序在基本有序时,效率较高
在待排序的记录个数较少时,效率较高
查找插入位置时采用折半查找法
void BInsertSort(SqList &L){
for(i=2;i<=L.length;++i){//依次插入第2~第n个元素
L.r[0]=L.r[i];//当前插入元素存到“哨兵”位置
low=1;
high=i-1;
while(low<=high){
mid=(mid+high)/2;
if(L.r[0].key<L.r[mid].key)high=mid-1;
else low=mid+1;
}//循环结束,high+1则为插入位置
for(j=i-1;j>=high+1;--j)
L.r[j+1]=L.r[j];
L.r[high+1]=L.r[0];
}
}
算法分析:
基本思想:先将整个待排记录序列分割成若干个系列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序算法,特点:
希尔排序特点
希尔排序算法(主程序)
void ShellSort(Sqlist &L,int dlta[],int t){
//按增量序列dlta[0..t-1]对顺序表L作希尔排序。
for(k=0;k<t;++k)
ShellInsert(L,dlta[k]);//一趟增量为dlta[k]的插入排序
}
void ShellInsert(SqList &L,int dk){
//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子
for(i=dk+1;i<=L.length;++i){
if(r[i].key<r[i-dk].key){
r[0]=r[i];
for(j=i-dk;j>0&&(r[0].key<r[j].key);j=j-dk)
r[j+dk]=r[j];
r[j+dk]=r[0];
}
}
}
希尔排序算法分析
希尔排序是一种不稳定的排序方法
基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
常见额交换排序方法:
冒泡排序O(n2)
快速排序O(nlog2n)
冒泡排序——基于简单交换思想
基本思想:每趟不断将记录两两比较,并按“前小后大”规则交换
总结:n个记录,总共需要n-1趟
第m趟需要比较 n-m次
void bubble_sort(SqList &L){
int m,i,j;
RedType x;//交换时临时存储
for(m=1;m<=n-1;m++){//总共需m趟
for(j=1;j<=n-m;j++)
if(L.r[j].key>L.r[j+i].key){//发生逆序
x=L.r[j];
L.r[j]=L.r[j+1];
L.r[j+1]=x;//交换
}
}
}
优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;
如何提高效率?
一旦某一趟比较时不出现记录交换,说明已排好序了,就可以结束本算法。
改进的冒泡排序算法
void bubble_sort(SqList &L){
int m,i,j,flag=1;
RedType x;//flag作为是否有交换的标记
for(m=1;m<=n-1&&flag==1;m++){
flag=0;
for(j=1;j<=m;j++)
if(L.r[j].key>L.r[j+1].key){//发生逆序
flag=1;//发生交换,flag置为1,若本趟没发生交换,flag保持为0
x=L.r[j];
L.r[j]=L.r[j+1];
L.r[j+1]=x;//交换
}
}
}
快速排序——改进的交换排序
基本思想:
基本思想:通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序。
具体实现:选定一个中间数作为参考,所有元素与之比较,小的调到其左边,大的调到其右边。
(枢轴)中间数:可以是第一个数、最后一个数、最中间一个数、任选一个数等。
void main(){
QSort(L,1,L.length);
}
void QSort(SqList &L,int low,int high){
if(low<high){//长度大于1
pivotloc=Partition(L,low,high);
//将L.r[low..high]一分为二,pivotloc为枢轴元素排好序的位置
QSort(L,low,pivotloc-1);//对低子表递归排序
QSort(L,pivotloc+1,high);//对高子表递归排序
}
}
int Partition(SqList &L,int low,int 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[low].key<=pivotkey)
++low;
L.r[high]=L.r[low];
}
L.r[low]=L.r[0];
return low;
}
快速排序算法分析
时间复杂度
空间排序
快速排序不是原地排序
由于程序中使用了递归,需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归,也需要用用户栈)
稳定性:快速排序是一种不稳定的排序方法
由于每次枢轴记录的关键字都是大于其它所有记录的关键字,致使一次划分之后得到的子序列(1)的长度为0,这时已经退化成为没有改进措施的冒泡排序。
快速排序不适于对原本有序或基本有序的记录序列进行排序。
划分元素的选取是影响时间性能的关键
输入数据次序越乱,所选划分元素值的随机性越好,排序适度越快,快速排序不是自然排序方法。
改变划分元素的选取方法,至多只能改变算法平均情况下的世界性能,无法改变最坏情况下的时间性能。即最坏情况下,快速排序的时间复杂性总是O(n2)
基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。
基本操作:
void SelectSort(SqList &K){
for(i=1;i<L.length;++i){
k=i;
for(j=i+1;j<=L.length;j++)
if(L.r[j].key<L.r[k].key) k=j;//记录最小值位置
if(k!=i)
L.r[i]←→L.r[k];//交换
}
}
时间复杂度
算法稳定性
则分别称该序列{a1 a2 … an}为小根堆和大根堆。
从堆的定义可以看出,堆实质是满足如下性质的完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点。。
堆排序:若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重新又建成一个堆,则得到n个元素的次小值(次大值)……如此反复,便能得到一个有序序列,这个过程称之为堆排序。
实现堆排序需解决两个问题:
小根堆:
堆的调整
筛选过程的算法描述为:
void HeapAdjust(elem R[],int s,int m){
/*已知R[s..m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s..m
]成为一个大根堆*/
rc=R[s];
for(j=2*s;j<=m;j*=2){//沿key较大的孩子结点向下筛选
if(j<m&&R[j]<R[j+1])//j为key较大的记录的下标
++j;
if(rc>=R[j]) break;
R[s]=R[j];//rc应该插入在位置s上
s=j;
}
R[s]=rc;//插入
}
可以看出:
对一个无序序列反复“筛选”就可以得到一个堆;即:从一个无序序列建堆的过程就是一个反复“筛选”的过程。
如何由无序序列建成一个堆?
单结点的二叉树是堆;
在完全二叉树中所有以叶子结点(序号i>n/2)为根的子树是堆。
这样,我们只需依次将以序号为n/2,n/2-1,……,1的结点为根的子树均调整为堆即可。
即:对应由n个元素组成的无序序列,“筛选”只需从第n/2个元素开始。
由于堆实质上是一个线性表,那么我们可以顺序存储一个堆。
从最后一个非叶子结点开始,以此向前调整:
由以上分析知:
若对一个无序序列建堆,然后输出根;重复该过程就可以由一个无序序列输出有序序列。
实质上,堆排序就是利用完成二叉树中父结点与孩子结点之间的内在关系来排序的。
堆排序算法如下:
void HeapSort(elem R[]){//对R[1]到R[n]进行堆排序
int i;
for(i=n/2;i>=i;i--)
HeapAdjust(R,i,n);//建初始堆
for(i=n;i>1;i--){//进行n-1趟排序
Swap(R[1],R[i]);//根与最后一个元素交换
HeapAdjust(R,1,i-1);//对R[1]到R[i-1]重新建堆
}
}
关键问题:如何将两个有序序列合成一个有序序列?
基本思想:分配+收集
也叫桶排序或箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接。
基数排序:数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、百…进行排序。
一、时间性能
二、空间性能
指的是排序过程中所需的辅助空间大小
三、排序方法的稳定性能
四、关于“排序方法的时间复杂度的下限”
本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)。
(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)
可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。