先贴出Java版本的快速排序。
public static void quickSort(int[] array, int l, int r){
if(l<r){
int i = l, j = r, x = array[l];
while(i < j){
while( i<j && array[j]>x)
j--;
if(i<j)
array[i++] = array[j];
while(i<j && array[i]<x)
i++;
if(i<j)
array[j--] = array[i];
}
array[i] = x;
quickSort(array,l,i-1);
quickSort(array,i+1,r);
}
else
return ;
}
作者在文中提到的挖坑填数是很形象的比喻,首先,我们用一个变量x记住基准数,这相当于已经挖了一个坑,我们从后往前遍历,当遍历到的数比基准数大,就继续向前遍历,否则,退出遍历,即此时索引得到的数比基准数小,则把这个数放到基准数的坑里,同时得到一个新坑。然后从前往后遍历,当遍历得到的数比基准数小时,就继续向后遍历,否则,推出遍历,即此时索引得到的数比基准数大,则把这个数放到刚才形成的新坑里,再从后向前遍历。。。直到从后面进行的遍历与从前面进行的遍历相接。
为什么这里的每一个条件都会有i<j的判断呢?因为作者为了使程序简洁,用了如下的语句
array[i++] = array[j];
i++是post-increment,即后加,意味着i 先返回,再自增1,上面语句执行了两个命令,相当于
array[i] = array[j];
i++;
所以,i或者j每次都会发生变化,因此始终要进行判断。
选定基准数执行后的结果是什么呢?基准数出现在它排序好后应当出现的位置,可以称之为“归位“(哈哈),其前面的数均比它小,其后面的数均比其大。
然后我们迭代的处理基准数左边的子数组和基准数右边的子数组。其过程是类似的,我们同样是选定一个基准数,让它”归位”,再迭代处理子数组,直到满足基本退出条件,即此时子数组,l>=r,即每一个子数组成了单个元素,即元素自身。
快速排序版本二:
public static void quickSort2(int[] array,int l, int r){
int i = l,j = r, x = array[(i+j)/2];
do{
while(array[i]<x)
i++;
while(array[j]>x)
j--;
if (i<=j){
swap(array,i++,j--);
}
}while(i<j);
if(j>l)
quickSort2(array,l,j);
if(i<r)
quickSort2(array,i,r);
}
这个版本与版本一的共同点是都是双面扫描,他们的区别是交换的数据,版本一是挖坑填数,版本二是前后扫描,前后交换。这里基准数是选择的中间的那个数,但实际上选择哪个数都是可以的,我们总会递归的选择到每一个数作为基准数。
快速排序版本三
public static void quickSort3(int[] array, int l, int r){
if(l<r){
int x = array[l];
int i = l,j;
for (j = l + 1;j<=r;j++){
if(array[j]<x){
swap(array,j,++i);
}
}
array[l] = array[i];
array[i] = x;
quickSort3(array,l,i-1);
quickSort3(array,i+1,r);
}
}
版本三是做了较大改变的,它是单边扫描的,只扫描基准数开始位置时右侧的子数组。这种方法之所以能够行得通的原因是我们只对基准数右侧的子数组进行处理,小于基准数的放在左侧,并对左边的索引自增(不知道这算不算左侧扫描,算的话也只是索引扫描,元素并没有扫描),大于基准数的留下。这样我们也能把基准数“归位”。