比较排序算法的简单介绍和复杂度分析
标题长坑,就拿最近有在用的来弄吧,先写个大概,以后逐渐完善。
对于以下代码,默认包含头文件:
#include<algorithm>
1.冒泡排序(Bubble Sort)
先从课本上最常见的冒泡排序说起吧,C++代码如下(个人习惯介绍算法使用C++,比伪代码还通俗易懂.......)
void BubbleSort(int *A, int n) { for (int i = 0;i < n;++i)//遍历数组 { for (int j = n - 1;j > i;--j)//倒叙到i { if (A[j - 1] > A[j]) std::swap(A[j], A[j - 1]); }//每轮循环后,都会把一个最小的元素放在最左边不遍历区 }//i轮后,数组排序完成 }
在下文中,我会给出实验测试算法复杂度的方法,我们不急,我先下结论,冒泡排序的复杂度是O(n^2)。
2.插入排序(Insert Sort)
从个人角度讲,在冒泡排序之后,个人喜欢先介绍插入排序,原因在于,在基本算法中,实验证明,insert sort是当n<=9时的最优秀算法,个人喜欢合并使用quick sort 和 insert sort。
void insertSort(int* A, int length) { for (int j = 1;j < length;++j)//从数组第二个数开始 { int key = A[j];//让选取的数做key //Insert A[j] into the sorted sequence A[0,...j-1] int i = j - 1;//从key前面的值开始倒叙遍历 while (i >= 0 && A[i]>key) { A[i + 1] = A[i];//不断地移动数组中元素,留出key的位置 i--; } A[i + 1] = key;//插入key } }
insert sort 同样是一个O(n^2)的算法。
赋值除了采用这种移动数组的方法,也可以使用对换插入元素的方法,方法如下:
//另一种赋值方法 #include <algorithm> void insertSortAnother(int A[], int length) { for (int j = 1;j < length;++j) { int k = j; int i = j - 1; while (i >= 0 && A[i] > A[k]) { std::swap(A[i], A[k]); k = i--; } } }
效率:T(n)=T(n-1)+n=T(1)+(1+2+3+...+n-1)=T(1)+n*(n-1)/2(最差情况),是一个O(n^2)算法,实际上,式中的n是最差情况,及式前每一个数都比所选数字大,如果只是乱序微调,n可以降到很小的数,以至于在极限情况下
T(n)=T(n-1)+C=T(1)+C*n,是一个O(n)算法,因而insert sort适合对已经基本排好序但是部分退化的数据进行调整。
另外在赋值过程中,可以采用binary search 寻找合适的插入节点来进一步优化算法,使insert sort在大数据前仍然不太逊色。有时间时我会补充此种插入排序。
3.快速排序(Quick Sort)
选择快排作为第三个介绍的算法,也是因为他在n值足够大时无与伦比的优越性,这里有时间我会展开说,先简单放一下代码。
void quickSort(int* A, int low, int high) { if (low + 1 >= high) return; int i = low; int j = high; do { while (A[i] <= A[high]) { ++i; } while (j > i && A[j] >= A[high]) { --j; } swap(A[i], A[j]); } while (i < j); swap(A[high], A[i]); quickSort(A, low, i - 1); quickSort(A, i + 1, high); }
快速排序是一种O(NlogN)的排序算法。当测试数据足够大时,其将会显现出无与伦比的优越性,接下来的测试比较中会有说明。
简单分析一下复杂度:
∵quicksort先从两边一起遍历将数组遍历完整的一遍,然后分成两部分变为两个子排序,假设两个自排序的数据规模是a和b
∴T(n)=T(a)+T(b)+a+b
如果a和b很均匀,及a+b+1=n,考虑到数字很大,a+b≈n
T(n)=2*T(n/2)+n
设n=2^k, T(2^k)=2*T(k-1)+2^k
=(2^(k-1))*T(1)+(1+2+4+8+...+k)*(2^k)
=(2^(k-1))*T(1)+1*(1-2*k)/(1-2)*(2^k)
T(n)=(n/2)*T(1)+(2*log(n)-1)*n=n*(T(1)/2+2*log(n)-1)
我们由此得出,在数据均匀(最佳情况)快速排序是一个O(n*log(n))的算法。
而在最差情况下,及每次分出的a和b规模都相差悬殊,a=n-1,b=1
T(n)=T(n-1)+T(1)+n=T(1)+(n-1)*(n+T(1))
我们由此得出,在数据极端(最差情况)快速排序是一个O(n^2)的算法。
而其他情况,快速排序的效率在这二者之间,因而,Pivot的选择十分重要,如果Pivot能尽量让数据均匀的分布在两侧,则能获得更高的效率。一种常用的做法是从数据的头,中,尾各选取一个数字,并选择三者的中间值作为pivot,这种做法大幅度的减少了极端数据的可能,但同时也要考虑选取pivot的这个算法本身的时间和空间消耗。
若不通过Pivot选择来优化,也可以通过根据数据量优化的方法,即当所排列的数据小于10个时,改用对小数据效率更高的InsertSort来进行排序(实验证明,在数据量为9个时,insert sort有着更佳的效率,考虑到被快排后的数据比较整齐,可以考虑在数据量更多时就停止快排),这种方法排除了quick sort遇到极端数据时的尴尬。另一种做法是当快速排序需要处理的数据规模更小时停止快速排序,之后对所有数据进行一次InsertSort,因为处理这种几乎排列完毕的数组时,insert sort的效率接近O(n)。
当快排到达一定深度后,用堆排序来完成剩下的工作也是一种不错的选择,这样避免了堆排序的最坏情况,使其保持了O(n*log(n))的复杂度,同时避开了快排的小规模数据处理问题。
在允许出现相同数字的一组数据中,产生最坏情况往往是相同数据导致的,即到某个子排序,所有数据都相同,导致快排进行了无意义的操作。一种可行的方案是将数据分为小于Pivot,等于Pivot,大于Pivot三组,考虑到分三组后实现难度大大增加,一种更简单的替换方法是,每次获取数据后检查一下最左端和最右端的数据,如果相等的话就采用其他排序如insert sort来进行处理。
4.归并排序(MergeSort)
归并排序以及其相关优化排序在一定数值范围内是一个十分优秀的排序算法,此处也将展开说明,代码如下:
#include<cstdio> const int INT_MAX = 100000000; const int INDEX_MAX = 10000; void mergeSort(int* A, int p, int r) { if (p < r) { int q = (p + r) / 2; mergeSort(A, p, q); mergeSort(A, q + 1, r); //实际排序 int n1 = q - p + 1; int n2 = r - q; int L[INDEX_MAX], R[INDEX_MAX]; //两个子数组均为从第二位开始使用,务必注意 for (int i = 1;i <= n1;++i) { L[i] = A[p + i - 1]; } for (int i = 1;i <= n2;++i) { R[i] = A[q + i]; } L[n1 + 1] = INT_MAX; R[n2 + 1] = INT_MAX; int i = 1; int j = 1; for (int k = p;k <= r;++k) { if (L[i] <= R[j]) { A[k] = L[i]; ++i; } else { A[k] = R[j]; ++j; } } } }
Merge Sort同样是一种O(NlogN)的算法。
5.选择排序(Selection Sort)
作为一种被我刚刚遗忘的O(N^2)算法,还是要说下的,就先给代码:
void SelectionSort(int *A, int length) { int f; for (int i = 0;i <= length - 2;++i) { f = i; for (int k = i + 1;k < length;++k) { if (A[k] < A[f]) f = k; } std::swap(A[i], A[f]); } }
直接选择排序的原理是遍历数组每一个位置,将从该位置到结束的所有点中最小的点放置在该位置。
效率:T(n)=T(n-1)+n=T(1)+(1+n-1)*(n-1)/2=T(1)+n*(n-1)/2,是一个O(n^2)算法。