【演算法】1、快速排序

来源:http://www.cnblogs.com/wangzhongqiu/archive/2017/08/03/7281801.html
-Advertisement-
Play Games

1、快速排序的基本思想: 快速排序使用分治的思想,通過一趟排序將待排序列分割成兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小。之後分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。 2、快速排序的三個步驟: (1)選擇基準:在待排序列中,按照某種方式挑出一個元素,作為 "基準"(p ...


1、快速排序的基本思想:

   快速排序使用分治的思想,通過一趟排序將待排序列分割成兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小。之後分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。

2、快速排序的三個步驟:

(1)選擇基準:在待排序列中,按照某種方式挑出一個元素,作為 "基準"(pivot)

(2)分割操作:以該基準在序列中的實際位置,把序列分成兩個子序列。此時,在基準左邊的元素都比該基準小,在基準右邊的元素都比基準大

(3)遞歸地對兩個序列進行快速排序,直到序列為空或者只有一個元素。

3、選擇基準的方式

對於分治演算法,當每次劃分時,演算法若都能分成兩個等長的子序列時,那麼分治演算法效率會達到最大。也就是說,基準的選擇是很重要的。選擇基準的方式決定了兩個分割後兩個子序列的長度,進而對整個演算法的效率產生決定性影響。

最理想的方法是,選擇的基準恰好能把待排序序列分成兩個等長的子序列

我們介紹三種選擇基準的方法

方法(1):固定位置

思想:取序列的第一個或最後一個元素作為基準

基本的快速排序

 

[cpp] view plain copy  
  1. int SelectPivot(int arr[],int low,int high)  
  2. {  
  3.     return arr[low];//選擇選取序列的第一個元素作為基準  
  4. }  

 

註意:基本的快速排序選取第一個或最後一個元素作為基準。但是,這是一直很不好的處理方法。

測試數據:

測試數據分析:如果輸入序列是隨機的,處理時間可以接受的。如果數組已經有序時,此時的分割就是一個非常不好的分割。因為每次劃分只能使待排序序列減一,此時為最壞情況,快速排序淪為起泡排序,時間複雜度為Θ(n^2)。而且,輸入的數據是有序或部分有序的情況是相當常見的。因此,使用第一個元素作為樞紐元是非常糟糕的,為了避免這個情況,就引入了下麵兩個獲取基準的方法。

方法(2):隨機選取基準

引入的原因:在待排序列是部分有序時,固定選取樞軸使快排效率底下,要緩解這種情況,就引入了隨機選取樞軸

思想:取待排序列中任意一個元素作為基準

隨機化演算法

 

[cpp] view plain copy  
  1. /*隨機選擇樞軸的位置,區間在low和high之間*/  
  2. int SelectPivotRandom(int arr[],int low,int high)  
  3. {  
  4.     //產生樞軸的位置  
  5.     srand((unsigned)time(NULL));  
  6.     int pivotPos = rand()%(high - low) + low;  
  7.   
  8.     //把樞軸位置的元素和low位置元素互換,此時可以和普通的快排一樣調用劃分函數  
  9.     swap(arr[pivotPos],arr[low]);  
  10.     return arr[low];  
  11. }  

測試數據:

測試數據分析::這是一種相對安全的策略。由於樞軸的位置是隨機的,那麼產生的分割也不會總是會出現劣質的分割。在整個數組數字全相等時,仍然是最壞情況,時間複雜度是O(n^2)。實際上,隨機化快速排序得到理論最壞情況的可能性僅為1/(2^n)。所以隨機化快速排序可以對於絕大多數輸入數據達到O(nlogn)的期望時間複雜度。一位前輩做出了一個精辟的總結:“隨機化快速排序可以滿足一個人一輩子的人品需求。”

方法(3):三數取中(median-of-three)

引入的原因:雖然隨機選取樞軸時,減少出現不好分割的幾率,但是還是最壞情況下還是O(n^2),要緩解這種情況,就引入了三數取中選取樞軸

分析:最佳的劃分是將待排序的序列分成等長的子序列,最佳的狀態我們可以使用序列的中間的值,也就是第N/2個數。可是,這很難算出來,並且會明顯減慢快速排序的速度。這樣的中值的估計可以通過隨機選取三個元素並用它們的中值作為樞紐元而得到。事實上,隨機性並沒有多大的幫助,因此一般的做法是使用左端、右端和中心位置上的三個元素的中值作為樞紐元。顯然使用三數中值分割法消除了預排序輸入的不好情形,並且減少快排大約14%的比較次數

舉例:待排序序列為:8 1 4 9 6 3 5 2 7 0

左邊為:8,右邊為0,中間為6.

我們這裡取三個數排序後,中間那個數作為樞軸,則樞軸為6

註意:在選取中軸值時,可以從由左中右三個中選取擴大到五個元素中或者更多元素中選取,一般的,會有(2t+1)平均分區法(median-of-(2t+1),三平均分區法英文為median-of-three)。

具體思想:對待排序序列中low、mid、high三個位置上數據進行排序,取他們中間的那個數據作為樞軸,並用0下標元素存儲樞軸。

即:採用三數取中,並用0下標元素存儲樞軸。

/*函數作用:取待排序序列中low、mid、high三個位置上數據,選取他們中間的那個數據作為樞軸*/
int SelectPivotMedianOfThree(int arr[],int low,int high)
{
    int mid = low + ((high - low) >> 1);//計算數組中間的元素的下標

    //使用三數取中法選擇樞軸
    if (arr[mid] > arr[high])//目標: arr[mid] <= arr[high]
    {
        swap(arr[mid],arr[high]);
    }
    if (arr[low] > arr[high])//目標: arr[low] <= arr[high]
    {
        swap(arr[low],arr[high]);
    }
    if (arr[mid] > arr[low]) //目標: arr[low] >= arr[mid]
    {
        swap(arr[mid],arr[low]);
    }
    //此時,arr[mid] <= arr[low] <= arr[high]
    return arr[low];
    //low的位置上保存這三個位置中間的值
    //分割時可以直接使用low位置的元素作為樞軸,而不用改變分割函數了
}

測試數據:

測試數據分析:使用三數取中選擇樞軸優勢還是很明顯的,但是還是處理不了重覆數組

優化1、當待排序序列的長度分割到一定大小後,使用插入排序。

原因:對於很小和部分有序的數組,快排不如插排好。當待排序序列的長度分割到一定大小後,繼續分割的效率比插入排序要差,此時可以使用插排而不是快排

截止範圍:待排序序列長度N = 10,雖然在5~20之間任一截止範圍都有可能產生類似的結果,這種做法也避免了一些有害的退化情形。摘自《數據結構與演算法分析》Mark Allen Weiness 著

 

[cpp] view plain copy  
  1. if (high - low + 1 < 10)  
  2. {  
  3.     InsertSort(arr,low,high);  
  4.     return;  
  5. }//else時,正常執行快排  

 

測試數據:

測試數據分析:針對隨機數組,使用三數取中選擇樞軸+插排,效率還是可以提高一點,真是針對已排序的數組,是沒有任何用處的。因為待排序序列是已經有序的,那麼每次劃分只能使待排序序列減一。此時,插排是發揮不了作用的。所以這裡看不到時間的減少。另外,三數取中選擇樞軸+插排還是不能處理重覆數組

優化2、在一次分割結束後,可以把與Key相等的元素聚在一起,繼續下次分割時,不用再對與key相等元素分割

舉例:

待排序序列 1 4 6 7 6 6 7 6 8 6

三數取中選取樞軸:下標為4的數6

轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6

             樞軸key:6

本次劃分後,未對與key元素相等處理的結果:1 4 6 6 7 6 7 6 8 6

下次的兩個子序列為:1 4 6 和 7 6 7 6 8 6

本次劃分後,對與key元素相等處理的結果:1 4 6 6 6 6 6 7 8 7

下次的兩個子序列為:1 4 和 7 8 7

經過對比,我們可以看出,在一次劃分後,把與key相等的元素聚在一起,能減少迭代次數,效率會提高不少

具體過程:在處理過程中,會有兩個步驟

第一步,在劃分過程中,把與key相等元素放入數組的兩端

第二步,劃分結束後,把與key相等的元素移到樞軸周圍

舉例:

待排序序列 1 4 6 7 6 6 7 6 8 6

三數取中選取樞軸:下標為4的數6

轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6

             樞軸key:6

第一步,在劃分過程中,把與key相等元素放入數組的兩端

結果為:6 4 1 6(樞軸) 7 8 7 6 6 6

此時,與6相等的元素全放入在兩端了

第二步,劃分結束後,把與key相等的元素移到樞軸周圍

 

結果為:1 4 66(樞軸)  6 6 6 7 8 7

此時,與6相等的元素全移到樞軸周圍了

之後,在1 4 和 7 8 7兩個子序列進行快排

代碼

void QSort(int arr[],int low,int high)
{
    int first = low;
    int last = high;

    int left = low;
    int right = high;

    int leftLen = 0;
    int rightLen = 0;

    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    
    //一次分割
    int key = SelectPivotMedianOfThree(arr,low,high);//使用三數取中法選擇樞軸
        
    while(low < high)
    {
        while(high > low && arr[high] >= key)
        {
            if (arr[high] == key)//處理相等元素
            {
                swap(arr[right],arr[high]);
                right--;
                rightLen++;
            }
            high--;
        }
        arr[low] = arr[high];
        while(high > low && arr[low] <= key)
        {
            if (arr[low] == key)
            {
                swap(arr[left],arr[low]);
                left++;
                leftLen++;
            }
            low++;
        }
        arr[high] = arr[low];
    }
    arr[low] = key;

    //一次快排結束
    //把與樞軸key相同的元素移到樞軸最終位置周圍
    int i = low - 1;
    int j = first;
    while(j < left && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i--;
        j++;
    }
    i = low + 1;
    j = last;
    while(j > right && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i++;
        j--;
    }
    QSort(arr,first,low - 1 - leftLen);
    QSort(arr,low + 1 + rightLen,last);
}

測試數據:

 測試數據分析:三數取中選擇樞軸+插排+聚集相等元素的組合,效果竟然好的出奇。

原因:在數組中,如果有相等的元素,那麼就可以減少不少冗餘的劃分。這點在重覆數組中體現特別明顯啊。

其實這裡,插排的作用還是不怎麼大的。

優化3:優化遞歸操作

快排函數在函數尾部有兩次遞歸操作,我們可以對其使用尾遞歸優化

優點:如果待排序的序列劃分極端不平衡,遞歸的深度將趨近於n,而棧的大小是很有限的,每次遞歸調用都會耗費一定的棧空間,函數的參數越多,每次遞歸耗費的空間也越多。優化後,可以縮減堆棧深度,由原來的O(n)縮減為O(logn),將會提高性能。

代碼:

 

[cpp] view plain copy  
  1. void QSort(int arr[],int low,int high)  
  2. {   
  3.     int pivotPos = -1;  
  4.     if (high - low + 1 < 10)  
  5.     {  
  6.         InsertSort(arr,low,high);  
  7.         return;  
  8.     }  
  9.     while(low < high)  
  10.     {  
  11.         pivotPos = Partition(arr,low,high);  
  12.         QSort(arr,low,pivot-1);  
  13.         low = pivot + 1;  
  14.     }  
  15. }  

註意:在第一次遞歸後,low就沒用了,此時第二次遞歸可以使用迴圈代替

測試數據:

測試數據分析:其實這種優化編譯器會自己優化,相比不使用優化的方法,時間幾乎沒有減少

優化4:使用並行或多線程處理子序列(略)

所有的數據測試:

概括:這裡效率最好的快排組合 是:三數取中+插排+聚集相等元素,它和STL中的Sort函數效率差不多

註意:由於測試數據不穩定,數據也僅僅反應大概的情況。如果時間上沒有成倍的增加或減少,僅僅有小額變化的話,我們可以看成時間差不多。

轉自:http://blog.csdn.net/insistgogo/article/details/7785038


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用Python自帶的函數strip可以剔除字元串開頭、結尾、兩端的空白 使用場景:用戶輸入驗證 strip : 去除字元串兩端的空白 rstrip : 去除字元串末尾(右端)的空白 lstrip : 去除字元串開頭(左端)的空白 示例: ...
  • 學了Java有一段時間了,自認為有一些基礎知識比較重要,因此記下來共用,不喜勿噴。 一、標識符 (1)定義:在Java語言中,凡是對類,方法,變數,包,參數等命名時,所使用的字元序列 (2)包含的內容:0-9、a-z、A-Z、&、_ (3)註意的規則:1.由字母、數字、下劃線和美元符號組成 2.不能 ...
  • 系統自動拋出的異常 所有系統定義的編譯和運行異常都可以由系統自動拋出,稱為標準異常,並且 Java 強烈地要求應用程式進行完整的異常處理,給用戶友好的提示,或者修正後使程式繼續執行。 語句拋出的異常 用戶程式自定義的異常和應用程式特定的異常,必須藉助於 throws 和 throw 語句來定義拋出異 ...
  • package java.util; public interface Enumeration<E> { boolean hasMoreElements(); E nextElement(); } public interface Iterator<E> { boolean hasNext(); E ...
  • 內部類的分類:常規內部類、靜態內部類、私有內部類、局部內部類、匿名內部類。 實例1:常規內部類 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //外部類 class Out { private int age = 12; ...
  • 一、首先說一下JDK中的動態代理: JDK中的動態代理是通過反射類Proxy以及InvocationHandler回調介面實現的 但是,JDK中所要進行動態代理的類必須要實現一個介面,也就是說只能對該類所實現介面中定義的方法進行代理,這在實際編程中具有一定的局限性,而且使用反射的效率也並不是很高。 ...
  • 在JDK的Collection中我們時常會看到類似於這樣的話: 例如,ArrayList: 註意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException。因此,為提 ...
  • 一、源碼解析 1、 LinkedList類定義。 LinkedList 是一個繼承於AbstractSequentialList的雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。LinkedList 實現 List 介面,能對它進行隊列操作。LinkedList 實現 Deque 介面,即能將 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...