1. 程式人生 > >快速排序--QuickSort,看完五分彩開獎網平臺搭建自己就能寫出來的快排思路推演

快速排序--QuickSort,看完五分彩開獎網平臺搭建自己就能寫出來的快排思路推演

遞歸 urn 歸並 輔助 dia pre 自己 wap 要花

快速五分彩開獎網平臺搭建論壇:haozbbs.com Q1446595067排序(QuickSort)介紹
首先發明者竟然敢給自己發明的算法叫做QuickSort,這個名字閃不閃亮?好比別的武功叫做六脈神劍、降龍十八掌,我這個叫做“天下無敵神功”。別的排序算法都是按照特點來起的,你這個不是應該叫分塊遞歸排序法嗎?或者和希爾一樣,叫做霍爾排序也可以啊,這麽高調是要幹啥啊?我給了他一次機會,特意去查了一下,這個名字並不是江湖朋友擡愛給的,就是發明者自己起的,社會社會。。。不過看完這篇博客,理解了快排的思想,你會發現這個名字名副其實。

思路推演:
思路是,假設一個數組,我們可以用一種辦法分成小數塊和大數塊,然後遞歸繼續分成小數塊和大數塊,最後每一塊都只有1個(或者0個)的時候,排序就完成了

為什麽快速排序是冒泡排序的改進版?
這是個結論,或者說是事實,但是不一定是算法發明者的初衷,發明者的思路,應該是先發現了能將數組分成大小兩塊的方法,然後延伸到可以通過遞歸拆分成最多一個的數據塊,進而達到排序的效果,這個我們在最後還會再說。
然而最終的模式變成,我們把大的數放後面,小的數放到前面(這個就是冒泡的套路,只不過冒泡是一個一個的,這是一次一塊的),然後不停的拆(分治),大的放後面,小的放前面,最終到每塊最多1個元素的時候,排序完成。
所以是冒泡排序的改進版,但是並不是看到冒泡而發明的,只是結果上成為了冒泡排序的改進版,不像插入排序和希爾排序,後者是看著前者而發明的。

理想情況是每次都能取到一組數的中位數作為基準數,這樣每次拆分都是平均的,這也是快排能達到O(nlogn)的情況。但是中位數的前提是已經排序,這樣就矛盾了。

既然數組是亂序的,基準數從哪裏取都一樣(其實不一樣,我們姑且先認為一樣),取0位置的數作為基準數,嘗試將數組變成左邊比這個數大,右邊比這個數小。

怎麽分塊
怎麽將一個數,放到合適的位置,讓左邊的都比它小,右邊的都比它大

思路1:
用low的方法怎麽實現,比如使用插入排序來實現:
假設四個數3 4 2 1 ,以3為基準數,我們可以這樣,找到小於3的數,放到第一位,然後把後面的都往後移動一位,變成2 3 4 1,然後找到1,放到第二位,後面移動一位,直到找不到比3更小的。。。是不是很low,這樣查找移動要消耗O(n^2)·····然後拆分需要花費O(logn)~O(n),那就是O(n^2*logn)到O(n^3),簡直瘋了

3 4 2 1
2 3 4 1
2 1 3 4

思路2:
再換個思路,用歸並排序的思路,假設我們可以在另一個相同長度的新數組newArr中安放元素,假設我們選擇了第0位為base,base=arr[0],為了讓數組變成base和左右兩塊,我們可以:
1、設置left = 0;
2、設置right = n - 1;
3、從1到最後遍歷數組 i,小於base從前面依次排放,newArr[left] = arr[i],left++;
大於base的從後面依次排放,newArr[right] = arr[i],right–;
4、遍歷完成後, 新數組肯定還空著一個坑,直接newArr[i] = base; 這樣就可以了
5、但是,如果我們想在排序中使用,還需要將新數組復制到原始數組,這也不失為一個辦法,並且還是穩定的算法,但是需要消耗O(n)的空間。
6、如果不復制,怎麽辦,快排是一種原地排序算法,in-place

真正的快排思路1、兩頭交換法:
網上的快排大體有兩種,什麽挖坑填坑法和兩頭交換法,我們先講的是兩頭交換法,因為這個思路比較直接,兩頭交換實現之後再去看挖坑填坑法。看到後面會發現這兩種叫法都是有問題的
網上有個坐在馬桶上看算法系列,講到快排的時候,說一定要先從右往左找。
在我看來其實先從左先從右是沒區別的,並且左邊更順,我們先嘗試推演完再說。
有點難,,,這是我最開始的推演思路,後面會簡化這個推演
1、我們先設置個很一般的情況,假設只有三個數B C A,基數是B,這兩個CA將來一定會變成AC,並且B一定會出現在A和C之間。
2、假設C之前只有小於B的數,A之後只有大於B的數,如:BAAAAAC XXXX ADEFG,這個結論也正確(我們先都不考慮等於的情況,等於的情況,其實只要是互斥的就可以,也就是如果從左邊找大等於的,從右邊就找小於的,反之亦然)。
3、繼續放大這個結論
從前面找到的第一個比目標大的數下標i,從後面找到的第一個比目標小的數下標j,如果這時數組沒相交,i < j,將來目標一定出現在這兩個數之間,一定可以交換使得數據趨於排序的最終結果;
如果在尋找的過程中i >= j,這個時候尋找就應該退出,這個時候不應該再出現CA交換的情況,分開討論:
如果i >= j,有兩種可能:
從左邊一直沒找到比B大的數,循環到endIndex然後自然退出,這個時候同樣B應該和i位置的互換,返回i,分塊完畢
從左邊找到第一個比B大的數之後,從右邊沒有找到比B小的數,,這個時候特征是i位置之後的數都比B大。i位置之前的數字都比B小。
因為i是第一個大數,所以i之前的數都比B小
從後面沒有找到更小數,說明i之後的數都比B大
B應該在i - 1的位置,返回i - 1,分塊完畢
也就是一共有三種情況:
a)找到了可交換的數,交換並繼續遞歸找
b)沒找到可交換的數,1是從左邊沒找到,說明B是最大的,返回endIndex;
c)2是後面的數以i為分界點,後面的比B大,前面的比B小,這時候應該返回i - 1。

上面的推演有點粗略,我們一邊總結一邊限定,並考慮邊界問題:
1)如何確定base的位置?
設置base下標為startIndex
設置i為開始位置startIndex+1
設置j為結束位置endIndex
// 從左邊循環到j,嘗試找比B大的數
從i開始往後找到第一個大於B的數,break,記錄下標為新的i
如果沒找到下標i會到達endIndex,為了區分i是break還是循環到最後退出,我們將i++寫在break後面,假設沒找到,i = endIndex + 1;
如果i > endIndex,沒找到說明B是最大的數,B應該在endIndex的位置,return endIndex值,本次分塊結束,進入下一次分區。
// 從右邊循環到i+1,嘗試找比B小的數
從j開始往前,直到i+1,嘗試找一個小於B的數。
如果找到,break,記錄新的下標j(遞歸,從i+1到j-1的位置繼續找,遞歸當前這一兩個循環)
如果沒找到j會停留在i+1的位置,為了區分j是因為找到了break,還是自然結束循環,將j–放到break後面。如果j==i一樣說明沒有找到

判斷如果i < j說明找到了可以互換的CA,將i位置元素和j位置元素互換,並取i+1和j - 1進入下一次尋找。
否則(其實只有i == j的情況),返回 i - 1,進入下一次分區。

寫代碼前還有兩個問題需要考慮
2)找到base的位置之後怎麽辦?
如果找到了base的位置,因為base本身已經是中間的數了,所以base不用再參與到左右兩邊的再次排序,那麽下一次遞歸的分區邊界為startIndex~(base -1)、 (base + 1) ~ endIndex。
這裏可以引出兩點:
a)每次快排分成左右兩塊的時候,整體扣掉了一個中間元素不需要再次參與排序,最好情況是不是比logn要小啊
b)可以引出快排的一種優化思路,假設有許多和基準數相同的數,我們應該找到這部分數的起止位置,假設判斷在B之後用的是大等於,那麽從B開始,找到那個位置。分塊的時候和B相同的不再參與分塊,這樣需要排序的個數又少了很多。
3)排序的遞歸什麽時候退出?
左右兩邊最多只有一個數的時候,這個時候startIndex <= endIndex,遞歸退出,也就是if(startIndex <= endIdex) return;
為什麽說至多只有一個數,有一個數的時候,startIndex == endIndex不難理解
有0個數的情況,B是邊界值,在startIndex或者endIndex的位置,下一次遞歸就是有一邊就是空的了,體現在代碼上下次邊界為本次的startIndex ~ startIndex - 1,或者endIndex + 1 ~ endIndex,這個時候就是startIndex = endIndex + 1,所以我們用startIndex <= endIndex來退出遞歸。

算法復雜度
每一層比較交換的過程會最終會把所有的元素點個名比較一次,從左到右,最終一定會相交,所以是O(n)。即使遞歸拆分成n個塊之後,合起來仍然是O(n)。
一共會交換多少層,取決於base實際在排序後的元素中的位置,這個就好像一個憑運氣的二分拆分,假設運氣爆棚,每次都能取到中位數,那麽需要比較logn次。假設運氣賊爛,每次都取到一頭的數,使得每次拆分都是一顆完全的歪脖子樹,也就是一條直線,就是n次。所以快速排序的復雜度介於O(nlogn)~O(n^2)之間

為了盡量縮小隨機的因素,各種改進版,,,也就是如何取base
1、最初是我們上面的例子。取0位作為base
2、三數取中(midian-of-three)假設數組已經一定程度有序,並且我們知道它一定程度有序,這個時候取base,也就是最左邊,將會是概率上最差的選擇。這個時候有從startIndex,endIndex,midIndex三個值中取中值的做法,這是一種極限思想,假設完全有序,那麽直接取中值是不是很安逸,具體涉及到數學思想,我沒水平說的太深
3、取隨機數,我個人覺得這個有點扯淡,在不知道順序情況下,0和隨機位置沒區別。在知道有一定順序的情況下,用三數取中。隨機怎麽看都不具有優勢。

穩定性:
不穩定的,跳躍的比較都不是穩定的,最簡單的例子,假設B C C A,在以B為基數進行比較的時候,A會和第一個C交換,這樣直接沒問中間有沒有和C等於的數據,所以直接打亂了順序

Java代碼實現:
快排的寫法特別容易出錯,可能在數據少且沒特性的時候,現象上是對的,其實是錯的。所以我弄了很長的數組,還會調整成一些特殊情況來驗證。

public static void main(String[] args) {
int[] arr = new int[]{14, 23, 1, 25,36,11, 9, 2, 1, 5, 14,1};

quickSort(arr);

System.out.println(Arrays.toString(arr));

}

public static void quickSort(int[] arr) {
doQuickSort(arr, 0, arr.length - 1);
}

public static void doQuickSort(int[] arr, int startIndex, int endIndex) {
// startIndex大等於endIndex的時候,遞歸退出
if (startIndex >= endIndex) {
return;
}
// 取第一個位置的元素作為基準元素
int base = arr[startIndex];
// 獲取中軸
int pivot = partition(arr, base, startIndex + 1, endIndex);
// 將startIndex和pivot互換,B應該和最後的元素互換
// 可以判斷是否等於,來決定是否交換
if(pivot != startIndex) {
swap(arr, startIndex, pivot);
}

// 根據中軸分成兩塊,遞歸排序,註意不需要再包括pivot
doQuickSort(arr, startIndex, pivot - 1);
doQuickSort(arr, pivot + 1, endIndex);

}

/**

  • 一邊交換,一邊找中軸
  • @param arr
  • @param startIndex
  • @param endIndex
  • @return
    */
    private static int partition(int[] arr, int base, int startIndex, int endIndex) {
    // 取i是startIndex+1
    int i = startIndex;
    // 取j是endIndex
    int j = endIndex;

    // 從左邊開始找到第一個大於base的數
    // 這裏註意控制一下大於小於是否包含等於,這裏其實隨便定義就好,只要左右互斥
    // 假設大於等於base認為是大於,那麽另一個方向就是小於,反之亦然
    // 如果找不到i推進到最後
    // 如果找到i停在找到的位置
    for (; i <= j;) {
    if (arr[i] > base) {
    break;
    }
    i++;
    }

    // 如果沒找到比B大的元素,說明B是最大的
    if(i == endIndex + 1){
    return endIndex;
    }

    // 從右邊,到i+1截止,嘗試找到第一個小於base的數
    // 如果程序提前退出,那麽i < j,否則 i == j
    for (; j >= i+1; ) {
    if (arr[j] <= base) {
    break;
    }
    j--;
    }

    // 如果找到了兩個,也就是能交換,則繼續遞歸尋找中軸
    if (i < j) {
    swap(arr, i, j);
    // 從i+1,到j-1繼續交換
    return partition(arr, base, i + 1, j - 1);
    } else {
    // 如果沒找到j,說明i之後的數(包括i)都大於B,B應該在i-1的位置
    return i - 1;
    }
    }

/**

  • 交換元素
  • @param arr
  • @param i
  • @param j
    */
    private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }
    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
    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
    另一種標準寫法的推演,所謂的挖坑填坑法:
    說實話我不太喜歡這種寫法,感覺不夠直接,後來想了很久,突然理解了,包括上面的推演都可以簡化成另一種思路。。。
    1、仍然設置最簡單的情況,B C A(這是最簡答的情況,C A可以看成兩個集合,思路相同),如何變得有序,我們之前的交換方法是直接交換C A,在不考慮B的情況下將數據分成大小兩塊,這兩塊其實是相鄰的,中間並沒有B的位置,B最後放到中間,是靠A中的最後一位元素和B進行交換得到的。
    把上面的過程抽象話,其實就是有一組亂序B C A,將來會變成A B C,為了變成A B C,我們如何交換的問題
    上面的做法是先交換C A,再交換B A,也就是先將最大值放到最後,再將中間值放到中間(也可以描述為先將最大值放到最後,再將最小值放到最前)
    這樣其實最終才需要交換B
    2、其實還有一種做法,先交換B A,再交換B C,先將最小值放到最前,再將中間值放到中間(相對的,或者描述為先將最小值放到最前,再將最大值放到最後。
    這樣每一次找到後都需要交換B,其實滿蛋疼的
    因為B一定在最前面,所以交換只有這兩種可能
    所以所謂的兩頭交換法和挖坑填坑法,不過是形上的稱法,歸根結底是為了交換三個數,CA互換,最後B再和A互換稱為兩頭交換法,因為B一直到AC完全分塊了才會和A的末尾數交換。
    每次都會BA互換,B再和C換稱為挖坑填坑法,因為總有一個實際意義上是空坑的位置由base來填。

網上總有人討論的從右開始還是從左開始問題
假設采取的是CA先交換,目的就是找到CA,先從後找到C和先從前面找到A沒有區別,所以從左從右都可以得到,只是寫法會略有不同而已
假設采取的是BA先交換,一定要從後面先找到A,看的明白不,BA要交換,我們已經捏了B在手裏,需要第一時間找到A,所以這種解法要從右邊找(這種找法的倒序也要從右邊找,大家自己想一想)

重新梳理並考慮邊界:
兩種不同的交換順序,導致了核心的代碼有兩個很大的區別。
1、前面一種方法,base一直到找到中軸才放下來,base要一直帶著走;後一種方法,base是每次交換之後的startIndex位置,base在每次遞歸找中軸的時候獲取
2、前面一種方法的交換,發生在找到中軸後;第二種方法的交換,在找到比base小的數,和找到比base大的數時都會發生
// 設置base下標為startIndex
設置i = startIndex + 1;
設置 j = endIndex;
從j往前找小於base數,如果找到,將base和消失交換,應該將這個數放到開始位置startIndex去,把base放在j位置
如果找不到(這裏所有的case全部過一遍,不要先合到一起),說明base是整個數組中最小的,直接return startIndex
在上面能找到的前提下,從i往後,找大於base的數,如果找到,那麽將base和大數交換,將j位置設置為i位置的數,將base放到i位置
能找到之後,從i作為startIndex,到j-1,作為endIndex,繼續找。
如果從i往後沒找到,說明j之後的數都大於base,j之前的數都小於base,返回j,分塊遞歸結束

Java代碼實現:
一開始實現代碼的時候,最好按照思路寫直接點的代碼,不要合並邏輯,最後再合並邏輯

public static void main(String[] args) {
int[] arr = new int[] {6,1,5,4,8,3,9,12,51,11,15,14,13,25,69,47,56,74,26,78};

quickSort(arr);

System.out.println(Arrays.toString(arr));

}

public static void quickSort(int[] arr) {
doQuickSort(arr, 0, arr.length - 1);
}

public static void doQuickSort(int[] arr, int startIndex, int endIndex) {
// startIndex大等於endIndex時候,退出
if (startIndex >= endIndex) {
return;
}
// 獲取中軸
int pivot = partition(arr, startIndex, endIndex);

// 根據中軸分成兩塊,遞歸排序,註意不需要再包括pivot
doQuickSort(arr, startIndex, pivot - 1);
doQuickSort(arr, pivot + 1, endIndex);

}

/**

  • 一邊交換,一邊找中軸
  • @param arr
  • @param startIndex
  • @param endIndex
  • @return
    */
    private static int partition(int[] arr, int startIndex, int endIndex) {
    // 取第一個位置的元素作為基準元素
    int base = arr[startIndex];
    // 取i是startIndex+1
    int i = startIndex + 1;
    // 取j是endIndex
    int j = endIndex;

    // 從右邊,到i截止,嘗試找到第一個小於base的數
    // 將這個小數放到前面去,這個時候其實j位置的實際意義是base,雖然base沒放進去,但是假設地櫃就此退出,base應該放到這個坑裏
    for (; j >= i; ) {
    if (arr[j] < base) {
    arr[startIndex] = arr[j];
    arr[j] = base;
    break;
    }
    j--;
    }
    // 如果沒找到,說明base是最小的,直接返回startIndex
    if(j < i){
    return startIndex;
    }

    // 從左邊到j,嘗試找比base大的數
    // 將較大值和base的位置互換,base將來會放在i位置
    for (; i <= j - 1;) {
    if (arr[i] >= base) {
    arr[j] = arr[i];
    // 需要將base和較大值交換
    arr[i] = base;
    break;
    }
    i++;
    }

    // 如果找到了兩個,也就是能交換,則base在i的位置,繼續遞歸尋找中軸
    if (i < j) {
    return partition(arr, i, j - 1);
    } else {
    // 如果沒找到,說明j位置之前的元素全部比base小,後面的全部比base大,base應該放到j的位置
    return j;
    }
    }

/**

  • 交換元素
  • @param arr
  • @param i
  • @param j
    */
    private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }
    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
    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
    後記:
    YY一下,看到最後,是不是發現這個算法的精髓是確定中軸的這個邏輯,並不是大家所說的分治,遞歸拆分更像是為了實現排序的輔助手段。也更能確定之前的猜測:作者當年是先發現了快速分成大小塊的方法,然後想到通過遞歸拆分實現排序。作者霍爾認識到這個算法在當時很快,所以稱呼它為QuickSort,估計老爺子也覺得以後不會有更牛叉的排序算法了。能發明出這麽個算法,肯定是個很聰明的人,聰明的人有點自負,起這個名字也可以接受吧。。。

快速排序--QuickSort,看完五分彩開獎網平臺搭建自己就能寫出來的快排思路推演