注:参考严老师的数据结构c语言实现,自复习用
目录
一.概述
一些概念以及知识点
- 排序方法的稳定性
稳定性 | 特点 |
---|---|
稳定 | 对于关键字相同的两个记录,排序前后其相对位置一定不变 |
不稳定 | 关键字相同的两个记录,排序前后其相对位置可能改变 |
- 存储器不同的划分
分类 | 描述 |
---|---|
内部排序 | 待排序记录存放在计算机随机存储器中进行排序 |
外部排序 | 待排序记录数量很大,内存不能容纳全部记录,在排序过程中需要对外存进行访问 |
- 根据不同原则对内部排序方法进行分类
分类 |
---|
插入排序 |
交换排序 |
选择排序 |
归并排序 |
计数排序 |
- 根据工作量
分类 | 特点 |
---|---|
简单的排序方法 | 时间复杂度:O(n2) |
先进的排序方法 | 时间复杂度:O(nlogn) |
基数排序 | 时间复杂度:O(nd) |
- 排序过程中的基本操作:比较和移动
二.插入排序
2.1 直接插入排序
- 基本思想:对于一组记录:R[1……n],依次将i=2……n,的R[i]插入到前面R[1……i-1]的有序排列中。
- 关键点:
1. 循环开始时,R[1]自己一个元素看作是一个有序排列,从i=2开始依次插入。
2. 首先令R[i]与R[i-1]做比较:若R[i]>=R[i-1]则已经有序不用插入进行。否则循环寻找插入位置。(注等号判断位置的放置要满足其稳定性特点)
3. 寻找插入位置:
4. R[0]作为排列的监视哨,在寻找R[i]在R[1……i-1]中寻找插入位置前先将R[i]记录于R[0]中,防止在寻找插入位置时被R[i-1]的记录覆盖掉而丢失信息。
5. j从第R[i-1]开始到R[1]倒序与R[0]进行关键字比较,若R[j]大于R[0],则将R[j+1]=R[j]赋值,则此时R[j+1]记录原R[j]信息,就不怕R[j]信息被覆盖掉而丢失。
6. 可以知道,上述循环结束后,R[j]为从后往前第一个关键字等于或小于R[0]的记录。则将R[0]插入到R[j+1]的位置就可以了。
注:j也可以从i开始倒序,R[0]比较R[j-1] - 算法分析:
指标 | 值 | 分析 |
---|---|---|
空间复杂度T(n) | O(1) | 只需要辅助空间R[0] |
时间复杂度S(n) | O(n2) | 当待排序序列为正序时,比较次数为n-1,移动次数为0;当待排序序列为反序时,比较次数为 ∑ i = 1 n − 1 i = n ( n − 1 ) 2 \sum_{i=1}^{n-1}i=\frac{n(n-1)}{2} ∑i=1n−1i=2n(n−1),移动次数为 ∑ i = 1 n − 1 i = n ( n − 1 ) 2 \sum_{i=1}^{n-1}i=\frac{n(n-1)}{2} ∑i=1n−1i=2n(n−1),共n(n-1);取最好情况与最坏情况的平均值,为 ( n + 1 ) ( n − 1 ) 2 \frac{(n+1)(n-1)}{2} 2(n+1)(n−1),即O(n2) |
2.2 折半插入排序
- 基本思想:为了弥补简单插入排序当n很大时比较和交换次数过多的缺陷,从比较次数着手,将简单插入中的逆序查找插入位置替换为二分查找插入位置,找到后再统一将插入位后面的记录后移。
- 算法分析
指标 | 值 | 分析 |
---|---|---|
空间复杂度 | O(1) | 需要low、high与m即可 |
时间复杂度 | O(n2) | 折半查找只是减少了比较次数,而没有减少交换次数,而直接插入排序中决定时间复杂度的主部是交换次数,故仍为O(n2) |
2.3 2-路插入排序
- 基本思想:在折半查找的基础上改进以减少交换次数。令附n个记录的辅助空间向量d[1……n],且d[1]=R[1]。再遍历R[2……n],较d[1]小则插入在d[1]前面(插入位置的前面的元素向前挪腾位置),否则插入在d[1]后面(插入位置后面的元素往后挪腾地方)。而将d看作循环向量,并设两个指针first和final,则d[1]"前面的"元素即d[first]~d[n],然后是d[1],再“后面的”即d[2]~d[final]。
- 算法分析
指标 | 值 | 分析 |
---|---|---|
空间复杂度 | O(n) | 需要d[1……n]作为辅助向量 |
时间复杂度 | O(n2) | 实际上交换次数减少到约为 n 2 8 \frac{n^2}{8} 8n2但仍为n2阶,且当d[1]为最小记录或最大记录时就完全失去其优越性 |
2.4 表插入排序
- 静态链表的存储结构
# define SIZE 100 //静态链表容量
typedef struct{
RcdType rc; //记录项
int next; //指针项
}SLNode; //表结点类型
typedef struct{
SLNode r[SIZE]; //0号单元为表头结点
int length; //静态链表长度
}SLinkListType; //静态链表类型
- 基本思想
- 0号元素的关键字取最大整数MAXINT,并与1号元素共同构成一个循环链表:
r[1].next=0;r[0].next=1;
- 依次将r[2……n]的结点通过修改指针的方式在逻辑上插入到循环链表中(不修改记录的存储位置)
- 为了实现随机查找,根据链表的指针对记录进行重排,实现存储位置与逻辑上的先后关系一致。
- 0号元素的关键字取最大整数MAXINT,并与1号元素共同构成一个循环链表:
- 算法
-
循环链表的顺序插入(略)
-
重新排列
关键点: 对于已经排好的一个记录,其next值为把它放到这里时原位置的记录被交换到的位置。这样子当后面继续进行排列时,当要找原位置的记录,就可以顺次找到其被交换到的位置。
void Arrange(SLinkListType &SL) { p = SL.r[0].next; for(int i=1;i<SL.length;++i) { while(p<i) p=SL.r[p].next; q = SL.r[p].next; if(p!=i) { Temp(r[p],r[i]); SL.r[i].next = p; } p=q; }//for }
-
- 算法分析
指标 | 值 | 分析 |
---|---|---|
空间复杂度S(n) | O(n) | 每条记录另需一个指针域 |
时间复杂度T(n) | O(n2) | 比较次数和直接插入的比较次数相同 |
2.5 希尔排序
shell排序
void ShellInsert(List &L,int n)//升序
{
int k=n;
for(k=n/2;k>=1;k/=2)
{
for(int i=k+1;i<n;i++)
{
int j=i-k;
int x=L[i]
while(j>0&&L[j]>L[i])
{
L[j+k]=L[j];
j-=k;
}//while
L[j+k]=x;
}//for 对不同的元素
}//for k取不同的增量
}//ShellInsert
三.快速排序
快速分类
一种方式
int findposition(int l,int r,LIST A)
{
ELemType p=A[l];
while(l<r)
{
while(l<r&&p<=A[r]) r--;
if(l<r)
A[l++]=A[r];
while(l<r&&p>A[l]) l++;
if(l<r)
A[r--]=A[l];
}
A[l]=p;
return l;
}
//升序排序
/*寻找中间值*/
int FindPivot(List &L,int i,int j)//在升序排列中,pivot为顺序第一个与首元不同的元素和首元元素比较大者
{
int first=L[i];
for(int k=i;k<j;k++)
{
if(L[k]>first)
return k;
else if(L[k]<first)
return i;
}//for
return 0;//终止条件,即i==j时返回0
}//FindPivot
/*分割*/
int Partion(List &L,int i,int j,int pivot)
{
int L,R,P=L[pivot];
L=i;
R=j;
do
{
swap(L[i],L[j]);//为了统一操作,先交换
while(L[L]<P)
L+=1;
while(L[R]>=P)
R-=1;
}while(L<=R);
return L;
}//Partion
/*快排,只符合分割线k左边小,自己和右边大于等于pivot,没有将pivot放于正确的位置*/
void QuickSort(List &L,int i,int j)
{
int pivot,k;
pivot=FindPivot(L,i,j);
if(pivot)//终止条件是i==j;
{
k=Partion(L,i,j,pivot);
QuickSort(L,i,k-1);
QuickSort(L,k,j);
}
}//QuickSort
O(n)=T(nlogn),是不稳定性分类。
还有一种快排会选定pivot令其归位于自己应该在的位置,然后对两边(不包括他自己)分别递归,此时就不是交换而是在重复信息上互相交换直至中间位置已经为重复的信息可作为枢轴位置直接赋值而不会有信息丢失。
四.选择排序
堆分类
//对一个结点整理堆
void PushDown(int first,int last,List &A)
{
int r=first;
while(r<=last/2)
{
if(r==last/2 && last%2==0)
{
if(A[r]>A[2*r]) swap(A[r],A[2*r]);
r=last;//结束while
}//if,有度为一的结点,则只有左节点而没有右节点,防止数组越界
else if(A[r]>A[2*r] && A[2*r]<A[2*r+1])
swap(A[r],A[2*r]);
else if(A[r]>A[2*r+1] && A[2*r+1]<A[2*r])
swap(A[r],A[2*r+1]);
else
r=last;//结束
}//while
}//PushDown
void Sort(int n,List &A)
{
int i,last=n;
for(i=n/2;i>=1;i--) PushDown(i,A);//堆整理
for(i=n;i>=2;i--)//第一个留在最前面就行
{
swap(A[1],A[i]);
PushDown(1,i-1);//第i个已经放为有序数了
}
}//Sort
五.归并排序
归并分类
//升序归并两个有序表
void Merge(int l,int m,int n,List A,List &B)
{
int i=l,k=l,j=m+1;
while(i<=m&&j<=n)
{
if(A[i]<=A[j]) B[k++]=A[i++];
else B[k++]=A[j++];
}//while
while(i<=m) T[k++]=A[i++];
while(j<=n) T[k++]=A[j++];
}//Merge
//以长度l依次合并两个有序表
void Mpass(int l,int n,List A,List &B)
{
int i;
for(i=1;i<=n-2*l+1;i+=2*l)
{
Merge(i,i+l-1,2+2*l-1,A,B);
}//for 两个l两个遍历归并
//剩下的是不满l或不满2l的
if(i+l-1<=n)
{
Merge(i,i+l-1,n,A,B);
}
else
{
for(int k=i;k<=n;k++) B[k]=A[k];
}
}//Mpass
//从l=1开始归并
void M_Sort(int n,List A)
{
int l=1;
while(l<n)
{
Mpass(l,n,A,B);
l=2*l;
Mpass(l,n,B,A);//注意交换
l=2*l;
}
}
六.基数分类
基数分类
适用于多关键字的分类。
- MSD法:最高位优先,需要依次根据关键字位次(从高到低)进行堆分类,最后再合并。
- LSD法:最低位优先,直接从关键字位次最低开始从低到高进行全排序即可。但是针对一个关键字进行排序时,只能用稳定的排序。
链式基数排序
针对LSD的排序:
- 对静态链表存储的n个记录进行排序
- RADIX个链表队列
- 根据关键字的从后往前的顺序将每个记录入队到相应的队列中。然后队列首尾相连即得对某一个关键字排序的结果。
七.各种内部排序算法的比较
6.1 汇总
6.2 说明
- 简单排序(直接插入、直接选择、冒泡排序)
都是两层循环嵌套。
稳定性:
直接插入:当插入条件为“严格小于/大于某个值时插入到该值的前面”即能保证稳定。
直接选择:因为选择过程中会把前面的换到后面去所以不稳定。
冒泡排序:显然。 - shell排序
希尔排序的时间性能在O(nlogn)与O(n2)之间。当n在某个特定的范围时,为n1.3。
稳定性:显然不稳定。 - 堆排序
首先初始化建堆,对n/2个数据做PushDown,每一次pushdown最坏情况为完全二叉树的层高,即log(n+1),时间复杂度为 (n/2)*(log(n+1));再依次取堆顶作为排列,操作时间复杂度为(n/2)(log(n+1)).故复杂度为O(nlogn).
稳定性:不稳定。 - 快速排序
若每次正好为序列中点,则为二叉树形式的排序,时间复杂度为O(nlogn)。若每次为头/尾,则退化成选择排序,O(n2).
每次快排都需要两个指针位和一个轴枢数值暂存位,一共是logn次。故为O(logn)。
稳定性:不稳定。 - 归并排序
每一次都是对n个元素进行操作,一共操作logn次(二叉树层高)故为(nlogn)。
需要辅助空间O(n)来做归并后的存储空间。(两个空间来回倒)。
只要确定归并时的先后规则是先左后右,则大小相同的元素的前后序位就能一直保持。 - 基数排序
共d个关键字,n个记录,每个记录的取值范围均为r。则LSD法,对d个关键字从最低位开始执行d次,每次要先对n个元素入相应的队的操作,再将r个队列收尾相连为r则时间复杂度为O(d(n+r)).
需要辅助空间:n个指针域,2r个队列指针域,即O(n+r);
显然是稳定的。
6.3 排序性能实验测试数据