高級排序演算法實現與優化

来源:https://www.cnblogs.com/geyouneihan/archive/2018/09/23/9693848.html
-Advertisement-
Play Games

本文用到的測試數據生成的代碼和分析: "《測試數據自動生成》" 文章圖片來源於 GitHub,網速不佳的朋友 "請點我看原文" 。 順便軟廣一下個人技術小站: "godbmw.com" 。歡迎常來 ♪\(^∇^\ \) 1. 談談高級排序 本文主要介紹高級排序演算法中的歸併排序和快速排序。他們有運用了 ...


本文用到的測試數據生成的代碼和分析:《測試數據自動生成》

文章圖片來源於 GitHub,網速不佳的朋友請點我看原文

順便軟廣一下個人技術小站:godbmw.com。歡迎常來 ♪(^∇^*)

1. 談談高級排序

本文主要介紹高級排序演算法中的歸併排序和快速排序。他們有運用了分支思想,並且大多通過遞歸來實現。

對於歸併排序,分為自上向底和自底向上排序。對於快速排序,有常見的二路快排和系統級常用的三路快速排序。

2. 歸併排序

2.1 設計和分析

在演算法思想上:歸併排序是分治法,所以需要等分數組,並且逐個完成排序,然後再合併在一起。而因為等分,所以樹結構是平衡的(快速排序就不一定,需要進一步優化)。

在空間使用上:歸併排序需要開啟輔助空間,所以,在演算法效率上自然比不上快速排序。

2.2 自頂向下的歸併

2.2.1 三處優化

第一處優化是關於選取中間索引值的問題。顯然,使用(l + r) / 2可能會造成溢出。

所以,此處應該是:int mid = l + (r - l) / 2;

同時,不能是 r + (l - r) / 2 。 比如: l = 0, r = 1 的時候,這條式子的結果和(l + r) / 2不同。因為 c++的自動向下取整

第二處優化是關於遞歸到底層的時候,比如被切分出來的數據長度小於 15,此時可以使用插入排序來優化。

第三處優化是當歸併前,先判斷前一部分數據的最後一個值和後一部分數據最後一個值的大小關係,再決定是否優化。

2.2.2 代碼實現

實現中比較困難的部分是歸併過程,在處理邊界的時候,需要特別註意。示意圖如下:

// 將 arr[l, ... , mid] 和 arr[mid, ... , r]兩個部分進行歸併
template <typename T>
void __merge(T arr[], int l, int mid, int r) {
    T* aux = new T[r - l + 1]; // 輔助空間
    for(int i = l; i<=r; i++) {
        aux[i - l] = arr[i];
    }
    int i = l, j = mid + 1;
    for(int k = l; k <= r; k++) {
        if( i > mid) {
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) {
            arr[k] = aux[i - l];
            i++;
        } else if(aux[i - l] < aux[j - l]) {
            arr[k] = aux[i - l];
            i++;
        } else {
            arr[k] = aux[j - l];
            j++;
        }
    }
    delete[] aux;
}

// 遞歸使用歸併排序, 對arr[l, ... , r]的範圍的數據進行排序
template <typename T>
void __mergeSort(T arr[], int l, int r) {
    // 遞歸到底層的情況
    if( r - l <= 15 ){
        SortBase::insertionSort(arr, l, r);
        return;
    }

    int mid = l + (r - l)/2;

    // 防止溢出:同時,不能是 r + (l - r) / 2 。 比如: l = 0, r = 1

    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid + 1, r);
    if(arr[mid] > arr[mid + 1]) {
        __merge(arr, l, mid, r);
    }
}

template <typename T>
void mergeSort(T arr[], int n) {
    __mergeSort(arr, 0, n-1);
}

2.3 自底向上的歸併

自底向上的歸併排序不如自頂向下的歸併好理解。但是可以不寫遞歸函數,並且可以訪問數組索引。

有道面試題:對於一個鏈表(每個節點存儲一個數據),要求在 O(NlogN)時間內完成排序,並且使用常數級別的空間。利用的就是

先看自底向上的歸併的實現,就會有思路了:

template <typename T>
void mergeSortBU(T arr[], int n) {
    int min_size = -1;
    for(int sz = 1; sz <= n; sz += sz) {
        for(int i = 0; i + sz < n; i += sz + sz) { // i + sz < n: 保證第二部分的數組存在,並且 i + sz -1 不越界
//                 對 arr[i, ... ,i+sz-1] 和 [i+sz, ... ,i+2*sz-1] 進行歸併
            if(arr[i + sz - 1] > arr[i + sz]) {
                __merge(arr, i, i + sz -1, min(i + sz + sz -1, n-1));
            }
        }
    }
}

這段代碼是針對數組的,如果針對鏈表,只需要移動指針即可,而空間也可以新開一個指針空間做原地操作。>>>請看這篇博文

3. 快速排序

3.1 二路快速排序

3.1.1 三處優化

第一處優化是關於遞歸到底層的時候,比如被切分出來的數據長度小於 15,此時可以使用插入排序來優化。

第二處優化是:隨機選擇標定元素。一般的快排選定的是最左邊的元素作為標定元素,排序後的數組標定元素移動到應該所處的位置,其前面是比他小的元素,後面是比他大的元素。

然而,無法保證快速排序遞歸樹的平衡度。比如:2, 2, 2,..., 2, 1 近乎有序且有大量重覆的數組。如果選定最左邊,快速排序就會退化到 O(N*N)。如下圖所示:

優化方法是:隨機選擇一個元素,與第一個元素交換後作為標定元素。這樣可以保證遞歸樹深度的期望值是 logN。

第三處優化是針對數組中有大量重覆元素的情況。在執行partition操作的時候,判斷是否移動交換元素的條件算上=即可。(具體可以看之後代碼)

3.1.2 代碼實現

template <typename T>
int __partition2(T arr[], int l, int r) {
    swap(arr[l], arr[rand()%(r - l + 1) + l]); // 隨機化防止樹不平衡
    T v = arr[l];

//        arr[l+1, ... , i) <= v; arr(j, ... , r] >= v
    int i = l + 1, j = r;
    while(true) {
        while(i <= r && arr[i] < v) i++;
        while(j >= l+1 && arr[j] > v) j--;
        if(i > j) break;
        swap(arr[i], arr[j]);
        i ++;
        j --;
    }
    swap(arr[l], arr[j]);

    return j;
}


template <typename T>
void __quickSort(T arr[], int l, int r) {
    if(r - l <= 15) {
        SortBase::insertionSort(arr, l, r);
        return;
    }
    int p = __partition2(arr, l, r);
    __quickSort(arr, l, p-1);
    __quickSort(arr, p+1, r);
}

template <typename T>
void quickSort(T arr[] ,int n) {
    srand(time(NULL));
    __quickSort(arr, 0, n-1);
}

3.2 三路快速排序

三路排序和二路不同的是:將相同的元素單獨放在一起,每次遞歸不再參與排序。

代碼中各個邊界變數的含義如下圖所示:

代碼實現:

template <typename T>
void __quickSort3Ways(T arr[], int l, int r) {
    if(r - l <= 15) {
        SortBase::insertionSort(arr, l, r);
        return;
    }

    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    int lt = l; // arr[l + 1, ... , lt] < v
    int gt = r + 1; // arr[gt, ... ,r] > v
    int i = l + 1; // arr[lt + 1, ... , i) == v
    while( i < gt ) {
        if(arr[i] < v) {
            swap(arr[i], arr[lt + 1]);
            lt ++;
            i ++;
        } else if(arr[i] > v) {
            swap(arr[i], arr[gt - 1]);
            gt --;
        } else {
            i ++;
        }
    }
    swap(arr[l], arr[lt]);
    __quickSort3Ways(arr, l, lt-1);
    __quickSort3Ways(arr, gt, r);
}

4. 性能測試

4.1 測試隨機數據

為了保證普適性,先測試大量隨機數據的演算法表現:

#include <iostream>
#include "SortHelper.h"
#include "SortBase.h"
#include "SortAdvance.h"

using namespace std;

int main() {
    int n = 100000, left = 0, right = n;
    int *arr = SortTestHelper::generateRandomArray<int>(n, left, 5);
    int *brr = SortTestHelper::copyArray<int>(arr, n);
    int *crr = SortTestHelper::copyArray<int>(arr, n);
    int *drr = SortTestHelper::copyArray<int>(arr, n);

    SortTestHelper::testSort<int>(brr, n, SortAdvance::mergeSort<int>, "merge sort");
    SortTestHelper::testSort<int>(crr, n, SortAdvance::mergeSortBU<int>, "merge sort from bottom to up");
    SortTestHelper::testSort<int>(drr, n, SortAdvance::quickSort<int>, "quick sort");
    return 0;
}

結果如下:

對於特殊數據,例如含有大量重覆元素的數組:

// ...
int *arr = SortTestHelper::generateRandomArray<int>(n, left, 5);
// ...

結果如下圖所示:

4.2 1 億數據量測試

因為使用的 CLion 做了安全限制,所以當數組大小開到 100w 的時候,就報出堆棧錯誤。

換用了 DevC 來跑 1 億的數據,快排本來是 17s(忘記截圖了),再跑就是 27s,如下圖所示:

大家可以在自己電腦跑一下百度百科的快排,就知道優化的作用了 :)

5. 感謝

本篇博客是總結於慕課網的《學習演算法思想 修煉編程內功》的筆記,liuyubobobo 老師人和講課都很 nice,歡迎去買他的課程。


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

-Advertisement-
Play Games
更多相關文章
  • 給出題目! "題目界面" 那麼,大家一看一般是一臉矇蔽 因為這確實聽刁鑽,許多人不會打二維線段樹,卻一直在想線段樹怎麼打,可悲~~(大佬:花了5分鐘打出二維線段樹,好難!)~~,那摸,大家,這道題怎麼做? 接下來會涉及到離散化與線段樹,請自學,抱歉⊙﹏⊙ 那麼,這道題呢,重要的是掃描線(如圖): 那 ...
  • 4)為什麼介面中的屬性和方法都預設為public?Sun公司當初為什麼要把java的介面設計發明成這樣? 【新手可忽略不影響繼續學習】(視頻下載) (全部書籍)答:如上所述,馬克-to-win:既然介面強於抽象類能勝任作為和外部系統打交道的合同。換句話說,一般來講和外部系統打交道,自然考慮用“介面” ...
  • 1. String 下麵代碼創建了幾個對象? String s1 = new String("Hello"); String s2 = new String("Hello"); 要想答對這道題,需要考慮String的一個常量池的概念。在執行代碼的時候,首先會判斷字元串常量池中是否存在"Hello", ...
  • Django自帶的用戶認證 我們在開發一個網站的時候,無可避免的需要設計實現網站的用戶系統。此時我們需要實現包括用戶註冊、用戶登錄、用戶認證、註銷、修改密碼等功能,這還真是個麻煩的事情呢。 Django作為一個完美主義者的終極框架,當然也會想到用戶的這些痛點。它內置了強大的用戶認證系統--auth, ...
  • # 有1、2、3、4個數字,能組成多少個互不相同且無重覆數字的三位數?都是多少? 1 sum = 0 2 values = range(1, 5) 3 for i in values: 4 for j in values: 5 for k in values: 6 if i != j and j ! ...
  • 給定一個二叉樹,找出其最大深度。 二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。 說明: 葉子節點是指沒有子節點的節點。 示例:給定二叉樹 [3,9,20,null,null,15,7], 返回它的最大深度 3 。 通過此題掌握樹的運用 題目分析:求二叉樹的深度; 大家可以瀏覽二叉樹的基本 ...
  • 很多人學習python,爬蟲入門,在python爬蟲中,有很多庫供開發使用。 用於請求的urllib(python3)和request基本庫,xpath,beautiful soup,pyquery這樣的解析庫。其中xpath中用到大量的正則表示式,對於新手來說,寫正則很容易出錯,在這裡,從beau ...
  • Spring Cloud Hystrix是一個容錯庫,它實現了斷路器模式,使得當服務發生異常時,會自動切斷連接,並將請求引導至預設的回調方法。 服務端 在Spring Tool Suite的文件菜單中,點擊新建Spring Starter Project。建立一個普通的Restful風格的服務。 a ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...