如果在各种排序算法中,只学一种,那这种算法就是快速排序。
正如它的名字,快速排序很快速,性能很好。而它也被评为“二十世纪十大最伟大的算法”之一。
下面就来学习一下这个算法到底是怎么样的吧。
直接给出一种实现代码:
private int Division(int[] list, int left, int right) { // 以最左边的数(left)为基准 int bs = list[left]; while (left < right) { // 从序列右端开始,向左遍历,直到找到小于base的数 while (left < right && list[right] >= bs) right--; // 找到了比base小的元素,将这个元素放到最左边的位置 list[left] = list[right]; // 从序列左端开始,向右遍历,直到找到大于base的数 while (left < right && list[left] <= bs) left++; // 找到了比base大的元素,将这个元素放到最右边的位置 list[right] = list[left]; } // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小; // 而left位置的右侧数值应该都比left大。 list[left] = bs; return left; } public void QuickSort(int[] list, int left, int right) { // 左下标一定小于右下标,否则就越界了 if (left < right) { // 对数组进行分割,取出下次分割的基准标号 int bs = Division(list, left, right); // 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, left, bs - 1); // 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, bs + 1, right); } }
我们的学习,并不是看过了“标准实现”代码就结束了,而是从这个时候才开始。把知识转化成自己理解的版本,才叫学会。
首先看到这个代码,我的感觉就是,不知道怎么调用。
下面给出调用的代码:
public static void Main(string[] args) { //Console.WriteLine("Hello World!"); int[] a={ 5, 1, 4, 8, 3, 9, 0, 2, 7, 6, 1, 2, 5 }; SortClass sc = new SortClass(); sc.QuickSort(a, 0, a.Length - 1); for (int i = 0; i < a.Length;i++) { Console.Write(a[i] + " "); } }
很明显,这个QuickSort是主干方法,也是递归方法。
这个方法要表达什么呢?递归方法两要素:1递归(终止)条件,2递归方式。
那这个递归方法的递归条件是什么呢?就是这个if语句,改写成更明显的方式:
public void QuickSort(int[] list, int left, int right) { // 左下标一定小于右下标,否则就越界了 if(left>=right) { return; } // 对数组进行分割,取出下次分割的基准标号 int bs = Division(list, left, right); // 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, left, bs - 1); // 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, bs + 1, right); }
但是这个输入参数太丑了,把它隐藏起来,使用重载方式:
public void QuickSort(int[] list) { int left = 0; int right = list.Length - 1; QuickSort(list, left, right); } public void QuickSort(int[] list, int left, int right) { // 左下标一定小于右下标,否则就越界了 if(left>=right) { return; } // 对数组进行分割,取出下次分割的基准标号 int bs = Division(list, left, right); // 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, left, bs - 1); // 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, bs + 1, right); }
这样调用的时候,就不用写第二个和第三个参数了,看起来舒服很多。
public static void Main(string[] args) { //Console.WriteLine("Hello World!"); int[] a={ 5, 1, 4, 8, 3, 9, 0, 2, 7, 6, 1, 2, 5 }; SortClass sc = new SortClass(); //sc.QuickSort(a, 0, a.Length - 1); sc.QuickSort(a); for (int i = 0; i < a.Length;i++) { Console.Write(a[i] + " "); } }
下面来分析核心的部分Division,这个方法是返回数组的一个元素的下标。这个元素左边的所有元素都不大于它;而这个元素的右边的元素都不小于它。也就是说这个元素的位置就是最终排好序后的最终位置。
private int Division(int[] list, int left, int right) { // 以最左边的数(left)为基准 int bs = list[left]; while (left < right) { // 从序列右端开始,向左遍历,直到找到小于base的数 while (left < right && list[right] >= bs) right--; // 找到了比base小的元素,将这个元素放到最左边的位置 list[left] = list[right]; // 从序列左端开始,向右遍历,直到找到大于base的数 while (left < right && list[left] <= bs) left++; // 找到了比base大的元素,将这个元素放到最右边的位置 list[right] = list[left]; } // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小; // 而left位置的右侧数值应该都比left大。 list[left] = bs; return left; }
快速排序的过程就是一边比较一边移动,
第一步:选定left索引的元素为基准值,从数组的右边向左边遍历,依次取出各值与基准值进行比较,如果发现某个值小于基准值,则停止遍历。将这个值放到数组left索引处。
第二步:再从left索引处(值已经更新),向右进行遍历,依次取出各值与基准值进行比较,如果发现某个值大于基准值,则停止遍历。将这个值放到数组right索引处。
然后重复进行这两步,一直到不满足left<right条件则停止。这时候left==right,而这个left和right的值,就是基准值最终应该排的位置。并且,在确定这个位置的同时,获得了两个子序列。它左侧的值都不大于它,它右侧的值都不小于它。
得到这个基准位置后,将其左右两侧的子序列,用递归的方式进行再切分。
多次递归调用,最终会将所有的元素都排好序。这时,算法也就完成了。
总结:快速排序是通过递归的方式实现的,它通过寻找切分点,同时完成了确定位置和移动元素两件事,效率是非常高的。
这个算法需要重点掌握。
我想,学习算法关键的是学习思想。通过学习快速排序,我觉得有两方面思想可以借鉴:
一是通过切分点的思想,将当前已排好序的元素从原问题中“剔除”,
二是“链式”跳转的这种感觉,对于处理类似的问题可以借鉴快速排序的写法。
最后给出完整代码:
package asen.yang; public class quickSort { public static void main(String[] args) { // TODO Auto-generated method stub int[] x = { 9, 7, 5, 3, 1, 8, 6, 4, 2, 0 }; QuickSort(x); for (int i = 0; i < x.length; i++) { System.out.print(x[i] + " "); } } private static int Division(int[] list, int left, int right) { // 以最左边的数(left)为基准 int bs = list[left]; while (left < right) { // 从序列右端开始,向左遍历,直到找到小于base的数 while (left < right && list[right] >= bs) right--; // 找到了比base小的元素,将这个元素放到最左边的位置 list[left] = list[right]; // 从序列左端开始,向右遍历,直到找到大于base的数 while (left < right && list[left] <= bs) left++; // 找到了比base大的元素,将这个元素放到最右边的位置 list[right] = list[left]; } // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小; // 而left位置的右侧数值应该都比left大。 list[left] = bs; return left; } public static void QuickSort(int[] list) { int left = 0; int right = list.length - 1; QuickSort(list, left, right); } public static void QuickSort(int[] list, int left, int right) { // 左下标一定小于右下标,否则就越界了 if (left < right) { // 对数组进行分割,取出下次分割的基准标号 int bs = Division(list, left, right); // 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, left, bs - 1); // 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, bs + 1, right); } } }