十大經典排序算法+sort排序


本文轉自:十大經典排序算法,其中有動圖+代碼詳解,本文簡單介紹+我的理解。

排序算法

經典的算法問題,也是面試過程當中常常被問到的問題。排序算法簡單分類以下:html

這些排序算法的時間複雜度等參數以下:git

其中,n表明數據規模,k表明桶的個數,In-place表明不須要額外空間,Out-place表明須要額外的空間。面試

冒泡排序(Bubble Sort)

最簡單易懂的排序方法。每次比較兩個元素,若是順序錯誤,則交換之。重複地訪問整個序列,直到沒有元素須要交換。算法

算法描述

  • 比較相鄰的元素。若是順序錯誤,就交換之;
  • 遍歷序列,重複步驟一,一次遍歷後最大元素處於最右邊;
  • 重複步驟二,每次重複,能夠將一個元素移至相應位置(已排好),直至全部元素排好;

算法分析

最佳狀況:\(T(n) = O(n)\),最差狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)shell

參考代碼

void bubbleSort(vector<int> &nums) {
    int n = nums.size();
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < n-1-i; j++) {
            if(nums[j] > nums[j+1]) {
                // 元素交換 
                nums[j] = nums[j]^nums[j+1];
                nums[j+1] = nums[j]^nums[j+1];
                nums[j] = nums[j]^nums[j+1];
            }
        }
    }
}

選擇排序(Selection Sort)

最穩定的排序方法之一,不管什麼狀況時間複雜度都是 \(O(n^2)\),不須要額外空間。簡單直觀,每次找到未排序中的最小(最大)元素,放至相應位置,執行(n-1)次排序完成。api

算法描述

  • 遍歷無序序列,找到最小(最大)元素,將該元素與對應位置交換;
  • 重複步驟一(n-1)次,將全部元素放至相應位置,即排序完畢;

算法分析

最佳狀況:\(T(n) = O(n^2)\),最差狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)數組

參考代碼

void selectSort(vector<int> &nums) {
    int n = nums.size(), minIndex;
    for(int i = 0; i < n-1; i++) {
        minIndex = i;
        for(int j = i+1; j < n; j++) {
            if(nums[j] < nums[minIndex])//尋找最小元素 
                minIndex = j;
        }
        if(i == minIndex) continue;//相同位置元素不可異或交換
        // 元素交換 
        nums[i] = nums[i]^nums[minIndex];
        nums[minIndex] = nums[i]^nums[minIndex];
        nums[i] = nums[i]^nums[minIndex];
    }
}

插入排序(Insertion Sort)

一樣是一種簡單易懂的排序算法,不須要額外空間。經過構建有序序列,對於未排序元素,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,一般採用in-place排序(即只需用到O(1)的額外空間的排序),於是在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。數據結構

算法描述

通常來講,插入排序都採用in-place在數組上實現。具體步驟以下:ide

  • 第一個元素視爲已排序;
  • 取出下一元素,在已排序列中從後向前掃描;
  • 若是該已排序元素大於新元素,將該元素移到下一位置;
  • 重複步驟3,直到找到已排序的元素≤新元素的位置,將新元素插入到該位置後;
  • 重複步驟2~4,直至所有排好序。

算法分析

最佳狀況:\(T(n) = O(n)\),最壞狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)函數

參考代碼

void insertSort(vector<int> &nums) {
    int n = nums.size(), prev, num;
    for(int i = 0; i < n; i++) {
        prev = i-1;//有序序列尾部 
        num = nums[i];//當前元素 
        while(prev>=0 && nums[prev]>num) {
            nums[prev+1] = nums[prev];//後移
            prev--; 
        }
        nums[prev+1] = num;
    }
}

希爾排序(Shell Sort)

第一個突破O(n^2)的排序算法,是簡單插入排序的改進版。它與插入排序的不一樣之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。

希爾排序的核心在於間隔序列的設定。既能夠提早設定好間隔序列,也能夠動態的定義間隔序列。動態定義間隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。

算法描述

  • 定義增量序列:{t1,t2,...,tk},其中通常有 t1>t2>...>tk=1;
  • 按增量序列個數k,對序列進行k 趟排序;
  • 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子序列進行直接插入排序。僅增量因子爲1 時,整個序列做爲一個序列來處理,序列長度即爲整個序列的長度。

舉例說明

上面的算法描述可能不是很好懂,舉個例子說明一下。對與{5, 2, 4, 1, 5, 9, 7, 8, 9, 0}序列,第一趟排序,增量t1=4(自定義),序列分爲{5, 5, 9},{2,9,0},{4,7},{1,8},分別對其進行插入排序,序列變爲{5,0,4,1, 5,2,7,8, 9,9};第二趟排序,增量t2=2,序列分爲{5,4,5,7,9},{0,1,2,8,9},對其進行插入排序,序列變爲{4,0, 5,1, 5,2, 7,8, 9,9};第三趟排序,增量t3=1,序列爲{4,0,5,1,5,2,7,8,9,9},對其進行插入排序,變爲{0,1,2,4,5,5,7,8,9,9}。

算法分析

希爾排序的時間複雜度和其增量序列有關係,這涉及到數學上還沒有解決的難題;不過在某些序列中複雜度能夠視爲O(n^1.3);希爾排序時間複雜度的下界是n*log^2 n。

參考代碼

void shellSort(vector<int> &nums) {
    int n = nums.size();
    int gap, i, j;
    
    for(gap = n/2; gap > 0; gap /= 2) {
        //插入排序簡潔寫法 
        for(i = gap; i < n; i++) {
            int num = nums[i];
            for(j = i-gap; j>=0 && nums[j]>num; j-=gap)
                nums[j+gap] = nums[j];
            nums[j+gap] = num;
        }
    }
}

歸併排序(Merge Sort)

和選擇排序同樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,時間複雜度始終都是O(n log n)。代價是須要額外的內存空間。

歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。歸併排序是一種穩定的排序方法。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。

歸併排序有很多的應用,好比求解逆序對問題,只須要在歸併排序的過程當中添加一行代碼就能夠。

算法描述

  • 把長度爲n的輸入序列分紅兩個長度爲n/2的子序列;(分)
  • 對這兩個子序列分別採用歸併排序;
  • 將兩個排序好的子序列合併成一個最終的排序序列。(合)

合併的過程須要額外的空間,利用一個新數組,比較兩個子序列,不斷將較小元素加入新數組,最後再將新數組更新至原序列。

算法分析

最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(nlogn)\),平均狀況:\(T(n) = O(nlogn)\)

空間複雜度爲\(O(n)\)

參考代碼

void Merge(vector<int> &nums, int first, int med, int last) {
    int i = first, j = med+1;
    
    vector<int> temp(nums.size());//額外空間 
    int cur=0;//當前位置
    while(i<=med && j<=last) {
        if(nums[i] <= nums[j])
            temp[cur++] = nums[i++];
        else
            temp[cur++] = nums[j++];
    }
    while(i <= med)
        temp[cur++] = nums[i++];
    while(j <= last)
        temp[cur++] = nums[j++];
        
    for(int m = 0; m < cur; m++)//更新數組 
        nums[first++] = temp[m];
}
void mergeSort(vector<int> &nums, int first, int last) {
    if(first < last) {
        int med = first+(last-first)/2;
        mergeSort(nums, first, med);
        mergeSort(nums, med+1, last);
        Merge(nums, first, med, last);
    }
}

快速排序(Quick Sort)

基本思想:選定一個排序基準進行一趟排序,將全部元素分爲兩部分(大於基準和小於基準),分別對兩部分在此進行快速排序。

快速排序能夠用於求解第K大問題,由於每一次排序以後,能夠固定一個元素。

算法描述

快速排序使用分治法來把一個序列分爲兩個子序列。具體算法描述以下:

  • 從序列中選取排序基準(pivot);
  • 對序列進行排序,全部比基準值小的擺放在基準前面,全部比基準值大的擺在基準的後面,序列分爲左右兩個子序列。稱爲分區操做(partition);
  • 遞歸,對左右兩個子序列進行快速排序。

算法分析

最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(n2)\),平均狀況:\(T(n) = O(nlogn)\)

參考代碼

void quickSort(vector<int> &nums, int left, int right) {
    if(left<right) {
        int l=left, r=right;
        int pivot = nums[left];//判斷標準值 
        while(l<r) {
            while(l<r && nums[r]>=nums[l])//必定記住要加等於號,在下面加也行
                r--;
            swap(nums[l], nums[r]);
            
            while(l<r && nums[l]<nums[r])//在這裏加等於號也行,但必須有一個加
                l++;
            swap(nums[l], nums[r]);
        }
        nums[l]=pivot;
        
        quickSort(nums, l+1, right);
        quickSort(nums, left, l-1);
    }
}

堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。

算法描述

  • 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區;
  • 將堆頂元素R[1]與最後一個元素R[n]交換,此時獲得新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且知足R[1,2…n-1]<=R[n];
  • 因爲交換後新的堆頂R[1]可能違反堆的性質,所以須要對當前無序區(R1,R2,……Rn-1)調整爲新堆,而後再次將R[1]與無序區最後一個元素交換,獲得新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。

可能看起來看起來有點複雜,在本文的參考連接中有動圖解釋,可能好容易理解一些。

算法分析

最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(nlogn)\),平均狀況:\(T(n) = O(nlogn)\)

參考代碼

int len;
void heapify(vector<int> &nums, int i) {
    int left = 2*i+1;
    int right = 2*i+2;
    int largest = i;
    
    if(left<len && nums[left] > nums[largest])
        largest = left;
    if(right<len && nums[right] > nums[largest])
        largest = right;
        
    if(largest != i) {
        swap(nums[i], nums[largest]);
        heapify(nums, largest);
    }
}
void buildMaxHeap(vector<int> &nums) {
    len = nums.size();
    for(int i = len/2; i>=0; i--)
        heapify(nums, i);
}
void heapSort(vector<int> &nums) {
    buildMaxHeap(nums);
    
    for(int i = nums.size()-1; i>0; i--) {
        swap(nums[0], nums[i]);
        len--;
        heapify(nums, 0);
    }
}

計數排序(Counting Sort)

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 做爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有肯定範圍的整數。

計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素C[i]是待排序數組A中值等於i的元素的個數。而後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。這種作法其實就是map的基本用法。

計數排序限制性太大,要求必須是肯定範圍的整數。實際作題中根本用不到,不過在某些特殊場景中可能能夠用上。

算法描述

  • 找出待排序的數組中最大和最小的元素;
  • 統計數組中每一個值爲i的元素出現的次數,存入數組C的第i項;
  • 對全部的計數累加(從C中的第一個元素開始,每一項和前一項相加);
  • 反向填充目標數組:將每一個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。

算法分析

當輸入的元素是n 個0到k之間的整數時,它的運行時間是 \(O(n + k)\)。計數排序不是比較排序,排序的速度快於任何比較排序算法。因爲用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。

最佳狀況:\(T(n) = O(n+k)\),最差狀況:\(T(n) = O(n+k)\),平均狀況:\(T(n) = O(n+k)\)

參考代碼

void countingSort(vector<int> &nums, int maxValue) {
    int bucket[maxValue+1] = {0};
    int n = nums.size();
    int sorted = 0;
    
    for(int i = 0; i < nums.size(); i++) {
        bucket[nums[i]]++;
    }
    for(int i = 0; i < maxValue+1; i++) {
        while(bucket[i] > 0) {
            nums[sorted++] = i;
            bucket[i]--;
        }
    }
}

桶排序(Bucket Sort)

桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定(代碼中經過設定每一個桶的容量間接設定此映射關係)。

桶排序 (Bucket sort)的工做的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每一個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序。

算法描述

  • 設置桶的數量:先設定桶的容量,再根據數組的最大值和最小值計算出桶的數量;
  • 遍歷輸入數據,而且把數據一個一個放到對應的桶裏去;
  • 對每一個不是空的桶進行排序:因爲每一個桶數據量很小,能夠採用插入排序;
  • 將非空桶裏排好序的數據拼接起來。

算法分析

桶排序最好狀況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,由於其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。

最佳狀況:T(n) = O(n+k),最差狀況:T(n) = O(n+k),平均狀況:T(n) = O(n2)。

參考代碼

void bucketSort(vector<int> &nums, int bucketSize) {
    int n = nums.size();
    if(n == 0) return;
    
    int minValue = nums[0], maxValue = nums[0];
    for(int i = 1; i < n; i++) {
        if(nums[i] > maxValue) maxValue = nums[i];
        if(nums[i] < minValue) minValue = nums[i];
    }
    
    if(bucketSize < 5) bucketSize = 5;//默認每一個桶的容量爲5
    int bucketNum = (maxValue-minValue)/bucketSize + 1;//桶的數量
    
    vector< vector<int> > buckets(bucketNum);
    
    for(int i = 0; i < nums.size(); i++)
        buckets[(nums[i]-minValue)/bucketSize].push_back(nums[i]);
        
    int sorted = 0;
    for(int i = 0; i < buckets.size(); i++) {
        insertSort(buckets[i]);//插入排序 
        for(int j = 0; j < buckets[i].size(); j++)
            nums[sorted++] = buckets[i][j];
    }
}

基數排序(Radix Sort)

基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,複雜度爲O(kn),n爲數組長度,k爲數組中的數的最大的位數;

基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此是穩定的。

算法描述

  • 取得數組中的最大數,並取得位數;
  • nums爲原始數組,從最低位開始取每一個位組成radix數組;
  • 對radix進行計數排序(利用計數排序適用於小範圍數的特色);

算法分析

最佳狀況:\(T(n) = O(n * k)\),最差狀況:\(T(n) = O(n * k)\),平均狀況:\(T(n) = O(n * k)\)

基數排序有兩種方法:MSD,從高位開始進行排序;LSD,從低位開始進行排序。

參考代碼

//LSD
void redixSort(vector<int> &nums, int maxDigit) {
    int mod = 10;
    int dev = 1;
    vector< vector<int> > buckets(10);
    
    for(int i = 0; i < maxDigit; i++, dev*=10, mod*=10) {
        for(int j = 0; j < nums.size(); j++) {
            int bid = nums[j] % mod / dev;//取出對應數位做爲桶編號
            buckets[bid].push_back(nums[j]); 
        }
        
        int sorted = 0;
        for(int i = 0; i < buckets.size(); i++) {
            for(int j = 0; j < buckets[i].size(); j++)
                nums[sorted++] = buckets[i][j];
            buckets[i].clear();
        }
    }
}

總結

冒泡排序是基礎,每輪遍歷將「最大元素」移至正確位置(「最右邊」),不穩定的O(n^2);

選擇排序要了解,選擇排序每輪遍歷將「最小(大)元素」移至正確位置(「最左(右)邊」),穩定的O(n^2);

插入排序最簡單,適合數據量較小的排序,依然是O(n^2);

希爾排序是插入排序升級版,很差用,爲O(nlog^2n);

歸併排序和快速排序要熟記原理並會寫代碼。時間複雜度都是O(nlogn),前者不穩定,後者穩定。最經常使用的排序方法。

堆排序代碼複雜,不太好理解,也很差用,爲O(nlogn)。

計數排序、桶排序、基數排序都不是比較排序,能夠歸爲一類,對數據有特殊的要求。其中計數排序是基礎,相似創建map對應;桶排序將數據的大小分到不一樣的桶中,桶內再微小排序;基數排序則是屢次的桶排序,每次桶排序根據對應數位將數據分到不一樣桶中。


本文版權歸做者AlvinZH和博客園全部,歡迎轉載和商用,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.

相關文章
相關標籤/搜索