快速排序是一种基于分治技术的排序算法。在一个给定的数列中,选择一个数作为分区的依据进行排序,使得数的左边都小于该数,数的右边都大于该数,然后将该数的左边和右边分别作为一个数列进行排序,一直重复以上操作,直到分区里只有一个数字为止。
在具体的实现中,快速排序的实现不止一种。比如如何确定分区的依据,如何排序使得分区数字左边所有数字都小于该数字,右边都大于该数字,这可以有很多不同的实现方式。下面讲到的是一种基于两次扫面子数列的方法。
2.实例
使用快速排序将下列数列按升序排序
6 4 1 19 10 5
第一次扫描,选择6作为分区依据,第一次扫描结束后6的左边都小于6,右边都大于6,因此从左边开始扫描时,如果这个数比6小,则继续扫描,直到找到一个数字比6大,左边停止扫描,右边开始扫描,如果这个数字比6大,则继续扫描,直到找到一个数字比6小,右边停止扫描,然后左右扫描停止的位置互换,然后重新开始左右扫描,直到坐标的索引值大于右边的索引值。根据这些分析,我们开始排序。
第一次扫描
设该数列为int number[6],坐标索引为left,右边索引为right
left=1左边开始扫描,4<6继续扫描;left=2, 1<6继续扫描;left=3,19>6,左边扫描停止;
right=5右边开始扫描,5<6右边扫描停止;左右互换数据后数列为:
6 4 1 5 10 19
left<right,扫描继续。left=4左边开始扫描,10>6坐标扫描停止;
right=4右边开始扫描,10>6,扫描继续;right=3,5<6,右边扫描停止;左右互换数据后数列为:
6 4 1 10 5 19
left>right,第一次扫描技术。扫描结束的条件是left>=right,因此当left>right时,最后一次交换回是错误的,因此扫描结束后,应该还原最后一次互换。left和right再次互换,数列如下:
6 4 1 5 10 19
将6和right的值互换,数列如下:
5 4 1 6 10 19
这样第一次扫描才正真的结束,6的左边的数都小于6,6右边的数都大于6
第二次扫描
分别将number[0..right-1]和number[right+1,5]看做两个数列,进行步骤一的操作。
5 4 1 6 10 19
设number[0..right-1]为number_left[0..right-1],left1=1开始左边扫描,4<5,继续扫描;left1=2,1<5,继续扫描,但是number_left只有三个数字,因此左边扫描结束;
right1=2开始右边扫描,1<5,右边扫描结束。互换left1和right1,数列如下:
5 4 1 6 10 19
left1==right1,number_left的扫描结束,left1和right1互换,5和right1互换位置,数列如下:
1 4 5 6 10 19
第三次扫描
分别对number_left[0..right1-1]和number_left[right1+1..right-1]排序,因为right1+1=3,right-1=2,所以number_left[right1+1..right-1]不存在。
设number_left[0..right1-1]为number_left_left[0..right1-1],left11=1左边开始扫描,4>1左边扫描结束;
right11=1右边扫描开始,4>1扫描继续;right11=0,1==1,右边扫描结束;left11和right11互换后数列如下:
4 1 5 6 10 19
left11>right11,number_left_left扫描结束,left11和right11互换,1和right11互换后数列如下:
1 4 5 6 10 19
到这里6之前的排序都结束了,然后再对number_right也使用上面描述的方法进行排序,这里就不在重复描述了。
3.代码
void quick_sort::quick_sort_with_array(int nums[], int begin_index, int end_index)
{
int index = 0;
if(begin_index<end_index)
{
index = partition(nums, begin_index, end_index);
quick_sort_with_array(nums, begin_index, index-1);
quick_sort_with_array(nums, index+1,end_index);
}
}
int quick_sort::partition(int nums[],int begin_index, int end_index)
{
int left=begin_index;
int right = end_index;
int number = nums[begin_index];
while(left<right)
{
while(nums[left]<=number && left<end_index) left++;
while(nums[right]>=number && right>0) right--;
swap(nums[left],nums[right]);
}
swap(nums[left],nums[right]);
swap(nums[begin_index], nums[right]);
return right;
}
void quick_sort::swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
4.算法分析
在快速排序中,比较和位置替换依然是主要操作。先来看一下比较操作。从上面的代码中我们可以看出,程序采用从左右两端扫描数列的方式,每次扫描结束后,一个数字就会被放到合适的位置,及排除出待排序序列,直到最后所有子数字的个数都是1.
设对一个长度为n的数列进行升序排序,如果给定的已经是一个按升序(或降序)排序的数列,这种情况下比较的次数最多,比较次数为:
C(n)=(n+1)+n+(n-1)+...+3=(n+1)*(n+2)/2-3
上面讨论的是最坏情况,如果排序的时候,分裂点都是中心位置,则为最优情况,比较次数为:
C(n)=2*C(n/2)+n (n>1,C(1)=0,C(2)=2)
设n=2^k,根据上面的公式可推算出:
在平时使用时,上面讨论的两种情况是不经常遇到的,下面我们来讨论一下快速排序在平均情况下的效率:
设有一个长度为n的数列,快速排序的平均键值比较次数为 Cavg(n),假设分区的分裂点s(0<= s<=n-1)位于每个位置的概率是1/n,可以得出下面的公式:
根据公式推算得:
因为1/(n+1)+1/n+1/(n-1)+...1/2为发散序列,无法算出精确值,只能是大约值,网上找到的是约等于lnn,但证明过程没有找到,证明就暂时到这里,以后再做修改吧。因此快排的平均复杂度为
从上面的计算结果可以看出,快排在平均情况仅比最优情况多执行了38%的操作。