算法种类 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
折半插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(1) | 不稳定 | |||
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
(一)插入排序
每次将一个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,直到全部记录插入完成
直接插入排序:
有序序列L[1…L-1] | L(i) | 无序序列L[i+1…n] |
---|
1)查找出L(i)在L[1…i-1]中的插入位置k
2)将L[k…i-1]中所有元素全部后移一个位置
3)将L(i)复制到L(k)
/*时间复杂度:O(n^2) 稳定性:稳定*/
void InsertSort(ElemType A[],int n) //直接插入排序
{
int i,j;
for(i=2;i<=n;i++)
if(A[i].key<A[i-1].key) //依次将A[2]~A[n]插入到前面有序序列
{
A[0]=A[i]; //复制为哨兵,A[0]不存放元素
for(j=i-1;A[0].key<A[j].key;--j) //从后往前查找待插入位置
A[j+1]=A[j]; //向后移动
A[j+1]=A[0]; //复制到插入位置
}
}
折半插入排序
在直接插入排序的基础上,查找有序字表使用折半查找
折半查找仅仅减少的比较元素次数,并没有减少元素的移动次数,所以时间复杂度和简单插入排序相同
/*时间复杂度:O(n^2) 稳定性:稳定*/
void InsertSort(ElemType A[],int n) //折半插入排序
{
int i,j,low,high,mid;
for(i=2;i<=n;i++)
if(A[i].key<A[i-1].key) //依次将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].key>A[0].key) high=mid-1;//查找左半子表
else low=mid+1; //查找右半子表
}
for(j=i-1;A[0].key<A[j].key;--j) //从后往前查找待插入位置
A[j+1]=A[j]; //向后移动
A[high+1]=A[0]; //复制到插入位置
}
}
希尔排序(缩小增量排序)
先将排序表分割成若干个形如L[i,i+d,i+2d,…i+kd]的特殊子表,分别进行直接插入排序,当表中元素已呈"基本有序"时,再对全体记录进行一次直接插入排序
1)取一个小于n的步长d1,将表中全部记录分为d1个组,所有距离为d1的倍数记录放在同一个组中,在各组中进行直接插入排序
2)取第二个步长d2<d1,重复上述过程直至dt=1
3)再对全体进行一次直接插入排序
/*时间复杂度:O(n^2) 稳定性:稳定*/
void ShellSort(ElemType A[],int n)
{
//1.记录前后位置的增量是dk
//2.A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for(int dk=n/2;dk>=1;dk/=2)
for(int i=dk+1;i<=n;i++)
if(A[i].key<A[i-dk].key)
{
A[0]=A[i];
for(j=i-dk;j>0&&A[0].key<A[j].key;j-=dk)
A[j+dk]=A[j];
A[j+dk]=A[0];
}
}
(二)交换排序
根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置
冒泡排序
待排序表长n,从前往后两两比较相邻元素的值,若为逆序则交换他们直至序列比较完成,称为一趟排序。这样最多做n-1趟冒泡就可把所有元素排序
/*时间复杂度:O(n^2) 稳定性:稳定*/
void BubbleSort(ElemType A[],int n)
{
for(i=0;i<n-1;i++)
{
flag=false;
for(j=n-1;j>i;j--)
if(A[j-1].key>A[j].key)
{
swap(A[j-1],A[j]);
flag=true;
}
if(flag==false) return;
}
}
快速排序
在待排序表L[1…n]中任取一个元素pivot作为基准,通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中所有元素小于pivot,L[k+1…n]中所有元素大于或等于pivot,则pivot放在其最终位置L(k)上,称为一趟快速排序。而后分别对两个子表进行递归直至每部分内只有一个元素或空为止,即所有元素放在了最终位置上
/*时间复杂度:O(nlogn) 稳定性:不稳定*/
void QuickSort(ElemType A[],int low,int high)
{
if(low<high)
{
int pivotpos=Partition(A,low,high); //将表划分为两个子表
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotopos+1,high);
}
}
int Partition(ElemType A[],int low,int high)
{ //一趟排序过程
ElemType 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;
}
初始排序表基本有序或基本逆序时,会得到最坏时间复杂度O(n^2)。理想时间复杂度为O(nlogn)
(三)选择排序
第i趟在后面n-i+1个待排序元素中去关键字最小的元素作为有序子序列的第i个元素,直至第n-1趟做完,待排元素仅剩下1个即排序完成
简单选择排序
/*时间复杂度:O(n^2) 稳定性:不稳定*/
void SelectSort(ElemType A[],int n)
{
for(int i=0;i<n-1;i++) //共进行n-1趟
{
min=i;
for(int j=i;j<n;j++)
if(A[j]<A[min])
min=j; //更新最小元素位置
if(min!=i) swap(A[i],A[min]);
}
}
堆排序
将L[1…n]看成是一棵看成一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或最小)的元素
大顶堆:L(i)>=L(2i)且L(i)>=L(2i+1) 小顶堆:L(i)<=L(2i)且L(i)<=L(2i+1)
对第n/2个结点为根的子树筛选,使该子树成为堆。之后向前依次对各结点为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不是则将左右子节点中较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,之道以该结点为根的子树构成堆为止
/*时间复杂度:O(n)*/
void BuildMaxHeap(ElemType A[],int len)
{//建立大顶堆
for(int i=len/2;i>0;i--) //从i=n/2~1反复调整堆
AdjustDown(A,i,len);
}
void AdjustDown(ElemType A[],int k,int len)
{//将元素k向下调整
A[0]=A[k]; //暂存要调整的元素A[k]
for(int i=2*k;i<=len;i*=2) //比较子结点
{
if(i<len&&A[i]<A[i+1])
i++; //取较大的子结点
if(A[0]>=A[i]) break;
else
{
A[k]=A[i]; //A[i]上移至双亲节点
k=i; //改变k值继续向下筛选
}
}
A[k]=A[0]; //被筛选结点值放入最终位置
}
堆排序步骤:
1)首先将n个元素建成初始堆,例如大顶堆中堆顶元素即为最大值
2)输出堆顶元素后,通常将堆底元素送入堆顶,堆顶元素下调以保持大顶堆
3)重复直至仅剩一个元素为止
/*时间复杂度:O(nlogn) 稳定性:不稳定 */
void HeapSort(ELemType A[],int len)
{
BuildMaxHeap(A,len); //初始建堆
for(int i=len;i>1;i--)
{
swap(A[i],A[1]); //输出堆顶元素,和堆底元素交换
AdjustDown(A,1,i-1); //把剩余的i-1个元素整理成堆
}
}
(四)归并排序
“归并”的含义是将两个或两个以上的有序表组合成为一个新的有序表
假设待排序表含有n个记录,则可以看成n个有序子表,两两归并得到n/2个长度为2的有序子表;再两两归并直至合成一个长度为n的有序表为止。这种方法称为2-路归并排序
/*时间复杂度:O(n)*/
ElemType *B=(ElemType*)malloc((n+1)*sizeof(ElemType)); //辅助数组B[]
void Merge(ElemType A[],int low,int mid,int high)
{//表A中两段A[low...mid]和A[mid+1...high]各自有序,将它们合并成一个有序表
for(int k=low;k<=high;k++)
B[k]=A[k]; //将A中所有元素复制到B中
int i,j,k;
for(i=low,j=mid+1,k=i;i<mid&&j<=high;k++)
{
if(B[i]<=B[j]) //比较B两段中的元素按大小复制到A中
A[k]=B[i++];
else
A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++]; //若第一个表未检测完,继续复制
while(j<=high) A[k++]=B[j++]; //若第二个表未检测完,继续复制
}
一趟归并排序的操作是调用merge()进行两两归并,整个归并排序需要进行logn趟
1)分解:将含有n个元素的待排表分成各含n/2个元素的子表采用2-路归并排序进行递归
2)合并:合并两个已排序的子表得到排序结果
/*时间复杂度:O(nlogn) 稳定性:稳定*/
void MergeSort(ElemType A[],int low,int high)
{
if(low<high)
{
int mid=(high+low)/2; //划分子序列
MergeSort(A,low,mid); //对左侧进行递归排序
MergeSort(A,mid+1,high); //对右侧进行递归排序
Merge(A,low,mid,high); //归并
}
}
(五)桶排序
桶排序(Bucket sort)是一种基于计数的排序算法,工作的原理是将数据分到有限数量的桶子里,然后每个桶再分别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。当要被排序的数据内的数值是均匀分配的时候,桶排序时间复杂度为O(n)。
简单来说,就是有一个数量为n的待排数组A,范围为0~Max。我们创建一个大小为Max+1的数组B,初始化为0。遍历数组A,当读取A[i]时,B[A[i]]+1(装进相应的桶里),这样在遍历完数组A之后直接按排序要求扫描数组B进行输出即可。
/*时间复杂度:O(n)*/
void BucketSort(int*A,int Max,int n)
{
int B[Max+1];
memset(B,0,sizeof(B));
for(int i=0;i<n;i++)
B[A[i]]++;
}