深入剖析快速排序过程

快速排序的过程十分简洁明了,就是将待排序元素序列进行切分,然后分支递归。以数组从小到大排序为例,现将快速排序算法中切分数组独立出来作为一个方法partition来说明。

独立partition方法出来之后的quick_sort的代码如下:

 

void quick_sort(int array[], int low, int high)
{
    if(low>= high)
        return;
    int index = partition(array, low, high);
    quick_sort(array, low, index-1);
    quick_sort(array, index+1, high);
}

 

 常见的切分思想则有有两种,下面就两种不同的思想来讨论之:

第一思想是自头向尾遍历数组,将小于枢纽值的元素插入到数组前段,其代码为:

 

int partion(int array[], int low, int high)
{
    int index = low;
    int pivot = array[high];
    for(int i = low; i < high; i++)
    {
        if(array[i] <= pivot)
        {
            swap(&array[i], &array[index++]);
        }
    }
    swap(&array[high], &array[index]);
    return _low;
}
 

这个切分数组的思想很简单,就是扫描数组,将比pivot小的元素依次置入到相应的位置。个人感觉这个方法虽然容易理解和实现,但交换的次数似乎多了那么一点....

 

第二思想则是头尾同时扫描数组,当然这种思想倒也有两种不同的实现方式,我们也会在下面看到这两种略有差别的实现方式。

方式一,代码如下:

 

int partion(int array[], int low, int high)
{
    int pivot = array[low];
    int _low = low;
    int _high = high;
    while(_low < _high)
    {
        while(_low<_high && array[_low] <= pivot)
        {
            _low++;
        }
        while(_low<_high && array[_high] >= pivot)
        {
            _high--;
        }
        swap(&array[_low], &array[_high]);
    }
    swap(&array[_low],&array[low]);
    return _low;
}
 

如果十分熟悉快速排序的人恐怕会一眼就可以看出来上述代码是错误的(那么这些朋友们应该可以跳过这么一大段了)。这里我们也要讨论的就是这个问题,pivot的取值与先_high--还是先_low++的顺序有联系吗?起初,我也认为这个决策的先后顺序对排序结果应该是没有什么影响的,草草了解了下其思想就匆匆带着代码上阵了,代码实现倒是很快,但纠结了很长时间愣是找不出来错在哪儿。调试了半天,弄得焦头烂额,最终决定坐下来好好分析一下,这才发现原来这个顺序还真是起着决定性的作用的。那么究竟问题出在哪里呢?问题就出来快速排序的思想并没掌握好吧(如果你也说不出个为什么顺序存在的意义的话)

 

以上面代码为例,当给定一数组[4,3,2,1,5]。开始时,_low指向的是元素4_high指向的元素是5;在循环结束还未执行最后的swap时,_low指向的元素是5_high指向的元素也是5,此时数组的状态是[4,3,2,1,5],而在swap之后,则是[5,3,2,1,4]。啊,竟然切片不成功?!

相反,我再把_high--_low++的顺序交换一下,得到如下代码:

 

int partion(int array[], int low, int high)
{
    int pivot = array[low];
    int _low = low;
    int _high = high;
    while(_low < _high)
    {
        while(_low<_high && array[_high] >= pivot)
        {
            _high--;
        }
        while(_low<_high && array[_low] <= pivot)
        {
            _low++;
        }
        swap(&array[_low], &array[_high]);
    }
    swap(&array[_low],&array[low]);
    return _low;
}
 

再给定同样的输入,却得到的是正确的结果。开始时,_low指向的是元素4_high指向的元素是5;在循环结束还未进行最后的swap时,_low指向的元素是1_high指向的元素也是1,此时数组的状态是[4,3,2,1,5],而在swap之后,则是[1,3,2,4,5]。现在可算是切片成功了!

 

恩,至此也基本说明了pivot值的设定与_high--_low++的先后顺序存在密切联系。(可悲的是,我找了好些本书,还真没有哪一本书上明确的说明了这个问题,都是直接给出伪码或源码了事,或许这个简单的问题还真算不上什么问题吧)

不过,针对前一个代码片段你可能会说执行 swap(&array[_low-1],&array[low]);不就行了?OK,那你可以先试试[4,3,2,1]再说吧。

 

好了,现在来分析一下为何pivot取值会导致其后代码的顺序不同呢。让我们还是以pivot=array[low]为例来说明吧。

首先我们要确定的是_low_high最终会相等(这可是循环结束的条件啊),_low++的目的是寻找第一个比pivot大的元素,_high--则是寻找第一个比pivot小的元素。在输出的正确结果中,_low/_high左边的元素会小于等于_low/_high所指向的元素,而其右边的元素则大于等于它。往回退一步到执行最后一个swap之前,_low/_high左边的元素除了pivot(array[low])外均小于等于pivot,右边则是全都大于等于pivot。但array[_low]pivot相比呢?因为它马上就要和array[low]交换了,所以其必须小于等于pivot。但在先执行_low++再执行_high--的情况下,上述给出的两个数组[4,3,2,1,5][4,3,2,1]中的情况则截然相反,第一个数组中是array[_low]大,第二个数组中是array[_high]小。因为这里我们总是先让_low++尽力去寻找第一个比pivot大的元素,然后才会轮到_high--去寻找第一个比pivot小的元素,但我们这里需要保证的是在执行最后一个swap时,array[_low/_high]必须得比pivot小。因此在这种情况下,当_low_high相遇时,_high就被动了,_high不能保证此时array[_low/_high]一定不比pivot大,要不swap就没有任何意义了。只有让_high占到主动才能达到我们的目的,此时就得先执行_high--了。当然,若设置pivotarray[high],则显然得先进行_low++

 

 

在方式二中,问题就显得没那么复杂了:

 

int partion(int array[], int low, int high)
{
    int pivot = array[low];
    int _low = low;
    int _high = high;
    while(_low < _high)
    {
        while(_low<_high && array[_high] >= pivot)
        {
            _high--;
        }
        array[_low] = array[_high];
        while(_low<_high && array[_low] <= pivot)
        {
            _low++;
        }
        array[_high] = array[_low];
    }
    swap(&array[_low],&array[low]);
    return _low;
}
 

 

 

 这个显然顺寻一眼就可以判断出来,压根就没有混淆性。而且其执行起来效率更高(如果完全不考虑编译器优化的话)。

 

最后以简单的表格来总结一下吧

pivot = array[low]

_low++_high--

pivot = array[low]

_high--_low++

pivot = array[high]

_low++_high--

pivot = array[high]

_high--_low++

PS:在随机化版本的快速排序中,也只是将随机选取的枢纽值与array[low]array[high]交换一下,最后所要面对的问题也还是一样的。

 

当然,这两种思想下数组切分的还是很有区别的,虽然感觉上第一种要好实现和好理解一点,但按我的理解,还是第二种思想要高效一点。其原因有二:

一. 第一种数组切分要进行的交换实在是太多了(相对来说),大约为第二种数组切分方式的两倍。(但时间复杂度都是O(n))

二. 另外感觉第一种数组切分情况下,在最终结果中与pivot相等的数要么全都集中到左子数组中(如果采用<=来比较)要么全都集中到右子数组中(如果采用<来比较);而在第二种切分数组方式下,则可以混合搭配<=,<>=,>,能将与之相等的元素更好的“均匀”分配到两个子数组中,可以更好的贯彻分治的思想。

 

 

不得不感叹算法的奇妙之处,有一个简单的快速排序也还是有那么些细节是极其需要注意的。同时也不得不意识到理解算法的原理与能够转换成代码来实现还是很有区别的,只有自己独立的做了才会真正意识到自己掌握还是没掌握….

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章