排序可是说是算法里面的经典问题了。排序算法的发展史,可以看作是算法进化史的一个缩影。
启蒙
在这一阶段的算法明确地提出了排序问题,并给出了简单易行的解决方案。虽然局限于奢侈的时间复杂度,但为后序的发展给出了良好的开端。
冒泡排序
冒泡排序几乎是每个初学者最早接触到的排序方案。其大体思想是每次遍历时,逐一比较相邻的两元素,将较大者交换至右侧。这样每次遍历都可以将当前序列的最大值交换至序列的最右端。不断进行以上的遍历方案,并在该过程中缩小待处理序列的规模,直至整个过程完成。
int* bubbleSort(int* A, int n) {
//长度为n的数组需要遍历n-1次
for(int i=0;i<n-1;++i){
//遍历开始时右侧已排好i个元素,故待处理区间为[0,n-i)
//因为需要j+1<n-i所以j的取值是[0,n-i-1)
for(int j=0;j<n-i-1;++j){
if(A[j]>A[j+1]){
swap(A[j],A[j+1]);
}
}
}
return A;
}
一个简单的冒泡排序示意图:
选择排序
选择排序更容易理解一些。其大体思想是每次从待处理序列中选出最小者交换至最左端,然后对剩下的序列继续该操作直至排序结束。
int* selectionSort(int* A, int n) {
//长度为n的数组需要遍历n-1次
for(int i=0;i<n-1;++i){
//每次找出最小值的下标
int min=i;
//A[i]暂定为最小值,故j遍历范围是[i+1,n)
for(int j=i+1;j<n;++j){
if(A[j]<A[min])
min=j;
}
//交换
swap(A[min],A[i]);
}
return A;
}
插入排序
插入排序很类似与打扑克牌时,把一张新摸到的牌插入到手牌里的处理方式。其大体思想是每次处理一个元素,该元素的左侧是已经处理好的有序数列(初始长度为1),将该元素插入到此数列的正确位置中去。
int* insertionSort(int* A, int n) {
//初始时认为A[0]是有序数列,待处理元素范围是[1,n)
for(int i=1;i<n;i++){
int get=A[i];
//j视作是待处理元素应插入数列中的位置,从i-1到0逆序搜索
int j=i-1;
//搜索条件:j未越界且位置j上的元素大于待处理元素
while(j>=0 && A[j]>get){
//相当于元素右移一位
A[j+1]=A[j];
j--;
}
//循环结束时,若j越界则需插入到A[0]处;
//否则A[j]<=get需要插入到A[j+1]处
A[j+1]=get;
}
return A;
}
其他
除了上述之外还有一些改良算法,比如对冒泡排序改良的鸡尾酒排序,不过并未降低时间复杂度;对选择排序的双向选择排序,有轻微的时间复杂度改良;对插入排序的二分法插入排序,可以节省比较次数,但避免不了遍历次数。
总结与对比
时间复杂度:O(N2)
共需要N-1次循环,每次需要遍历N-…
成熟
成熟时期的排序算法可以达到O(NlogN)的时间复杂度,基本上已经是排序的极限了。尽管后序有O(N)的方案,但也仅仅在特定条件下有效。
快速排序
快速排序的思路不难想到,找出一个基准元素,小于它的放数组左边,大于它的放右边,然后对左右的子数组分别进行如上操作。其核心手段在于对数组进行调节,使得小于和大于基准元素的数据分布于它的异侧。
这个问题早期有这样一种解法:指针i和指针j分别指向首尾,并逐步向中间靠拢,指针i找到一个大于基准的元素,等待j指针找到一个小于基准的元素,两者交换。
不过现在比较流行的解法是下面这样的:给定一个小于等于区间(初始右边界为-1);当发现一个小于基准的元素,就把它与小于等于区间的右侧元素交换,同时该区间扩充一位。
归并排序
将两个有序数组合并是比较容易的,那么