quick sort的几种实现

简介

    之所以要写点和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的过程在一些具体的应用中还有不同的实现方法,很多细节针对有重复元素等情况体现出来的效果也是各不相同的。值得去细细的体会。

 

参考材料

Algorithms

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值