快速排序的三种实现及两种优化

一、快速排序的概念

快速排序(Quicksort)是对冒泡排序的一种改进。

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

二、快排的三种实现方式

实现方式一:左右指针法
算法思想:在待排序序列中选择一个数据作为基准值(暂且将区间右端数据作为基准值),定义两个指针left,right开始分别指向待排序区间的两端,左指针向右找比基准值大的数据,找到后停下来,接着右指针向左开始找比基准值小的数据,找到后停下来,交换左右指针所指数据,直到两指针相遇,出循环后将左指针指向的值与基准值进行交换,交换成功后比基准值小的数据都在其左边,比基准值大的数据都在基准值的右边,换言之基准值已经处于合适的位置。接着递归对基准值的左区间和右区间进行排序,区间只有一个数据默认已经有序。

算法执行过程:
《快速排序的三种实现及两种优化》

算法核心代码:

//左右指针法
int PartSort1(int* a, int left, int right)//左右指针法
{
    int mid = GetMidIndex(a, left, right);
    QSwap(&a[mid], &a[right]);

    int key = a[right];//用区间的最右边的值作为基准值
    int keyidx = right;
    while (left <right)
    {
        while (left < right && a[left] <= key)
            ++left;
        while (left < right && a[right] >= key)
            --right;
        if (a[left] != a[right])
            QSwap(&a[left], &a[right]);
    }
    QSwap(&a[left], &a[keyidx]);
    return left;
}

//递归实现
void QuickSort1(int* a, int left,int right)//递归实现
{
    assert(a);
    if (left < right)
    {
        int div = PartSort1(a, left, right);

        QuickSort1(a, left, div-1);//递归左区间
        QuickSort1(a, div+1, right);//递归右区间
    }
}

实现方式二:挖坑法
算法思想:在待排序序列中选择一个数据作为基准值(暂且将区间右端数据作为基准值),首次将坑设在基准值处,定义两个指针left,right开始分别指向待排序区间的两端,左指针向右找比基准值大的数据,找到后将该值填入坑中并且将坑的位置更新在左指针所指位置,接着右指针向左开始找比基准值小的数据,找到后将该值填入坑中并且将坑的位置更新在右指针所指位置,直到两指针相遇,出循环后比基准值小的数据都在其左边,比基准值大的数据都在基准值的右边,换言之基准值已经处于合适的位置。接着递归对基准值的左区间和右区间进行排序,区间只有一个数据默认已经有序。

算法执行过程:
《快速排序的三种实现及两种优化》

核心代码:

//挖坑法
int PartSort2(int* a, int left, int right)//挖坑法
{
    int mid = GetMidIndex(a, left, right);
    QSwap(&a[mid], &a[right]);

    int key = a[right];//选区间的最右边的值作为基准值
    int blank = right;//坑基准值的下标
    while (left < right)
    {
        while (left < right && a[left] <= key)
            ++left;
        if (left != right)
        {
            QSwap(&a[left], &a[blank]);//用找到的比基准值大的值填坑
            blank = left;
        }

        while (left < right && a[right] >= key)
            --right;
        if (left != right)
        {
            QSwap(&a[right], &a[blank]);//用找到的比基准值小的值填坑
            blank = right;
        }   
    }
        return left;
}

//递归实现
void QuickSort1(int* a, int left,int right)//递归实现
{
    assert(a);
    if (left < right)
    {
        int div = PartSort2(a, left, right);

        QuickSort1(a, left, div-1);//递归左区间
        QuickSort1(a, div+1, right);//递归右区间
    }
}

算法实现3:前后指针法
算法思想:在待排序序列中选择一个数据作为基准值(暂且将区间右端数据作为基准值),定义两个指针prev和cur,cur首次指向待排序区间的第一个元素,prev指向cur的前一个位置,只要cur没走到区间末尾,就在中间找比基准值小的数据,每找到一次将prev++,然后如果二者所指元素不同就将其所指元素交换,直到cur走到区间的最右端退出循环,之后将prev++,将cur和prev所指的数据进行交换。至此基准值被排序到正确位置,赌鬼其左右区间即可完成整个排序。

算法的执行过程:
《快速排序的三种实现及两种优化》

三、快排的性能分析

快速排序是一种快速的分而治之的算法,它是已知的最快的排序算法,其平均运行时间为O(N*1ogN) 。它的速度主要归功于一个非长紧凑的并且高度优化的内部循环。但是他也是一种不稳定的排序,当基准数选择的不合理的时候他的效率又会编程O(N*N)。

快速排序的最好情况
快速排序的最好情况是每次都划分后左右子序列的大小都相等,其运行的时间就为O(N*1ogN)。

快速排序的最坏情况
快速排序的最坏的情况就是当分组重复生成一个空序列的时候,这时候其运行时间就变为O(N*N)

快速排序的平均情况
平均情况下是O(N*logN),证明省略。

四、快排的两种优化

快速排序优化1:三数取中法
因为虽然快速排序整体的效率可观,但是当最坏情况发生时它的效率就会降低,为了降低最坏情况发生的概率,我们可以做如下改进。

当我们每次划分的时候选择的基准数接近于整组数据的最大值或者最小值时,快速排序就会发生最坏的情况,但是每次选择的基准数都接近于最大数或者最小数的概率随着排序元素的增多就会越来越小,我们完全可以忽略这种情况。但是在数组有序的情况下,它也会发生最坏的情况,为了避免这种情况,我们在选择基准数的时候可以采用三数取中法来选择基准数。
三数取中法:
选择这组数据的第一个元素、中间的元素、最后一个元素,这三个元素里面值居中的元素作为基准数。

代码实现:

//优化1:三数取中
int GetMidIndex(int* a, int left, int right)//优化1:三数取中法
{
    int mid = left + ((right - left) >> 1);//取区间中间元素的下标
    if (a[left] > a[mid])//left > mid
    {
        if (a[left] < a[right])//mid < left < right
            return left;
        else//left > mid,left > right
        {
            if (a[right] > a[mid])//mid < right < left
                return right;
            else//right < mid < left
                return mid;
        }
    }
    else//left < mid
    {
        if (mid > right)//left < mid < right
            return mid;
        else//mid < left,mid < right 
        {
            if (left < right)//mid < left < right
                return left;
            else
                return right;
        }
    }
}   

快速排序优化2:小区间优化
当划分的子序列很小的时候(一般认为小于13个元素左右时),我们在使用快速排序对这些小序列排序反而不如直接插入排序高效。因为快速排序对数组进行划分最后就像一颗二叉树一样,当序列小于13个元素时我们再使用快排的话就相当于增加了二叉树的最后几层的结点数目,增加了递归的次数。所以我们在当子序列小于13个元素的时候就改用直接插入排序来对这些子序列进行排序。

代码实现:

//优化2:小区间优化---当序列元素小于13时,直接采用直接插入排序
void InsertSort(int* a, int left, int right)
{
    int end = 0;
    for (int i = 1; i < right; i++)
    {
        end = i - 1;
        int temp = a[i];
        while (end >= 0)
        {
            if (a[end] > temp)
            {
                a[end + 1] = a[end];
                --end;
            }
            else
                break;
        }
        a[end + 1] = temp;
    }
}

五、快排的完整代码

#include<iostream>
#include<cassert>
#include<stack>
using namespace std;

void QSwap(int* x, int *y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}
//优化1:三数取中
int GetMidIndex(int* a, int left, int right)//优化1:三数取中法
{
    int mid = left + ((right - left) >> 1);//取区间中间元素的下标
    if (a[left] > a[mid])//left > mid
    {
        if (a[left] < a[right])//mid < left < right
            return left;
        else//left > mid,left > right
        {
            if (a[right] > a[mid])//mid < right < left
                return right;
            else//right < mid < left
                return mid;
        }
    }
    else//left < mid
    {
        if (mid > right)//left < mid < right
            return mid;
        else//mid < left,mid < right 
        {
            if (left < right)//mid < left < right
                return left;
            else
                return right;
        }
    }
}   
//优化2:小区间优化---当序列元素小于13时,直接采用直接插入排序
void InsertSort(int* a, int left, int right)
{
    int end = 0;
    for (int i = 1; i < right; i++)
    {
        end = i - 1;
        int temp = a[i];
        while (end >= 0)
        {
            if (a[end] > temp)
            {
                a[end + 1] = a[end];
                --end;
            }
            else
                break;
        }
        a[end + 1] = temp;
    }
}

//左右指针法
int PartSort1(int* a, int left, int right)//左右指针法
{
    int mid = GetMidIndex(a, left, right);
    QSwap(&a[mid], &a[right]);

    int key = a[right];//用区间的最右边的值作为基准值
    int keyidx = right;
    while (left <right)
    {
        while (left < right && a[left] <= key)
            ++left;
        while (left < right && a[right] >= key)
            --right;
        if (a[left] != a[right])
            QSwap(&a[left], &a[right]);
    }
    QSwap(&a[left], &a[keyidx]);
    return left;
}
//挖坑法
int PartSort2(int* a, int left, int right)//挖坑法
{
    int mid = GetMidIndex(a, left, right);
    QSwap(&a[mid], &a[right]);

    int key = a[right];//选区间的最右边的值作为基准值
    int blank = right;//坑基准值的下标
    while (left < right)
    {
        while (left < right && a[left] <= key)
            ++left;
        if (left != right)
        {
            QSwap(&a[left], &a[blank]);//用找到的比基准值大的值填坑
            blank = left;
        }

        while (left < right && a[right] >= key)
            --right;
        if (left != right)
        {
            QSwap(&a[right], &a[blank]);//用找到的比基准值小的值填坑
            blank = right;
        }   
    }
        return left;
}
//前后指针法
int PartSort3(int* a, int left, int right)//前后指针法
{
    int cur = left;
    int prev = cur-1;
    int key = a[right];
    while (cur != right)
    {
        if (a[cur] < key)
        {
            ++prev;
            if (cur != prev)
                QSwap(&a[cur],&a[prev]);//让prev指针一直指向比基准值小的数据
        }
        cur++;
    }
    ++prev;
    QSwap(&a[cur], &a[prev]);
    return prev;
}

//递归实现
void QuickSort1(int* a, int left,int right)//递归实现
{
    assert(a);
    if (left < right)
    {
        //int div = PartSort1(a, left, right);
        //int div = PartSort2(a, left, right);
        int div = PartSort3(a, left, right);

        /*if (left-right+1 <= 13) { InsertSort(a, left, right); return; }*/
        QuickSort1(a, left, div-1);
        QuickSort1(a, div+1, right);
    }
}
//非递归实现
void QuickSort2(int* a, int left, int right)//非递归实现
{
    stack<int> s;
    s.push(right);
    s.push(left);
    while (!s.empty())
    {
        int begin = s.top();//区间左边界
        s.pop();
        int end = s.top();//区间右边界
        s.pop();
        int div = PartSort1(a, begin, end);
        if (begin < div - 1)//模拟递归左区间
        {
            s.push(div - 1);
            s.push(begin);
        }
        if (div + 1 < end)//模拟递归右区间
        {
            s.push(end);
            s.push(div + 1);
        }
    }
}

void QPrint(int* a, int len)
{
    for (int i = 0; i < len; i++)
    {
        cout << a[i] << " ";
    }
    cout << endl;
}

void Test1()
{
    int arr1[] = { 2, 4, 5, 8, 1, 3, 0, 6, 7 };
    int sz1 = sizeof(arr1) / sizeof(arr1[0]);
    QuickSort1(arr1, 0, sz1 - 1);
    //QuickSort2(arr1, 0, sz1 - 1);
    QPrint(arr1, sz1);

    int arr2[] = { 9, 1, 2, 3, 4, 5, 6, 7, 8 };//只需要两趟冒泡即可
    int sz2 = sizeof(arr2) / sizeof(arr2[0]);
    //QuickSort1(arr2, 0, sz2 - 1);
    QuickSort2(arr2, 0, sz2 - 1);
    QPrint(arr2, sz2);

    int arr3[] = { 6, 3, 2, 1, 4, 5, 7, 8, 9 };//后半部分无序排序(7 8 9)
    int sz3 = sizeof(arr3) / sizeof(arr3[0]);
    //QuickSort1(arr3, 0, sz3 - 1);
    QuickSort2(arr3, 0, sz3 - 1);
    QPrint(arr3, sz3);

    int arr4[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };//正序找最大值,逆序找最小值
    int sz4 = sizeof(arr4) / sizeof(arr4[0]);
    //QuickSort1(arr4, 0, sz4 - 1);
    QuickSort2(arr4, 0, sz4 - 1);
    QPrint(arr4, sz4);
}

运行结果:
《快速排序的三种实现及两种优化》

点赞