快速排序 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,多一次比较。

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

时间: 2024-10-05 23:50:11

快速排序 partition函数的所有版本比较的相关文章

快速排序中的partition函数详解

快速排序的精髓就在partition函数的实现.我们构建两个指针,将数组分为三部分,黑色部分全部小于pivot,中间蓝色部分都大于pivot,后面红色部分未知.i指针遍历整个数组,只要它指向的元素小于pivot就交换两个指针指向的元素,然后递增. // arr[]为数组,start.end分别为数组第一个元素和最后一个元素的索引 // povitIndex为数组中任意选中的数的索引 int partition(int arr[], int start, int end, int pivotInd

快速排序的Partition函数

1 //数组中两个数的交换 2 static void swap(int[] nums, int pos1, int pos2){ 3 int temp = nums[pos1]; 4 nums[pos1] = nums[pos2]; 5 nums[pos2] = temp; 6 } 7 /** 8 * 快速排序中,在数组中选择一个数字,将数组中的数字分为两部分 9 * start, end 介于 0 与 nums.length之间 10 */ 11 static int partition(i

快速排序 归并排序的非递归版本 备忘

首先,归并排序,分治,递归解决小的范围,再合并两个有序的小范围数组,便得到整个有序的数组. 这是很适合用递归来写的,至于非递归,便是从小到大,各个击破,从而使得整个数组有序.代码如下: void merge(vector<int> &A, int left, int mid, int right) { int i=left,j=mid+1; vector<int> tmp(right-left+1,0); int k=0; while(i<=mid&&

c++多线程编程:实现标准库accumulate函数的并行计算版本

今天使用c++实现了标准库头文件<numeric>中的accumulate函数的并行计算版本,代码如下,注释写的比较详细,仅对其中几点进行描述: ①该实现假定不发生任何异常,故没有对可能产生的异常进行处理 ②第42行的语句: const unsigned int num_thread = std::min((hardware_thread != 0 ? hardware_thread : 2), max_thread); 要运行的线程数是计算出的最大线程数和硬件线程数量的较小值.这是因为若运行

partition函数两种实现方法

patition函数根据某种比较关系将数组分成两部分,下面根据元素比某个数字大或小,以此为基准划分,给出两种实现方式 1)若数组为a[0]~a[n-1],函数调用如下 partition(a,-1,n-1)a[n-1]一般作为基准元素所在的位置,返回基准元素应该放置的下标 int partition(int *a, int i, int j, int pivot){ do{ while (a[++i] < pivot); while ((j > 0) && (a[--j] &g

find_if函数与partition函数的转换

编写程序,求大于等于一个给定长度的单词有多少.我们还会修改输出,使程序只打印大于等于给定长度的单词. 使用find_if实现的代码如下: #include<algorithm> #include<vector> #include<iostream> #include<string> using namespace std; void biggies(vector<string> &words,vector<string>::s

寻找序列中最小的第N个元素(partition函数实现)

Partition为分割算法,用于将一个序列a[n]分为三部分:a[n]中大于某一元素x的部分,等于x的部分和小于x的部分. Partition程序如下: long Partition (long a[], long p1, long p2) {//对a[p1]~a[p2]进行分割,返回分割点的序号, p1, p2分别为元组的第一 //个和最后一个元素 long i, j; int x; i = p1; j = p2; x = a[i]; while (i<j) {while ( a[j] >

字符串的partition函数

partition函数 str1='sdga2a34'aa=str1.partition('a') print(aa) """ ('sdg', 'a', '2a34') """ rpartition函数,和上面的函数不同,它是从右边开始切割的 str1='sdga2a34' aa=str1.rpartition('a') print(aa)""" ('sdga2', 'a', '34') ""&q

快速排序中的partition函数的枢纽元选择,代码细节,以及其标准实现

很多笔试面试都喜欢考察快排,叫你手写一个也不是啥事.我很早之前就学了这个,对快速排序的过程是很清楚的.但是最近自己尝试手写,发现之前对算法的细节把握不够精准,很多地方甚至只是大脑中的一个映像,而没有理解其真正的本质意图.于是今天结合了<数据结构>(严蔚敏),和<算法导论>进行一番探究. 首先先给出快速排序的严蔚敏版的实现(实际上这部分的partition也是算法导论里面思考题的实现方式,细节可能不一样): 1 public class QuickSort implements So