1、初識快速排序
同冒泡排序一樣,快速排序也屬於交換排序,通過元素之間的比較和交換位置來達到排序的目的。
不同的是:
- 冒泡排序在每一輪中只把1個元素冒泡到數列的一端
- 快速排序則在每一輪挑選一個基準元素,並讓其他比它大的元素移動到數列一邊,比它小的元素移動到數列的另一邊,從而把數列拆解成兩個部分;這種思路就叫作分治法,如下圖所示:
使用分治法的優勢
假如給出一個8個元素的數列,一般情況下,使用冒泡排序需要比較7輪,每一 輪把1個元素移動到數列的一端,時間複雜度是O(n²);快速排序的流程如下:
【原數列在每一輪都被拆分成兩部分,每一部分在下一輪又分別被拆分成兩部分,直到不可再分爲止】
每一輪的比較和交換,需要把數組全部元素都遍歷一遍,時間複雜度是O(n);
假如元素個數是n,那麼平均情況下需要遍歷logn輪,因此快速排序算法總體的平均時間複雜度是O(nlogn)
2、基準元素的選擇
在分治過程中,以基準元素爲中心,把其他元素移動到它的左右兩邊;
選擇基準元素最簡單的方式是選擇數列的第1個元素,如下圖:
例外:假如有一個原本逆序的數列,期望排序成順序數列,如下圖:
【整個數列並沒有被分成兩半,每一輪都只確定了基準元素的位置,在這種情況下,數列的第1個元素要麼是最小值,
要麼是最大值,根本無法發揮分治法的優勢,快速排序需要進行n輪,時間複雜度退化成了O(n²)】
避免上面這種情況,可以隨機選擇一個元素作爲基準元素,並且讓基準元素和數列首元素交換位置
通過上述方法,可以有效地將數列分成兩部分,但即使隨機選擇基準元素,也會有極小的機率選到數列的最大值或最小 值,同樣會影響分治的效果;所以,快速排序的平均時間複雜度是O(nlogn),但最壞情況下的時間複雜度O(n²)
【在後文中,直接將首元素作爲基準元素】
3、元素的交換
選定了基準元素之後要進行元素的交換,即把其他元素中小於基準元素的都交換到基準元素一邊,大於基準元素的都交換到基準元素另一邊,實現有兩種方法:雙邊循環法和單邊循環法
雙邊循環法(遞歸)
給出原始數列如下,要求對其從小到大進行排序
1、選定基準元素pivot,並且設置兩個指針left和right,指向數列的最左和最右兩個元素
2、進行第1次循環,從right指針開始,讓指針所指向的元素和基準元素做比較【如果大於或等於pivot,則指針向左移動;如果小於pivot,則right指針停止移動,切換到left指針】;
在當前數列中1<4,所以right直接停止移動,換到left指針;讓指針所指向的元素和基準元素做比較【如果小於或等於 pivot,則指針向右移動;如果大於pivot,則left指針停止移動】;
由於left開始指向的是基準元素,判斷肯定相等,所以left右移1位,如下圖:
由於7>4,left指針在元素7的位置停下;這時讓left和right指針所指向的元素進行交換
3、進入第2次循環,重新切換到right指針,向左移動;right指針先移動到8,8>4,繼續左移;由於2<4,停止在2的位置,後續步驟如下圖所示:
雙邊循環法代碼實現
Code:【使用遞歸】
import java.util.Arrays;
/**
* @ClassName QuickSort
* @Description 快速排序
* @Date: 2020/3/28
* @Version 1.0
*/
public class QuickSort {
public static void quickSort(int[] arr,int startIndex,int endIndex){
//遞歸結束條件:startIndex >= endIndex
if(startIndex >= endIndex){
return;
}
// 獲取基準元素
int pivotIndex = partiton(arr,startIndex,endIndex);
// 根據基準元素,分成兩部分進行遞歸排序
quickSort(arr,startIndex,pivotIndex -1);
quickSort(arr,pivotIndex + 1,endIndex);
}
//雙邊循環法
//arr 帶交換的數組
//startIndex 起始下標
//endIndex 結束下標
public static int partiton(int[] arr,int startIndex,int endIndex){
//取第一個位置(也可以隨機)的元素爲基準元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while (left != right){
//控制right指針比較並左移
while (left<right && arr[right]>pivot){
right--;
}
//控制left指針比較並右移
while (left < right && arr[left] <= pivot){
left++;
}
//交換left和right指針所指向的元素
if(left < right){
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指針重合點交換
arr[startIndex] = arr[left];
arr[left] = pivot;
return left;
}
public static void main(String [] args){
int[] arr = new int[]{4,7,6,5,3,2,8,1};
System.out.println("排序前:" + Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("排序後:" + Arrays.toString(arr));
}
}
編譯輸出:【partition方法則實現了元素的交換,讓數列中的元素依據自身大小分別交換到基準元素的左右兩邊】
單邊循環法(遞歸)
雙邊循環法從數組的兩邊交替遍歷元素,雖然更加直觀,但是代碼實現相對煩瑣;
單邊循環法只從數組的一邊對元素進行遍歷和交換,較爲簡單,詳細過程:
給出原始數列如下,要求對其從小到大進行排序:
1、選定基準元素pivot,同時設置一個mark指針指向數列起始位置,這個mark指針代表小於基準元素的區域邊界
2、從基準元素的下一個位置開始遍歷數組
- 如果遍歷到的元素大於基準元素,續往後遍歷;
- 如果遍歷到的元素小於基準元素,則需要做兩件事:
- 1、把mark指針右移1 位,因爲小於pivot的區域邊界增大了1;
- 2、讓最新遍歷到的元素和mark指針所在位置的元素交換位置,【因爲最新遍歷的元素歸屬於小於pivot的區域】
遍歷到元素7,7>4,所以繼續遍歷
來遍歷到的元素是3,3<4,所以mark指針右移1位
讓元素3和mark指針所在位置的元素交換,因爲元素3歸屬於小於pivot 的區域
3、繼續遍歷,後續步驟如圖所示:
單邊循環法代碼實現
Code:【使用遞歸】
/**
* @ClassName QuickSort
* @Description 快速排序
* @Date: 2020/3/28
* @Version 1.0
*/
public class QuickSort2 {
public static void quickSort(int[] arr,int startIndex,int endIndex){
//遞歸結束條件:startIndex >= endIndex
if(startIndex >= endIndex){
return;
}
// 獲取基準元素
int pivotIndex = partiton(arr,startIndex,endIndex);
// 根據基準元素,分成兩部分進行遞歸排序
quickSort(arr,startIndex,pivotIndex -1);
quickSort(arr,pivotIndex + 1,endIndex);
}
//單邊循環法
//arr 帶交換的數組
//startIndex 起始下標
//endIndex 結束下標
public static int partiton(int[] arr,int startIndex,int endIndex){
//取第一個位置(也可以隨機)的元素爲基準元素
int pivot = arr[startIndex];
int mark = startIndex;
for(int i = startIndex+1;i<endIndex;i++){
if(arr[i]<pivot){
mark++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
public static void main(String [] args){
int[] arr = new int[]{4,7,6,5,3,2,8,1};
System.out.println("排序前:" + Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("排序後:" + Arrays.toString(arr));
}
}
編譯輸出:【partition方法則實現了元素的交換,讓數列中的元素依據自身大小分別交換到基準元素的左右兩邊】
非遞歸實現
絕大多數的遞歸邏輯,都可以用棧的方式來代替
代碼中一層一層的方法調用,本身就使用了一個方法調用棧;
【每次進入一個新方法,就相當於入棧;每次有方法返回,就相當於出棧】
所以,可以把原本的遞歸實現轉化成一個棧的實現,在棧中存儲每一次方法調用的參數。
非遞歸代碼實現
Code:【非遞歸】
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
/**
* @ClassName QuickSort
* @Description 快速排序
* @Date: 2020/3/28
* @Version 1.0
*/
public class QuickSort3 {
public static void quickSort(int[] arr,int startIndex,int endIndex){
//用一個集合棧來代替遞歸的函數棧
Stack<Map<String,Integer>> quickSortStack = new Stack<Map<String,Integer>>();
// 整個數列的起止下標,以哈希的形式入棧
Map rootParam = new HashMap();
rootParam.put("startIndex",startIndex);
rootParam.put("endIndex",endIndex);
quickSortStack.push(rootParam);
// 循環結束條件:棧爲空
while (!quickSortStack.isEmpty()){
//棧頂元素出棧,得到起止下標
Map<String,Integer> param = quickSortStack.pop();
//得到基準元素位置
int pivotIndex = partiton(arr,param.get("startIndex"),param.get("endIndex"));
//根據基準元素分成兩部分,把每一部分的起止下標入棧
if(param.get("startIndex")< (pivotIndex -1)){
Map<String,Integer> leftParam = new HashMap<String, Integer>();
leftParam.put("startIndex",param.get("startIndex"));
leftParam.put("endIndex",pivotIndex-1);
quickSortStack.push(leftParam);
}
if(pivotIndex + 1 < param.get("endIndex")){
Map<String,Integer> rightParam = new HashMap<String, Integer>();
rightParam.put("startIndex",pivotIndex+1);
rightParam.put("endIndex",param.get("endIndex"));
quickSortStack.push(rightParam);
}
}
}
//單邊循環法
//arr 帶交換的數組
//startIndex 起始下標
//endIndex 結束下標
public static int partiton(int[] arr,int startIndex,int endIndex){
//取第一個位置(也可以隨機)的元素爲基準元素
int pivot = arr[startIndex];
int mark = startIndex;
for(int i = startIndex+1;i<= endIndex;i++){
if(arr[i]<pivot){
mark++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
public static void main(String [] args){
int[] arr = new int[]{4,7,6,5,3,2,8,1};
System.out.println("排序前:" + Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println("排序後:" + Arrays.toString(arr));
}
}
編譯輸出:【非遞歸方式代碼的變動只發生在quickSort方法中; 該方法引入了一個存儲Map類型元素的棧,用於存儲每一次交換時的起始下標和結束下標。
**每一次循環,都會讓棧頂元素出棧,通過partition方法進行分治,並且按照 基準元素的位置分成左右兩部分,左右兩部分再分別入棧,**當棧爲空時,說明排序 已經完畢,退出循環】
———————————————————————————————————————
內容來源:《漫畫算法》
關注公衆號,回覆 【算法】,獲取高清算法書!