上面的代码很罗嗦,关键在于锚值的处理上面绕了弯路。如何绕了弯路呢?我们还是简单回顾一下上述的过程:首先,我们设定第一个值是锚值,然后从第二个元素起,开始向后遍历,寻找第一个比锚值大的数值的位置;然后,从相反的方向,找到第一个比锚值小的数值的位置;接下来将两个值交换,重复这个过程,直到迭代指示器“相遇”。在这个过程中,锚值的位置,也就是数组的第一个元素并没有改变,它只是静静的等待两个迭代指示器相遇,然后与正确位置的值相交换——而这个“正确的位置”,既可以是锚值本身所在的位置,也可以是锚值前后的位置,这取决于迭代指示器最后一次遍历的方向——这才产生了上一篇文章中的if判断代码,才使代码变得更加难看。
直接看一下严蔚敏的《数据结构》,他是这么处理的:他利用了锚值位置上的值,参与到交换过程之中。算法的过程变为:首先,右迭代指示器从右向左遍历,找到第一个比锚值小的数值的位置,并将锚值(默认是数组第一个元素)与当前位置的数值相交换——此时,右迭代指示器停止,迭代器指向的位置作为临时变量,存放锚值,该位置向右的序列,即在右迭代器遍历过的位置上,满足快速排序的要求(右端的数值比锚值大,左端的数值比锚值小),该位置左侧的数值待排序,该位置作为下一次交换的位置;接下来,左迭代指示器从左向右遍历,(左端迭代器的初始值是锚值的下一个元素,即数组的第二个元素),直到遇到第一个比锚值大的元素,该元素与锚值所在位置元素(右迭代器所指向的位置)进行交换,锚值位置变到左迭代器指向的位置,左迭代器遍历过的位置上的元素都满足快速排序需求;接下来,右迭代器继续向左移动,重复上述的过程,直到两个迭代器相遇,此时左右迭代器所指的位置就是锚值其时的位置。
这部分代码如下:
int elem = Array[iLow];
int l = iLow, r = iHigh;
while(l < r)
{
while(l < r && Array[r] >= elem) r--;
if (l < r)
{
Array[l++] = Array[r];}
while(l< r && Array[l] <= elem) l++;
if (l < r)
{
Array[r--] = Array[l];}}
Array[r] = elem;
这个过程中,算法利用了锚值所在的位置,作为临时变量,进行数值交换;左右迭代器分别找到应该交换的数值的位置,不是相互交换,而是与锚值所在的位置进行交换。这样,最后迭代器相遇的位置,位置左边,都是比锚值小的数值;位置右边都是比锚值大的数值,而迭代器相遇的位置,就是锚值最终应该在的位置。之后,分别对迭代器左右两端的子序列进行分治求解。完整的算法如下:
void QuickSort2 (int Array[], int iLow, int iHigh)
{
if(iLow < iHigh)
{
int elem = Array[iLow];
int l = iLow, r = iHigh;
while(l < r)
{
while(l < r && Array[r] >= elem) r--;
if (l < r)
{
Array[l++] = Array[r];}
while(l< r && Array[l] <= elem) l++;
if (l < r)
{
Array[r--] = Array[l];}
} // while
Array[r] = elem;
// 对子序列递归调用
QuickSort2(iLow,r-1);
QuickSort2(r+1,iHigh);
} // if
}
这样代码就干净了很多。很多老师在讲快速排序的时候,侧重分治的思想、复杂度的分析等,而在实际中最关键的问题“如何将序列分成两个子序列,满足快速排序要求”,尤其是以上数据交换的细节,讲的不是很细。——其时不怨老师,还是自己不经常实践。
另外,大家看代码,函数最开始的时候都有一个if判断,判断"if(iLow < iHigh)",这个是做什么的?“防卫性编程”,判断万一调用的时候输入有错误?去掉行么?后半句对了,但是不是“防卫性编程”。快速排序是递归算法,总有一个终止条件,上面的判断就是终止条件。当iLow比iHigh大的时候,就说明序列已经有序了,不用进一步递归作调整了。
《算法导论》还有不同的方法,下一篇再详细说。