上篇我们探讨了几种O(N2 )(冒泡排序、选择排序、插入排序)和O(N1.25 )(希尔排序)的排序算法,但是面对巨量的数据面前它们又是低效率的,因此,这节将探讨高等排序算法。
上篇 算法:排序(一)
计数排序/桶排序
==Update 2020/07/14:此处计数排序/桶排序实现不具有实用性,具体请看最新的排序文章:线性时间排序 ==
计数排序/桶排序真的很简单,很暴力,很快速,很。。。占内存。 ~~
允许我盗用一下别人的图来说明这个排序思想。图来自《啊哈!算法》
如上,将数据放入一个数组里,可以起到统计数字数量的作用(计数排序),也可以起到排序的作用(桶排序),其实这玩意我经常用来去重。。。
这个排序算法能达到O(N)级别,肥肠快~相比于O(N2 )算法,处理1000个数据时快1000倍!
只是,真的很占内存,如果要1E9的数据,你还会用桶排序吗?而且,你还需要数据最大不超过哪个数字才行。
1 2 3 4 5 6 7 8 9 10 #include "SortUtils.hpp" void Sort (int a[], int n) { int bucket[100 ] = { 0 }; for (int i = 0 ; i < n; ++i) bucket[a[i]] = 1 ; for (int i = 0 , j = 0 ; j < 100 ; j++) if (bucket[j] == 1 ) a[i++] = j; }
结果:
1 2 3 4 5 6 7 8 Source Array: 33 61 12 89 23 5 17 1 99 36 48 Sorted Array: 1 5 12 17 23 33 36 48 61 89 99 | Check Sorted: yes | Array Length: 11 | Swap times: 0 |Compare times: 0 | Run time(μs): 1 μs | Run time(ms): 0.001 ms
归并排序
归并排序跟其他排序不同,其他排序是改变原来数组的内容,归并排序只能新建一个数组将排序后的数字放进去,具体思想如下:(图自这里 )
先将数组分成两部分,直到剩下一个数字,然后按序合并。合并有序序列需要一定的技巧,不能单纯地将两个数组首尾相接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "SortUtils.hpp" #include <memory.h> int * Conquer (int * a, int an, int * b, int bn) { int *res = new int [an+bn]; for (int ia = 0 , ib = 0 , ir = 0 ; ir < an+bn; ++ir) if (ib == bn || (ia != an && a[ia] < b[ib])) res[ir] = a[ia++]; else if (ia == an || (ib != bn && b[ib] < a[ia])) res[ir] = b[ib++]; delete [] a; delete [] b; return res; } int * Devide (int a[], int n) { if (n == 1 ) return a; int mid = n/2 ; int *xleft = new int [mid], *xright = new int [n-mid]; memcpy (xleft, a, sizeof (int )*mid); memcpy (xright, a+mid, sizeof (int )*(n-mid)); return Conquer(Devide(xleft, mid), mid, Devide(xright, n-mid), n-mid); } void Sort (int a[], int n) { int *cpy = Devide(a, n); memcpy (a, cpy, sizeof (int )*n); delete [] cpy; }
你也可以不用新建一块空间存储新的数据,不过还是需要有个临时的位置存放原数据,如果这样的话可能跟高效,毕竟没有涉及大量的数组创建和删除操作。而且这样不需要考虑分的操作了,因为分的结果就是每个数字取出来的结果嘛(如上图绿线一行)。
效果还是很高效的:(swap的确用不上,compare就很难获取了所以。。。这两个值为0)
1 2 3 4 5 6 | Check Sorted: yes | Array Length: 1000 | Swap times: 0 |Compare times: 0 | Run time(μs): 659 μs | Run time(ms): 0.659 ms
分割
分割不是排序的一种,但是理解它有助于理解下面的快速排序。分割很简单,选定一个数,通过算法,使得这个数左边都小于这个数,这个数右边都大于这个数,注意左右两边不一定有序,只需满足大小关系即可。
这里 有大量很详细地说明分割的过程。需要图来解释一切的同学点进去吧←_←
在开始写代码之前,由于我们的SortUtils.hpp是适合于排序的,而分割不是排序,因而需要修改一点东西,比如修改Check函数,其他都是修改输出的字而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifdef PARTITION extern int splitMid; bool Check (int n) { for (int i = 0 ; i < splitMid; ++i) if (__nums[i] > __nums[splitMid]) return false ; for (int i = splitMid+1 ; i < n; ++i) if (__nums[i] < __nums[splitMid]) return false ; return true ; } #else bool Check (int n) { for (int i = 0 ; i < n-2 ; ++i) if (__nums[i] > __nums[i+1 ]) return false ; return true ; } #endif
我们统一选最后一个数为分割的标准:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define PARTITION #include "SortUtils.hpp" int splitMid;#define PARTITION_TYPE 1 void Sort (int a[], int n) { splitMid = n-1 ; #if PARTITION_TYPE == 1 int i, j; for (i = j = -1 ; j < n-1 ; ++j) if (CompareTo(j+1 , splitMid) <= 0 ) Swap(j+1 , ++i); splitMid = i; #elif PARTITION_TYPE == 2 int i = 0 , j = n-2 ; while (i < j) { while (i < j && CompareTo(i, splitMid) < 0 ) ++i; while (j > i && CompareTo(j, splitMid) > 0 ) --j; if (i != j) Swap(i++, j--); } if (CompareTo(i, splitMid) > 0 || CompareTo(++i, splitMid) > 0 ) { Swap(i, splitMid); splitMid = i; } #endif }
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #类型1 Source Array: 33 61 12 89 23 5 17 1 99 36 48 Split Array: 33 12 23 5 17 1 36 [48] 99 61 89 |Check Splited: yes | Array Length: 11 | Swap times: 8 |Compare times: 11 | Run time(μs): 2 μs | Run time(ms): 0.002 ms #类型2 Source Array: 33 61 12 89 23 5 17 1 99 36 48 Split Array: 33 36 12 1 23 5 17 [48] 99 61 89 |Check Splited: yes | Array Length: 11 | Swap times: 3 |Compare times: 11 | Run time(μs): 2 μs | Run time(ms): 0.002 ms
快速排序
还记得当年的 希尔排序吗?希尔排序就是利用插入排序的优点改进的,快速排序其实是利用分割的优点改进的。分割的复杂度是O(N),非常高效,因此可以进行利用。
其实。。。网上大部分关于快速排序的都是在说分割,很少真正关于快速排序本身的,反正我找不到。。。所以,不给你们放图了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "SortUtils.hpp" int Partition (int a[], int s, int e) { int splitMid = e-1 , i, j; for (i = j = s-1 ; j < e-1 ; ++j) if (CompareTo(j+1 , splitMid) <= 0 ) Swap(++i, j+1 ); return i; } void QuickSort (int a[], int s, int e) { if (e-s <= 1 ) return ; int mid = Partition(a, s, e); QuickSort(a, s, mid); QuickSort(a, mid+1 , e); } void Sort (int a[], int n) { QuickSort(a, 0 , n); }
如你所见,快速排序采用了分治的思想,利用递归的方法完成排序。然后结果如下:
1 2 3 4 5 6 7 8 Source Array: 33 61 12 89 23 5 17 1 99 36 48 Sorted Array: 1 5 12 17 23 33 36 48 61 89 99 | Check Sorted: yes | Array Length: 11 | Swap times: 27 |Compare times: 38 | Run time(μs): 5 μs | Run time(ms): 0.005 ms
性能
1000条数据已经无法显示算法的优越性了,于是,我生成了1E5个整数进行测试。
首先是希尔排序:
1 2 3 4 5 6 | Check Sorted: yes | Array Length: 100000 | Swap times: 2073536 |Compare times: 3179386 | Run time(μs): 36751 μs | Run time(ms): 36.751 ms
之后是归并排序:
1 2 3 4 5 6 | Check Sorted: yes | Array Length: 100000 | Swap times: 0 |Compare times: 0 | Run time(μs): 23038 μs | Run time(ms): 23.038 ms
最后是快速排序:
1 2 3 4 5 6 | Check Sorted: yes | Array Length: 100000 | Swap times: 1019079 |Compare times: 1995044 | Run time(μs): 25031 μs | Run time(ms): 25.031 ms
可以看到,快速排序不总是那么快的哦~要根据情况选择适合的排序方式才是坠重要滴!
这里 有python版本的排序算法解释,写的和配的图都很好~
最后引用一张图,结束排序算法的探讨。另外纠正下,最好时间应该用小欧o表示的,最差时间用大欧O表示。。。反正没所谓啦!~这不是重点。。。