快速排序

快速排序思想

快速排序號(hào)稱20世紀(jì)最偉大的十大算法之一,也是nlogn級(jí)別的排序算法,它的思想是類似冒泡排序,是一種交換排序,同時(shí)加入分治法。

1593061994(1).jpg

上圖中我們選取待排序數(shù)組第一個(gè)元素為基準(zhǔn)元素,通過(guò)比較交換,將比基準(zhǔn)元素小的元素放在左邊,比基準(zhǔn)元素大的放在右邊。那么此時(shí)基準(zhǔn)元素(紫色元素),就放在了最終排序后數(shù)組應(yīng)該在的位置。然后通過(guò)同樣的方式,將左邊(綠色)和右邊(橙色)部分排序。過(guò)稱如下:

1593062585(1).jpg

每輪分成3個(gè)步驟:

  1. 選取基準(zhǔn)元素
  2. 基準(zhǔn)元素方法排好序后的位置
  3. 繼續(xù)拆分,直到剩下一個(gè)待排序元素

如何編碼 ?

當(dāng)知道了思想后,下面就要考慮到編碼。不管是算法還是排序中,邊界的定義和指針的指向是最重要的兩個(gè)因素,只有定義好了邊界范圍和指針的含義才能寫(xiě)好算法。

待排序數(shù)組  4 7 6 5 3 2 8 1
算法.png

按照上面的思想,定義一下邊界:

  1. 選取待排序數(shù)組第一個(gè)元素為基準(zhǔn)元素4
  2. 左邊邊界為l(left),右邊界為r(right),形成全閉區(qū)間
  3. j指向左邊<=4區(qū)域的第一個(gè)元素
  4. i執(zhí)行待比較的元素e

執(zhí)行邏輯 單邊循環(huán)法
由上面得知,兩個(gè)區(qū)間的范圍是[l+1...j]<=4和[j+1...r]>4,經(jīng)過(guò)上面一輪比較后,4就在放在排好序的位置,然后分成左右2部分,然后這兩部分再分別繼續(xù)上面的步驟。

代碼如下:

   /**
     * 單邊循環(huán)法
     * @param arr 排序數(shù)組
     * @param l 左邊界 起始0
     * @param r 右邊界 其實(shí)length-1
     */
    public static void quickSort(int[] arr, int l, int r) {
        if (l >= r) {//只有一個(gè)元素 無(wú)需排序
            return;
        }

        //找到 arr[l] 的位置
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);

    }

    /**
     * 
     * @param arr
     * @param l
     * @param r
     * @return
     */
    public static int partition(int[] arr, int l, int r) {
        int v = arr[l];

        //左邊區(qū)間為[l+1...j] 初始時(shí)j=l那么[l+1...l]是個(gè)無(wú)效區(qū)間
        //其實(shí)就是初始化時(shí) 左邊區(qū)間是沒(méi)有元素的
        int j = l;

        for (int i = l + 1; i <= r; i++) {
//        if(arr[i]>v){
//            //上面知道 i++ 但是已經(jīng)在循環(huán)中了 那么就什么都不做
//        }

            if (arr[i] <= v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, l, j);
        return j;
    }

上面的代碼,完全是按照我們分析的步驟寫(xiě)的。理解后在看下開(kāi)始的圖:

1593062585(1).jpg

更能理解這里的思想和在遞歸中都做了什么

雙邊循環(huán)法

上面我們用的是單邊循環(huán)法,也就是i指針從左到右一直移動(dòng)。下面再介紹下雙邊循環(huán)法,也就是從兩邊循環(huán)。

1593070285(1).jpg

雙邊循環(huán),那么也就是需要兩個(gè)指針一起移動(dòng)。

  1. 數(shù)組范圍[satrtIndex...endIndex]
  2. left代表左區(qū)間最后一個(gè)元素[startIndex...left] 并不斷向右移動(dòng)
  3. left代表右區(qū)間最后一個(gè)元素[right...endIndex] 并不斷向左移動(dòng)
  4. 當(dāng) left和right 指向同一個(gè)元素截至

接下來(lái)進(jìn)行第1次循環(huán),從right指針開(kāi)始,讓指針?biāo)赶虻脑睾突鶞?zhǔn)元素做
比較。如果大于pivot ,則指針向左移動(dòng);如果小于等于pivot ,則right指針停
止移動(dòng),切換到left指針。

1593070285(1).jpg

由于1eft開(kāi)始指向的是基準(zhǔn)元素,判斷肯定相等,所以left右移1位。

由于7>4 , 1eft指針在元素7的位置停下。這時(shí),讓left和right指針?biāo)赶虻脑?br> 素進(jìn)行交換。

1593070810(1).jpg

第二次循環(huán)

1593071025(1).jpg

代碼實(shí)現(xiàn)

public static void quickSort(int[] arr, int startIndex, int endIndex) {
        if (startIndex >= endIndex) {
            return;
        }

        int p = partition(arr, startIndex, endIndex);
        quickSort(arr, startIndex, p - 1);
        quickSort(arr, p + 1, endIndex);
    }

    public static int partition(int[] arr, int startIndex, int endIndex) {
        int v = arr[startIndex];
        int left = startIndex;
        int right = endIndex;

        while (left != right) {

            //TODO 這里移動(dòng)是先移動(dòng)右邊 再移動(dòng)左邊 否則當(dāng) left=3 right=5 時(shí)
            //先移動(dòng)左邊 那么left=right的位置在5的位置,那么4和5交換,最后顯然不是有序的
            while (right > left && arr[right] > v) {
                right--;
            }
            while (left < right && arr[left] <= v) {
                left++;
            }

            if (left < right) {
                swap(arr, left, right);
            }


        }
        swap(arr, startIndex, left);
        return left;

    }

注意點(diǎn)

在TODO位置處這里移動(dòng)是先移動(dòng)右邊 再移動(dòng)左邊 否則當(dāng) left=3 right=5 時(shí),先移動(dòng)左邊 那么left=right的位置在5的位置,那么4和5交換,最后顯然不是有序的

復(fù)雜度分析
上圖中也知道8個(gè)元素遞歸3輪,4個(gè)元素2輪,2個(gè)元素1輪,正好是log28=3,log24=2,log2^2=1,每輪排序O(n) ,所以是nlogn

優(yōu)化

1. 數(shù)據(jù)小的時(shí)候用插入排序,這個(gè)優(yōu)化和歸并一樣
2. 隨機(jī)選取基準(zhǔn)元素的選擇
1593072531(1).jpg

如上圖,當(dāng)數(shù)組像上面時(shí),每次選的基準(zhǔn)值要么最大,要么最小,就無(wú)法起到分治的效果,從而退化成O(n^2),隨意可以隨機(jī)原則數(shù)組中的數(shù)值,然后與arr[0]交換,再在排序

3. 雙路排序
4. 三路排序

雙路排序

雙路排序思想,當(dāng)數(shù)組相同元素比較多的時(shí)候也會(huì)退化成O(n^2),如給定一個(gè)數(shù)組如下:

算法.png

左邊相同元素非常多。導(dǎo)致分治不平均。最后算法退化O(n^2)
雙路排序就是將拆分的2個(gè)數(shù)組分配到兩端,左邊遍歷的元素為ei,右邊遍歷元素為ej,當(dāng)ei>4=和ej<=4時(shí)交換。那么此時(shí)相同的元素就會(huì)分到2端,而不會(huì)只在一端,退化成O(n^2)

代碼實(shí)現(xiàn)

 //對(duì)arr[l...r]進(jìn)行快速排序
    private static void quickSort(Comparable[] arr, int l, int r) {

//        if (l >= r) {//當(dāng)l=r的時(shí)候說(shuō)明只有一個(gè)元素  那么他就是有序的
//            return;
//        }
        if (r - l <= 15) {
            Main5.sort(arr, l, r);
            return;
        }

        /**
         * TODO 優(yōu)化1 : 上面邊界的優(yōu)化  對(duì)于小規(guī)模數(shù)組, 使用插入排序
         *
         *
         *  if( r - l <= 15 ){
         *      InsertionSort.sort(arr, l, r);
         *      return;
         *  }
         *
         *
         */

        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);


    }

    /**
     * 這個(gè)的特點(diǎn)就是  當(dāng)出現(xiàn)了值相等的情況 我們不具體像第一版代碼 給他劃分到某一端  而是取分布在兩端了
     *
     * @param arr
     * @param l
     * @param r
     * @return
     */
    // 雙路快速排序的partition
    // 返回p, 使得arr[l...p-1] <= arr[p] ; arr[p+1...r] >= arr[p]
    // 雙路快排處理的元素正好等于arr[p]的時(shí)候要注意,詳見(jiàn)下面的注釋:)
    private static int partition(Comparable[] arr, int l, int r) {


        // TODO 最重要的優(yōu)化:  隨機(jī)在arr[l...r]的范圍中, 選擇一個(gè)數(shù)值作為標(biāo)定點(diǎn)pivot
        // 這樣我們和l交換 這樣退化成一個(gè)鏈表 也就是O(n*n)的概率是非常低的
        swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);

        //我們找到這個(gè)數(shù)組中第一個(gè)位置 也就是l的位置 為基準(zhǔn)數(shù)
        Comparable v = arr[l];

        // TODO arr[l+1...i) <= v; arr(j...r] >= v
        int i = l + 1;
        int j = r;

        while (true) {
            // 注意這里的邊界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
            // 思考一下為什么?
            //TODO 因?yàn)?如果相等 就違背了我們這么設(shè)計(jì)的初衷
            // 如果我們存在很多相同的元素 原來(lái)的排序就會(huì)導(dǎo)致 相同的元素永遠(yuǎn)在一側(cè) ,如果這里加上等于號(hào)
            // [1,1,1,1,1,1,2] 按照之前的邏輯 v我們?nèi)?  那么就會(huì)導(dǎo)致 一側(cè)是全數(shù)據(jù)
            // 另一側(cè)只有2  如過(guò)不加等于 那么 根據(jù)我們的邏輯會(huì)分成[111] [1112] 分布均勻


            while (i <= r && arr[i].compareTo(v) < 0) {
                i++;
            }
            
            while (j >= l + 1 && arr[j].compareTo(v) > 0) {
                j--;
            }
            // 對(duì)于上面的兩個(gè)邊界的設(shè)定
            // 注意這里的邊界, j >= l + 1和i <= r, 不能是j > l + 1和i < r
            // TODO 因?yàn)?這里的區(qū)間是arr[l+1...i) <= v; arr(j...r] >= v i和j是指向?qū)⒁判虻脑?            //      所以i<r時(shí) i還沒(méi)有比較
            // 大家可以參考: http://m.itdecent.cn/p/e0364a3166f9
            if (i > j) {
                break;
            }

            swap(arr, i, j);
            i++;
            j--;
        }

        swap(arr, l, j);
        return j;
    }
    

三路排序

明白了雙路排序,再來(lái)看三路排序,其本質(zhì)的事項(xiàng)是一樣的.

圖片.png

將數(shù)組分成3部分,并按照?qǐng)D中標(biāo)示定義邊界位置

  • lt<v 右邊界
  • i待查看的元素
  • gt >v 左邊界

e==v

圖片.png

直接i++

e<v

圖片.png
  1. arr[i]與arr[lt+1]交換
  2. lt++
  3. i++

e>v

圖片.png
  1. arr[i]與arr[gt+1]交換
  2. gt--

==注意此時(shí)i是不用變的因?yàn)間t+1的位置也是未排序的,交換后i位置還是待排序元素,所以i不變==

最后排序完成后的樣子

圖片.png

代碼實(shí)現(xiàn)
private static void quickSort(int[] arr, int l, int r) {
        if (l >= r)
            return;

        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }


    public static int partition(int[] arr, int l, int r) {
        int v = arr[l];
        //[l+1...i) (j...r]
        int i = l + 1, j = r;

        while (true) {
            while (i <= r && arr[i] < v) {
                i++;
            }

            while (j >= l + 1 && arr[j] > v) {
                j--;
            }
            if (i > j) {
                break;
            }

            Main.swap(arr, j, i);
            j--;
            i++;
        }

        Main.swap(arr, l, j);

        return j;
    }

思考

隨機(jī)數(shù)組第K大的元素

快速排序 經(jīng)過(guò)隨機(jī)選擇,當(dāng)partition后返回P
當(dāng)arr[k]>arr[p] 那么在右側(cè),否則在左側(cè),
那么每次查找就減少一半 
最終通過(guò)O(n)完成查找

O(n)=n/2+n/4+n/...=2n
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容