progress

I'd rather be anything but ordinary

0%

Sort

排序方法已经学了不少了,是时候总结下各种排序,在我看来排序总共分为两种,基于比较的排序和不基于比较的排序。当然这是一种分类,也可以按照排序是否稳定分类。

基于比较的排序

这里先要说明,基于比较的排序(comparsion-based algorithm)最优时间复杂度为$O(nlogn)$,证明如下:
对于每次比较,相当于一个三叉的决策树,此树的高h=(log3(n!))=log3e.ln(n!),由stirling逼近公式得h>=O(nlogn).

冒泡排序(bubbleSort)

每个人对冒泡排序并不陌生,相信每个人都可以轻松地写出来,对此不再赘述,仅给出实现。

void bubbleSort(vector<int>& A){
bool sorted=false;int n=A.size();
while(!sorted){
sorted=true;
for(int i=1;i<n;++i){
if(A[i]<A[i-1])
swap(A[i],A[i-1]);
}
n--;
}
}

插入排序(insertionSort)

插入排序,每次排序将后面未就绪的数不断插入到前面已就绪的数中。

void insertionSort(vector<int>& A){
for(int j=1;j<A.size();++j){
int key=A[j],i=j-1;
while(i>=0&&key<=A[i]){
A[i+1]=A[i--];
}
A[i+1]=key;
}
}

我们分析下他的时间复杂度$O(n^2)$,和冒泡相比并未得到改变,其实仔细观察下每次迭代,都需要在前面已就绪的位置查找,对于有序向量的查找,我们可以采用二分查找,(误)时间复杂度为$O(logn)$,总复杂度达到$O(nlogn)$,已达最优下界。由于向量插入也需要$O(n)$的操作,所以时间复杂度仍为$O(n^2)$.
具体实现如下:

int binSearch(vector<int>& A,int &key,int lo=0,int hi=A.size()){
while(lo<hi){
int mi=(lo+hi)>>1;
(key<A[mi])?hi=mi:lo=mi+1;
}
return --lo;
}
void insert(vector<int>& A,int key,int lo,int hi){
while(hi>lo)
A[hi]=A[hi-1];
A[lo]=key;
}
void insertionSort(vector<int>& A){
for(int i=1;i<A.size();++i){
int key=A[i];
insert(A,key,binSearch(A,key,0,i),i);
}
}

选择排序(selectionSort)

选择排序和插入排序正相反,他是在前面中未就绪元素中选取最大值,插入在至后面已就绪的位置。
具体实现如下:

int max(int lo,int hi){
int maxNum=hi;
while(lo<hi--)
if(A[hi]>A[maxNum])
maxNum=hi;
return maxNum;
}
void selectionSort(vector<int>& A,int lo=0,int hi=A.size()){
while(lo<--hi){
swap(A[max(lo,hi)],A[hi]);
}
}

时间复杂度为$O(n^2)$,

归并排序(mergeSort)

介绍了几个时间复杂度为$O(n^2)$的算法,下面开始介绍时间复杂度为$O(nlogn)$的算法。
归并排序是典型的分治算法(divide and conquer),首先他将原序列分解为单独的单个元素,然后再不断合并各个子序列。
实现如下:

void mergeSort(vector<int>& A,int lo,int hi){
if(hi-lo<2) return ;//递归基
int mi=(lo+hi)>>1;
mergeSort(A,lo,mi); mergeSort(A,mi,hi);
merge(A,lo,mi,hi);
}
void merge(vector<int>& _elem,int lo,int mi,int hi){
int *A=_elem+lo;
int lb=mi-lo; int *B=new int[lb];
for(int i=0;i<lb;B[i]=A[i++]);
int lc=hi-mi;int *C=_elem+mi;
/* for(int i=0,j=0,k=0;(j<lb)||(k<lc);){
if((j<lb)&&(!(k<lc)||(B[j]<=C[k]))) A[i++]=B[j++];
if((k<lc)&&(!(j<lb)||(C[k]<B[j]))) A[i++]=C[k++];
}*/
for(int i=0,j<0,k=0;j<lb;){
if((lc<=k)||B[j]<=C[k]) A[i++]=B[j++];
if((k<lc)&&C[k]<B[j]) A[i++] = C[k++];
}
}

对于时间复杂度的分析如下:
$T(n)=2O(n/2)+O(n)$
$T(1)=O(1)$
得$T(n)/n=O(n/2)/(n/2)+O(1)$
若$S(n)=T(n)/n$

$S(n)=S(n/2)+O(1)$
$S(n)=S(n/4)+O(2)$
·····
$S(n)=S(n/2^k)+O(k)=O(logn)$
所以有$T(n)=S(n)
n=O(nlogn)$
时间复杂度为O(nlogn),前面已经证明$O(nlogn)$已经为基于比较的最优下界。

快速排序

快速排序和归并排序类似,都是分治思想,不过归并主要时间消耗在合并,快排主要时间消耗在划分子序列。
实现如下:

int partition(int* A,int lo,int hi){
int pivot=A[lo];
while(lo<hi){
while((lo<hi)&&(pivot<=A[hi])) hi--;
A[lo]=A[hi];
while((lo<hi)&&(A[lo]<=pivot)) lo++;
A[hi]=A[lo];
}
A[lo]=pivot;
return lo;
}
void quickSort(int *A,int lo,int hi){
if(hi-lo<2) return ;
int mi=partition(A,lo,hi-1);
quickSort(A,lo,mi);
quickSort(A,mi+1,hi);
}

平均时间复杂度为O(nlogn).

堆排序

const int maxn=1000;
int sz=0;
int heap[maxn];
void push(int x){
int i=sz++;int p;
while(i>0){
p=(i-1)/2;
if(heap[p]<x) break;
heap[i]=heap[p];
i=p;
}
heap[i]=x;
}
int pop(){
int x=heap[--sz];
int res=heap[0];
int i=0;
while(i*2+1<sz){
int a=i*2+1,b=i*2+2;
if(heap[a]>heap[b]) swap(a,b);
if(heap[a]>=x) break;
heap[i]=heap[a];
i=a;
}
heap[i]=x;
return res;
}

非基于比较的排序

计数排序(countingSort)

基数排序(radixSort)

桶排序(bucketSort)