zoukankan      html  css  js  c++  java
  • 快速排序 partition函数的所有版本比较

    partition函数是快排的核心部分

    它的目的就是将数组划分为<=pivot和>pivot两部分,或者是<pivot和>=pivot

    其实现方法大体有两种,单向扫描版本双向扫描版本,但是具体到某个版本,其实现方法也是千差万别,参差不齐。本着严谨治学的态度,我将目前所接触的所有实现列举出来,并作出比较。除了伪代码,我也会给出相应的C&C++实现,供读者参考。

    单向扫描:

    下面是算法导论中例子

    PARTITION(A, p, r)
        x = A[r]
        i = p - 1
        for j = p to r - 1
            if A[j] <= x
                i = i + 1
                exchange A[i] with A[j]
        exchange A[i + 1] with A[r]
        return i + 1
    
    int partition(int a[], int p, int r)
    {
        int x = a[r];
        int i = p - 1;
        int j = p;
        for (; j < r; ++j)
            if (a[j] <= x)
                swap(&a[++i], &a[j]);
        swap(&a[i + 1], &a[j]);
        return i + 1;
    }

    这个是标准的单向扫描,其思路是:

    将小于或等于pivot的元素通过交换全部移到前面去,这里需要注意的是i的作用,这是个哨兵,用于记录交换后的位置,也就是i之前的元素都是交换好了的。

    下面是一些可以变动的地方:

    1.可以将小于pivot的元素移到前面去,而不是小于等于,这样可以减少些交换次数,同理,可以将大于pivot的元素移到后面去,不过这样就需要倒序遍历了

    2.或者是将i的初始值设置为p,而不是p-1;

    3.可以将pivot设置成第一个元素;

    4.存在i=j的情况,这时候的交换就是多余的,可以优化掉。

    下面是稍作优化的版本

    int partition(int a[], int p, int r)
    {
        int x = a[r];
        int i = p;
        int j = p;
        for (; j < r; ++j)
            if (a[j] < x) {
                if (i != j)
                    swap(&a[i], &a[j]);
                i++;
            }
        swap(&a[i], &a[j]);
        return i;
    }

    双向扫描:

    算法导论上的课后题有该算法,但是错误百出,这里以《算法》第四版的方法为例

    PARTITION(A, p, r)
        x = A[p]
        i = p
        j = r + 1
        while true
            repeat
                j = j - 1
            until A[j] <= x
            repeat
                i = i + 1
            until A[i] >= x
            if i >= j
                break
            exchange A[i] with A[j]
        exchange A[p] with A[j]
        return j
    
    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p;
        int j = r + 1;
        while (true) {
            while (a[--j] > x);
            while (a[++i] < x);
            if (i >= j)
                break; 
            swap(&a[i], &a[j]);
        }
        swap(&a[j], &a[p]);
        return j;
    }

    其思路是从左到右找到大于等于pivot的元素,从右到左找到小于等于pivot的元素,然后将这两个元素交换,直到左右扫描相遇,最后还要进行一次交换,将pivot调整到正确位置

    这是上面程序的变种,看起来差别很大,不过原理是相同的

    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p + 1;
        int j = r;
        while (i <= j) {
            while (a[j] > x) j--;
            while (a[i] < x) i++;
            if (i >= j)
                break;
            swap(&a[i++], &a[j--]);
        }
        swap(&a[j], &a[p]);
        return j;
    }

    我们看一下它的扫描条件,一个是大于等于,一个是小于等于,也就是说左右扫描点存在都等于pivot的情况,这时候我们是不用交换的。根据互补原理,一个扫描点条件是大于等于,那么另一扫描点条件应该是互补条件小于,这样两个扫描点交换就不会出现交换相等元素的情况。

    另外程序还存在着巨大的溢出漏洞,内层的while循环如:

    while (a[i] < x) i++;

    我们无法保证其不会越界,事实上,我经过测试,发现i的值一旦越界就不确定了,虽然都能保证i >= j的临界条件,但我们还是应该尽量避免越界问题

    可以在循环中加入越界条件

    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p;
        int j = r + 1;
        while (true) {
            while (i < j && a[--j] >= x);
            if (i >= j) break;
            while (i < j && a[++i] < x);
            if (i >= j) break; 
            swap(&a[i], &a[j]);
        }
        swap(&a[j], &a[p]);
        return j;
    }

    变种的防越界版如下

    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p + 1;
        int j = r;
        while (true) {
            while (i <= j && a[j] >= x) j--;
            if (i > j) break;
            while (i <= j && a[i] < x) i++;
            if (i > j) break;
            swap(&a[i++], &a[j--]);
        }
        swap(&a[j], &a[p]);
        return j;
    }

    左右扫描的版本还有很多,让我们再来举几个例子

    网上流传比较广的一个版本是下面这个

    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p;
        int j = r;
        while (i < j)
        {
            while (i < j && a[j] >= x) j--;
            if (i >= j) break;
            a[i++] = a[j];
            while (i < j && a[i] < x) i++;
            if (i >= j) break;
            a[j--] = a[i];
        }
        a[i] = x;
        return i;
    }

    仔细观察会发现,它与我们上面介绍的版本几乎如出一辙,不同的是,它没有使用swap交换元素,而是依次覆盖,最后再把pivot归位

    具体过程可以参阅:http://blog.csdn.net/morewindows/article/details/6684558

    算法的时间复杂度是O(n),但是为什么要写成双循环呢?我们完全可以把它改成单循环,代码如下:

    int partition(int a[], int p, int r)
    {
        int x = a[p];
        int i = p + 1;
        int j = r;
        while (i <= j) {
            if (a[j] > x)
            {
                j--;
                continue;
            }
            if (a[i] < x)
            {
                i++;
                continue;
            }
            swap(&a[i++], &a[j--]);
        }
        swap(&a[j], &a[p]);
        return j;
    }

    但是,并不推荐这种做法,因为每次判断i的时候,势必会再次判断j,多一次比较。

    总结:个人推荐单向扫描的优化版本,双向扫描可以看到会有越界的问题,为了防止越界付出了一定代价。

  • 相关阅读:
    使用openssl搭建CA并颁发服务器证书
    PKCS#1规范阅读笔记2--------公私钥ASN.1结构
    PKCS#1规范阅读笔记1--------基本概念
    Chrome 扩展机制
    Docker部署zookeeper集群和kafka集群,实现互联
    ASP.NET Identity实现分布式Session,Docker+Nginx+Redis+ASP.NET CORE Identity
    Transmission添加SSL访问
    重磅来袭,水木PC客户端全面改版,欢迎使用!
    CLR via C# 3rd
    IL命令
  • 原文地址:https://www.cnblogs.com/sdlwlxf/p/5131793.html
Copyright © 2011-2022 走看看