博主之前刷过一遍《算法导论》(Introduction to Algorithms),但是看得多写得少,于是最近抽空动手写写下里面的算法。排序是经典问题了,于是决定将书中的排序算法一一使用C++实现。这篇博客一方面是对排序算法本身的总结(这个书上都有啦),另一方面是博主在使用C++实现算法时的一些体会,更多的是算法细节,希望有助于加深对算法的理解的。
排序算法总结
排序算法,介绍算法导论中的七种算法,它们的时间复杂度和空间复杂度如下表。插入排序代表了最坏的情况,归并排序,堆排序,快速排序基本都到达了使用内存排序算法的时间下界[mathjax]$latex n\lg (n)$,而后面三种排序,是使用了内存寻址的方式,实现了线性时间排序,桶排序是较为一般的方法,而基数排序和计数排序则需要被排序数字为整数。具体的实现推荐参考算法导论,这篇博客主要提供代码解释。
[table id=1 /]
[table id=2 /]
上面的gif摘自另一篇总结排序算法的博客。再简单介绍两种本博客不详细给出代码的算法。
鸡尾酒排序
鸡尾酒排序也叫作定向冒泡排序,与冒泡排序不同之处在于从低到高然后从高到低的比较每个元素,稍微提高了冒泡排序的性能。
希尔排序
希尔排序是插入排序的改进版本,但是希尔排序是不稳定的排序算法。改进之处在于将比较的全部元素分为几个区域来提升排序的性能。这样可以让一个元素一次性朝最终位置前进一大步,然后算法在取越来越小的步长进行排序,最后一步就是普通的插入排序。
插入排序
插入排序的核心就是维持一个循环不变式,在循环不变式内部的数组是已经排好序的,我们把后面的数组一个个的加进来。
#include <iostream>
#include <cstdio>
int insertionSort(int* a, int len){
// 核心是维持循环不变式
if (len < 2)
return 0;
for (int j = 1; j < len; ++j){
int i = j;
while (i >= 0){
if (a[i-1] > a[i]){
int tmp = a[i-1];
a[i-1] = a[i];
a[i] = tmp;
}
i--;
}
}
return 0;
}
int outputArray(int* a, int len){
for (int i = 0; i < len; ++i)
std::cout << a[i] << " ";
std::cout << std::endl;
return 0;
}
int heapSort(int *a, int length);
int mergeSort(int *a, int p, int r);
int quickSort(int *a, int p, int r);
int quickSortS(int *a, int p, int r);
int radixSort(int *a, int length);
int main(int argc, char *argv[]){
int a[13] = {12, 36, 34, 9, 35, 127, 55, 91, 82, 55, 64, 1000, 2};
insertionSort(a, 13);
//mergeSort(a, 0, 12);
//heapSort(a, 13);
//quickSortS(a, 0, 12);
//quickSort(a, 0, 12);
//radixSort(a, 13);
outputArray(a, 13);
system("pause");
return 0;
}
归并排序
归并排序采用了分解、解决、合并的分治策略。最重要的是合并步骤,也就是下面的merge函数,在merge中需要使用额外的内存,这是归并排序的不足。
#include <cstdio>
#include <iostream>
int merge(int *a, int p, int q, int r){
int nl = q - p + 1;
int nr = r - q;
int *L = new int[nl+1];
int *R = new int[nr+1];
for (int ii = 0; ii < nl; ++ii){
L[ii] = a[p+ii];
}
L[nl] = INT_MAX;
for (int ii = 0; ii < nr; ++ii){
R[ii] = a[q+ii+1];
}
R[nr] = INT_MAX;
int i = 0, j = 0;
for (int k = p; k <= r; ++k){
if (L[i] <= R[j]){
a[k] = L[i];
i++;
} else{
a[k] = R[j];
j++;
}
}
delete[] L, R;
return 0;
}
int mergeSort(int *a, int p, int r){
if (p < r){
int q = p + (r - p) / 2;
mergeSort(a, p, q);
mergeSort(a, q+1, r);
merge(a, p, q, r);
}
return 0;
}
堆排序
堆排序算法通过一个大(小)顶堆的性质来实现排序。大顶堆的性质是父亲比儿子们都要大,所以第一个元素总是最大的,在排序时不断将最大的元素提取出来,提取出来的元素就形成了排序后的数组。
此外,堆排序算法还可以形成一个优先队列,参考算法导论。
#include <cstdio>
#include <iostream>
inline int left(int i){
return 2 * i + 1;
}
inline int right(int i){
return 2 * i + 2;
}
inline int parent(int i){
return (i - 1) / 2;
}
int maxHeapify(int *a, int i, int heapsize){
int maxv = a[i];
int maxi = i;
if (left(i) < heapsize){
if (a[left(i)] > maxv){
maxv = a[left(i)];
maxi = left(i);
}
}
if (right(i) < heapsize){
if (a[right(i)] > maxv){
//maxv = a[right(i)];
maxi = right(i);
}
}
if (maxi != i){
int tmp = a[i];
a[i] = a[maxi];
a[maxi] = tmp;
maxHeapify(a, maxi, heapsize);
}
return 0;
}
int buildMaxHeap(int *a, int length){
for (int i = length/2-1; i >= 0; i--){
maxHeapify(a, i, length);
}
return 0;
}
int heapSort(int *a, int length){
buildMaxHeap(a, length);
for (int i = length - 1; i > 0; --i){
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
maxHeapify(a, 0, length);
}
return 0;
}
快速排序
快速排序也是通过分治策略来实现的排序,每次将一个元素排到正确位置。快速排序是原址排序,所以合并过程省略了,原址排序也是快速排序最大的优势。
此外,下面还贴出了针对相同元素的快速排序算法。相当于原快速排序是二指针问题,因为增加了一个区间,所以变成了三指针问题。
#include <cstdio>
#include <vector>
using namespace std;
int partition(int *a, int p, int r){
int x = a[r];
int i = p - 1, j = p;
for (int j = p; j < r; ++j){
if (a[j] <= x){
++i;
int tmp = a[j];
a[j] = a[i];
a[i] = tmp;
}
}
++i;
int tmp = a[i];
a[i] = a[r];
a[r] = tmp;
return i;
}
int quickSort(int *a, int p, int r){
if (p < r){
int q = partition(a, p, r);
quickSort(a, p, q-1);
quickSort(a, q+1, r);
}
return 0;
}
vector<int> partitionS(int *a, int p, int r){
vector<int> q;
int x = a[r];
int i = p, j = p, k = r;
while (j <= k){
if (a[j] > a[i]){
swap(a[j], a[k]);
k--;
} else if(a[j] < a[i]){
swap(a[j], a[i]);
i++;
j++;
} else{
j++;
}
}
q.push_back(i-1);
q.push_back(j);
return q;
}
int quickSortS(int *a, int p, int r){
if (p < r){
vector<int> q = partitionS(a, p, r);
quickSortS(a, p, q[0]);
quickSortS(a, q[1], r);
}
}
基数排序
基数排序虽然贴着线性时间的标签,但是常数项还是比较大的,就是通过对每个位进行一次稳定排序,来实现整体的排序。下面的代码中使用的是最低有效位的方式进行排序。
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int radixSort(int *a, int len){
int base = 10;
int maxvalue = a[0];
for (int i = 1; i < len; ++i){
if (a[i] > maxvalue){
maxvalue = a[i];
}
}
vector<vector<int>> buckets(base);
for (int i = 1; i <= maxvalue; i = i*base){
for (int j = 0; j < len; j++){
buckets[(a[j] / i) % base].push_back(a[j]);
}
int k = 0;
for (int j = 0; j < base; j++){
for (auto num : buckets[j]){
a[k++] = num;
}
buckets[j].clear();
}
}
return 0;
}
计数排序&桶排序
这个就不多讲了,计数排序直接将数组下标作为地址,而计数排序就可以看做桶大小为1的桶排序。
