插入排序
- 直接插入排序
算法思想:在插入第i(i>01个元素时,前面i-1个元素已经排好序;
解决的问题:
(1)如何构造初始的有序序列:将第1个元素视作初始的有序序列,依次将第2个直至最后一个插入有序表
(2)如何查找待插入记录的插入位置;
void InsertSort(int r[],int n)
{
int j; //i:指向无序序列的第1个元素;j:指向有序序列最后1个元素;
for(int i=2;i<=n;++i) //将第1个元素视作有序序列,逐个从2-n无序序列中,拿1个元素插入到有序序列
{ r[0]=r[i]; //r[0]暂存作用,存储待插入有序序列的元素
j=i-1; //j指向待插入有序序列元素的前一个位置,j用于将元素移动方便待插入元素插入合适位置
while(r[0]<r[j]) //逐个后移元素,直至待插入元素>=有序序列中某一元素,将其插入后一位置
{ r[j+1]=r[j];
j--;
}
r[j+1]=r[0];
}
}
适用于待排序序列基本有序或者待排序元素数量较少的情况
- 希尔排序:基于直接插入排序,使得待排序序列基本有序,如果待排序序列数量n较小,则排序效率将大大提高,这便是希尔排序
算法思想:将整个待排序序列分割成若干个子序列,在子序列内部进行直接插入排序,待整个序列基本有序时,对全体记录进行直接插入排序;
(1)按照相隔某个增量的记录组成一个子序列;增量:d1=n/2,d(i+1)=di/2;
(2)在子序列内进行直接插入排序,无论前进还是后移,每次变量是d,取代之前的1;
void ShellSort(int a[],int n)
{ for(d=n/2;d>=1;d=d/2) //区别1:外循环计算增量d的值,首次:d=n/2;之后,d为上一轮整理的1/2;
{ for(int i=d+1;i<=n;++i) //区别2:初始位置是i=d+1;
{ a[0]=a[i];
j=i-d; //区别3:所有+1或者-1都替换为+d或-d;
while(j>0 && a[0]<a[j]){ //区别4:因为d一般大于1,所以存在j<0的风险,所以:保护一下
a[j+d]=a[j];
j=j-d;}
a[j+d]=a[0];
}
}
}
对自己的建议:在熟练掌握直接插入排序的基础上,掌握以上4个区别,直接改,便是希尔排序;
希尔排序采用分治法思想,其时间复杂度取决于:增量的函数
交换排序
主要操作是:交换;算法思想:从待排序序列中选取两个记录,对他们的关键码进行比较;如果反序,调整他们的存储位置;
- 冒泡排序
冒泡排序:
算法思想:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止
解决的问题(相比较于经典的冒泡排序):
(1)在一趟冒泡排序中,如果有多个记录位于最终位置,应该如何记录?
方法:定义exchange变量,记录交换的位置,在整体交换结束后,exchange记录的是最后一次交换的位置,那么exchange之前为无序区,之后为有序区(不再参与交换)
(2)如何确定冒泡排序的有序区和无序区,使得已经位于最终位置的记录不再参与下一次排序?
方法:定义bound变量,bound=exchange,记录无序区的最后一个记录的位置,则下一轮冒泡排序的排序区域是:r[0]-r[bound]
(3)如何判别冒泡排序的结束?
方法:每趟排序开始的时候,定义exchange=0,如果存在排序,则exchange>0;若exchange=0,则冒泡排序结束
void BubbleSort(int r[],int n)
{ exchange=n-1; //1.初始化exchange
while(exchange) //2.在exchange=0的情况下结束的前提条件下,进行以下内容
{ bound=exchange; //3.确定本轮冒泡排序的无序区范围
exchange=0; //4.定义exchange=0检验本轮是否存在排序的情况并呼应第2步
for(int i=0;i<bound;++i) //5.对无序区进行冒泡排序(0~bound)
{ if(r[i]>r[i+1])
{ p=r[i];r[i]=r[i+1];r[i+1]=p;}
exchange=i; //6.利用exchange记录本轮最后一次交换的位置
}
}
}
- 快速排序:基于冒泡排序,增大记录的比较和移动距离
算法思想:(递归本质)首先选择一个轴值-比较的基准,一轮排序后,轴值左侧的都是小于等于轴值的记录,右侧都是大于等于轴值的记录,不断循环以上内容
解决的问题:
(1)如何选取轴值:很多种选择方法,默认:以第一记录为轴值;【第1步】
(2)如何分割:i=first,j=end;首先,j从后往前扫描,一旦r[j]<r[i],违背规定的前小后大,所以交换r[i]和r[j]的值,此时:i++;接着,i从前往后扫描,一旦r[j]<r[i],违背规定的前小后大,所以交换r[i]和r[j]的值,此时:j--;最终 产生的结果是,轴值所在位置左边<=它,右边>=它;【第2步】
(3)如何处理分割后的2个待排序子序列:对【第2步】执行结束后产生的2个子序列递归地执行快速排序
(4)如何判别快速排序的结束:当first不再<end
int partition(int r[],int first,int end) //解决第一二个问题,第1步&第2步
{ int i=first,j=end,k;
while(i<j){
while(i<j && r[i].key<=r[j].key) j--;
if(i<j){
k=r[i].key;r[i].key=r[j].key;r[j].key=k;i++;}
while(i<j && r[i].key<=r[j].key) i++;
if(i<j){
k=r[i].key;r[i].key=r[j].key;r[j].key=k;j--;}
}
return i;
}
void QuickSort(int r[],int first,int end) //解决第三个问题,第3步
{ if(first<end){ //解决第四个问题,第4步【递归函数的出口】
pos=partition(r,first,end); //确定轴值所在位置,轴值将序列划分为2个子序列
QuickSort(r,first,pos-1); //递归处理前半序列
QuickSort(r,pos+1,end); //递归处理后半序列
}
}
快速排序的时间复杂度取决于递归的深度,递归的深度取决于轴值选取的好坏;最好的情况:O(nlog2^n)每次划分后,左右序列长度相同;最坏情况:O(n^2);平均情况:O(nlog2^n)
一般时间复杂度最坏的情况,是因为轴值选择不当的原因;
快速排序不适合序列基本有序或者序列很短的情况,一般序列基本有序或者序列较短,直接插入排序和冒泡排序很适合;
快速排序一定程度上牺牲了空间复杂度,换取了时间上的高效,因为快速排序使用了递归,占用了一定的系统栈;
冒泡排序是对相邻记录进行比较和交换,每次只能改变一对逆序记录;快速排序是从待排序序列两端开始,逐渐向中间靠拢,每经过一次交换,有可能改变几对逆序序列,从而加快了快速排序.
选择排序
- 简单选择排序
主要操作是选择;
算法思想:每次从待排序序列中选出关键码最小的记录,添加到有序序列中.
解决问题:
(1)如何在待排序序列中选出关键码最小的记录:设置min记录1轮遍历中关键码最小的记录位置;
(2)如何确定待排序序列中关键码最小的记录在有序序列中的位置:
void SelSort(int r[],int n)
{
for(int i=0;i<n-1;i++){
min=i; //i记录每轮遍历无序区的起点位置,并将i值赋给min用于标记本轮遍历中关键码最小的记录位置
for(int j=i+1;j<n;j++){ //从min后一位开始遍历无序区,用min记录本轮循环最小关键码的位置
if(r[j]<r[min])
min=j;}
if(min!=i){ //如果1轮循环遍历后的min的位置,不在初始位置,交换
int p=r[min];
r[min]=r[i];
r[i]=p;}
}
}
时间复杂度:O(n^2)-二重循环
- 堆排序(天才算法)
算法思想:每次构造一个堆,将堆的根节点(最大值||最小值)添加到有序序列中.将剩余序列继续调整为一个堆,循环 以上内容直至堆中只有一个记录.
堆的定义:堆是具有下列性质的完全二叉树;每个结点的值都小于或等于左右孩子的值的堆叫小根堆;每个结点的值都大于或等于左右孩子的堆叫大根堆;
小根堆:
1.小根堆的根结点是所有结点的最小者;2.较小结点靠近根结点但不绝对;
大根堆:
1.大根堆的根结点是所有结点的最大者;2.较大结点靠近根结点但不绝对;
解决问题:
(1)如何由一个无序序列建成初始堆:
(2)如何处理堆顶记录:第k次处理堆顶记录是将堆顶记录r[1]与序列中第n-k+1个记录r[n-k+1]进行交换
(3)如何调整剩余记录成一个新的堆:第k次调整剩余记录,此时,剩余记录有n-k个,调整根结点至第n-k个记录.
前提条件:【完全二叉,左右是堆,先上后下】在完全二叉树的前提条件下,从下[i=n/2]往上[++i]开始,保证左右子树是堆
堆调整:在一棵完全二叉树中,根结点左右子树均是堆,如何调整根结点使得整个完全二叉树成为堆.
建成一个大根堆,解决了第一个问题:
void shift(int r[],int k,int end) //k作为起点,end作为终点;k又同时作为根结点,保证围绕k的根结点的左右子树成为堆
{ i=k;j=2*i; //i:根结点,也就是起始结点;j:根结点左子树
while(j<=end){
if(j<end && r[j]<r[j+1]) //j<end成立:k作为根结点,拥有左右子树;r[j]<r[j+1]:左子树小于右子树,将j放置于左右子树较大位置;
j++;
if(r[i]<r[j]){ //比较根节点与左右子树较大者,将较大者调整为根结点
p=r[i];r[i]=r[j];r[j]=p;}
i=j; //因为上述的结点的调整可能造成下边的堆被破坏,所以:继续对下边的堆进行调整
j=2*j; //此处的函数关系是:完全二叉树的性质关系【好好记忆理解一下】
}
}
for (int k=n/2;k>=1;k--) //从下至上地遍历每一个根结点,使得围绕每一个根结点左右子树成为堆
shift(r,k,n);
void HeapSort(int r[],int n) //堆排序主程序
{ for(int k=n/2;k>=1;--k){ //1.初建堆
shift(r,k,n);}
for(k=1;k<n;++k){ //2.【解决了第2个问题】k作为根结点从第1位开始遍历到第n-1位,每一次将堆顶记录与无序序列的最右端记录进行交换;此时会破坏原本的堆平衡,所以:
p=r[1];r[1]=r[n-k+1];r[n-k+1]=p;
shift(r,1,n-k);} //3.【解决了第三个问题】重新调整堆:以堆的第1位记录作为开始,以n-k位作为结束
}
时间复杂度-最好,最坏,平均:O(nlog2^n);空间复杂度:需要一个辅助空间r[0];时间复杂度,空间复杂度越低的算法,是个好算法;本算法本尊便是!
归并排序
分配排序