快速排序是在已知的排序算法中排序速度最快的,它的时间复杂度是O(NlogN),之所以特别快,只要是由于内部循环非常的精炼并且高度优化。和归并排序类似,快排也是基于分治的递归算法,将一个序列S快拍分为四步:
1.如果S中只有1个或者0个元素,则结束排序
2.在S中选取一个元素v作为快排使用的“枢轴”;
3.将S中除去枢轴v的元素分成两个分别大于枢轴和小于数轴的不相交的序列;
4.分别对对小于和大于枢轴的序列进行上述的递归。
分析快排的四个步骤不难发现,一般的快排算法还有很多可以优化的地方,针对前三步有以下的优化策略:
1.实际上不管是什么样的排序算法,对于很小规模的序列进行排序都很难体现其优越性,并且有的算法看起来还很臃肿;另外还有就是递归算法虽然看起来很简洁,但是比不上循环实现的效率。基于这两点,可以看出在适当的时候结束快排的递归就很有必要。
2.如果对一个元素个数为N的逆序的序列进行顺序排序,并且每次选取的“枢轴“都是当前序列中第一个元素,这样就会出现最坏的情况(即进行了N次递归嵌套),算法退化到时间复杂度为O(N2),示意图:
省略...
由此看出”枢轴“的选取是非常重要的,一个理想的数轴是选取数列中位于中间的元素,这样每次划分都可以将一半数据放在枢轴前一半在后,类似于平衡的二叉排序树,树的高度降到了最小。
########################################################################################
针对快排的两条优化策略:
在递归到规模较小的时候进行插入排序,最恰当的阈值还没有有效的证明,这里我选取了N=10,
源码如下(插入排序已经在”排序问题“的第一节时候分析了):
/*insert sort*/ void insert_sort(int arr[], int count) { int i, pos; int tmp; for(pos = 1; pos < count; pos++) { tmp = arr[pos]; for(i = pos; i > 0 && arr[i - 1] > tmp; i--) { arr[i] = arr[i - 1]; } arr[i] = tmp; } }
有几种方法可以”枢轴“进行选取:
1.选取指定位置元素作为枢轴,这样很可能导致最坏情况的产生;
2.产生一个随机数指向位置的元素作为数轴,但是由于目前的计算机都是确定的有限自动机,产生的都是伪随机数(如果种子选取的不好可能会成第一种情况),产生一个随机数的代价很大,在高速排序算法中得不偿失。最主要的是一个随机数并不能有效的降低排序树的高度。
3.估算出序列大致的中值。可以选取几个位置的元素计算出中值,并将其作为整个序列中值的估计值。这里使用第一最后和中间三个元素的中值作为“枢轴”,源码如下:
/*找出枢轴,头尾中间三个元素的中位数*/ int median3(int arr[], int left, int right) { int center; center = (left + right) / 2; if(arr[left] > arr[center]) { swap(&arr[left], &arr[center]); } if(arr[left] > arr[right]) { swap(&arr[left], &arr[right]); } if(arr[center] > arr[right]) { swap(&arr[center], &arr[right]); } swap(&arr[center], &arr[right - 1]); return arr[right -1]; }
从参考书上看到还有分割的优化策略:
第一步是通过将枢轴与最后元素交换使得枢轴元离开要被分割的数据段。i从第一个元素开始而j从倒数第二个元素开始,示意图:
在分割截断要做的就是把所有相对于枢轴的小元素移到枢轴的左边,把所有相对于数轴大的元素移到枢轴的右边。
1.当i在j的左边时,将i右移跳过小于枢轴的元素;
2.将j左移跳过大于枢轴的元素;
3.当i和j停止时,i指向的是一个大于枢轴的元素,而j指向的是一个小于枢轴的元素,这个时候互换i和j指向的元素
4.一直重复上边的步骤,直到i和j相遇。
一趟排序完成。源码如下:
/*快排的主例程*/ void q_sort(int arr[], int left, int right) { int i, j; int pivot; if(left + CUT_OFF <= right) { pivot = median3(arr, left, right); i = left; j = right - 1; while (1) { while(arr[++i] < pivot){} while(arr[--j] > pivot){} if(i < j) { swap(&arr[i], &arr[j]); } else { break; } } swap(&arr[i], &arr[right - 1]); q_sort(arr, left, i - 1); q_sort(arr, i + 1, right); } /*如果序列小于10,则使用插入排序*/ else { insert_sort(arr+left, right - left + 1); } }
在上边的排序中,如果i和j遇到了等于枢轴元素的关键字,那么就让i和j都停止,这样可以将等于枢轴的关键字等分的划入枢轴两边的序列中,尽可能的将两边元素数保持接近以降低树的高度。
快排的驱动程序:
int quick_sort(int arr[], int count) { q_sort(arr, 0 , count - 1); }
要处理的第一个例程是枢轴的选取,最简单的是对A[left],A[right], A[center]进行排序,三个元素中最大值被放在A[right]中,把A[center]元素与A[right-1]的元素互换做为枢轴元素,此时应将i和j分别初始化为left+1和right-2。因为A[left]比枢轴元素小,所以可以将它做j的警戒标志。因为到最后i将停在哪些等于枢轴元素的关键字上,所以将枢轴存储在A[right-1]上,做为警戒。
同样用前几个排序算法使用的随机数文件做测试,10w随机数排序时间如下:
90w随机数字的排序时间: