快速排序是排序算法中效率比较高的一种,也是面试常被问到的问题。
快速排序(Quick Sort)是对冒泡排序的一种改进。它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。(数据结构,严蔚敏)。
更具体一点的说,首先我们从待排序列中选取一个元素作为支点(pivot),这个支点的选择可以任意选取,所以经常直接拿第一个元素作为支点,更好一点的办法是三者取中法,即取第一个元素,最后一个元素,中间一个元素,这三个值中间的中值(非最大最小)作为支点。支点选好之后,把支点放到待排序序列的第一个位置,如果你选的是第一个元素作为支点的话,不用做什么,如果是其他情况,那么就把选好的支点跟第一个元素交换一下。然后通过代码将所有比支点小的记录都放在它之前的位置,将所有比它大的记录都放到它的位置之后。由此最后可以以该支点的位置作分界线,将序列分割成两个子序列。这个过程我们称为一趟快速排序(或一次划分)。
一次快速排序的具体实现:
将支点放在序列的第一个位置,最左边。然后从高位下标high(初始值为最后一个元素的位置)开始往前搜索,找到第一个小于支点的元素,将其和支点元素交换,即把一个比支点小的元素放在了支点的位置,而支点现在所处的新位置之后的元素都是比其大的,因为这个位置是我们从高位找到第一个比支点小的元素的原先位置;然后从低位low(初始值为第一个元素的位置)开始往后搜索,找到第一个大于支点的记录,将其和支点元素交换,由于前面的交换中支点所处的位置之后的元素都比其大,通过这一次的交换的话,这个较大的元素现在所在的位置往后的元素都是大于支点的,而且,支点回到的新的位置之前的元素都是比支点小的。重复这两步,直到从低位搜索和从高位搜索的两个index(也可以理解为指针)相遇,即low == high。
其实,上面提到的每次交换元素的操作可以精简一下,对支点记录的赋值是多余的,因为只有在一趟排序结束时,才能确定支点的最后位置。所以有一个临时变量将支点的值记录下来就可以了。每次交换时,只把需要变换位置的元素放到新的位置就可以了。
下面是代码,这段代码采用第一个元素作为支点,如果没有用第一个元素作为支点的话,那么交换一下,把支点放到第一个位置即可,然后采用下面相同的代码:
static int Partition(int[] numbers, int low, int high) { int pivot = numbers[low]; while (low < high) { while (low < high && numbers[high] >= pivot) { high--; } numbers[low] = numbers[high]; while (low < high && numbers[low] <= pivot) { low++; } numbers[high] = numbers[low]; } numbers[low] = pivot; return low; }
上面是一次快速排序的代码,最后返回的是支点的位置。下面就是对支点左右两边的序列分别进行快速排序,当子序列只有两个元素时,再通过一次排序该子序列就是有序的了,由此实现整个序列有序。可见,这是一个递归的操作。只有当序列长度大于1时才进行快速排序,因为只有一个元素的序列肯定是有序的,所以跳出递归的条件是low<high。只有一个元素时,low 是等于 high的。
static void QuickSort(int[] numbers, int low, int high) { if (low < high) { int partitionLocation = Partition(numbers, low, high); QuickSort(numbers, low, partitionLocation - 1); QuickSort(numbers, partitionLocation + 1, high); } }
调用的过程就如:
static void Main(string[] args) { int[] numbers = { 49, 38, 65, 97, 76, 13, 27, 49 }; QuickSort(numbers, 0, numbers.Length - 1); }
就平均时间而言,快速排序是目前认为是最好的一种内部排序方法。(数据结构,严蔚敏)。
最后,快速排序的平均时间复杂度是O(nlogn),最坏情况是O(n2),如序列本身就有序时,将蜕变为冒泡排序。空间复杂度为O(logn),这是因为递归过程要维护一个栈。
如果不好理解的话,最好的办法就是对着代码,手动走几遍,或者找些有配图的书或文章看看。