快速排序的三种方式以及快排的优化

一.快速排序的基本思想

关于快速排序,它的基本思想就是选取一个基准,一趟排序确定两个区间,一个区间全部比基准值小,另一个区间全部比基准值大,接着再选取一个基准值来进行排序,以此类推,最后得到一个有序的数列。

二.快速排序的步骤

  • 1.选取基准值,通过不同的方式挑选出基准值。
  • 2.用分治的思想进行分割,通过该基准值在序列中的位置,将序列分成两个区间,在准值左边的区间里的数都比基准值小(默认以升序排序),在基准值右边的区间里的数都比基准值大。
  • 3.递归调用快速排序的函数对两个区间再进行上两步操作,直到调用的区间为空或是只有一个数。

三.关于选取基准值的方式

1.固定位置选取基准值
基本思想:选取第一个或最后一个元素作为基准值。
这里写图片描述
如上是以第一个数作为选取基准的方式的第一趟排序的结果,接着就是对分好的两个区间再进行递归的快排。

但是,这种选取基准值的方法在整个数列已经趋于有序的情况下,效率很低。比如有序序列(0,1,2,3,4,5,6,7,8,9),当我们选取0为基准值的时候,需要将后面的元素每个都交换一遍,效率很低。所以这种以固定位置选取基准值的方式,只适用于该序列本身并不是趋于有序的情况下,比如一串随机数列,此时的效率还能够差强人意。

为了避免这种已经有序的情况,于是有了下面两种选取基准值的方式

下面是关于选取固定基准值快排的代码

int SelectPivot(int* a, int left, int right)//选取基准值函数
{
    return a[left];
}

void QuickSort(int* a, int left, int right)
{
    assert(a);
    int i, j;
    int pivot = SelectPivot(a, left, right);//确定基准值 
    if (left < right)
    {
        i = left + 1;//以第一个数left作为基准数,从left+1开始作比较
        j = right;

        while (i < j)
        {
            if (a[i] > pivot)//如果比较的数比基准数大
            {
                swap(a[i], a[j]);//把该比较数放到数组尾部,并让j--,比较过的数就不再比较了
                j--;
            }
            else
            {
                i++;//如果比较的数比基准数小,则让i++,让下一个比较数进行比较
            }
        }

        //跳出while循环后,i==j
        //此时数组被分成两个部分,a[left+1]-a[i-1]都是小于a[left],a[i+1]-a[right]都是大于a[left]
        //将a[i]与a[left]比较,确定a[i]的位置在哪
        //再对两个分割好的部分进行排序,以此类推,直到i==j不满足条件

        if (a[i] >= a[left]) //这里必须要用>=,否则相同时会出现错误
        {
            i--;
        }

        swap(a[i], a[left]);

        QuickSort(a, left, i);
        QuickSort(a, j, right);
    }
}

//测试函数
int main()
{
    int a[] = { 2,5,4,9,3,6,8,7,1,0};
    const size_t n = sizeof(a) / sizeof(a[0]);
    QuickSort(a, 0, n - 1);
    Print(a, n);
    system("pause");
    return 0;
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

2.随机选取基准
基本思想:选取待排序列中任意一个数作为基准值。
因为快排函数部分的代码是一样的,只是选取基准值部分的函数不相同,下面只附上选取基准值函数的代码

int SelectPivot(int* a, int left, int right)//选取基准值函数
{
    srand((unsigned)time(NULL));
    int pivotPos;
    if (left < right)//这里需要保证传进来的left必须小于left
    {
        pivotPos = rand() % (right - left) + left;
    }
    else
    {
        pivotPos = left;//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
    }

    swap(a[pivotPos], a[left]);
    return a[left];
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

引入随机化快速排序的作用,就是当该序列趋于有序时,能够让效率提高,大量的测试结果证明,该方法确实能够提高效率。但在整个序列数全部相等的时候,随机快排的效率依然很低,它的时间复杂度为O(N^2),但出现这种最坏情况的概率非常的低,所以它还是一种效率比较好的方法,一般情况下都能够达到O(N*lgN)。

3.三数取中法选取基准值
基本思想:取第一个数,最后一个数,第(N/2)个数即中间数,三个数中数值中间的那个数作为基准值。举个例子,对于int a[] = { 2,5,4,9,3,6,8,7,1,0};,‘2’、‘3’、‘0’,分别是第一个数,第(N/2)个是数以及最后一个数,三个数中3最大,0最小,2在中间,所以取2为基准值。

下面也是附上三数取中法的代码

int SelectPivot(int* a, int left, int right)//选取基准值函数
{
    int mid;
    if (left < right)
    {
        mid = (right - left) / 2;
    }
    else
    {
        return a[left];//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
    }

    if (a[mid] > a[right])
    {
        swap(a[mid], a[right]);
    }
    if (a[left] > a[right])
    {
        swap(a[left], a[right]);
    }
    if (a[mid] > a[left])
    {
        swap(a[mid], a[left]);
    }
    //上面三步完成之后,a[left]就是三个数中最小的那个数
    return a[left];
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

采用三数取中法很好的解决了很多特殊的问题,但对于很多重复的序列,效果依然不好。于是在这三种选取基准值的方法下,另外地还有三种优化方法。

四.快速排序的优化

优化一:当待排序序列的长度分割到一定大小后,使用插入排序。
优化原因:对于待排序的序列长度很小或是基本趋于有序时,快排的效率还是插排好。

自定义截止范围:序列长度N=10。当待排序的序列长度被分割到10时,采用快排而不是插排。

if (n <= 10)//当整个序列的大小n<=10时,采用插排
{
    InsertSort(a, n);
}
   
   
  • 1
  • 2
  • 3
  • 4

优化二:在一次排序后,可以将与基准值相等的数放在一起,在下次分割时可以不考虑这些数。
这里写图片描述

因为这一次改动的代码比较多,所以再继续把所有的代码全部拿出来看一下

int SelectPivot(int* a, int left, int right)//选取基准值函数
{
    int mid;
    if (left < right)
    {
        mid = (right - left) / 2;
    }
    else
    {
        return a[left];//在递归调用里走到这一步,肯定是left=right,直接让pivotPos=left
    }

    if (a[mid] > a[right])
    {
        swap(a[mid], a[right]);
    }
    if (a[left] > a[right])
    {
        swap(a[left], a[right]);
    }
    if (a[mid] > a[left])
    {
        swap(a[mid], a[left]);
    }
    //上面三步完成之后,a[left]就是三个数中最小的那个数
    return a[left];
}

void QuickSort(int* a, int left, int right)
{
    assert(a);
    if (n <= 10)//当整个序列的大小n<=10时,采用插排
    {
        InsertSort(a, n);
        return;
    }
    int i, j;
    int pivot = SelectPivot(a, left, right);//确定基准值 
    if (left < right)
    {
        i = left + 1;//以第一个数left作为基准数,从left+1开始作比较
        j = right;

        while (i < j)
        {
            if (a[i] == pivot)//处理与基准值相等的数,都放到数组末尾
            {
                swap(a[i], a[j]);
                --j;
            }
            else if (a[i] > pivot)//如果比较的数比基准数大
            {
                while (1)
                {
                    if (a[j] == pivot)//如果要换的数值等于基准值,让j--,与前一个交换
                    {
                        --j;
                    }
                    else
                    {
                        break;
                    }
                }
                swap(a[i], a[j]);//把该比较数放到数组尾部,并让j--,比较过的数就不再比较了
                --j;
            }
            else
            {
                ++i;//如果比较的数比基准数小,则让i++,让下一个比较数进行比较
            }
        }

        //跳出while循环后,i==j
        //此时数组被分成两个部分,a[left+1]-a[i-1]都是小于a[left],a[i+1]-a[right]都是大于a[left]
        //将a[i]与a[left]比较,确定a[i]的位置在哪
        //再对两个分割好的部分进行排序,以此类推,直到i==j不满足条件

        if (a[i] >= a[left]) //这里必须要用>=,否则相同时会出现错误
        {
            i--;
        }


        swap(a[i], a[left]);
        int tmp = right;//用tmp表示从后往前第一个不是基准值的数
        while (tmp > i)
        {
            if (a[tmp] == pivot)
            {
                --tmp;
            }
            else//else表示没有与基准值重复的值
            {
                QuickSort(a, left, i - 1);
                QuickSort(a, i + 1, right);
                return;
            }
        }
        int pos = tmp;//因为后面要用到tmp,所以运算的话用一个pos来代替tmp进行运算
        int r = right;//因为后面要保证right还是先前的右边界,所以运算的话用另外一个变量来表示
        int count = 0;
        while (pos > i&&r > tmp)
        {
            swap(a[pos], a[r]);
            --r;
            --pos;
            ++count;//换了一次让count++
        }

        QuickSort(a, left, i-1);//对左区间快排,i-1是左区间的最后一个数
        QuickSort(a, right-count+1, right);//对右区间快排,right-count+1是右区间的第一个数
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

这种聚集与基准值相等的值的优化方法,在解决数据冗余的情况下非常有用,提高的效率也是非常多。

优化三:优化递归操作
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化

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

只是在尾部的递归调用的时候做了以下改变

while (left < right)
{
    QuickSort(a, left, i - 1);//对左区间快排,i-1是左区间的最后一个数
    left = right - count + 1;
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

其实这种优化编译器会自己优化,相比不使用优化的方法,时间几乎没有减少。

所以到这里,总结一下,对于快速排序(一组随机数组),效率最快的优化方案应该是三数取中法+插排+聚集相等元素,对于尾递归可以有也可以没有,对于效率的变化改变不大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值