经典的排序算法--快速排序

快排的核心思想:确定基准,然后按照基准进行分割。递归

0.快速排序的框架

快速排序(升序)的思想就是先确定一个基准,然后通过partition函数去让数组保持小于等于和大于分别置于数组两端。然后通过递归不断对数组进行划分,最终达到有序。
例如:ABCDEFGHIJK;

  1. 如果此序列的基准为F,那么我们最终通过一次partition要达到的效果就是:ABCDE都小于等于F,GHIJK都大于F。ABCDE和GHIJK不需要保证相互有序。
  2. 紧接着,我们分别对ABCDE和GHIJK进行partition过程。递归对其分割子序列进行partition。
  3. 在递归进行回退时,所有的子序列已经有序,所以最终的序列形成了整体有序。

总的来说,快速排序是一种分治思想。

到这,很多人大体应该明白快排的基本思路了,但是一个疑问来了:如果找到基准,而且partition过程是怎样的?

partition过程是快速排序中的关键点,它有多种变体实现:

  • Hoare版本
  • Lomuto版本(算法导论版)
  • 等等…
public class QuickSort {

    public static void main(String[] args) {
        QuickSort qs = new QuickSort();
        int[] s = new int[] { 3, 1, 5, 7, 4, 3, 2, 1, 6 };
        qs.quickSort(s, 0, s.length-1);
        System.out.println(Arrays.toString(s));
    }

    public void sort(int[] s, int low, int high) {
        if (low < high) {
            int mid = partition2(s, low, high);
            quickSort(s, low, mid - 1);
            quickSort(s, mid + 1, high);
        }
    }

    public void swap(int[] s, int a, int b) {
        int tmp = s[a];
        s[a] = s[b];
        s[b] = tmp;
    }

    public int partition(int[] s, int low, int high) {
        //具体实现版本见下方
    }
}

1.Hoare版本快速排序

Hoare版本的partition过程中可能最困惑你的是:为什么s[i] = s[j];和s[j] = s[i];可以直接赋值,那岂不是把值给覆盖了?NO,完全不会的。仔细分析你会发现,被覆盖的数其实都是有备份的,这个备份的初始动力是来源于对基准s[i]的备份,所以在每次覆盖前,我们都能找到被覆盖数的备份。此处要正确理解备份的含义。

    public int partition(int[] s, int low, int high) {
        int i = low, j = high;//定义两个指针,j指针指向从尾到首遍历中大于基准t的值,i指针指向从首到尾遍历中小于等于基准t的值
        int t = s[i];//基准定义为t,实际选取了low位置上的值为基准
        while (j > i) {
            //从尾部到首部遍历,发现小于等于基准t的值则进行对s[i]的覆盖
            while (s[j] > t && j > i)
                j--;
            s[i] = s[j];
            //从首部到尾部遍历,发现大于基准t的值则进行对s[j]的覆盖
            while (s[i] <= t && j > i)
                i++;
            s[j] = s[i];
        }
        s[j] = t;//此时i等于j,将基准覆盖该值
        return j;
    }

2.Lomuto版本快速排序(算法导论版)

Lomuto版本的partition过程则没有了覆盖,取而代之的是swap互相替换。理解Lomuto版本的partition过程最重要就是要搞懂i和j的作用。

    public int partition(int[] s, int low, int high) {
        int i = low;//指向小于等于基准t的‘当前j范围’内的最高位,比较拗口,细细体会
        int t = s[low];
        for (int j = i + 1; j <= high; j++) {//j作用就是遍历指针
            if (s[j] <= t) {//遇到小于等于基准t的数,与i+1位的值进行交换,实质是与大于基准t的最低位的数进行交换
                i++;
                swap(s, i, j);
            }
        }
        swap(s, low, i);//交换基准与小于等于基准的最高位
        return i;
    }

3.基准问题

到这,你会发现一个问题,也是我们一开始提到的问题之一,基准的选择,我们可以看到在上述的两种partition过程中,基准都是选取的数组最低位值。然而,数组可能会出现一种可怕的情况,对于升序,我们可能每次都选取到数组中的最大值,结果就是我们的快速排序退化了,退化为插入排序了。
那么我们怎样才能避免这种情况的发生呢?下面我们列举几种解决方法。

  • 随机基准(每次基准的选择都是随机产生的,不进行人为指定)
  • 三数取中法(选取数组首尾和中间位的值,找其中的中间值作为基准,较为靠谱,不会严重退化)

有了思路,解决起来就容易多了,只需要改变partition中基准的选择即可。我们下面给出三数取中的函数,So easy…

    //三数取中基准选择函数
    public static void getMid(int[] s, int low, int high) {
        int mid = (low+ high) / 2;
        //三数排序
        if (s[low] > s[mid]) {
            swap(s, low, mid);
        }
        if (s[low] > s[high]) {
            swap(s, low, high);
        }
        if (s[high] < s[mid]) {
            swap(s, high, mid);
        }
        //基准和首位值进行交换
        swap(s, low, mid);
    }

参考:

猜你喜欢

转载自blog.csdn.net/tb3039450/article/details/66973684
今日推荐