欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

快速排序(终极版)

程序员文章站 2022-07-03 17:40:04
...

1.思路
基本快速排序程序有两种写法,双指针法和选定某一方向以此遍历法,这两种方法在代码中分别对应PartitionDownSTD函数和PartitionUp函数。
在基本快速排序的基础上可以进行优化,其中包括“三数取中确定key值”、“数组较小时选用插入排序”、“聚集相等元素”、“尾递归优化”等。

2.代码

#include <iostream>
#include <algorithm>
#include <ctime>
using namespace std;

const int K = 3;

void InitArr(int *nArr, int nLen) {     //初始化数组
    srand(10); //确保每次初始化的数组相同
    for(int i = 0; i < nLen; ++i) {
        nArr[i] = rand() % 10;
        //nArr[i] = i;
    }
}

void PrintArr(int *nArr, int nLen) {
    for(int i = 0; i < nLen; ++i) {
        cout << nArr[i] << " ";
    }
    cout << endl;
}

void Swape(int *p1, int *p2) {
    int nTmp = *p1;
    *p1 = *p2;
    *p2 = nTmp;
}

void RandomSort(int *nArr, int nLen) {
    srand(10);
    for(int i = 0; i < nLen; ++i) {
        int nIndex = rand() % nLen;
        Swape(&nArr[i], &nArr[nIndex]);
        //Sleep(2000);                       //等待2s,更新随机种子
    }
}



//递增排序
int PartitionUp(int *nArr, int nLeft, int nRight) {
    int nKey = nArr[nRight];
    int i = nLeft - 1;

    for(int j = nLeft; j < nRight; ++j) {
        if(nArr[j] < nKey) {    //反正不稳定,就用<代替≤,省取相等情况下多余的交换运行时间
            i++;
            Swape(&nArr[j], &nArr[i]);     //不稳定排序的原因 eg:1 5 8 8 6 11 7
        }
    }
    Swape(&nArr[i + 1], &nArr[nRight]); //将主元素插入到中间
    return i + 1;
}

//递增排序,双指针实现
int PartitionDownSTD(int *nArr, int nLeft, int nRight) {
    int i = nLeft;
    int j = nRight + 1;

    while(true) {
        while(nArr[++i] < nArr[nLeft]) {
            if(i == nRight) break;
        }
        while(nArr[--j] > nArr[nLeft]) {
            if(j == nLeft) break;
        }
        if(i >= j) break;
        Swape(&nArr[i], &nArr[j]);
    }
    Swape(&nArr[nLeft], &nArr[j]);
    return j;
}


//递减排序
int PartitionDown(int *nArr, int nLeft, int nRight) {
    int nKey = nArr[nRight];
    int i = nLeft - 1;

    for(int j = nLeft; j < nRight; ++j) {
        if(nArr[j] > nKey) {
            ++i;
            Swape(&nArr[i], &nArr[j]);          //不稳定排序的原因
        }
    }
    Swape(&nArr[i + 1], &nArr[nRight]);       //将主元素插入到中间
    return i + 1;
}

void SelectIndex(int *nArr, int nLeft, int nRight) {
    /*引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,
      但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴*/
    int nMid = nLeft + ((nRight - nLeft) >> 1);
    if(nArr[nMid] > nArr[nRight]) {  //目标:nArr[nMid] <= nArr[nRight]
        Swape(&nArr[nMid], &nArr[nRight]);
    }
    if(nArr[nLeft] > nArr[nRight]) { //目标:nArr[nLeft] <= nArr[nRight]
        Swape(&nArr[nLeft], &nArr[nRight]);
    }
    if(nArr[nLeft] > nArr[nMid]) {   //目标:nArr[nLeft] <= nArr[nMid]
        Swape(&nArr[nLeft], &nArr[nMid]);
    }
    /*此时:nArr[nLeft] <= nArr[nMid] <= nArr[nRight]
      将选择的数与nArr[nRight]交换,即将选择的数作为Key值进行排序
      该方法对升序数组的排序时间优化明显,但对于降序数组,由于Swape执行的次数较多,相比于随机选取法耗时较多*/
    Swape(&nArr[nMid], &nArr[nRight]);

}

int RandomPartition(int *nArr, int nLeft, int nRight) {

    SelectIndex(nArr, nLeft, nRight);
    return PartitionUp(nArr, nLeft, nRight);
}

void InsertSort(int *nArr, int nLeft, int nRight) {
    for(int i = nLeft + 1; i <= nRight; ++i) {
        int nTmp = nArr[i];
        int j;
        for(j = i - 1; j >= nLeft && nArr[j] > nTmp; --j) {
            nArr[j + 1] = nArr[j];
        }
        nArr[j + 1] = nTmp;
    }
}

void QuickSortSTD(int *nArr, int nLeft, int nRight) {
    if(nLeft >= nRight) return;

    //分解
    int nTmpPos = PartitionDownSTD(nArr, nLeft, nRight);

    //解决、合并
    QuickSortSTD(nArr, nLeft, nTmpPos - 1);
    QuickSortSTD(nArr, nTmpPos + 1, nRight);

}

void QuickSortRandom(int *nArr, int nLeft, int nRight) {
    if(nLeft < nRight) {
        int nTmpPos = RandomPartition(nArr, nLeft, nRight);
        QuickSortRandom(nArr, nLeft, nTmpPos - 1);
        QuickSortRandom(nArr, nTmpPos + 1, nRight);
    }
}

void QuickInsertSortRandom(int *nArr, int nLeft, int nRight) {
    if(nLeft < nRight) {
        if(nRight - nLeft > K) {
            int nTmpPos = RandomPartition(nArr, nLeft, nRight);
            QuickInsertSortRandom(nArr, nLeft, nTmpPos - 1);
            QuickInsertSortRandom(nArr, nTmpPos + 1, nRight);
        } else {
            InsertSort(nArr, nLeft, nRight);
        }
    }
}

//尾递归优化
void QuickInsertSortRandomTail(int *nArr, int nLeft, int nRight) {
    if(nRight - nLeft <= K) {
        InsertSort(nArr, nLeft, nRight);
        return;
    }

    while(nLeft < nRight) {
        int nTmpPos = RandomPartition(nArr, nLeft, nRight);
        QuickInsertSortRandom(nArr, nLeft, nTmpPos - 1);
        nLeft = nTmpPos + 1;
    }
}

//带聚集相等元素的递增快速排序
void QuickInsertSameSortRandom(int *nArr, int nLeft, int nRight) {
    if(nRight - nLeft <= K) {             //数组较短,采用插入排序
        InsertSort(nArr, nLeft, nRight);
        return;
    }

    int i = nLeft - 1, j = nRight;
    int p = nLeft - 1, q = nRight;

    SelectIndex(nArr, nLeft, nRight);    //3 Random Choose Key Value
    int key = nArr[nRight];

    while(true) {
        while(nArr[++i] < key)
            if(i == nRight) break;
        while(nArr[--j] > key)
            if(j == nLeft) break;

        //pointers cross
        if(i == j && nArr[i] == key)
            Swape(&nArr[++p], &nArr[i]);
        if(i >= j) break;

        Swape(&nArr[i], &nArr[j]);

        if(nArr[i] == key)
            Swape(&nArr[i], &nArr[++p]);
        if(nArr[j] == key)
            Swape(&nArr[j], &nArr[--q]);
    }

    //将相等元素交换到中间,结束时j指向较小数组集的末尾,i情况不定
    i =  j + 1;
    for(int k = nLeft; k <= p; ++k)
        Swape(&nArr[k], &nArr[j--]);
    for(int k = nRight; k >= q; --k)
        Swape(&nArr[k], &nArr[i++]);

    QuickInsertSameSortRandom(nArr, nLeft, j);
    QuickInsertSameSortRandom(nArr, i, nRight);

}


int main() {
    int Len = 50000;
    int Arr1[Len], Arr2[Len], Arr3[Len], Arr4[Len], Arr5[Len];
    time_t t1, t2, t3, t4, t5, t6;

    InitArr(Arr1, Len);
    InitArr(Arr2, Len);
    InitArr(Arr3, Len);
    InitArr(Arr4, Len);
    InitArr(Arr5, Len);




//    PrintArr(Arr, Len);
//    RandomSort(Arr, Len);
//    PrintArr(Arr, Len);

    t1 = clock();
    QuickSortSTD(Arr1, 0, Len - 1);
    t2 = clock();
    QuickInsertSortRandom(Arr2, 0, Len - 1);
    t3 = clock();
    QuickInsertSameSortRandom(Arr3, 0, Len - 1);
    t4 = clock();
    InsertSort(Arr4, 0, Len - 1);
    t5 = clock();
    sort(Arr5, Arr5 + 100000);
    t6 = clock();


    cout << "QuickSortSTD costs: " << double(t2 - t1)/CLOCKS_PER_SEC << "s" << endl;
    cout << "QuickInsertSortRandom costs: " << double(t3 - t2)/CLOCKS_PER_SEC << "s" << endl;
    cout << "QuickInsertSameSortRandom costs: " << double(t4 - t3)/CLOCKS_PER_SEC << "s" << endl;
    cout << "InsertSort costs: " << double(t5 - t4)/CLOCKS_PER_SEC << "s" << endl;
    cout << "STL_Sort costs: " << double(t6 - t5)/CLOCKS_PER_SEC << "s" << endl;
//    PrintArr(Arr, Len);
    return 0;
}

3.运行结果
快速排序(终极版)
该待排序数组包含极多重复数据,从结果中可以看出,三数取中+插排+聚集相等元素的方法效果最佳,其次为基本快排(因为裸的快排比较函数运行较少),再后面依次为:STL库sort函数、三数取中+插入、插入排序。在网上找到的其他人做的实验结果如下:

快速排序(终极版)
这里效率最好的快排组合 是:三数取中+插排+聚集相等元素,它和STL中的Sort函数效率差不多。注意:由于测试数据不稳定,数据也仅仅反应大概的情况。如果时间上没有成倍的增加或减少,仅仅有小额变化的话,我们可以看成时间差不多。

补充:
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。

void QSort(int arr[],int low,int high)
{ 
    int pivotPos = -1;
    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    while(low < high)
    {
        pivotPos = Partition(arr,low,high);
        QSort(arr,low,pivot-1);
        low = pivot + 1;
    }
}

递归调用函数,当调用次数太多时,真的很容易卡死……