快速排序的三种实现方式以及非递归版本

来源:互联网 发布:three.js 全景图切换 编辑:程序博客网 时间:2024/06/05 18:47

一、快速排序的基本思想

快速排序利用了分治的思想,分而治之。通过一趟排序将序列分为两部分,其中一部分比较关键字小,一部分比关键字大。之后继续对这两个子序列重复此过程,直到整个序列都有序。

二、快速排序的三个步骤

  1. 选基准值:在要排序的序列中选一个几个基准值,作为划分区间的 关键字。通常选取关键字的方式对效率有很大的影响。
  2. 分割序列:按照基准值把序列分为两部分,左边的小于基准值,右边的大于基准值。
  3. 重复分割:对子序列重复上面的步骤,知道序列为空或者只有一个元素。当递归处理的所有子序列都返回时,序列有序。

三、三种实现方式

注:下面代码中所有区间都是左闭右开,[ left, right),且排序都是从大到小。且基准值默认选择序列的最后一个元素

一、版本一填充法

思路:两个指针 begng作为序列的开始 和 end 序列的结束。

将序列的最后一个元素保存起来,作为基准值。

开始划分序列:直到begin < end 不成立

begin 从左往右找比 基准值 大的元素,将该值填充到 end 的位置(end 位置上的值已经保存到 key),end– ,因为已经划分好了一个元素。
end 从右往左找比 基准值小的元素,如将该值填充到 begin 的位置(begin 位置上的值已经保存到 end位置),begin++,已经划分好一个元素。

循环结束时,将 key 填充到 end 的位置,然后将 end 返回,作为下一次划分序列的边界。

// 版本一 填充法int partition(vector<int> &v, int left, int right){    int begin = left;    int end = right - 1;    int key = v[end];          // 选基准    while (begin < end)    {        while (begin < end && v[begin] <= key)            begin++;        if (begin < end)            v[end--] = v[begin];        while (begin < end && v[end] >= key)            end--;        if (begin < end)            v[begin++] = v[end];    }    v[end] = key;    return end;}void QuickSort(vector<int>& v, int left, int right){    if (left < right)    {        int boundary = partition(v, left, right);        QuickSort(v, left, boundary);        QuickSort(v, boundary + 1, right);    }}

二、版本二 交换法

和上面的版本一类似,不同之处是,这里不是填充,而是将找到的元素交换。

// horn版本int partition(vector<int> &v, int left, int right){    int begin = left;    int end = right - 1;    int key = v[end];    while (begin < end)    {        while (begin < end && v[begin] <= key)            begin++;        while (begin < end && v[end] >= key)            end--;        if (begin < end)            std::swap(v[begin], v[end]);    }    if(end != right -1)     std::swap(v[end], v[right-1];)    return end;}void QuickSort(vector<int>& v, int left, int right){    if (left < right)    {        int boundary = partition(v, left, right);        QuickSort(v, left, boundary);        QuickSort(v, boundary + 1, right);    }}

我认为一,二的主要不同在于一个是填充一个是交换,当比较对象较大时,填充的效率是高于交换,不需要在构造一个临时变量。

三、版本三 双指针前移法

该方法短小精悍。

思路:
两个指针从同一个方向走。
cur 指向当前元素,每次循环都要往前走,直到遍历完序列,所以循环结束的条件就是 cur < right ,right 为 序列的右边界。

prev 指向cur的前一个位置。

如果cur 指向的值小于 基准值, 那么就让 prev前进一个位置,并判断 cur 是否等于 prev,如果不等,说明它两不在同一位置,那么交换位置上的值。

cur 要么相邻,要么中间间隔的都是别基准值大的元素。cur 的目的就是替 prev 清理前进路上的障碍—“比基准值小的元素”。剩下的都是比基准值大的元素, 只要当 cur 找到比基准值小的,就放心的交换。

循环结束时,需要将基准值,即序列的最后一个元素,与 prev下一个位置上的元素 交换。因为这个prev 所在的位置是比基准值小的元素,那么prev 的下一个位置一定比 prev 大,将这个比基准值大的元素与基准值交换就完成了一次划分。

int partition1(vector<int> &v, int left, int right){    int prev = left - 1;    int cur = left;    int key = v[right - 1];    while (cur < right)    {        if (v[cur] < key && ++prev != cur)            std::swap(v[cur], v[prev]);        cur++;    }    if (v[++prev] != key)        std::swap(v[prev], v[right - 1]);    return prev;}void QuickSort(vector<int>& v, int left, int right){    if (left < right)    {        int boundary = partition1(v, left, right);        QuickSort(v, left, boundary);        QuickSort(v, boundary + 1, right);    }}

非递归版本

每次用到递归,大家都会担心递归太深是否会栈溢出?我测试了900000000个int 整型的排序(3.35GB左右内存),栈也木有溢出啊。。

尽管如此还是给出非递归的实现版本吧,比较栈资源宝贵。

利用栈将序列起始位置与结束位置保存起来。

void QiockSortNor(vector<int> &v, int left, int right){    if (left >= right)        return;    stack<int> s;    s.push(left);    s.push(right);    while (!s.empty())    {        int right = s.top();        s.pop();        int left = s.top();        s.pop();        if (left < right)        {            int boundary = partition1(v, left, right);            // 左区间            s.push(left);            s.push(boundary);            // 右区间            s.push(boundary + 1);            s.push(right);        }    }}

简单总结一下:

快速排序,是一种不稳定的排序,时间复杂度平均为 O(log2n),逆序序列下效果最差,逼近 O(n2),起空间复杂度是 log2n,因为需要空间来保存递归的区间范围。

原创粉丝点击