设n个记录{R1,R2,…,Rn},其关键字序列{K1,K2,…,Kn},重新确定1,2,…,n的一种排列p1,p2,…pn, 使得Kp1≤Kp2≤…≤Kpn,从而将{R1,R2,…,Rn}重新排列为{Rp1,Rp2,…,Rpn}的操作。
稳定排序和不稳定排序
关键字不是主关键字,排序结果不唯一;
对关键字相同的记录(设Ki=Kj (i≠j)),若排序前i
不稳定的排序方法:排序后的序列中Rj领先于Ri
内部排序:整个排序过程不需要访问外存便能完成
外部排序:参加排序的记录数量很大,内存无法一次性容纳全部记录,则在排序过程中需对外存进行访问,不断地在内、外存之间进行数据交换。
比较两个关键字的大小
移动记录,和存储方式有关
typedef struct {
KeyType key; //关键字项
//记录中的其它数据项
}RecordType; //记录类型
typedef struct {
RecordType r[MAXSIZE+1]; //r[0]未用
int length; //待排序记录的个数
}SqList; //顺序表类型
是主要借助插入操作完成排序的一类方法
直接插入排序
思想
将无序序列的一个或几个记录按关键字的大小,插入到已排好序的有序序列中,使得有序序列的长度增加,而无序序列的长度减少。
实现过程
初始时认为第1个记录已排好序;
从待排序的序列中取出第1个记录,按关键字的大小,插入到已排好序的有序序列中,使之成为长度加1的有序序列,而待排序的序列长度减1;
重复执行上一步操作,直至待排序的记录都插入到有序序列中为止,即最终待排序的序列长度为0,而有序序列中包含了所有记录。
第i(2≤i≤n)趟直接插入排序
在有序表r[1…i-1]中插入记录r[i],使得子序列r[1…i]成为长度为i的有序表
监视哨
在有序序列中查找插入位置:采用顺序查找算法。在有序序列中从后向前进行查找,边查找边后移关键字大于r[i].key的记录。为避免每次都要检查数组下标是否出界,在r[0]处设监视哨。
算法
void InsertSort(SqList &L)
{
//用直接插入排序方法对L指示的顺序表进行排序
for (i=2; i<=L.length; i++)
if(LT(L.r[i].key,L.r[i-1].key))
{
L.r[0]=L.r[i]; //设监视哨
L.r[i]=L.r[i-1];
for(j=i-2;LT(L.r[0].key,L.r[j].key);j--)
L.r[j+1]=L.r[j]; //记录后移
L.r[j+1]=L.r[0];
}
}
- 效率分析
- 空间复杂度:O(1),需要一个监视哨的辅助空间
- 稳定性:是稳定的排序方法
- 时间复杂度:O(n2)
- 优缺点
优点:
在待排序序列是“正序”或接近“正序”时该算法的时间复杂度可提高至O(n)的数量级;
由于该算法简单,所以在待排序序列长度n较小时(待排序的记录个数比较少),其时间效率也是较高的
缺点:时间效率整体不高
- 改进方法
减少记录的移动次数
减少关键字之间的比较次数
其它插入排序方法
折半插入排序
稳定,适用于待排序记录数量很大的情况。
思想
在有序表R[1…i-1]中插入R[i]时,由于R[1…i-1]是一个按关键字有序的序列,因此可以用折半查找来确定R[i]的插入位置
实现过程
Step1:用折半查找确定R[i]在R[1…i-1]中的插入位置
Step2:最后一个记录到插入位置的记录依次后移一个位置
Step3:插入记录R[i]
void BinInsertSort(SqList &L)
{
for ( 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(LT(L.r[0].key,L.r[m].key)) high=m-1;
else low=m+1;
}//while
for(j=i-1;j>=high+1;j--) L.r[j+1]=L.r[j]; //记录后移
L.r[high+1]=L.r[0]; //插入第i个记录
}//for
}//BinInsertSort
- 效率分析
- 空间复杂度:O(1),需要一个监视哨的辅助空间
- 稳定性:是稳定的排序方法
- 时间复杂度:O(n2)
/*------------------------------------------------*/
- 表插入排序方法
若希望在排序过程中不移动记录,只能改变存储结构,使用静态链表存储待排序序列,进行表插入排序
设静态链表中已排好序的子序列以循环链表表示,并设数组中下标为0的分量为头结点,令其关键字为排序过程中不可能达到的最大值MAXINT。
/*------------------------------------------------*/
- 希尔排序
特点: 相隔某个增量的记录组成一个子序列。
希尔排序又称“缩小增量排序”
- 思想
先将待排序记录序列按指定的增量间隔分割成若干子序列分别进行直接插入排序
不断缩小增量间隔,重复以上操作
待整个序列中记录“基本有序” 时,最后再对全体记录进行一次直接插入排序。
- 实现过程
- 算法实现
- 一趟希尔排序
void ShellInsert(SqList &L,int dk)
{
//对L指示的顺序表进行一趟希尔排序,增量为dk
for (i=dk+1; i<=L.length ; ++i)
if (LT(L.r[i].key, L.r[i-dk].key))
{
//将L.r[i]插入到有序子序列中
L.r[0] = L.r[i];
for(j=i-dk; j>0 && LT(L.r[0].key, L.r[j].key); j-=dk)
L.r[j+dk] = L.r[j];
L.r[j+dk] = L.r[0];
}
} // ShellInsert
- 缩小增量
void ShellSort (SqList &L,int dlta[],int t)
{
//按增量序列dlta[0..t-1]对L指示的顺序表进行希尔排序
int k;
for (k=0; k<t; ++k ) ShellInsert(L,dlta[k]);
} //Shellsort
- 效率分析
- 空间复杂度:O(1),需要一个暂存记录的辅助空间
- 稳定性:不稳定
- 时间复杂度
其运行时间取决于增量序列 ,是增量和记录数的函数
/*------------------------------------------------*/
冒泡排序
思想
一旦进行比较的两个记录的关键字逆序(即顺序与排序要求相反),则交换它们的位置。
冒泡排序过程
第1趟:
从第1到第n个记录,若相邻的两个记录逆序,则交换之;否则继续比较下面两个相邻记录……结果使关键字最大的记录被安置到第n个位置上。
第i趟
从第1到第n-i+1个记录,依次比较两个相邻记录的关键字,逆序时交换之, 使其中关键字最大的记录被交换到第n-i+1个位置上。
结束条件
在一趟排序过程中未出现交换记录的操作,则整个序列有序。
算法实现
void BubbleSort(SqList &L)
{
//对L指示的顺序表进行冒泡排序
for(i=1;i<L.length;i++)
{
//最多进行n-1趟
flag=TRUE; // flag:标志
for(j=1;j<=L.length-i;j++)
if( LT(L.r[j+1].key,L.r[j].key))
{
flag=FALSE;
temp=L.r[j+1];L.r[j+1]=L.r[j];L.r[j]=temp;
}
if(flag) return;
}//for
}//BubbleSort
- 效率分析
- 空间复杂度:O(1),需要一个供交换用的辅助空间
- 稳定性:是稳定的排序方法
- 时间复杂度:O(n2)
/*------------------------------------------------*/
快速排序
思想
在待排序序列中任选一个记录作为枢轴,通过一趟排序将待排序记录分割成独立的两部分,其中前一部分记录的关键字均小于等于枢轴的关键字,后一部分记录的关键字都大于等于枢轴的关键字;
分别对这两个子序列按照同样方法再进行快速排序(划分),直到分割出的每个子序列只包含一个记录为止;
此时整个序列达到有序
一趟快速排序
思想
通常选取待排序序列的第一个记录作为枢轴(或称支点),然后将关键字比枢轴关键字小的记录都放到它前面,将关键字比枢轴关键字大的记录都放到它后面。因此一趟快速排序之后,以枢轴最后的位置为分界线,将待排序序列分成独立的两部分。其中前一部分记录的关键字都不大于后一部分记录的关键字。
过程
选取待排序序列的第一个记录作为枢轴(或称支点),将其关键字暂存在变量pivotkey中;
设置两个指针(实际上是整型数,指示数组下标)low和high,其初值位置分别为待排序序列的下界和上界位置;
先从high所指位置开始向左搜索,找到第一个关键字小于pivotkey的记录,将它和枢轴记录互换位置;
接着从low所指位置开始向右搜索,找到第一个关键字大于pivotkey的记录,将它和枢轴记录互换位置;
重复以上两步,直至low=high为止。
实现
int Partition(SqList &L,int low,int high)
{
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[low]<->L.r[high];
}//while
return low; //返回枢轴所在位置
}// Partition
- 改进
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];
}//while
L.r[low]=L.r[0];
return low;
}// Partition
- 递归调用
void QSort ( SqList &L,int low, int high )
{
if (low < high)
{
pivotloc=Partition(L,low,high);
QSort (L, low, pivotloc-1) ; //对低端子序列递归排序
QSort (L, pivotloc+1, high ); //对高端子序列递归排序
}
}// QSort
void QuickSort ( SqList &L )
{
//对L指示的顺序表进行快速排序
QSort ( L, 1, L.length );
} // QuickSort
- 效率分析
- 空间复杂度:O(logn),需要一个栈空间
若每趟排序都能将记录序列均匀分割成长度相近的两个子序列,则栈的最大深度为 (包括最外层参数进栈)
若每趟排序后枢轴都偏向子序列的一端,则栈的最大深度为n。
- 改进
在一趟排序后比较分割出的2个子序列的长度,然后先对长度短的子序列进行下一趟快排,这样栈的最大深度可降为O(logn)
- 稳定性:不稳定
- 时间复杂度:O(nlogn)
就平均时间而言,快速排序性能最好
若经过每一趟快速排序得到的两个子序列的长度基本相等,则快速排序的时间效率最高: O(nlogn)
若初始序列按关键字有序或基本有序(正序或逆序),则枢轴总是子序列中关键字最小或最大的记录,这样每趟快排划分出的两个子序列都有一个接近空序列,此时快速排序将蜕化为冒泡排序:O(n2)
- 改进
使用“三者取中法”来选取枢轴记录,即:比较r(low).key、r(high).key和r((low+hight)/2).key,然后取三者之中关键字居中的那个记录作为枢轴。
/*------------------------------------------------*/
简单选择排序
思想
每一趟在i…n共n-i+1(i=1,2,……n-1)个记录中选取关键字最小的记录使之成为有序序列的第i个记录。
过程
第1趟
在第1到第n共n个记录中通过n-1次关键字之间的比较,选取关键字最小的记录,若它不是第1个记录,则交换之;
第i趟
在第i到第n共n-i+1个记录中通过n-i次关键字之间的比较,选取关键字最小的记录,若它不是第i个记录,则交换之;
算法
void SelectSort ( SqList &L )
{
//对L指示的顺序表进行简单选择排序
for(i=1; i<L.length; ++i)
{
minloc=i;
for(j=i+1;j<=L.length;j++)
if(LT(L.r[j].key, L.r[minloc].key)) minloc=j;
if (i!=minloc)
{
L.r[i] <-> L.r[minloc];}
}//for
} // SelectSort
- 分析
- 空间复杂度:O(1),需要一个供交换用的辅助空间
- 稳定性:不稳定
- 时间复杂度:O(n2)
/*------------------------------------------------*/
树形选择排序
思想
借鉴锦标赛赛制安排的思想
分析
/*------------------------------------------------*/
堆排序
堆
算法
void HeapAdjust(SqList &H, int s, int m)
{
/*已知H->r[s..m]中记录的关键字除H->r[s].key之外均满足堆的定义,本函数调整H->r[s]的关键字,使H->r[s..m]成为一个大顶堆*/
rc= H.r[s]; //rc暂存子树根结点的元素值
for(j=2*s; j<=m; j*=2)
{
if (j<m && LT(H.r[j].key, H.r[j+1].key)) ++j;
if (!LT(rc.key, H.r[j].key)) break;
H.r[s]= H.r[j]; s=j;
}
H.r[s]=rc; //把rc放入到最终应该在的位置
}// HeapAdjust
- 堆排序算法
void HeapSort(SqList &H)
{
// 对H指向的顺序表进行堆排序
for(i=H.length/2; i>0; --i )
HeapAdjust(H, i, H.length);
for(i=H.length; i>1; --i )
{
H.r[1]<->H.r[i];
HeapAdjust(H,1,i-1);
}//for
}// HeapSort
- 效率分析
- 空间复杂度:O(1),需要一个暂存记录的辅助空间
- 稳定性:不稳定
- 时间复杂度:O(nlogn)
/*------------------------------------------------*/
思想
通常采用2-路归并算法
将两个或两个以上有序子序列“归并”为一个有序序列
过程
初始时可看成n个有序子序列,每个子序列长度为1
两两归并得到n/2个长度为2或1的有序子序列
再两两归并……直至得到一个长度为n的有序序列为止
效率分析