void InsertSort(int A[],int n){
int i,j;
for (i=2;i<=n;i++) //数组从A[1]开始计数,依次将A[2]-A[n]插入到前面已排序序列
if(A[i]<A[i-1]){ //若A[i]的关键字小于其前驱,需将A[i]插入到i-1前面的有序表
A[0]=A[i]; //复制为哨兵,A[0]不存放元素
for(j=i-1;A[0]<A[j];--j) //从后往前查找待插入位置
A[j+1]=A[j]; //向后挪位
A[j+1]=A[0]; //复制到插入位置(之所以为j+1是因为之前的j--)
}
}
(2)二分插入排序
void InsertSort(int A[],int n){
int i,j,low,high,mid;
for(i=2,i<=n;i++){ //依次将A[2]-A[n]插入到前面已排序序列
A[0]=A[i]; //复制为哨兵,A[0]不存放元素
low=1;high=i-1; //设置折半查找的范围
while(low<=high){ //找到待插位置
mid=(low+high)/2;
if(A[mid]>A[0]) high=mid-1;
else low=mid+1;
}
for(j=i-1;j>=high+1;--j)
A[j+1]=A[j]; //后移
A[high+1]=A[0]; //复制
}
}
void ShellSort(int A[],int n){
for(dk=n/2;dk>=1;dk=dk/2) //步长变化
for(i=dk+1;i<=n;++i) //对每一小组进行调整
if(A[i]<A[i-dk]){ //后面比前面小
//对A[i],A[i-dk],A[i-2*dk]...使用直插法,只不过把1换成了dk
A[0]=A[i]; //暂存在A[0]
for(j=i-dk;j>0&&A[0]<A[j];j-=dk)
A[j+dk]=A[j];
A[j+dk]=A[0];
}
}
void BubbleSort(int A[],int n){
for(i=1;i<n;i++){
int swap=0; //交换标志
for(j=1;j<=n-i;j++){
if(A[j]]>A[j+1]){ //逆序,交换
A[0]=A[j+1];
A[j+1]=A[j];
A[j]=A[0];
swap=1;
}
if(swap==0)break; //此趟冒泡没有发生交换,排序结束
}
}
}
改进的冒泡排序
void Bubble_Modified_Sort(int A[],int n){
i=n;
while(i>1){
LastExchangeIndex=1;
for(j=1;j<i;j++){
if(A[j]>A[j+1]){ //逆序,交换
A[0]=A[j+1];
A[j+1]=A[j];
A[j]=A[0];
LastExchangeIndex=j; //记录交换位置
}
}
i=LastExchangeIndex;
}
}
int Quick_Partition(int A[],int i
,int j){ //划分操作
//一次划分,左端位置为i,右端位置为j,划分元为A[i]
A[0]=A[i]; //暂存A[0]
while(i<j){
while(i<j&&A[j]>=A[0]) j--; //右边所指比基准数大,不用交换,j往左移继续找
if(i<j){ //把右边所指比基准数小的填到坑里
A[i]=A[j];
i++; //i右移,换左边找
}
while(i<j&&A[i]<=A[0]) i++; //左边所指比基准数小,不用交换,i往右移继续找
if(i<j){ //把左边所指比基准数大的填到坑里
A[j]=A[i];
j--; //j左移,换右边找
}
}
A[i]=A[0]; //将基准数放入调整后的位置
return i; //返回基准数位置
}
void QuickSort(int A[],int low,int high){ //快速排序(递归)
while(low<high){ //递归到每部分只有一个空元素为止
int i=Quick_Partition(A,low,high); //将表一分为二
Quick_Partition(A,low,i-1);
Quick_Partition(A,i+1,high);
}
}
性能分析:
空间复杂度:平均O(㏒n),最坏O(n)
为什么空间复杂度是O(㏒n)?
因为快排是递归的,需要借助一个递归工作栈来存放每层递归的数据,即应等于二叉树的最大深度。最好:㏒(n+1)向上取整,最差:O(n),平均:O(㏒n)
时间复杂度:平均O(n㏒n),最坏O(n²)
不稳定
void SelectSort(int A[],int n){
for(i=1;i<n;i++){ //n-1趟选择
int min=i; //记录最小元素位置
for(j=i+1;j<=n;j++) //从A[i...n-1]中选择最小的元素
if(A[j]<A[min]) min=j; //更新最小元素位置
if(min!=i){ //若最小不在第i个元素,则将他两交换
A[0]=A[min];
A[min]=A[i];
A[i]=A[0];
}
}
}
(6)堆排序
堆的一些性质
本质是一种数组对象
任意叶子结点小于(或大于)其所有父节点,对左右孩子的大小关系不做任何要求。
堆排序,就是基于大顶堆或者小顶堆的一种排序方法。
若想得到升序,则建立大顶堆;若想得到降序,则建立小顶堆。
基本思想:
step1——构建大(小)顶堆:从最后一个非叶子结点(n/2向下取整)从后往前执行下沉操作
step2——取顶,与最后元素交换
step3——对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;
step4——重复至堆中只有1个元素为止
void Sift(int A[],int low,int high){ //大顶堆调整算法
int i=low,j=2*i; //A[j]是A[i]的左孩子结点
int temp=A[i]; //temp暂存根数据
while(j<=high){
if(j<high&&A[j]<A[j+1]) ++j; //若存在右子树且右子树根的关键字大,则沿右分支调整
if(temp<A[j]){ //若比存的根节点还大,就交换
A[i]=A[j]; //A[j]换到父节点
i=j; //修改i,j值,以便继续向下调整
j=2*i;
}
else break; //调整结束
}
A[i]=temp; //当前待调整的结点放到比其大的孩子结点位置上
}
void HeapSort(int A[],int n){ //堆排序
int i,temp;
for(i=n/2;i>=1;--i) //建立初始堆,i为最后一个非叶子结点
Sift(A,i,n);
for(i=n;i>=2;--i){ //进行n-1次循环,完后堆排序(注:从最后一个元素开始调整)
//交换堆顶元素A[1]和堆中最后一个元素
temp=A[1];
A[1]=A[i];
A[i]=temp;
Sift(A,1,i-1) //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
}
}
(7)二路归并排序
2路归并一般用在内部排序中,多路归并一般用在外部磁盘数据排序中。
基本思想:分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕。
归并排序其实要做两件事:
分解----将序列每次折半拆分
合并----将划分后的序列段两两排序合并
因此,归并排序实际上就是两个操作,拆分+合并
与快速排序的区别与联系:
1.联系:
➀两者均采用分治法
➁均采用递归调用实现
2.区别:
➀快速排序:“先治后分”——当两个子数组有序时,整体也就有序,不需要归并这一步。
➁归并排序:“先分后治”——每一轮排序后需要归并,以达到局部的上一层有序。
➂递归调用发生点不同:快速排序递归发生在处理整个数组之后;而归并排序发生在之前。
代码:
如何合并?
L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。
step1——首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[]
step2——重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[]
step3——此时将temp[]中的元素复制给L[],则得到的L[first…last]有序
如何分解?
采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列分组;直到分组后组内只有一个元素,此时可认为组内所有元素有序,则分组结束。
void Merge(int A[],int B[],int low,int mid,int high){
//表A[low...mid],A[mid+1...high]各自有序,将其合并为有序表B[low...high]
i=low;j=mid+1;k=i;
while(i<=mid&&j<=high){
if(A[i]<A[j])
B[k++]=A[i++];
else
B[k++]=A[j++];
}
while(i<=mid) B[k++]=A[i++]; //若第一个表没检测完,直接复制
while(i<=high) B[k++]=A[j++]; //第二个....
for(int k=low;k<=high;k++)
A[k]=B[k];
}
void MergeSort(int A[],int B[],int low,int high){
if(low==high) B[high]=A[high];
else{
int mid=(low+high)/2; //从中间划分两个子序列
MergeSort(A,B,low,mid); //从左侧子序列进行递归排序
MergeSort(A,B,mid+1,high); //从右侧子序列进行递归排序
Merge(A,B,low,mid,high); //将两个有序表合成一个有序表
}
}
void Binary_MergeSort(int A[],int n){
MergeSort(int A[],int B[],1,n);
}
(9)基数排序
每趟排序后均会使一个记录放在最终位置的排序:简单选择排序、冒泡排序、堆排序。
经过i趟排序后,插入排序前i+1个元素局部有序;2路归并排序每2i个元素有序。
折半插入排序的比较次数与序列初态无关,都是O(nlogn),而直接插入排序的比较次数与序列初态有关,为O(n)~O(n²)。
冒泡排序的交换次数为排序中逆序的个数。
绝大部分内部排序只适用于顺序存储结构。
虽然众多排序算法的平均时间复杂度都为O(nlogn) ,但快排的常数因子是最小的,平均性能是最好的。
当快排每次的枢纽都能将表对半分时,速度最快;表本身已经有序或逆序,速度最慢。
快排阶段性排序结果特点:第i趟完成时,会有i个以上的数出现在它的最终位置,即他左边的数都比他小, 右边的数都比他大。
快排的过程构成一棵树,递归深度即递归树的高度。枢纽值每次都将子表等分时,递归树高度为logn;枢纽值每次都是字表的最大或最小值时,递归树退化为单链表,树高为n。
取一大堆数据中的k个最值时,优先采用堆排序。
如何判断一组序列是不是堆?将其表示为完全二叉树,再看父子结点关系是否满足堆的定义。
堆的几种操作分析:
(1)筛选堆的过程——自底向上调整
➀先把数据全部按完全二叉树的顺序堆起来。
➁从最后一棵子树,从下往上,从右往左,调整堆顺序。
(2)删除堆顶的最大元素——自顶向下调整
注:删除比较是将左右孩子中最大的那个与父节点交换,所以要先左右孩子比较,再将最大的孩子与父节点比较,需要2次。
(3)往一个完整的堆中插入元素——自底向上调整
选择排序算法的比较次数始终为n(n-1)/2,与序列状态无关。
调整堆和建初始堆的时间复杂度不同,因为建初始堆的时候每一步都要多次调整堆。调整堆:O(logn),建初始堆:O(n)。