快速排序优化

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/qq_37653144/article/details/83042293

       快速排序是一种以分治为基本思想的排序算法,它将待排序数组分成两个子数组,将两部分再独立地排序。其中涉及到三个重要操作:选择基准元素、切分、对切分得到的子数组进行快排(分治)。因此对快速排序的优化也应针对这三部分进行。

优化基准元素选择

       快速排序最理想的情况是每次都能将序列切分成相同大小的两部分,因此切分所用的基准元素如何选择就成了影响算法性能的关键。最简单的实现就是采用相对位置固定的基准元素——一般是序列的第一个或最后一个元素。但这样往往会导致糟糕性能的出现,因为这样很容易出现很不平衡的切分序列。例如针对已经有序的序列或含有大量重复元素的序列。

(1)随机选择基准元素

       选取固定位置的元素作为基准对于已经有序的序列(不一定是初始序列就全部有序,子序列有序也可能会导致性能降低)是糟糕的选择,随机选取基准元素就可以大大改善对此类序列的排序性能。

#include <vector>
#include <random>

using namespace std::vector;
using std::random_device;
using std::uniform_int_distribution;

void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    if (first < last) {
        vector<int>::size_type i = first, j = last;
        random_device rd;
        uniform_int_distribution<int> d(1, arr.size()-1);
        vector<int>::size_type index = d(rd);
        swap(arr[first], arr[index]);
        int key = arr[first];
        while (i != j) {
            while (j > i && arr[j] > key)
                --j;
            arr[i] = arr[j];
            while (i < j && arr[i] < key)
                ++i;
            arr[j] = arr[i];
        }
        arr[i] = key;
        QuickSort(arr, first, i-1);
        QuickSort(arr, i+1, last);
    }
}

(2) 中位数取样

       既然我们想要快排在每一次切分中都能获得两个等长的子序列,那么自然会想到用中位数。最简单的方法是在序列的第一个元素、最后一个元素和中间元素中选出中位数来作为基准元素。我们也可以从由左中右三个中选取扩大到五个元素中或者更多元素中选取,一般的,会有(2t+1)平均分区法(median-of-(2t+1),三平均分区法median-of-three)。以下源码采用最简单的实现。

#include <vector>

using namespace vector;

void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    if (first < last) {
        vector<int>::size_type i = first, j = last, mid = first+((last-first)>>1);
        if (arr[mid] > arr[last])
            swap(arr[mid], arr[last]);
        if (arr[first] > arr[last])
            swap(arr[first], arr[last]);
        if (arr[mid] > arr[first])
            swap(arr[mid], arr[first]);
        int key = arr[first];
        while (i != j) {
            while (j > i && arr[j] > key)
                --j;
            arr[i] = arr[j];
            while (i < j && arr[i] < key)
                ++i;
            arr[j] = arr[i];
        }
        arr[i] = key;
        QuickSort(arr, first, i-1);
        QuickSort(arr, i+1, last);
    }
}

       中位数取样法对于部分或全部已有序的序列也有很好的性能。

 优化小序列

       对于小序列来说,快速排序比插入排序要慢,因此在快排切分到子序列足够小时改调用插入排序能很好地提升性能。

#include <vector>

using namespace vector;

InsertionSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    if (first < last) {
        for (vector<int>::size_type i = 1; i < arr.size(); ++i)
            if (a[i] < a[i-1])
                for (vector<int>::size_type j = i; j < arr.size() && a[j] < a[j-1]; --j)
                    swap(a[j], a[j-1]);
    }
}

void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    if (first < last) {
        if (last <= first+15) {
            InsertionSort(arr, first, last);
            return ;
        }
        vector<int>::size_type i = first, j = last;
        int key = arr[first];
        while (i != j) {
            while (j > i && arr[j] > key)
                --j;
            arr[i] = arr[j];
            while (i < j && arr[i] < key)
                ++i;
            arr[j] = arr[i];
        }
        arr[i] = key;
        QuickSort(arr, first, i-1);
        QuickSort(arr, i+1, last);
    }
}

尾递归优化

       如果待排序的子序列划分极不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多,很容易导致栈溢出,并且系统在维护函数栈及栈跳转等方面的时间和空间开销都很大。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(nlogn),将会提高性能。 

#include <vector>

using namespace vector;

void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    while (first < last) {
        vector<int>::size_type i = first, j = last, key = arr[first];
        while (i != j) {
            while (j > i && arr[j] > key)
                --j;
            arr[i] = arr[j];
            while (i < j && arr[i] < key)
                ++i;
            arr[j] = arr[i];
        }
        arr[i] = key;
        QuickSort(arr, first, i-1);
        first = i+1;
    }
}

三向切分

       前面说到的优化思路都无法解决对含有大量重复元素的序列的排序性能低下的问题。针对这一类序列,我们希望避免对于重复元素进行排序,但之前的优化都不能解决这个问题,快速排序的递归性会使元素全部重复的子序列经常出现。

       一个简单的做法是将序列切分为三部分,分别对应小于、等于和大于基准元素的序列元素。实现这一点需要维护三个指针:一个指针lt使得arr[first, lt-1]的元素都小于基准元素,一个指针gt使得[gt+1, last]的元素都大于基准元素,最后一个指针使得[lt, i-1],也是[lt, gt]的元素都等于基准元素。

#include <vector>

using namespace vector;

void QuickSort(vector<int> &arr, vector<int>::size_type first, vector<int>::size_type last) {
    if (first < last) {
        vector<int>::size_type lt = first, gt = last, i = first;
        while (i <= gt) {
            if (arr[i] < key) {
                swap(arr[i], arr[lt]);
                ++lt, ++i;
            }
            else if (arr[i] > key) {
                swap(arr[i], arr[gt]);
                --gt;
            }
            else
                ++i;
        }
        QuickSort(arr, first, lt-1);
        QuickSort(arr, gt+1, last);
    }
}

       然而三向切分快排对于没有(或很少)重复元素的序列的性能反而会比普通快排要慢,因为它的交换次数要比普通快排多。

总结

       以上都是对单一优化方式进行分析,如果将这些优化思路合理地组合起来得到的效果会更好,例如:中位数取样+小序列插排+三向切分+尾递归的组合效率就比任何一种单一优化方式性能要好。

       总之,没有最好的算法,只有最合适的算法——从以上对快排的优化可以得出这样的结论 。根据数据特点因地制宜选择合适的算法才是我们要做的。

猜你喜欢

转载自blog.csdn.net/qq_37653144/article/details/83042293