插入排序(直接插入排序,折半查找插入排序,2-路插入排序,表插入排序,希尔排序)
直接插入排序(n个元素非递减排序)
原理:将一个记录插入到已经排好序的有序表中(将序列第一个记录看做只有一个记录的有序序列),得到新的记录数+1的有序表
//顺序表结构
#define MAXSIZE 20
typedef struct{
int r[MAXSIZE];//默认是短整形
int length;//表长
}SqList;
步骤:
将第i个记录插入前面含有第1到第i-1个记录的有序表:
- r[0]=r[i],在r[0]处设置监视哨兵
- j=i-1,从后往前令r[j]与r[0]哨兵比较
- 若r[0]<r[j],则r[j+1]=r[j],记录后移,否则退出for循环
- r[j+1]=r[0],将哨兵赋值,插入正确位置
- 形成含有i个记录的有序表
重复上述步骤n-1趟,得到非递减有序表
void InsertSort(SqList *L)
{ //将第一个记录视作有序表
for(int i=2;i<=L->length;i++)//从第2个记录开始插入
{
if(r[i]<r[i-1]){//待插入记录若大于等于有序表最后一个记录,则已经有序
r[0]=r[i];//否则赋值给哨兵
int j;
for(j=i-1;r[0]<r[j];j--)
r[j+1]=r[j];//往后移动记录
r[j+1]=r[0];//插入记录
}
}
return;
}
直接插入排序示例:
直接插入排序的基本操作为:比较和关键字和移动记录
当待排序列记录按关键字非递减有序排列时(正序):
关键字比较次序为n-1趟排序,每趟排序比较1次,共n-1次比较,比较完发现有序,无需移动记录
当待排序序列记录按关键字非递增有序排列时(逆序):
比较次数:第i个记录需要和前面i-1个记录以及第0个哨兵记录共i次,从第2个记录开始共n-1趟,共(n+2)(n-1)/2次
移动记录次数:第个记录需要赋值给哨兵1次,对i-1个记录后移i-1次,从哨兵赋值插入1次,每趟i+1次,n-1趟共(n+4)(n-1)/2次
若待排序记录是随机顺序的,各种排列概率等可能,则取上述最大值最小值的平均值,比较次数和移动记录次数约为n^2/4
直接插入的时间复杂度为O(n^2)
直接插入排序的改进:
改进方向:减少基本操作
- 减少比较次数:折半插入排序---折半查找插入位置
- 减少移动次数:2-路插入排序---通过2条路前或后移动记录
- 改变存储结构:表插入排序----不移动记录,通过修改指针值代替移动记录
折半插入排序:减少关键字的比较次数,迅速找到插入位置,但是移动记录次数不变
void BInsertSort(SqList *L){
for(int i=2;i<=L->Length;i++){
if(r[i]<r[i-1]){//已经有序,无需折半插入查找插入排序
r[0]=r[i];//设置哨兵
int low=1;int high=i-1;
while(low<=high){//折半查找插入位置
int m=(low+high)/2;//折半
if(r[0]<r[m]) high=m-1;//插入点在低半区
else low=m+1;//插入点在高半区
}
for(int j=i-1;j>=high+1;j--)//high+1处为插入位置
r[j+1]=r[j];//记录后移
r[high+1]=r[0];//插入记录
}
}
}
折半插入的时间复杂度依然为O(n^2)
2-路插入排序:减少记录的移动次数,关键是构造辅助循环数组
为什么能减少移动次数:
直接插入排序:无论被插入记录大小,通过不断的关键字比较,将记录只能向后移,找到插入位置并且插入
2-路插入排序:先将被插入记录关键字和temp[0]比较大小,根据比较结果分别选择后移或者往前移,这样比较次数能减少一半
插入位置的寻找:1.通过折半比较查找 2.顺序比较查找
void Path2InsertSort(int *arr, int *temp, int n)//待排序整形数组和数组长度
{
temp[0]=arr[0];//数组头值赋值给临时数组
int first=0,final=0;//将final和first指针指向temp[0]
for(int i=1;i<n;i++){
if(arr[i]>=temp[0]){//插入到temp[0]之后的有序序列
int j;
for(j=final;arr[i]<temp[j];j--)
temp[j+1]=temp[j];//记录后移
temp[(j+1)%n]=arr[i];//插入记录
final=(final+1)%n;//final后移
}else{
int j;
for(j=first;arr[i]>temp[j];j++)
temp[j-1]=temp[j];//记录前移
temp[(j-1+n)%n]=arr[i];//插入记录
first=(first-1+n)%n;first前移
}
}
return;
}
直接插入排序平均移动次数为(n^2)/4, 2-路插入排序为其一半,故为(n^2)/8
示例:
2-路插入的时间复杂度依然为O(n^2)
缺点:如果temp[0]中关键字为序列关键字中最小值或者最大值,则2-路插入会失去两端都能够移动记录的优越性,变成直接插入排序!!
表插入排序:
静态链表结构:
#define SIZE 100
typedef struct
{
int rc; //记录项
int next; //指针项,由于在数组中,所以只需要记录下一个结点所在数组位置的下标即可。
}SLNode;
typedef struct
{
SLNode r[SIZE]; //存储记录的链表
int length; //当前链表长度
}SLinkListType;
表插入排序:
步骤:
- 将表头结点和第1个结点链接成第一个非递减循环链表,
- 将第i个结点依次插入到循环链表,保持非递减有序
- 从头结点开始查找插入位置,知道j=0,返回到表头结点
- 查找到插入位置后改变前后结点指针,将第i个结点插入循环链表
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
void ListInSertSort(SLinkListType *L)
{
L->r[0].rc=INT_MAX;//初始化表头结点
L->r[0].next=1;
L->r[1].next=0;//将下标为‘1’的分量和表头结点构成静态循环链表
for(int i=2;i<=L->length;i++){
int j=L->r[0].next;//每一趟插入前,将指针j指向第一个结点
int pre=0;//指向j的前驱结点,此时为头结点
while(j!=0){//当j=0时,说明遍历回到头结点,遍历结束
if(L->r[i]<L->r[j])//找到插入位置
break;//调出循环
pre=j;
j=L->r[j].next;//当前结点指针后移
}
L->r[pre].next=i;//前驱结点指针指向i
L->r[i].next=j;//i结点指针指向j
}
return;
}
但是重点来了:对无需链表进行表插入,得到有序链表,依然只能进行顺序查找,不能随机查找,我们需要对记录重新排列
void Arrange(SLinkListType *L)
{
int p=L->r[0].next;int q;
for(int i=1;i<=L->length;i++)//静态链表中已经按关键字非递减有序
{ //第i个记录在当前已经重排的表中位置不会小于i,因为前面i-1个位置被重排完毕了,可能被交换记录到其他位置
while(p<i) p=L->r[p].next;//找到第i个记录,并用P指向其位置
q=L->r[p].next;//找到当前第i个记录位置后,用q指向下一个记录可能的位置,可能被替换走
if(P!=i){
int key=L->r[i].rc;
L->r[i].rc=L->r[p].rc;
L->r[p].rc=key;//交换记录的值
L->r[i].next=p;//指向被移走的记录,代指原来此处记录的去处,可以通过while找回
}
p=q;
}
return;
}
表插入排序:
不移动记录,通过改变指针,间接使记录有序,n个记录,改变2n次指针值。比较次数和直接插入相同,没有变化
表插入的时间复杂度依然为O(n^2)
希尔排序(Shell's Sort)
希尔排序又称之为缩小增量排序,是属于插入排序类的一种排序方式,是相对前述插入排序的改进
改进方向:
-
直接插入排序在对几乎已经排好序的数据进行操作时,效率高,能够达到线性排序的效率。
-
直接插入排序在序列长度n很小的时候效率也比较高
希尔排序正式从这两点出发对直接插入排序进行改进而得到的一种插入排序方法
改进方法:
- 通过设置一个增量,将整个待排序记录分割成若干个子序列分别进行直接插入排序
- 通过不断缩小增量,将子序列长度减小,从而进一步提高直接插入效率
- 待整个记录基本有序,最后通过大小为1的增量,对全体记录进行一次直接插入排序
故希尔排序有名为缩小增量排序。
void ShellSort(Sqlist *L, int dlta[],int m)//dlta[ ]为增量数组,t为增量数组大小
{
for(int i=0;i<m;i++)
ShellInSert(L, dlta[i]);//对增量dlta[k]进行一趟插入排序
return;
}
void ShellInSert(SqList *L,int dk)
{
for(int i=0;i<dk;i++){//增量为dk的每一趟排序有dk个间隔为dk的子序列
for(int j=i+dk;j<=L->length;j=j+dk){//从子序列的第二个记录开始进行直接插入排序
if(L->r[j]<L->r[j-dk]){//若小于前面间隔为dk的记录,则查找插入,否则已经有序
int key=L->r[j];//用key暂存L->r[j]待插入记录值
int k;
for(k=j-dk;k>=0&&key<L->r[k];k=k-dk)
L->r[k+dk]=L->r[k];//记录后移
L->r[k+dk]=key;//插入记录
}
}
}
return;
}
时间复杂度分析:
时间复杂度跟增量序列的设置有关,分析起来比较复杂,有可能是O(n^2),O(n^1.5),O(n^1.3),但是希尔排序不是稳定排序
注意:增量序列虽然有多种取法,但是最后一个增量值应该为1,且其他增量值不能有除1以外的公因子