接下來介紹一下快排:
假設我們現在對“6 1 2 7 9 3 4 5 10 8”這個10個數進行排序。首先在這個序列中隨便找一個數作爲基準數(不要被這個名詞嚇到了,就是一個用來參照的數,待會你就知道它用來做啥的了)。爲了方便,就讓第一個數6作爲基準數吧。接下來,需要將這個序列中所有比基準數大的數放在6的右邊,比基準數小的數放在6的左邊,類似下面這種排列:
3 1 2 5 4 6 9 7 10 8
在初始狀態下,數字6在序列的第1位。我們的目標是將6挪到序列中間的某個位置,假設這個位置是k。現在就需要尋找這個k,並且以第k位爲分界點,左邊的數都小於等於6,右邊的數都大於等於6。想一想,你有辦法可以做到這點嗎?
排序算法顯神威
方法其實很簡單:分別從初始序列“6 1 2 7 9 3 4 5 10 8”兩端開始“探測”。先從右往左找一個小於6的數,再從左往右找一個大於6的數,然後交換他們。這裏可以用兩個變量i和j,分別指向序列最左邊和最右邊。我們爲這兩個變量起個好聽的名字“哨兵i”和“哨兵j”。剛開始的時候讓哨兵i指向序列的最左邊(即i=1),指向數字6。讓哨兵j指向序列的最右邊(即j=10),指向數字8。
第一步:
首先哨兵j開始出動。因爲此處設置的基準數是最左邊的數,所以需要讓哨兵j先出動,這一點非常重要(請自己想一想爲什麼)。哨兵j一步一步地向左挪動(即j--),直到找到一個小於6的數停下來。接下來哨兵i再一步一步向右挪動(即i++),直到找到一個數大於6的數停下來。最後哨兵j停在了數字5面前,哨兵i停在了數字7面前。
現在交換哨兵i和哨兵j所指向的元素的值。交換之後的序列如下:
6 1 2 5 9 3 4 7 10 8
第二步:
到此,第一次交換結束。接下來開始哨兵j繼續向左挪動(再友情提醒,每次必須是哨兵j先出發)。他發現了4(比基準數6要小,滿足要求)之後停了下來。哨兵i也繼續向右挪動的,他發現了9(比基準數6要大,滿足要求)之後停了下來。此時再次進行交換,交換之後的序列如下:
6 1 2 5 4 3 9 7 10 8
第三步:
第二次交換結束,“探測”繼續。哨兵j繼續向左挪動,他發現了3(比基準數6要小,滿足要求)之後又停了下來。哨兵i繼續向右移動,糟啦!此時哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。說明此時“探測”結束。我們將基準數6和3進行交換。交換之後的序列如下:
3 1 2 5 4 6 9 7 10 8
總結:
到此第一輪“探測”真正結束。此時以基準數6爲分界點,6左邊的數都小於等於6,6右邊的數都大於等於6。回顧一下剛纔的過程,其實哨兵j的使命就是要找小於基準數的數,而哨兵i的使命就是要找大於基準數的數,直到i和j碰頭爲止。
OK,解釋完畢。現在基準數6已經歸位,它正好處在序列的第6位。此時我們已經將原來的序列,以6爲分界點拆分成了兩個序列,左邊的序列是“3 1 2 5 4”,右邊的序列是“9 7 10 8”。接下來還需要分別處理這兩個序列。因爲6左邊和右邊的序列目前都還是很混亂的。不過不要緊,我們已經掌握了方法,接下來只要模擬剛纔的方法分別處理6左邊和右邊的序列即可。
這是爲什麼呢?
快速排序之所比較快,因爲相比冒泡排序,每次交換是跳躍式的。每次排序的時候設置一個基準點,將小於等於基準點的數全部放到基準點的左邊,將大於等於基準點的數全部放到基準點的右邊。這樣在每次交換的時候就不會像冒泡排序一樣每次只能在相鄰的數之間進行交換,交換的距離就大的多了。因此總的比較和交換次數就少了,速度自然就提高了。當然在最壞的情況下,仍可能是相鄰的兩個數進行了交換。因此快速排序的最差時間複雜度和冒泡排序是一樣的都是O(N2),它的平均時間複雜度爲O(NlogN)。其實快速排序是基於一種叫做“二分”的思想。
package com.example;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
public class QuickSort {
public static void main(String[] args) {
QuickSort quickSort = new QuickSort();
// 測試快排的效率:
int number = 100000;
int[] array = new int[number];
for (int i = 0; i < array.length; i++) {
array[i] = new Random().nextInt(number);
}
//配合後面的元素輸出,測試快排是否排序準確:
// int[] array = new int[]{181, 181, 187, 181};
System.out.println("數組準備完畢~");
SimpleDateFormat formatter = new SimpleDateFormat("yyyy年-MM月dd日-HH時mm分ss秒");
long start = System.currentTimeMillis();
Date sdate = new Date(start);
System.out.println("開始時間:"+formatter.format(sdate));
quickSort.quickSort(array, 0, array.length - 1);
long end = System.currentTimeMillis();
Date edate = new Date(end);
System.out.println("結束時間:"+formatter.format(edate));
System.out.println("quickSort 用時:" + (end - start));// 測試結果: 元素爲5萬個時:11毫秒。50萬:66毫秒。100萬:136毫秒
//遍歷輸出數組元素:
// quickSort.traverseArray(array);
}
/**
* 快排的實現
*
* @param target
* @param left
* @param right
*/
public void quickSort(int[] target, int left, int right) {
if (left >= right) {
return;
}
int pivot = target[left];// 基準點
int temp;
int i = left;
int j = right;//爲什麼要聲明i和j,因爲後面做迭代的時候還需要用到最初的left和right
while (i < j) {//驗證array數組至少有2個元素,纔要做排序
/**
* 提問:
* 爲什麼是 while裏的判斷,爲什麼是 “target[j] >= pivot”,而不是“target[j] > pivot”???
* 答: 數組[181,181,187,181],分別用上面兩種while去測試:
* 如果是">="時,因爲 181 >= 181 成立,所以right就會從右往左移;
* 如果是">"時,因爲 181 > 181 成立,所以right就不會左移。
* 重點!!!right或left,必須有一方得是移動的!!!否則程序就會進入死循環!!!
*/
// 如果right一直都大於或等於pivot,則繼續走,直到找到比pivot小的:
while (target[j] >= pivot && i < j) {
j--;
}
// 如果left一直都小於等於pivot,則繼續走,直到找到比pivot大的:
while (target[i] <= pivot && i < j) {
i++;
}
// 此時right < pivot, left > pivot,將i和j做交換:
if (i < j) {//這裏做判斷是爲了right到了left位置時,不用再將執行下面這三行代碼了:
temp = target[i];
target[i] = target[j];
target[j] = temp;
}
}
// left和right相遇了:
// ①將相遇點的元素和pivot做交換:
target[left] = target[j];
target[j] = pivot;
// ②基準點兩邊的元素的分別再做排序:
quickSort(target, left, j - 1);
quickSort(target, j + 1, right);
}
//遍歷數組
public void traverseArray(int[] array) {
for (int element : array) {
System.out.println(element);
}
}
}