排序
内部排序和外部排序
定义:
内部排序:在内存中排序;
外部排序:有辅助存储的排序(有可能有内存)
时间效率:比较次数和移动次数
空间效率:占内存辅存空间的大小
稳定性:A和B的关键字相等,排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
规则:插入排序,交换排序,选择排序,归并排序
插入排序
基本思想
每步将一个待排序的对象,按其关键字大小,插入到前面已经排好序的数列中适当的位置
直接插入排序
顺序查找后进行插入排序
基本步骤:在R[1…i-1]中查找R[i]的插入位置;
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[0]=L.[i];//哨兵
L.r[i]=L.r[i-1];//开始后移
for(j=i-2;L.r[0].key<L.r[j].key;--j)//往前找
{
L.r[j+1]=L.r[j];
}
L.[j+1]=L.r[0];
}
}
}
时间复杂度O(n^2),空间复杂度O(I)
最好的情况:每趟只需要比1次,总比较次数n-1
最坏的情况:第i趟比较i次
平均比较次数和移动次数为(n^2)/4
折半插入排序
void BInsertSort(SQList &L)
{
for(i=0;i<=L.Length;i++)
{
L.r[0]=L.r[i];
low=1;
high=i-1;
while(low<=high)
{
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>=high+1;--j)L.r[j+1]=L.r[j];//元素往后移
L.r[high+1]=L.[0]//赋值
}
}
分析:减少了比较次数,但没有减少移动次数
平均性能优于直接插入排序
时间复杂度:O(n^2);空间复杂度O(I)
希尔排序
基本思想:先将整个待排记录序列分割成若干个子序列(减少记录数量),分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
子序列的构成不是简单地逐段分割,而是将相隔某个增量dk的记录组成一个子序列让增量dk逐趟缩短,直到dk=1
void ShellSort(SqList &L,int dt[],int t)
{
//按增量序列dt[0,...t-1]对顺序表做Shell排序
for(k=0;k<t;k++)
{
ShellInsert(L,dt[k]);
}
}
void ShellInsert(SqList &L,int dk)
{
for(i=1+dk;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]=r[0];//插入排序
}
}
}
void shell_sort(const int start, const int end) {
int increment = end - start + 1; //初始化划分增量
int temp{ 0 };
do { //每次减小增量,直到increment = 1
increment = increment / 3 + 1;
for (int i = start + increment; i <= end; ++i) { //对每个划分进行直接插入排序
if (numbers[i - increment] > numbers[i]) {
temp = numbers[i];
int j = i - increment;
do { //移动元素并寻找位置
numbers[j + increment] = numbers[j];
j -= increment;
} while (j >= start && numbers[j] > temp);
numbers[j + increment] = temp; //插入元素
}
}
} while (increment > 1);
}
分析:空间复杂度O(1),是一种不稳定的排序方式
交换排序
基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序为止
起泡排序
优点:理顺数据,挤出最大值,没有交换动作时结束函数
时间复杂度O(n^2)
void bubble_sort(SqList &L)
{
int m,i,j,flag=1;RedType t;
m=L.length-1;
while((m>0)&&(flag==1))
{
flag=0;
for(j=1;j<=m;j++)
{
if(L.r[j].key>L.r[j+1].key)
{
flag=1;
t=L.r[j];
L.r[j]=L.r[j+1];
L.r[j+1]=t;
}
}
m--;
}
}
快速排序
基本思想:任取一个元素为中心,所有比它大的元素前放,所有比它小的元素后放。对各子表重新选取中心元素并依据此规则进行调整
时间复杂度O(nlog2n),空间效率O(log2n)【递归部分的辅助空间】
最坏的情况:关键字基本排好序的情况下,时间复杂度为O(n^2)
结论:就平均计算时间来说,快速排序是目前学过的最好的排序方法。具有不稳定性。
void main()
{
QSort(L,1,L.length);
}
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);
}
}
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;
}
选择排序
基本思想:每一趟在后面n-i+1个中选出关键码最小的对象,作为有序序列的第i个记录
简单选择排序
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]<L.r[k].key)k=j;
}
if(k!=i)
{
t=L.r[k];
L.r[k]=L.r[i];
L.r[i]=t;
}
}
}
移动次数:最好情况——0;最坏情况——3(n-1)【每次赋值就算一次移动】
比较次数(n-i)连加
时间效率:O(n^2)
空间复杂度:O(1)
稳定
树形选择排序(锦标赛排序)
简单排序没有利用前一次的比较结果,最小的数求出后,应该从被他打败过的数挑选出次小的数
比较次数:[log2n]+1
堆排序(重要⭐)
定义:树状结构,上一层的数一直小于/大于它的孩子结点
基本思想:将无序序列建成一个堆;输出堆顶的最大(小)值;使剩余的n-1个元素又调整成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列
步骤:从第n/2个元素起,至第一个元素止,进行反复筛选;【选出大的值作为根节点】
输出堆顶元素后,以堆中最后一个元素替代之;【扣除已经确定的数】
将根节点与左右子树根结点比较,并与最小者交换
重复直至叶子结点,得到新的堆
大根堆/小根堆
分析:时间效率:O(nlog2n)
空间效率:O(1)
稳定性:不稳定,适用于n较大的情况
void BuildHeap(SeqList R)
{
//将初始文件R[1..n]构造为堆
int i;
for(i=n/2; i>0; i--)
Heapify(R,i,n); //将R[i..n]调整为大根堆
}
//2.========大根堆调整=====
void Heapify( SeqList R,int low,int high)
{
//有左孩子且左孩子数据大于双亲结点
if(low*2<=high&&R[low].key<R[low*2].key)
{
R[0]=R[low];
R[low]=R[low*2];
R[low*2]=R[0];
}
有右孩子且右孩子数据大于双亲结点
if(low*2+1<=high&&R[low].key<R[low*2+1].key)
{
R[0]=R[low];
R[low]=R[low*2+1];
R[low*2+1]=R[0];
}
}
//3.========堆排序=====
void HeapSort(SeqList R)
{
//对R[1..n]进行堆排序,不妨用R[0]做暂存单元
int i;
BuildHeap(R); //将R[1..n]构造为初始大根堆
for(i=n; i>1; i--)
{ //对当前无序区R[1..i]进行堆排序,共做n-1趟。
R[0]=R[1];
R[1]=R[i];
R[i]=R[0]; //将堆顶和堆中最后一个记录交换
//将R[1..i-1]重新调整为堆,仅有R[1]可能违反堆性质。
Heapify(R, 1, i-1);
}
}
归并排序
定义:将两个或两个以上的有序表组合成一个新有序表
分析:时间效率O(nlog2n);空间效率:O(n);稳定性:稳定
void MergePass(SeqList R, int length)
{
int m=n/length;//求总组数
if(m*length!=n)m++;//除法是向下取整的,若非整除组数需+1
for(int i=1;i<=m;i+=2)//二路归并
{
int a,b,k;//定义两组的判断位置和已排好序部分的下一个位置
a=(i-1)*length+1;//a组的起始位置
b=a+length;//b组的起始位置
k=a;// 已排好序部分的下一个位置
if (b>n)return;//不存在b组的情况,排序完毕
//确定b组的结束位置,调用一次归并的函数(a组的结束位置只有一种情况)
if((i+1)*length<=n)
MergeOne(R,a,b,i*length,(i+1)*length);
else
MergeOne(R,a,b,i*length,n);
}
}
SeqList R1; //辅助空间
void MergeOne(SeqList R,int s1,int s2,int e1,int e2)//合并两组数据
{
int i=s1,j=s2,p=s1; //置初始值
while(i<=e1 && j<=e2) //两子文件非空时取其小者输出到R1[p]上
{
R1[p++]=(R[i].key<=R[j].key)?R[i++]:R[j++];
}
while(i<=e1) //若第1个子文件非空,则复制剩余记录到R1中
{
R1[p++]=R[i++];
}
while(j<=e2) //若第2个子文件非空,则复制剩余记录到R1中
{
R1[p++]=R[j++];
}
for(p=s1;p<=e2;p++)
{
R[p]=R1[p]; //归并完成后将结果复制回去
}
}
//2.========二路归并排序=====
void MergeSort(SeqList R)
{
int length;
for (length=1; length<n; length*=2) //做[lgn]趟排序
MergePass(R,length); //有序长度≥n时终止
}
基数排序
n个记录,每个记录有d位关键字,关键字取值范围rd[如十进制为10]
时间效率O(d(n+rd));空间效率O(n+rd)
外部排序P165
外排总时间=产生初始归并段的时间+外存信息读写时间+内部归并所需时间
外存信息读写时间远大于排序时间,所以外排时间取决于读写外存的次数
假设待排记录系列含有m个初始归并段,外排采用k路归并,则归并趟数s=[logkm]
胜者树和败者树都是完全二叉树,是树形选择排序的一种变型。每个叶子结点相当于一个选手,每个中间结点相当于一场比赛,每一层相当于一轮比赛。
胜者树
胜者树的一个优点是,如果一个选手的值改变了,可以很容易地修改这棵胜者树。只需要沿着从该结点到根结点的路径修改这棵二叉树,而不必改变其他比赛的结果。
败者树
败者树是胜者树的一种变体。在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,需要加一个结点来记录整个比赛的胜利者。采用败者树可以简化重构的过程。
败者树简化了重构。败者树的重构只是与该结点的父结点的记录有关,而胜者树的重构还与该结点的兄弟结点有关。
每次从k个组中的首元素中选一个最小的数,加入到新组,这样每次都要比较k-1次,故算法复杂度为O((n-1)*(k-1)),而如果使用败者树,可以在O(logk)的复杂度下得到最小的数,算法复杂度将为O((n-1)*logk), 对于外部排序这种数据量超大的排序来说,这是一个不小的提高。
置换-选择排序的操作
长度分析:内存可读入m个记录,合并段的平均长度为2m
败者树实现置换-选择排序
typedef struct
{
RcdType rec;//记录
KeyType key;//关键字
int rnum;//所属归并段的段号
}RcdNode,WorkArea[w];
void Replace_Selection(LoserTree &ls,WorkArea &wa,FILE *fi,FILE *fo)
{
Construct_Loser}
最佳归并树
http://data.biancheng.net/view/79.html
排序算法比较
快速比较是基于比较的内部排序中平均性能最好的
基数排序时间复杂度最低,但对关键字结构有要求【知道各级关键字的主次关系和取值范围】
习题
1.快速排序在下列( )情况下最易发挥其长处。
A.被排序的数据中含有多个相同排序码
B.被排序的数据已基本有序
C.被排序的数据完全无序
D.被排序的数据中的最大值和最小值相差悬殊
答案:C
解释:B选项是快速排序的最坏情况。
2.不稳定排序有希尔排序、快速排序、堆排序;稳定排序有直接插入排序、折半插入排序、冒泡排序、归并排序、基数排序、简单选择排序。
3.下列排序算法中,( )不能保证每趟排序至少能将一个元素放到其最终的位置上。
A.希尔排序 B.快速排序 C.冒泡排序 D.堆排序
答案:A
解释:快速排序的每趟排序能将作为枢轴的元素放到最终位置;冒泡排序的每趟排序能将最大或最小的元素放到最终位置;堆排序的每趟排序能将最大或最小的元素放到最终位置。
3.给出如下关键字序列{321,156,57,46,28,7,331,33,34,63},试按链式基数排序方法,列出每一趟分配和收集的过程。
答案:
按最低位优先法 →321→156→57→46→28→7→331→33→34→63
分配 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
321 33 34 156 57 28
331 63 46 7
收集 →321→331→33→63→34→156→46→57→7→28
借助于快速排序的算法思想,在一组无序的记录中查找给定关键字值等于key的记录。设此组记录存放于数组r[l…n]中。若查找成功,则输出该记录在r数组中的位置及其值,否则显示“not find”信息。请简要说明算法思想并编写算法。
[题目分析]把待查记录看作枢轴,先由后向前依次比较,若小于枢轴,则从前向后,直到查找成功返回其位置或失败返回0为止。
[算法描述]
int index (RecType R[],int l,h,datatype key)
{int i=l,j=h;
while (i<j)
{ while (i<=j && R[j].key>key) j--;
if (R[j].key==key) return j;
while (i<=j && R[i].key<key) i++;
if (R[i].key==key) return i;
}
cout<<“Not find”; return 0;
}//index