QuickSort 快速排序到底快在哪裡?

2021-02-19 老馬嘯西風
創作目的

最近想系統整理一下資料庫的索引系列,然後就牽扯到了二分查找,二分查找的前提需要排序。

排序工作中我們用的很多,不過很少去關心實現;面試中,排序的出場率非常高,以此來驗證大家是否懂得「算法」。

無論如何,排序算法都值得每一位極客去學習和掌握。

快速排序 Quicksort

快速排序(有時稱為分區交換排序)是一種有效的排序算法。

由英國計算機科學家Tony Hoare於1959年開發[1],並於1961年發表[2],它仍然是一種常用的排序算法。如果實施得當,它可以比主要競爭者(合併排序和堆排序)快兩倍或三倍。

Quicksort是一種分而治之的算法。它通過從數組中選擇一個「pivot」元素並將其他元素劃分為兩個子數組(根據它們是否小於或大於基準數)來工作。然後將子數組遞歸排序。這可以就地完成,需要少量額外的內存來執行排序。

快速排序是一種比較排序,這意味著它可以對定義了「小於」關係的任何類型的項目進行排序。

快速排序的有效實現不是穩定的排序,這意味著不會保留相等排序項的相對順序。

快速排序的數學分析表明,平均而言,該算法採用O(n log n)比較來對n個項目進行排序。

在最壞的情況下,它會進行O(n^2)比較,儘管這種行為很少見。

算法流程

Quicksort是一種分而治之的算法。

首先將輸入數組分為兩個較小的子數組:低元素和高元素。

然後,它將對子數組進行遞歸排序。就地Quicksort的步驟是:

從數組中選擇一個稱為基準數的元素。

分區:對數組重新排序,以使所有值小於基準數的元素都位於基準數之前,而所有值大於基準數的元素都位於基準數之後(相等的值可以任意選擇)。分割之後,基準數處於其最終位置。這稱為分區操作。

將上述步驟遞歸地應用於值較小的元素子數組,並分別應用於值較大的元素的子數組。

遞歸的基本情況是大小為零或一的數組,這些數組按定義順序排列,因此它們不需要排序。

基準數選擇和分區步驟可以通過幾種不同的方式完成。具體實施方案的選擇會極大地影響算法的性能。

例子

上面的算法直接說,不免有些抽象。

我們舉一個例子,假如排序 {6 1 2 7 9 3 4 5 10 8}。

這個例子的圖片是參考網上的一篇文章的,畫的生動有趣,便於大家理解。

基準數

我們第一步,需要選擇一個基準數,為了簡單,直接選擇第一個數 6 作為比較的基準。

分區

這裡實際上是「雙指針」的思想,從兩邊開始比較。

第一次交換

先從右往左找一個小於6的數,再從左往右找一個大於6的數,然後交換他們。

輸入圖片說明

滿足條件的左邊是 7, 右邊 是 5,執行交換:

輸入圖片說明第二次交換

接下來,繼續走。

輸入圖片說明

滿足條件的左邊的是 9,右邊的是 4,執行交換:

輸入圖片說明第三次交換

然後右邊的右邊的哨兵向左,找到了滿足條件的元素 3(比 6 小);左邊的向右移動。

發現兩個人已經碰到一起了,說明本次的探測已經結束了。

我們需要把基準數放在交換到這個位置上。

輸入圖片說明

交換之後:

輸入圖片說明遞歸

然後我們將上面三步的策略,應用於左右兩個數組。

你問我哪兩個數組?

實際上就是根據基準數分割的 2 個 數組:

{3 1 2 5 4 6 9 7 10 8}

根據 6 拆分為:

{3 1 2 5 4} 和 {9 7 10 8}

輸入圖片說明java 代碼實現

我們一起來看一下 java 的代碼實現。

核心代碼實現

package com.github.houbb.sort.core.api;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.sort.core.util.InnerSortUtil;

import java.util.List;

/**
 * 快速排序
 * @author binbin.hou
 * @since 0.0.2
 */
public class QuickSort extends AbstractSort {

    private static final Log log = LogFactory.getLog(QuickSort.class);

    @Override
    protected void doSort(List<?> original) {
        this.quickSort(original, 0, original.size()-1);
    }

    /**
     * 快速排序
     * @param original 原始列表
     * @param left 左邊
     * @param right 右邊
     * @since 0.0.2
     */
    @SuppressWarnings("all")
    private void quickSort(final List<?> original,
                           final int left,
                           final int right) {
        if(left > right) {
            return;
        }

        // 左右兩邊的哨兵指針
        int leftIx = left;
        int rightIx = right;
        // 比較基準,直接取最左邊的元素
        Comparable basic = (Comparable) original.get(leftIx);

        while (leftIx < rightIx) {
            // 右邊,從右向左移動找到第一個小於基準的數
            while (leftIx < rightIx && InnerSortUtil.gte(original, rightIx, basic)) {
                rightIx--;
            }

            // 左邊,從左向右移動找到第一個大於基準的數
            while (leftIx < rightIx && InnerSortUtil.lte(original, leftIx, basic)) {
                leftIx++;
            }

            // 判斷是否滿足交換的條件
            if(leftIx < rightIx) {
                InnerSortUtil.swap(original, leftIx, rightIx);
            } 
        }

        // 更新基準的信息(i == j)
        // 將最左邊位置的元素,和此時的哨兵位置交換
        InnerSortUtil.swap(original, left, leftIx);

        // 執行遞歸調用
        quickSort(original, left, leftIx-1);
        quickSort(original, leftIx+1, right);
    }

}

InnerSortUtil 工具類

為了便於後期復用,我們把交換和比較做了抽成單獨的方法:

package com.github.houbb.sort.core.util;

import java.util.List;

/**
 * 內部比較輔助類,可能會變更。
 * 外部不要使用
 * @author binbin.hou
 * @since 0.0.2
 */
public final class InnerSortUtil {

    /**
     * 執行數據的交換
     * @param original 原始
     * @param i 第一個
     * @param j 第二個
     * @since 0.0.1
     */
    @SuppressWarnings("unchecked")
    public static void swap(List original,
                      int i, int j) {
        Object temp = original.get(i);
        original.set(i, original.get(j));
        original.set(j, temp);
    }

    /**
     * 是否大於等於元素
     * @param original 原始
     * @param i 索引
     * @param target 指定元素
     * @since 0.0.2
     */
    @SuppressWarnings("all")
    public static boolean gte(List original, int i, Comparable target) {
        Comparable comparable = (Comparable) original.get(i);
        return comparable.compareTo(target) >= 0;
    }

    /**
     * 是否小於等於元素
     * @param original 原始
     * @param i 索引
     * @param target 指定元素
     * @since 0.0.2
     */
    @SuppressWarnings("all")
    public static boolean lte(List original, int i, Comparable target) {
        Comparable comparable = (Comparable) original.get(i);
        return comparable.compareTo(target) <= 0;
    }

}

工具方法

為了快速排序便於使用,我們將其封裝為工具類:

/**
 * 快速排序
 * @param <T> 泛型
 * @param list 列表
 * @since 0.0.2
 */
public static <T extends Comparable<? super T>> void quick(List<T> list) {
    Instances.singleton(QuickSort.class).sort(list);
}

代碼測試測試代碼

我們就以開始的例子作為測試案例。

List<Integer> list = Arrays.asList(6,1,2,7,9,3,4,5,10,8);
System.out.println("開始排序:" + list);
SortHelper.quick(list);
System.out.println("完成排序:" + list);

測試日誌

為了便於大家閱讀和理解過程,我們在核心的實現代碼中加了一點兒魔法,不,一點兒日誌。

數據交換時

// 判斷是否滿足交換的條件
if(leftIx < rightIx) {
    InnerSortUtil.swap(original, leftIx, rightIx);
    if(log.isDebugEnabled()) {
        String info = String.format("l=%s, r=%s, lx=%s, rx=%s, 交換後:%s",
                left, right, leftIx, rightIx, original);
        log.debug(info);
    }
} else {
    if(log.isDebugEnabled()) {
        String info = String.format("l=%s, r=%s, lx=%s, rx=%s, 無交換",
                left, right, leftIx, rightIx);
        log.debug(info);
    }
}

基準歸位時

// 更新基準的信息(i == j)
// 將最左邊位置的元素,和此時的哨兵位置交換
InnerSortUtil.swap(original, left, leftIx);
if(log.isDebugEnabled()) {
    String info = String.format("l=%s, lx=%s, 基準數歸位:%s",
            left, leftIx, original);
    log.debug(info);
}

測試日誌

測試日誌如下:

開始排序:[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
l=0, r=9, lx=3, rx=7, 交換後:[6, 1, 2, 5, 9, 3, 4, 7, 10, 8]
l=0, r=9, lx=4, rx=6, 交換後:[6, 1, 2, 5, 4, 3, 9, 7, 10, 8]
l=0, r=9, lx=5, rx=5, 無交換
l=0, lx=5, 基準數歸位:[3, 1, 2, 5, 4, 6, 9, 7, 10, 8]
l=0, r=4, lx=2, rx=2, 無交換
l=0, lx=2, 基準數歸位:[2, 1, 3, 5, 4, 6, 9, 7, 10, 8]
l=0, r=1, lx=1, rx=1, 無交換
l=0, lx=1, 基準數歸位:[1, 2, 3, 5, 4, 6, 9, 7, 10, 8]
l=0, lx=0, 基準數歸位:[1, 2, 3, 5, 4, 6, 9, 7, 10, 8]
l=3, r=4, lx=4, rx=4, 無交換
l=3, lx=4, 基準數歸位:[1, 2, 3, 4, 5, 6, 9, 7, 10, 8]
l=3, lx=3, 基準數歸位:[1, 2, 3, 4, 5, 6, 9, 7, 10, 8]
l=6, r=9, lx=8, rx=9, 交換後:[1, 2, 3, 4, 5, 6, 9, 7, 8, 10]
l=6, r=9, lx=8, rx=8, 無交換
l=6, lx=8, 基準數歸位:[1, 2, 3, 4, 5, 6, 8, 7, 9, 10]
l=6, r=7, lx=7, rx=7, 無交換
l=6, lx=7, 基準數歸位:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
l=6, lx=6, 基準數歸位:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
l=9, lx=9, 基準數歸位:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
完成排序:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

這個日誌,再對照一下開始的解釋,聰明的你一定可以理解地更加深入!

開源地址

為了便於大家學習,上面的排序已經開源,開源地址:

https://github.com/houbb/sort

歡迎大家 fork/star,鼓勵一下作者~~

小結

快速排序之所比較快,因為相比冒泡排序,每次交換是跳躍式的。

每次排序的時候設置一個基準點,將小於等於基準點的數全部放到基準點的左邊,將大於等於基準點的數全部放到基準點的右邊。這樣在每次交換的時候就不會像冒泡排序一樣每次只能在相鄰的數之間進行交換,交換的距離就大的多了。

只能說,分治算法,永遠滴神!

希望本文對你有幫助,如果有其他想法的話,也可以評論區和大家分享哦。

各位極客的點讚收藏轉發,是老馬持續寫作的最大動力!

相關焦點

  • Python中的快速排序算法,快速排序的優缺點,中級python技術點
    Python中的快速排序算法就像合併排序一樣,快速排序算法採用分治法的原理將輸入數組分為兩個列表,第一個包含小項目,第二個包含大項目。然後,該算法將對兩個列表進行遞歸排序,直到對結果列表進行完全排序為止。劃分輸入列表稱為對列表進行分區。
  • 分而治之和快速排序
    二、快速排序2.1 快速排序原理第一個重要的D&C算法-快速排序,快速排序是一種排序算法。速度比選擇排序快得多。使用快速排序對數組進行排序,對排序算法來說,最簡單的數組是什麼樣子的呢?就是根本不需要排序的數組。例如:空數組和只包含一個元素的數組。所以基線條件為數組為空數組或者數組只包含一個元素,在這種情況下,只需要原樣返回數組,根本不需要排序。
  • 快速排序 Quicksort
    快速排序(quick sort)的採用了分而治之(divide and conquer)的策略:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞歸地解這些子問題,然後將這些子問題的解組合為原問題的解。    1. 算法思想        快速排序的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分:分割點左邊都是比它小的數,右邊都是比它大的數。
  • 一場排序引發的風波——計數排序(O(n)複雜度)
    對於計數排序,百度百科是這麼說的:計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定範圍內的整數排序時,它的複雜度為Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。
  • Python實現經典排序算法,再也不怕面試官讓我手寫快排了!
    有更強大的算法,包括合併排序和快速排序,但是這些實現是遞歸的,在處理小型列表時通常無法擊敗插入排序。如果列表足夠小,可以提供更快的整體實現,則某些快速排序實現甚至在內部使用插入排序。Timsort還在內部使用插入排序對輸入數組的一小部分進行排序。
  • Python,Numpy,Pandas……數據科學家必備排序技巧
    現在,人們使用的排序算法與根據名字聯想的略有不同。通過kind = quicksort意味著排序實際是從introsort算法開始的。若[它]沒有明顯進展,則會切換成堆排序算法。執行該操作最壞的情況就是產生快速排序O(n * log(n))。Stable會自動為正在排序的數據類型選擇最穩定的排序算法。
  • 為什麼要排序?排序算法的性能提升之道
    圖源:unsplash排序有什麼用?想像一下,如果字典不是按照字母順序排列,查找一個單詞,你得查到什麼時候?這就是為什麼人們引入了分類的概念,因為其極大地幫助我們快速搜索物品。快速排序快速排序也被稱為分區排序。該排序算法因其分而治之的概念相較於之前的算法效率更高首先確定一個主元,然後找到該主元位置的正確索引,將該數組分為兩個子數組。一個子數組包含小於主元的元素,另一個子數組包含大於主元的元素。然後,遞歸調用這兩個子數組,直到無法進一步劃分數組為止。
  • 用Python手寫五大經典排序算法,看完這篇終於懂了!
    有更強大的算法,包括合併排序和快速排序,但是這些實現是遞歸的,在處理小型列表時通常無法擊敗插入排序。如果列表足夠小,可以提供更快的整體實現,則某些快速排序實現甚至在內部使用插入排序。Timsort還在內部使用插入排序對輸入數組的一小部分進行排序。
  • ...選擇,插入,希爾,歸併,快排,堆排序,計數排序,桶排序,基數排序)
    console.log(mergeSort(arr));總結:最佳情況:O(n)最壞情況:O(nlogn)平均情況:O(nlogn)七、快速排序顧名思義,快排,速度就是快,效率高,處理大數據的最快的排序算法之一。思路:也是分治法的思想,通過一趟排序將待排序的元素分為獨立的兩個部分,其中一部分記錄的關鍵字均比另外一部分的關鍵字小,然後分別對這兩部分繼續進行排序,已達到整個序列有序。
  • 一個快速排序寫了快 10000 字?
    快速排序今天我們來說一下快速排序,這個排序算法也是面試的高頻考點,原理很簡單,我們一起來扒一下他吧。我們先來說一下快速排序的基本思想,很容易理解。1.先從數組中找一個基準數2.讓其他比它大的元素移動到數列一邊,比他小的元素移動到數列另一邊,從而把數組拆解成兩個部分。
  • 第89天:NumPy 排序和篩選函數
    排序算法速度時間複雜度空間複雜度穩定性quicksort(快速排序)1o(n^2)0否mergesort(歸併排序)2O(n*log(n))~n/2是heapsort(堆排序)3O(n*log(n))0否numpy.sort(a, axis, kind, order)這個排序函數有4個參數,我們來看看參數的說明:參數說明a要排序的數組axis排序數組的軸,如果沒有數組會被展開,
  • 快速排序算法介紹
    它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
  • 排序算法:快速排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助今天介紹一下快速排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,參照遞歸表達式漸進複雜度分析快速排序的時間複雜度遞歸表達式為,其中a=2,b=2,d=1由算法導論的主定理可以推導出,快速排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • 八大經典排序算法詳解
    •冒泡排序最好的時間複雜度為O(n)。冒泡排序的最壞時間複雜度為O(n^2)。因此冒泡排序總的平均時間複雜度為O(n^2)。•算法適用於少量數據的排序,是穩定的排序方法。,傳入數字的起點和終點 } }快速排序•快速排序原理:1.如果數組S中元素是0或者1,則返回;2.區數組S中任一元素v,稱之為樞紐元;3.將S-{v}(S中剩餘的元素)劃分成連個不相交的集合:S1={S-{v}|x<=v}和S2={S-{v}|x>=v};4.返回{quicksort(s1)}後跟v,繼而返回{quicksort(S2)}。
  • 程式設計師必知必會的十大排序算法
    緒論身為程式設計師,十大排序是所有合格程式設計師所必備和掌握的,並且熱門的算法比如快排、歸併排序還可能問得比較細緻,對算法性能和複雜度的掌握都有要求。本文將帶你捋一捋常見的十大排序算法,讓你輕輕鬆鬆掌握!快速排序是對冒泡排序的一種改進,採用遞歸分治的方法進行求解。
  • 程式設計師必知必會的 10 個排序算法
    ,並且熱門的算法比如快排、歸併排序還可能問的比較細緻,對算法性能和複雜度的掌握有要求。>快速排序快速排序是對冒泡排序的一種改進,採用遞歸分治的方法進行求解。對於快排來說,「基本思想」是這樣的快排需要將序列變成兩個部分,就是「序列左邊全部小於一個數」,「序列右面全部大於一個數」,然後利用遞歸的思想再將左序列當成一個完整的序列再進行排序,同樣把序列的右側也當成一個完整的序列進行排序。
  • 重溫7種排序算法 第一篇,交換排序算法:冒泡排序、快速排序
    排序算法可分為以下幾類:    交換排序算法:冒泡排序、快速排序;    選擇排序算法:簡單選擇排序、堆排序;    插入排序算法:直接插入排序、希爾排序;    歸併排序算法。    主要做了以上7種排序算法的筆記,字數可能較多,所以分為幾篇。
  • 為什麼排序的複雜度為 O(N log N) | Linux 中國
    基本上所有正而八經的算法教材都會解釋像快速排序(quicksort)和堆排序(heapsort)這樣的排序算法有多快,但並不需要複雜的數學就能證明你可以逐漸趨近的速度有多快。關於標記的一個嚴肅說明:大多數計算機專業的科學家使用大寫字母 O 標記來指代「趨近,直到到達一個常數比例因子」,這與數學專業所指代的意義是有所區別的。
  • 遞歸與快速排序
    今天我們要說的是快速排序,快速排序也是使用了分治法的一個典型的算法,通過每次選取一個元素作為中間元素,將比這個元素小的放在元素的一側