排序算法-nlogn级别

来源:互联网 发布:酒瓶设计软件 编辑:程序博客网 时间:2024/05/20 15:40

对于排序算法中最优秀的莫过于nlogn级别的算法。基本都是采用的分治思想。

比如归并排序和快速排序。

这两个算法可以分为两类。因为思想上有所不同,侧重点也不同。下面来分析下这两种算法。


1.归并排序

思想,使用分治思想,使用递归将数据不停二分。然后再使用O(n)级别的算法进行归并操作。二分的层数将会是logn,而每一层归并操作的效率是n。所以最终复杂度为nlogn。

根据归并排序的思想(重点在于归并,而不是二分)我们有两种选择

(1)一种是利用递归,进行自上而下的二分,然后进行归并。

(2)另一种是直接使用迭代的方式自底向上的进行归并。

分析:

自顶向下的递归的方式,由于涉及到函数参数的入栈和出栈操作。会降低效率。(存在递归调用溢出的风险)

自底向上的迭代的方式,不涉及入栈出栈操作。(不存在递归调用溢出的风险)


优化方式:以上两种算法都可以进行两种优化

(1)当进行小区段的排序时可以使用插入排序法,因为小区段有序的概率更高。区段值需要统计考虑。

(2)让进行merge操作时,可以判断左边的右端是不是小于或等于右边的左端。如果是可以跳过此次的merge操作,从而提高性能。

/*归并排序Remarks:前闭后闭的空间*/template<typename T>void merge(T * arr, int left, int middle, int right) {T * iArrBack = new T[right - left + 1];//copyfor (int i = 0; i < right - left + 1; ++i) {iArrBack[i] = arr[i + left];}for (int i = 0, j = middle - left + 1, k = left; k <= right; ++k) {if (i > middle - left) {arr[k] = iArrBack[j];++j;}else if (j > right - left) {arr[k] = iArrBack[i];++i;}else if (iArrBack[i] > iArrBack[j]) {arr[k] = iArrBack[j];++j;}else {arr[k] = iArrBack[i];++i;}}delete[] iArrBack;}template<typename T>void MergeSrotCall(T * arr, int left, int right) {if (left >= right)return;int middle = (right + left) / 2;MergeSrotCall(arr, left, middle);MergeSrotCall(arr, middle + 1, right);merge(arr, left, middle, right);}template<typename T>void MergeSrot(T * arr, int n) {MergeSrotCall(arr, 0, n - 1);}

/*归并排序:迭代版本*/template <typename T>void MergeSortPro(T * arr, int n) {for (int i = 0; i < n; i += 150)insertionSortPro(arr, i, min(n - 1, i + 150));for (int iGroupSize = 1; iGroupSize <= n; iGroupSize+=iGroupSize)for (int i = 0; i + iGroupSize - 1 < n; i += (iGroupSize + iGroupSize))if(arr[i+iGroupSize-1]>arr[i+iGroupSize])merge(arr, i, i + iGroupSize - 1, min(i + iGroupSize + iGroupSize - 1, n - 1));//insertionSrotPro(arr, n);}

2.快速排序

算法的思想:取一个key值,利用此key值将数据分为两部分,然后再对左右两部分分别递归操作。二分思想能尽可能的减少总的遍历次数从而提高效率。

分析效率:

(1)理想情况下,如果每次取的key刚好是中间的值,那二分层数就是logn,每一次的快排的partition操作为O(n)于是能达到nlogn。

(2)数据有序度较高的情况:key值的选取如果取的位置是固定位置,比如左边第一个,那这种情况下的二分及其不均匀,将会导致左边没有,只有右边,那么层数将会是n层,那么效率将变成n^2级别。

解决方法:随机的获取key值,那么n较大的情况下,第一层取到端值的概率接近n/1.那么这个概率很小。一层一层这样下去,最终取得非端直的概率从统计角度讲,会接近99.99%

(3)数据大量重复的情况,比如全是0,那么我们的排序会一从左边一直移动到右边,因为相等的情况我并不会去交换。因为这样会大量消耗存储性能(赋值操作)。

解决方法:<1>采用双路的情况,但是相等的情况也进行交换,最终发现存储性能的消耗远小于端值对性能的影响。

 <2>更好的方式是采用三路的快排。把等于的部分也单独作为一个区域。这样等于的部分不用进行下一次的快排。(三路情况存储性能消耗较大,所以有序度较低的情况效率会略小于双路,但是差距不大,在100w数据个数内基本无差距)

快速排序(重点在于二分,二分会直接决定算法性能【从(2)分析中可以看出】)。


单路:

/*快速排序*///[left, right]template <typename T>int __partition(T * arr, int left, int right) {//[left, right]swap(arr[left], arr[rand() % (right - left + 1) + left]);//随机取key为了优化,近乎有序的数组int iKey = arr[left];int j = left;for (int i = left + 1; i <= right; ++i)if (arr[i] < iKey)swap(arr[i], arr[++j]);swap(arr[j], arr[left]);return j;}//[left, right]template <typename T>void quickSortCall(T * arr, int left, int right) {//if (left >= right)//return;if (right - left < 30) {//32个insertionSortPro(arr, left, right);//优化return;}//Partitionint iPartition = __partition(arr, left, right);quickSortCall(arr, left, iPartition);quickSortCall(arr, iPartition + 1, right);}template <typename T>void QuickSort(T * arr, int n) {srand(time(NULL));quickSortCall(arr, 0, n - 1);}

2.双路

Remake:这里双路第一种是来源百度百科,经过思考发现,这个算法实现,虽然在数据赋值次数上有优势,但是无法针对多重复值情况进行优化。

所以最好采用下面的那种双路。

1.找到一个后赋值/*在partition操作时,采用双路*/template <typename T>int __partitionPro(T * arr, int left, int right) {//[left, right]swap(arr[left], arr[rand() % (right - left + 1) + left]);//随机取key为了优化,近乎有序的数组int iKey = arr[left];int i = left, j = right;while (i < j) {for (; i < j && arr[j] >= iKey; --j) {}arr[i] = arr[j];for (; i < j && arr[i] <= iKey; ++i) {}arr[j] = arr[i];}arr[i] = iKey;return j;}template <typename T>void quickSortCallPro(T * arr, int left, int right) {//if (left >= right)//return;if (right - left < 150) {//32个insertionSortPro(arr, left, right);//优化return;}//Partitionint iPartition = __partitionPro(arr, left, right);quickSortCallPro(arr, left, iPartition);quickSortCallPro(arr, iPartition + 1, right);}template <typename T>void QuickSortPro(T * arr, int n) {srand(time(NULL));quickSortCallPro(arr, 0, n - 1);}2.找到两个后交换(能针对高度重复的数据较快的排序)/*在partition操作时,采用双路*/template <typename T>int __partitionPro(T * arr, int left, int right) {//[left, right]swap(arr[left], arr[rand() % (right - left + 1) + left]);//随机取key为了优化,近乎有序的数组int iKey = arr[left];int i = left + 1, j = right;while (true) {while (i <= right && arr[i] < iKey)++i;while (j >= left && arr[j] > iKey)--j;if (i > j)break;swap(arr[i], arr[j]);++i;--j;}swap(arr[left], arr[j]);return j;}template <typename T>void quickSortCallPro(T * arr, int left, int right) {//if (left >= right)//return;if (right - left < 150) {//32个insertionSortPro(arr, left, right);//优化return;}//Partitionint iPartition = __partitionPro(arr, left, right);quickSortCallPro(arr, left, iPartition);quickSortCallPro(arr, iPartition + 1, right);}template <typename T>void QuickSortPro(T * arr, int n) {srand(time(NULL));quickSortCallPro(arr, 0, n - 1);}

3.三路

/*在partition操作时,采用三路*/template <typename T>void __partitionThree(T * arr, int left, int right) {//if (left >= right)//return;if (right - left < 150) {//32个insertionSortPro(arr, left, right);//优化return;}//[left, right]swap(arr[left], arr[rand() % (right - left + 1) + left]);//随机取key为了优化,近乎有序的数组int iKey = arr[left];int iL = left + 1, i = left + 1, iR = right;while (true) {if (arr[i] == iKey)++i;else if (arr[i] < iKey) {swap(arr[i], arr[iL]);++iL;++i;}else {swap(arr[i], arr[iR]);--iR;}if (i > iR)break;}swap(arr[left], arr[iL - 1]);__partitionThree(arr, left, iL - 2);__partitionThree(arr, iR + 1, right);}template <typename T>void QuickSortThree(T * arr, int n) {srand(time(NULL));__partitionThree(arr, 0, n - 1);}


经过测试不管是双路经过重复值优化的快排还是三路快排,在任何情况下都会比归并快。


总结

我最欣赏的是针对快速排序的特点key的位置决定性能这一点进行的三种优化。体现了一种非常存粹且优秀的优化思想。
原创粉丝点击