输入: 一个 n n n个数的序列 [ a 1 , a 2 , . . . , a n ] [a1,a2,...,an] [a1,a2,...,an]。
输出: 输出原序列的重排,使得 a 1 < = a 2 < = . . . < = a n a1<=a2<=...<=an a1<=a2<=...<=an。
在实际中,待排序的数很少时单独的数值,它们通常是记录的数据集一部分。每个记录包含一个关键字,就是排序问题中的重排的值。记录剩余部分为卫星数据,通常与关键字一同存取。在实际中,当一个排序算法重排关键字时,也必须要重排卫星数据。如果每个记录包含大量卫星数据,我们通常重排记录指针的数组,而不是记录本身,这样可以降低数据移动量。
排序按照是否进行元素之间比较分为比较排序和非比较排序。
其中比较排序有常见的如:
而非比较排序有:计数排序 桶排序 基数排序
特殊的有,按照输入数组是否仅有常数个元素需要在排序过程中存储在数组之外,分为原址排序和非原址排序。
另外,按照元素 a a a和元素 b b b满足 a = b a=b a=b,排序前的前后关系和排序后是否一样分为稳定排序和不稳定排序。
排序算法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 原址 | 稳定 |
---|---|---|---|---|---|---|
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 原址 | 稳定 |
希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 原址 | 不稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 原址 | 不稳定 |
堆排序 | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( 1 ) O(1) O(1) | 原址 | 不稳定 |
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 原址 | 稳定 |
归并排序 | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n ) O(n) O(n) | 非原址 | 稳定 |
快速排序 | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n) | 非原址 | 不稳定 |
计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 非原址 | 稳定 |
桶排序 | O ( n + k ) O(n+k) O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n + k ) O(n+k) O(n+k) | 非原址 | 稳定 |
基数排序 | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n + k ) O(n+k) O(n+k) | 非原址 | 稳定 |
冒泡排序顾名思义就是小的元素就如水泡一样往上冒。重复走过要排序的数列,一次比较两个元素,如果他们打的顺序作物就把它们交换过来。
#include<bits/stdc++.h>
using namespace std;
template<typename comparable>
void Bubblesort(vector<comparable> &a){
int flag=1; //记录是否比较
for(int i=0;flag;i++){ //上次冒泡没有比较过,就跳出循环
flag=0;
for(int j=a.size()-1;j>i;j--)
if(a[j]<a[j-1]){ //这里注意是小于,保证稳定排序
swap(a[j],a[j-1]);
flag=1;
}
}
}
最直观的排序,原址稳定排序,就是复杂度不行,不适合大数组排序。
选择排序,是找到未排序数组的最小元素,放到排序好数组的末尾。直到所有元素都是已排序的。
n个记录经过选择排序n-1趟可以得到有序结果。
#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Selectsort(vector<comparable> &a){
for(int i=0;i<a.size();i++){
int minid=i; //记录最小值的位置
for(int j=i+1;j<a.size();j++)
if(a[j]<a[minid]){
minid=j;
}
swap(a[i],a[minid]); //交换最小值的值到第i个
}
}
表现最稳定的排序算法之一,因为无论什么数据都是O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。另外不占用额外内存空间。
插入排序构建有序序列,对后面的元素插入到正确的位置。也是非常直观呢!
插入排序是原址的。
#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Insertsort(vector<comparable> &a){
for(int j=1;j<a.size();j++){
auto key = a[j]; //待插元素
int i=j-1;
while(i>=0 && a[i]>key){ //这里用>保证稳定性
a[i+1]=a[i]; //比a[j]大的后面凉快去!
i--;
}
a[i+1]=key; //最后把key换到正确的位置
}
}
插入排序是原址排序,经常作为其他方法收尾时的选择。
第一个突破 O ( n 2 ) O(n^2) O(n2)的排序,是插入排序的改进版。它会优先比较距离较远的元素,希尔排序又名缩小增量排序。
#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void Shellsort(vector<comparable> &a){ //其实就是间隔版的插入排序
//利用性质,大间隔排好序,小间隔排序时不会影响大间隔排序,牛!时间也快起来了
for(int gap=a.size()/2;gap>0;gap/=2)
for(int j=gap;j<a.size();j++){ //下面就是插入排序套路
auto key=a[j];
int i=j-gap; //跳gap排序,比如a[0] a[gap]
while(i>=0 && a[i]>key){
a[i+gap]=a[i]; //a[j]=a[0]
i-=gap; //每次减少gap i=-gap;
}
a[i+gap]=key; //最后插入key,a[0]=a[j]
}
}
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。
前面所讨论的排序算法都是复杂度为 O ( N 2 ) O(N^2) O(N2)的低效率排序算法。下面的算法都是时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)的高级算法。
归并算法是基于分治策略。归并算法的基础是合并两个已经有序的子数组,将两个已经有序的子数组进行合并是容易的。比如两个有序子数组 A A A和 B B B,然后有一个输出数组 C C C。此时你需要三个位置索引 i 、 j i、j i、j和 k k k,每次比较 A [ i ] A[i] A[i]与 B [ j ] B[j] B[j],然后将最小者复制到 C [ k ] C[k] C[k],同时递增相应的位置索引。重复上述过程知道某一个子数组遍历完,未遍历完的子数组剩余部分直接复制到输出数组就完成整个合并过程。
利用合并,归并排序算法的步骤为:
(1)将数组分为两个大小相等的子数组;
(2)对每个子数组进行排序,除非子数组比较小,否则利用递归方式完成排序;
(3)合并两个有序的子数组,完成排序。
#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
void merge(vector<comparable> &a,vector<comparable> &tmp,int left,int right,int end){
int tmpPos = left;
int leftEnd = right-1;
int num = end-left+1;
//O(n)合并两个子数组直到一个子数组遍历完
while(left<=leftEnd && right<=end){
if(a[left]<=a[right]) tmp[tmpPos++]=a[left++];
else tmp[tmpPos++]=a[right++];
}
//处理剩余数
while(left<=leftEnd) tmp[tmpPos++]=a[left++];
while(right<=end) tmp[tmpPos++]=a[right++];
//结果返回给原数组
while(num--) a[end]=tmp[end],end--;
}
//下面是不停二分数组,递归调用环节O(lgn)
template <typename comparable>
void Mergesort(vector<comparable> &a,vector<comparable> &tmp,int left,int right){
if(left<right){
int mid = left+right>>1;
Mergesort(a,tmp,left,mid);
Mergesort(a,tmp,mid+1,right);
merge(a,tmp,left,mid+1,right);
}
}
归并排序是稳定的,但是不是原址的,但表现比选择排序好很多。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
#include<bits/stdc++.h>
using namespace std;
template <typename comparable>
//选择快排主元,一半都小于,一半都大于
const comparable& median(vector<comparable> &a,int left,int right){
int mid = left+right>>1;
if(a[mid]<a[left]) swap(a[mid],a[left]);
if(a[left]>a[right]) swap(a[left],a[right]);
if(a[mid]>a[right]) swap(a[mid],a[right]);
// left位置的值小于等于pivot,right位置的值一定大于等于pivot,
// 要分割的数组变成left+1到right-1
swap(a[mid],a[right-1]); //将pivot放到right-1位置处
return a[right-1];
}
template <typename comparable>
void Quicksort(vector<comparable> &a,int left,int right){
if(left+1<right){
auto pivot = median(a,left,right);
int i=left,j=right-1;
while(1){
while(a[++i]<pivot){}
while(a[--j]>pivot){}
if(i<j) swap(a[i],a[j]);
else break;
}
swap(a[i],a[right-1]);
//对子数组递归
Quicksort(a,left,i-1);
Quicksort(a,i+1,right);
}else if(left<right) if(a[left]>a[right]) swap(a[left],a[right]); //只有两个元素特殊情况
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
#include<bits/stdc++.h>
using namespace std;
#define lf(root) 2*root+1
//#define rt(root) 2*root+2
template <typename comparable>
// 堆排序辅助函数
// v是存储堆的数组,i是要下沉的节点,n代表当前堆的大小
void siftDown(vector<comparable> &a,int i,int n){
int child;
auto tmp=a[i];
while(lf(i)<n){
child=lf(i); //左子节点
//寻找最大子节点
if(child!=n-1 && a[child]<a[child+1]) child++;
if(tmp<a[child]) {//子节点上移
a[i]=a[child];
i=child;
}else break;
}
a[i]=tmp; //下沉到正确位置
}
template <typename comparable>
void Heapsort(vector<comparable> &a){
//建堆
for(int i=a.size()/2-1;i>=0;i--) siftDown(a,i,a.size());
//删除重复根节点
for(int i=a.size()-1;i>0;i--){
swap(a[i],a[0]); //交换根节点与最右子节点
siftDown(a,0,i); //下沉根节点
}
}
一般合并排序和快速排序用的很多,一定要会手撕!当然也可以自己看c++ STL库查看sort实现源码,仅仅只用了快速排序,还结合了插入排序和堆排序。可以参照