简介
之所以要写点和quick sort相关的,主要是因为我们很多时候只是关注一下某些问题的一个标准答案。实际上在我们碰到不同的情形,在原有问题的基础上做一点小小的变动,会带来更理想的结果。这里针对传统的实现,有相同元素的实现和非递归的实现做了一个探讨。
第一种实现
我们知道quick sort的过程其实描述还是比较简单的。它主要就是挑选一个中间值,通过partition方法将整个数组划分成小于这个值和大于这个值的两个部分。然后再针对这两个部分递归的去排序。因此从一个宏观的角度来说,一个典型的实现如下:
void quickSort(int[] a, int l, int r){
if(l < r) {
int q = newPartition(a, l, r);
quickSort(a, l, q - 1);
quickSort(a, q + 1, r);
}
}
因为是递归调用的,我们每次取值的范围就是在中间值划分后的结果里。partition的实现如下:
int partition(int[] a, int l, int r) {
int x = a[r];
int i = l - 1;
for(int j = l; j < r; j++) {
if(a[j] <= x) {
i++;
swap(a, i, j);
}
}
swap(a, i + 1, r);
return i + 1;
}
这里我们是取的这个数组里最右边的那个值来划分整个数组。这里的数字i表示小于或等于给定值的范围。每次碰到小于等于给定值的时候就递增i,然后交换当前元素和i所在元素的位置。我们都知道,quick sort的时间复杂度平均为O(NlogN),在最坏的情况下为O(N * N)。
除了上面的实现方式,还要一种实现的思路,就是设定两个索引,一个指向数组的头一个指向数组的尾。假设它们分别定义为i, j,分别表示小于划分元素的索引位置和大于划分元素的索引。假设以数组的第一个元素作为划分元素的话。我们可以采用两头凑的方式。每次从左到右的去遍历比较数组,如果碰到大于划分元素的则停止,然后再看右边从右到左的去比较,碰到比划分元素小的也暂停,交换i, j两个位置的元素。继续上述的过程直到i >= j。按照这个思路,这种实现的代码如下:
int partition(int[] a, int lo, int hi) {
int i = lo, j = hi + 1;
while(true) {
while(a[++i] < a[lo])
if(i == hi) break;
while(a[lo] < a[--j])
if(j == lo) break;
if(i >= j) break;
swap(a, i, j);
}
swap(a, lo, j);
return j;
}
这种默认的情况还是比较好理解的,可是,在遇到一些拥有相同元素的情况下, 我们是否有什么办法来改进一下呢?因为这些元素如果和划分值是相同的,我们完全可以将他们集中在一块,这样可以直接将它们整个都剥离出来不用参加后面的排序,这不就间接使得需要排序的数据范围缩小了吗?这样也可以提高一点效率啊。
有相同值元素的实现
按照前面的思路,我们这里需要一个元素来记录小于划分值的范围。这里肯定也需要一个元素来记录大于划分值的范围。而在它们两个值中间的不正好就是等于划分值的么?于是我们可以实现如下的代码:
public static void sort(int[] a, int lo, int hi) {
if(hi < lo) return;
int lt = lo, i = lo + 1, gt = hi;
int v = a[lo];
while(i <= gt) {
if(a[i] < v) swap(a, lt++, i++);
else if(a[i] > v) swap(a, i, gt--);
else i++;
}
sort(a, lo, lt - 1);
sort(a, gt + 1, hi);
}
这里的代码没有使用前面的那个划分方法,但是基本的思路是差不多的。我们用lt表示小于划分值的范围,gt表示大于划分值的范围。于是当给定值小于划分值的时候,lt++, i++。因为这里是取的数组最左边的元素作为划分中间值,所以lt表示等于中间值的最左边的那个元素的索引。这里最难理解清楚的是针对a[i] < v, a[i] > v和a[i] = v这3种情况。尤其要注意的就是为什么我们当a[i] < v的时候, lt,i都要加1而a[i] > v的时候只要gt--。因为我们知道当a[i] < v的时候lt++,这之后lt指向的其实已经是最左边的那个和划分值相等的元素了,而之前lt指向的元素就是划分元素v,每次递增后得到的值不可能大于v。而右边的gt所在的元素则还有可能小于v,所以每次gt--的同时还不能i++。
详细的情况我们可以针对下图来分析:
非递归实现
除了前面的两种实现,还有一个比较有意思的实现就是非递归实现。一般我们都习惯于用递归的方式去实现。但是用一些辅助存储,我们也可以用非递归的方式来实现。
基本的思路如下,我们用一个额外的栈来保存每次划分的开头和结尾部分。每做一次划分就将两边的划分边界都保存起来。然后不断的出栈,在取出的出栈序列里如果表示右边界已经小于等于左边界了,表示这一步的划分结束。
按照这个思路,得到的代码实现如下:
public static void iterSort(int[] a, int l, int r) {
Stack<Integer> stack = new Stack<Integer>();
push2(stack, l, r);
while(!stack.empty()) {
int left = stack.pop();
int right = stack.pop();
if(right <= left) continue;
int i = partition(a, left, right);
if(right - i > i - left) {
push2(stack, i + 1, right);
push2(stack, left, i - 1);
} else {
push2(stack, left, i - 1);
push2(stack, i + 1, right);
}
}
}
这里还有用到一个改进,每次将划分后比较长的那一段先压栈。然后push2的实现如下:
public static void push2(Stack<Integer> stack, int l, int r) {
stack.push(r);
stack.push(l);
}
我们要注意首先入栈的是右边界,因为栈是先入后出的。
总结
Quick sort的过程在一些具体的应用中还有不同的实现方法,很多细节针对有重复元素等情况体现出来的效果也是各不相同的。值得去细细的体会。