图解快速排序的三种方式

4,465 阅读4分钟

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

快速排序介绍

概念

贴一段搜狗百科的解释:

快速排序(Quicksort)是对冒泡排序的一种改进,由东尼·霍尔在1960年提出。
快速排序是指通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序。整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤

  1. 从数列中挑出一个元素,称为“基准”(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition) 操作。
  3. 递归地(recursively)把小于基准值元素的子数列大于基准值元素的子数列排序。

递归到最底部时,数组的长度是1时,也就是已经排序好了。

补充

1、快速排序采取的是分治法的思想,可以看到每次分区操作都是将一个序列分解为两个序列,当序列长度为0或者1时,则此序列是排好序的。

2、快速排序算法是不稳定的,何为不稳定,何为稳定,不稳定指对于在序列中两个相同的元素,在排序后他们的前后顺序发生了变化,而稳定则相反,有些人可能会想,我两个元素都是相同的,谁前谁后不都一样吗?但在实际的开发中,真实情况往往是复杂的,比如:对一组学生元素进行排序,要求先按照学号进行排序,再按照成绩进行排序,如果第二次排序采用的是不稳定的排序算法,将导致成绩相同的学生学号不是有序的。

三种基准分治法

待排序序列:

image.png

挖坑法

1、选取基准,例如选取第一个元素作为基准(把基准挖掉),申明左右指针分别指向数组的头尾

image.png

2、将右指针向左移动,当当前元素小于基准时停下,并将当前元素挖走,填到左指针指向的位置(坑)

image.png

3、将左指针向右移动,当当前元素大于基准时停下,并将当前元素挖走,填到右指针指向的位置(坑)

image.png

4、继续走第2跟第3步,直到左指针跟右指针相等

image.png

5、再将基准填到指针指向的位置

image.png

至此,基准的左边元素都小于等于基准,基准的右边元素都大于等于基准,再递归将左右子数组也按照刚才的步骤处理即可。

温馨提示:代码的调用方式为quick_sort(nums,0,nums.lenght-1);

代码贴贴:

public void quick_sort(int nums[],int l,int r){
    //递归临界条件
    if(l >= r) return;
    //选取数组第一个元素为基准
    int x = nums[l];
    int ll = l,rr = r;
    while(l < r){
        //右指针向左移动,寻找比基准小的元素
        while(r > l && nums[r] >= x){
            r--;
        }
        //将右指针指向的元素挖掉,填到左指针指向的位置
        if(l < r){
            nums[l] = nums[r];
            l++;
        }
        //左指针向右移动,寻找比基准大的元素
        while(r > l && nums[l] < x){
            l++;
        }
        //将左指针指向的元素挖掉,填到右指针指向的位置
        if(l < r){
            nums[r] = nums[l];
            r--;
        }
    }
    //将基准指填到左右指针指向的位置
    nums[l] = x;
    //递归排序左数组
    quick_sort(nums,ll,l-1);
    //递归排序右数组
    quick_sort(nums,l+1,rr);
}

左右指针法

1、选取基准,例如选取第一个元素作为基准,申明左右指针分别指向数组的头尾

image.png

2、将右指针向左移动,当当前元素小于基准时停下

image.png

3、将左指针向右移动,当当前元素大于基准时停下

image.png

4、将两个指针的值进行交换

image.png

5、循环2-4步骤,直到左右指针相遇

image.png

6、将基准值跟指针指向位置的值进行交换

image.png

至此,基准的左边元素都小于等于基准,基准的右边元素都大于等于基准,再递归将左右子数组也按照刚才的步骤处理即可。

代码贴贴:

public void quick_sort(int nums[],int l,int r){
    //递归临界条件
    if(l >= r) return;
    //选取数组第一个元素为基准
    int x = nums[l];
    int ll = l,rr = r;
    while(l < r){
        //右指针向左移动,寻找比基准小的元素
        while(r > l && nums[r] >= x){
            r--;
        }
        //左指针向右移动,寻找比基准大的元素
        while(r > l && nums[l] <= x){
            l++;
        }
        //将左右指针指向元素进行交换
        if(l < r){
            int temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
        }
    }
    //将基准跟指针位置元素进行交换
    nums[ll] = nums[l];
    nums[l] = x;
    //递归排序左数组
    quick_sort(nums,ll,l-1);
    //递归排序右数组
    quick_sort(nums,l+1,rr);
}

前后指针法

1、选取基准,例如选取第一个元素作为基准,申明pre指针(前指针)指向序列开头(index=0),cur指针(后指针)则为pre+1

image.png

2、将cur指针向右移动,直到遇到比基准小的元素

image.png

3、将pre指针向左移动一位(+1),如果pre跟cur不相等,则交换两个指针的元素

image.png

4、继续重复2-3步骤,直到cur指针到序列尾

image.png

5、将pre指针位置的元素跟基准进行交换

image.png

至此,基准的左边元素都小于等于基准,基准的右边元素都大于等于基准,再递归将左右子数组也按照刚才的步骤处理即可。

温馨提示:有些文章的讲解,将最后一个元素作为基准,pre指针为-1,cur指针为0,在cur指针到达末尾时,pre指针先加一,再跟基准进行交换,这种方式也是OK的。

代码贴贴:

void quick_sort(int[] a,int left,int right)
{
    //递归临界条件
    if (left>=right) return;
    //后指针
    int pre = left;
    //前指针
    int cur = left+1;
    //取基准值
    int key = a[left];
    while (cur <= right)
    {
        /*将前指针与基准值进行比较,前指针比基准值小将后指针自增1,前指针比基准值大后指针不变
        当前指针满足条件且不等于后指针时,前后指针交换值*/
        if (a[cur] < key && ++pre != cur)
        {
           //前后指针交换值
            int t = a[cur];
            a[cur] = a[pre];
            a[pre] = t;
        }
        //前指针向后移动
        cur++;

    }
    /*当cur==right时,交换pre与基准位置的值,
    得到两个子区间,使得左子区间值都小于pre位置的值,右子区间的值都大于pre位置的值*/
    a[left] = a[pre];
    a[pre] = key;
    //递归操作
    quick_sort(a,left,pre-1);   //左子区间
    quick_sort(a,pre+1,right);  //右子区间
}

结尾

本文结合文字图片讲解了快速排序的三种方式,分别为:挖坑法,左右指针法,前后指针法,相比于前两种方式,前后指针法的两个指针的方向都向右,对于单链表等仅支持单向移动的数据结构也同样适用,更加灵活。快速排序是不稳定的。快速排序在最优的情况下,时间复杂度为O( nlogn ),最差情况下为O( n^2 ),平均为O( nlogn ),在实际开发中,其优化的空间很大,下一篇文章讲解快速排序的几种优化方式。