快速排序算法思想及其优化

前言:快速排序算法是一种排序执行效率很高的排序算法,平均时间复杂度为O (nlogn),明显优于其他排序算法。而且各大互联网公司会高频率问到的快速排序,所以有必要好好学习一下。

一、排序算法简介

1、排序算法分类

十种常见排序算法可以分为两大类:

 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

2 、排序算法复杂度

3 、排序算法相关概念

 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

 不稳定排序:选择排序、希尔排序、堆排序、快速排序

 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

如果想要了解十大常见排序算法的算法思想和实现代码的,请参考这篇博客:十大排序算法

二、快速排序算法思想

快速排序(QuickSort)是对冒泡排序的一种改进。 快速排序由C. A. R. Hoare在1960年提出。快速排序是一种排序执行效率很高的排序算法,它利用分治法来对待排序序列进行分治排序。

1、快速排序算法思想

它的基本思想主要是通过一趟排序将待排记录分隔成独立的两部分,其中的一部分比关键字小,后面一部分比关键字大,然后再对这前后的两部分分别采用这种方式进行排序,通过递归的运算最终达到整个序列有序。

2、快速排序算法流程

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

3、快排思路举例

我们从一个待排序数组来逐步说明快速排序的算法思路:

  1. 假设我们对数组{7, 1, 3, 5, 13, 9, 3, 6, 11}进行快速排序。
  2. 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
  3. 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边
  4. 此时得到类似于这种排序的数组{3, 1, 3, 5, 6, 7, 9, 13, 11}。
  5. 在初始状态下7是第一个位置,现在需要把7挪到中间的某个位置k,也即k位置是两边数的分界点。
  6. 那如何做到把小于和大于基准数7的值分别放置于两边呢,我们采用双指针法从数组的两端分别进行比对
  7. 先从最右位置往左开始找直到找到一个小于基准数的值,记录下该值的位置(记作 i)。
  8. 再从最左位置往右找直到找到一个大于基准数的值,记录下该值的位置(记作 j)。
  9. 如果位置i<j,则交换i和j两个位置上的值,然后继续从(j-1)的位置往前(i+1)的位置往后重复上面比对基准数然后交换的步骤。
  10. 如果执行到i==j,表示本次比对已经结束,将最后i的位置的值与基准数做交换,此时基准数就找到了临界点的位置k,位置k两边的数组都比当前位置k上的基准值或都更小或都更大。
  11. 上一次的基准值7已经把数组分为了两半,基准值7算是已归位(找到排序后的位置)
  12. 通过相同的排序思想,分别对7两边的数组进行快速排序,左边对[left, k-1]子数组排序,右边则是[k+1, right]子数组排序
  13. 利用递归算法,对分治后的子数组进行排序。

快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小于基准值的都交换到左边,大于基准值的都交换到右边,这样不会像冒泡一样每次都只交换相邻的两个数,因此比较和交换的此数都变少了,速度自然更高。当然,也有可能出现最坏的情况,就是仍可能相邻的两个数进行交换。

快速排序基于分治思想,它的时间平均复杂度很容易计算得到为O(NlogN)。

  • 空间复杂度:快速排序使用递归,递归使用栈,因此它的空间复杂度为O(logn)
  • 稳定性:快速排序无法保证相等的元素的相对位置不变,因此它是不稳定的排序算法

三、Java实现快速排序算法

方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从找一个小于6的数,再从找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

/**
 * 快速排序
 * @param array
 */
public static void quickSort(int[] array) {
    int len;
    if(array == null
            || (len = array.length) == 0
            || len == 1) {
        return ;
    }
    sort(array, 0, len - 1);
}

/**
 * 快排核心算法,递归实现
 * @param array
 * @param left
 * @param right
 */
public static void sort(int[] array, int left, int right) {
    if(left > right) {
        return;
    }
    // base中存放基准数
    int base = array[left];
    int i = left, j = right;
    while(i != j)
   {
        // 顺序很重要,先从右边开始往左找,直到找到比base值小的数
        while(array[j] >= base && i < j) {
            j--;
        }

        // 再从左往右边找,直到找到比base值大的数
        while(array[i] <= base && i < j) {
            i++;
        }

        // 上面的循环结束表示找到了位置或者(i>=j)了,交换两个数在数组中的位置
        if(i < j) {
            int tmp = array[i];
            array[i] = array[j];
            array[j] = tmp;
        }
    }

    // 将基准数放到中间的位置(基准数归位)
    array[left] = array[i];
    array[i] = base;

    // 递归,继续向基准的左右两边执行和上面同样的操作
    // i的索引处为上面已确定好的基准值的位置,无需再处理
    sort(array, left, i - 1);
    sort(array, i + 1, right);
}

为什么在while循环中,为什么要从右边开始?

总结来说就一句话:因为我们此处设置的基准数在左边,如果从左边开始那么会丢失数据,并且不能正确排序。

问题在于当我们先从左边开始时,那么 i 所停留的那个位置肯定是大于基数6的,如果i=j交换基准数和i交换位置时,那么就不满足快速排序条件,左边有数比基准数大。

如果我们非要从左边开始也行,我们只要设置基准数在右边就可以,也就是从基准数的对面开始

四、快速排序算法优化

快速排序的独特之处在于,其速度取决于选择的基准值。

在快速排序算法中,我们希望对序列进行划分时,能将一个序列进行两等分,但是我们在使用时始终选取第一个元素为基准值,这样就会导致在一些情况(比如是降序排列的序列)下,无法将序列进行等分或是近似等分。如果我们每次选择基准值是一个中间值是不是能尽可能的将待排序的序列做到一个趋近于等分的情况呢?接下来就是由这种思想而提出的优化方法。

1、三点中值法

思想:在左边界p,右边界r和中间下标mid所代表的元素之间选择一个作为基准点,选择方法为找到这三个值中中间那个作为基准点。

  代码如下:

int partition(int a[],int p,int r){
    //选取一个基准点
    int midIndex= p+((r-p)>>1);//中间下标
    int midValue=-1;//中间值下标
    if(a[p]<=a[midIndex]&&a[p]>=a[r]) midValue=p;
    else if(a[r]<=a[midIndex]&&a[r]>=a[p]) midValue=r;
    else midValue=midIndex;
    swap(a[midValue],a[p]);

    int x=a[l];//基准点
    int l=p+1,q=r;
    while(l<=q){
        while(a[l]<=x&&l<=q) ++l;
        while(a[q]>x&&l<=q) --q;
        if(l<=q) swap(a[l],a[q]);
     }
     swap(a[p],a[q]);
     return q;
}

 在这三个点中找中间值作为基准点在某些情况下也并不能代表整个序列的中间值,为了能得到一个真正的中间值,我们在此基础上又提出了进一步求中间值的方法。

2、绝对中值法

思想:将一个数组分成每五个元素一组,若最后一组元素不足五个,也作为一组。然后使用一个数组来存储每个划分中的中间值,得到每个划分的中间值后,取这个数组的中间值,即为所求的中间值。

得到中间值的代码如下:

void insertsort(int a[],int p,int q){//插入排序
    int temp;
    for(int i=p;i<=q;i++){
        if(a[i]<a[i-1]){
            temp=a[i];
            for(int j=i;j>=0;j--){
                if(j>0&&a[j-1]>temp) a[j]=a[j-1];
                else {
                    a[j]=temp;
                    break;
                }
            }
        }
    }
}
int getmid(int a[],int p,int r){
    int length=r-p+1;
    int grouplength=(length%5==0)?(length/5):(length/5+1);//每五个元素一组,得到划分的个数
    int *medians=new int[grouplength];//定义存储中间值的数组
    int midIndex=0;
    for(int i=0;i<grouplength;i++){
        if(i==grouplength-1){//最后一组时 ,需要单独处理,因为这组中元素个数小于5个
            insertsort(a,p+j*5,r);//调用插入排序
            medians[midIndex++]=a[(p+j*5+r)/2];//存入数组
        }else {
            insertsort(a,p+j*5,p+j*5+4);
            medians[midIndex++]=a[(p+j*5+2)];
        }
    }
    insertsort(mediams,0,grouplength-1);
    return medians[grouplength/2];
}

这种方法得到中间值的时间复杂度为O(n),但是通常我们并不使用它,更多的是使用三点中值法。

3、当序列中元素较少时,使用插入排序

思想:虽说插入排序的时间复杂度为O(n2),但是实际上是:n(n-1)/2。快速排序的时间复杂度为O(nlogn),实际上是:n(logn+1)。经过计算后发现当n<=8时,插入排序所消耗的时间是少于快速排序的。

经过优化后的代码是:

quicksort(int a[],int l,int r){//快排递归形式
    if(l<r){
        if((r-l+1)<=8) insertsort(a,l,r);//调用插入排序
        else{
            int q=partition(a,l,r);//找到中间数,不一定是中位数
            quicksort(a,l,q-1);
            quicksort(a,q+1,r);
        }
    }
}

上述就是比较常用的三种优化方法了。

参考链接:

基于Java实现的快速排序

快速排序的优化

快速排序法为什么一定要从右边开始的原因

猜你喜欢

转载自blog.csdn.net/CSDN2497242041/article/details/106927885
今日推荐