圖解排序算法(03) -- 徹底搞懂快速排序

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方法進行分治,並且按照 基準元素的位置分成左右兩部分,左右兩部分再分別入棧,**當棧爲空時,說明排序 已經完畢,退出循環】
在這裏插入圖片描述———————————————————————————————————————
內容來源:《漫畫算法》
關注公衆號,回覆 【算法】,獲取高清算法書!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章