一:插入排序
插入排序是一种简单直观的排序方法,其基本思想在于每次将一个待排序记录,按其关键字大小插入到前面已经排序好的子序列中,直至全部记录插入完成。
由此引申三种重要排序:直接插入排序,折半插入排序,希尔排序
1:直接插入排序
1)查找A[i]在A[1...i-1]中的位置k
2)A[k...i-1]中所有元素后移一位
3)将A[i]复制到A[k]
void InsertSort(int A[],int n){
int i,j;
for(i=2;i<=n;i++){ //依次将A[2...n]插入到前面已经排好的序列
if(A[i].key<A[i-1].key){
A[0]=A[i]; //复制为哨兵
}
for(j=i-1;A[0].key<A[j].key;j--) //从后往前查找
A[j+1]=A[j]; // 元素后移
A[j+1]=A[0];//复制到插入位置
}
}
空间复杂度:O(1)
时间复杂度:n-1趟插入插入,每趟操作都分为比较关键词和移动元素,而比较关键词和移动元素取决于待排序表的初始状态。最好情况——表中元素有序,此时只需比较,无需移动,时间复杂度为O(n),而平均时间复杂度O(n^2)
稳定性:稳定
适用性:适用于顺序存储和链式存储的线性表(PS:大部分排序算法都仅适用于顺序存储的线性表)
2:折半插入排序
对直接插入排序做适当改进即可,在查找有序子表时进行折半查找即可
void InsertSort(int A[],int n){
int i,j,low,mid,high;
for(i=2;i<=n;i++){ //依次将A[2...n]插入到前面已经排好的序列
A[0]=A[i] ;//将A[i]缓存在A[0];
low=i,high=i-1;
while(low<=high){
mid=(low+high)/2; //取中间点
if(A[mid].key>A[0].key) low=mid-1; //查左子表
else high=mid+1;//查右子表
}//(low>high跳出)
for(j=i-1;j>=high+1;j--)
A[j+1]=A[j];
A[high+1]=A[0]; //插入操作
}
}
显然,折半插入仅仅减少了比较的次数,约为O(nlogn),比较次数与待排序表的初始状态无关,仅取决于表中元素个数n。
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
3:希尔排序
上面讲解可以看出,直接插入排序算法适用于基本有序的排序表和数量不大的排序表。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1)插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2)但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
基本思想:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量等于1,该方法实质上是一种分组插入方法
对顺序表做希尔排序,与直接插入排序的相比较:
1)前后记录位置的增量是dk,不是1
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].key<A[i-dk].key)
A[0]=A[i]; //暂存在A[0]
for(j=i-dk;j>0 && A[0].key<A[j].key;j-=dk)//从后往前查找
A[j+dk]=A[j]; // 元素后移
A[j+dk]=A[0]; //复制到插入位置
}
}
空间复杂度:O(1)
时间复杂度:未知
稳定性:由于希尔排序是多次分组插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
二 :交换排序
1:冒泡排序
基本思想:相邻比较,若逆序,则交换位置,n-1趟可完成排序,且每一趟冒泡都能将一个元素放在最终位置。
void BubbleSort(int A[],int n){
//升序为例
for(int i=0;i<n-1;i++){
bool flag=false;
for(int j=n-1;j>=0;j--){
if(A[j-1].key>A[j].key)
swap(A[j-1],A[j]) ; //逆序则交换
flag=true;
}
if(flag==false)
return ; //本趟未交换,则表已经有序,return
}
}
时间复杂度:O(1)
空间复杂度:最好O(n)(表有序),平均O(n^2)
稳定性:显然是稳定的
2:快速排序
快排是对冒泡排序的改进,思想是基于分治的:
1)在待排表A[1...n]中取一个元素pivot为基准,
2)待排表元素小于pivot放A[1..K-1], 待排表元素大于等于pivotA[k+1..n],pivot元素放在最终位
这个过程称为一趟快排,然后对两子表递归的进行上述过程,直到每部分只有一个元素或为空为止
#include<cstdio> //不稳定排序
#include<algorithm>
using namespace std;
const int maxn=100;
void Quicksort(int *a,int left,int right){
if(left>right) return ;
int i=left,j=right;
int temp=a[left];//主元
while(i!=j){
while(i<j && a[j]>=temp) j--;
while(i<j && a[i]<=temp) i++;
if(i<j){
swap(a[i],a[j]);
}
}
swap(a[left],a[i]);
Quicksort(a,left,i-1);//
Quicksort(a,i+1,right);
}
/*
int Partition(A,int low,int high){
int pivot=A[low]; //枢轴值
while(low<high){
while(low<high && A[high]>=pivot) high--;
A[low] =A[high];
while(low<high && A[low]<=pivot) low++;
A[high] =A[low];
}
A[low]=pivot;
return low;
}
void QuickSort(int A[],int low,int high){
if(low<high){
//Partition()划分操作,划分为满足条件的俩个子表
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
*/
int main(){
int n;
int a[maxn];
if(n>maxn) n=maxn;
printf("Please input the number of numbers you want to sort:\n");
while(~scanf("%d",&n)){
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
Quicksort(a,0,n-1);
printf("sorted result:\n");
for(int i=0;i<n;i++){
printf("%d ",a[i]);
}
}
return 0;
}
测试用例截图:
分析:
时间复杂度:快排的运行时间与划分是否对称有关,最坏情况是初始排列表基本有序或者基本逆序,此时时间复杂度为O(n^2),平均时间复杂度O(nlogn)
空间复杂度:快排是递归的,需要借助一个递归栈来保存每一层递归调用必要信息,其容量与递归深度一致,最坏情况进行n-1次递归调用,栈深度为O(n),平均栈深度O(logn).因此空间复杂度最坏O(n),平均O(logn)
稳定性:不稳定。例如A={3,2,2},一趟快排后 A={3,2,2},最终排序序列也为A={2,2,3}
注意:在快速排序中,每一趟并不产生有序子列,但每一趟后将一个元素(基准元素)放到最终位置。
三:选择排序
1:简单选择排序
基本思想:每一趟(例如第i趟)在后面n-i+1个待排序元素中选取最小的关键字元素,最为有序子列的第i个元素,n-1趟可排序完成。
//简单选择排序
void SelectSort(int A[],int n){
for(int i=0;i<n-1;i++){
int min=i; //记录最小元素位置
for(int j=i+1;j<n;j++) //在A[i..n-1]中找最小元素
if(A[j].key<A[min].key) min=j //刷新min下标
if(min!=i) swap(A[i],A[min]); //与第i个元素交换位置
}
}
时间复杂度:元素间的比较次数与序列初始状态无关,始终为n(n-1)/2,所以时间复杂度为O(n^2)
空间复杂度:O(1)
稳定性:不稳定,例如A={2,2,1},最终排序结果为A={1,2,2},含有相同关键字的元素相对位置发生变化。
2:堆排序(划重点)
/*
* 堆排序是一种复杂度为Nlog(N)的排序算法。对应二叉堆。
* 二叉堆是一颗完全二叉树,一般可以直接用数组实现。它的特点:
* 1. 父节点的键值总是大于等于(或小于等于)任何一个子节点的值。
* 2. 每一个节点的左右子树都是一个二叉堆。
* 当父节点的值都大于等于子节点的值,这样的二叉堆叫做大顶堆。
* 当父节点的值都小于等于子节点的值,叫做小顶堆
*/
以大根堆为例:
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=100;
void AdjustDown(int A[],int k,int len){
//函数AdjustDown()将元素k向下进行调整
A[0]=A[k]; //A[0]暂存
for(int i=2*k;i<=len;i*=2){
if(i<len && A[i]<A[i+1])
i++; //取key值较大的子节点下标
if(A[0]>A[i]) break; //筛选结束
else{
A[k]=A[i]; //将A[i]调整到双亲结点
k=i; //修改k值,继续向下调整
}
}//for
A[k]=A[0]; //被筛选结点放到最终位置
}
void BuildMaxhead(int A[],int len){
for(int i=len/2;i>0;i--) //从i=A[n/2]~1,反复调整
AdjustDown(A,i,len);
}
void HeadSort(int A[],int len){
BuildMaxhead(A,len); //初始建堆
for(int i=len;i>1;i--){ //n-1趟交换和建堆过程
swap(A[i],A[1]);
AdjustDown(A,1,i-1);//把剩余i-1个元素整理成堆
}
}
int main(){
int n;
int a[maxn];
if(n>maxn) n=maxn;
printf("Please input the number of numbers you want to sort:\n");
while(~scanf("%d",&n)){
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
HeadSort(a,n);
printf("sorted result:\n");
for(int i=1;i<=n;i++)
printf("%d ",a[i]);
}
return 0;
}
测试用例截图:
分析:
时间复杂度:建堆时间O(n),之后有n-1次向下调整,向下调整复杂度O(h),最好、最坏、平均时间复杂度都是O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
注意:每一趟都能将一个元素放在最终位置
四:归并排序和基数排序
1:归并排序
核心思想:通过先递归的分解数列,再合并数列就完成了归并排序。
假定排序表有n个记录,则可看作是n个有序的子表,每个字表长度为一,然后两两归并,得到[n/2](向上取整)个长度为2或为1的有序表;两两归并.....如此重复,直至合并为一个长度为n的有序表,这种排序为二路归并。
例如:
初始关键字 49 38 65 97 76 13 27
一趟后: 38 49 65 97 13 76 27
两趟后: 38 49 65 97 13 27 76
三趟后: 13 27 38 49 65 76 97
以二路归并为例:
//归并排序
//通过先递归的分解数列,再合并数列就完成了归并排序。
#include<iostream>
#include<windows.h>
using namespace std;
int n;
int *B=(int *)malloc((n+1)*sizeof(int));// 辅助数组
void Merge(int A[],int low,int mid,int high){
for(int i=low;i<=high;i++){
B[i]=A[i];
}
int i,j,k;
for( i=low,j=mid+1,k=i;i<=mid && j<=high;k++){
if(B[i]<=B[j]) //比较 B 的左右两段 ,稳定排序 ,谁小放谁
A[k]=B[i++];
else
A[k]=B[j++];
}//跳出时左半段走完或者右半段走完
while(i<=mid) A[k++]=B[i++];
while(j<=high) A[k++]=B[j++];
}
void MergeSort(int A[],int low,int high){
if(low<high){
int mid=(low+high)/2; //中间划分两部分
MergeSort(A,low,mid); //合并左
MergeSort(A,mid+1,high);//合并右
Merge(A,low,mid,high); //归并
}//if
}
int main(){
DWORD star_time = GetTickCount();
cin>>n;
int A[n];
for(int i=0;i<n;i++)
cin>>A[i];
MergeSort(A,0,n-1);
for(int i=0;i<n;i++)
cout<<A[i]<<" ";
cout<<endl;
DWORD end_time = GetTickCount();
cout << "这个程序运行时间为:" << (end_time - star_time) << "ms." << endl;
system("pause");
return 0;
}
测试用例截图:
分析:
时间复杂度:每一趟归并时间复杂度O(n),共进行logn(以2为底,向上取整)趟。因此时间复杂度O(nlogn)
空间复杂度:O(n)
稳定性:Merge()不改变相同关键字记录的相对次序,因此是稳定的
注意:一般而言,对与N个元素进行K-路归并排序时,排序趟数m满足K^m=N,从而m=logk N(m为整书,结果向上取整)
2:基数排序
采用多关键字排序(基于关键字各位的大小进行排序),借助“分配”和“收集”两种操作对关键字进行排序
代码后续给出!!!
时间复杂度:O(d(n+r)),与序列初始状态无关
空间复杂度:一趟排序需要存储空间r(r个队列),以后重复使用,所以空间复杂度O(r)