去年,刚刚老同学庆祝本科毕业十周年。十年了,很多当年老师教的基础知识都忘记了。这几天捡起来再看看,复习一下,顺便写成博客。因为毕业后干的是码农的活儿,所以尽量用代码说话。
快速排序,在《算法导论》中是经典的分治算法,就是将问题分解成若干的子问题,子问题的解组成了整个问题的解。快速排序也是经典的递归算法的应用。
快速排序的函数原型通常定义如下:
void QuickSort (int Array[], intiLow, int iHigh)
快速排序的基本思想是:首先设定一个锚值(通常取序列的第一个元素),将数字序列分成两部分,一部分比锚值大,另一部分比锚值小;然后递归调用函数本身,递归对序列的这两部分进行求解。函数的框架很清晰,如下:
void QuickSort (int Array[], intiLow, int iHigh)
{
if (iLow < iHigh)
{
// 选取锚值
int iVal = Array[iLow];
// todo: 将数字序列分成两部分,一部分比锚值大,另一部分比锚值小
......
// 递归求解两个子问题
QuickSort (iLow, j-1);
QuickSort (j+1, iHigh);}
}
算法的关键其实在“todo”部分,即,如何“将数字序列分成两部分,一部分比锚值大,另一部分比锚值小”。对于这个问题的处理,我参考的两本书上 有不同的思路,一本书是严蔚敏的《数据结构》,另外一本是《算法导论》。在直接看书之前,我个人还是习惯自己想想,能否自己写出来。
如何“将数字序列分成两部分,一部分比锚值大,另一部分比锚值小”?
分别从序列的两端对序列进行遍历:从序列的左端(起始端)向后遍历,寻找第一个元素,其值比锚值小;从序列的右端(终端)向前遍历,寻找第一个元素,其值比锚值大;然后,交换左右两端的元素;重复上述过程,直到两个遍历指示器相遇。代码如下:
int i = iLow+1, j = iHigh;
while (i < j)
{// 从序列的左端(起始端)向后遍历,寻找第一个元素,其值比锚值小
while (i < j && Array[i] < iVal)i++;
// 从序列的右端(终端)向前遍历,寻找第一个元素,其值比锚值大
while (i < j && Array[j] > iVal)j--;
// 交换左右两端的元素;重复上述过程,直到两个遍历指示器相遇
if (i < j)
{int iTemp = Array[i];
Array[i] = Array[j];
Array[j] = iTemp;
i++;
j--;}
}
调试以上代码的时候,发现最终结果有错误。其问题根源在于如下问题:当两个遍历指示器相遇的时候,锚值元素应该放到哪儿?再具体一些,当两个指示器 相遇的时候,此时i的值等于j的值,那么Array[j]与锚值是什么关系?这个得分情况看,当相遇前遍历的方向是从前向后,即动的元素是i,则 Array[j]的值在上一轮比较中大于锚值,此时锚值正确的位置应该是j-1;否则,当相遇前遍历的方向是从右向左,则Array[j]的值小于锚值, 锚值正确的位置是j。总而言之,以上的代码,Array[j]的值可以大于也可以小于锚值,造成锚值最后的位置不确定,那么要加入代码来判断上述情况,并根据其位置进行递归调用。代码如下:
if (Array[j] < iVal ) // i和j相遇前最后一次遍历是从右向左,锚值的正确位置是j
{// 锚值目前在Array[iLow],将它放到Array[j],并将Array[j]的值(比锚值小)放到锚值前面
Array[iLow] = Array[j];
Array[j] = iVal;
// 锚值的位置在j,两个数字子序列是[iLow,j-1]和[j+1, iHigh],递归解决
QuickSort (iLow, j-1);
QuickSort (j+1, iHigh);}
else // i和j相遇前最后一次遍历是从左向右,锚值的正确位置是j-1
{// 锚值目前在Array[iLow],将它放到Array[j-1],并将Array[j-1]的值(比锚值小,Array[j]的值比锚值大)放到锚值前面
Array[iLow] = Array[j-1];
Array[j-1] = iVal;
// 锚值的位置在j-1,两个数字子序列是[iLow,j-2]和[j, iHigh],递归解决
QuickSort (iLow, j-2);
QuickSort (j, iHigh);}
方便起见,将上述代码合并成完整代码,如下:
void QuickSort (int Array[], intiLow, int iHigh)
{
if (iLow < iHigh)
{
// 选取锚值
int iVal = Array[iLow];
// 将数字序列分成两部分,一部分比锚值大,另一部分比锚值小
int i = iLow+1, j = iHigh;
while (i < j)
{// 从序列的左端(起始端)向后遍历,寻找第一个元素,其值比锚值小
while (i < j && Array[i] < iVal)i++;
// 从序列的右端(终端)向前遍历,寻找第一个元素,其值比锚值大
while (i < j && Array[j] > iVal)j--;
// 交换左右两端的元素;重复上述过程,直到两个遍历指示器相遇
if (i < j)
{int iTemp = Array[i];
Array[i] = Array[j];
Array[j] = iTemp;
i++;
j--;}
} // while
// 递归求解两个子问题
if (Array[j] < iVal ) // i和j相遇前最后一次遍历是从右向左,锚值的正确位置是j
{// 锚值目前在Array[iLow],将它放到Array[j],并将Array[j]的值(比锚值小)放到锚值前面
Array[iLow] = Array[j];
Array[j] = iVal;
// 锚值的位置在j,两个数字子序列是[iLow,j-1]和[j+1, iHigh],递归解决
QuickSort (iLow, j-1);
QuickSort (j+1, iHigh);}
else // i和j相遇前最后一次遍历是从左向右,锚值的正确位置是j-1
{// 锚值目前在Array[iLow],将它放到Array[j-1],并将Array[j-1]的值(比锚值小,Array[j]的值比锚值大)放到锚值前面
Array[iLow] = Array[j-1];
Array[j-1] = iVal;
// 锚值的位置在j-1,两个数字子序列是[iLow,j-2]和[j, iHigh],递归解决
QuickSort (iLow, j-2);
QuickSort (j, iHigh);} // if
} // if
}
见过这么麻烦的快速排序么?见过这么啰嗦的代码么?虽然十年过去了,不过记得教科书上的代码没有这么多。不过,经过测试,这部分代码真的能够正确work!说明我理解算法的思路是正确的,在关键问题上(锚值位置的处理)走了弯路。下一篇,看看弯路在哪里,更正过来。
to be continued......