常見排序演算法總結

JoJo_bai發表於2021-03-17

氣泡排序:

基本思路:每次冒泡總會將當前陣列最大資料通過相鄰資料比較交換,排到陣列尾,因此一次冒泡至少排好一個數 據的順序,經過n次冒泡就會將當前陣列排好順序。

空間複雜度:O(1),因為只涉及相鄰資料互換,所以只需要常量級的臨時空間,是原地排序演算法。

穩定性:在氣泡排序中只有交換才改變資料順序,而為了保證資料穩定性,當兩個資料相等時不進行資料互換。

時間複雜度:最好情況下,資料已經是有序的了,只需要進行一次冒泡,因此時間複雜度為O(n),最壞情況下資料 完全是倒敘的,因此需要N次冒泡,時間複雜度為O(n2)。平均時間複雜度為O(n2)

插入排序

基本思路:將資料中的陣列分為兩個區間分別為已排序區間和未排序區間,每次將未排序區間中的一個元素挑出插 入已排序區間中的合適位置,直到未排序區間中的元素為空,排序結束。

空間複雜度:因為不需要額外的空間,所以空間複雜度為O(1),是原地排序演算法。

穩定性:當未排序區間元素插入已排序區間時,碰到相同的元素,後排的元素插入已經排好元素的後方,就不會打 亂相同元素的原相對順序,因此屬於穩定排序。

時間複雜度:最好情況下,陣列為有序的,因此不需要進行插入操作,只需從頭到尾進行一次遍歷比較即可,因此 時間複雜度為O(n),最壞情況下,陣列為倒序,每次插入相當於在陣列第一個位置插入新的資料,因 此時間複雜度為O(n2)。平均時間複雜度為O(n2)。

選擇排序

基本思路:選擇排序與插入排序類似,也是分為已排序區間和未排序區間,不同的是選擇排序選擇未排序區間最小 值放在已排序區間末尾之後的一個元素也就是未排序區間開頭,而未排序區間開頭的元素放在原未排 序區間最小值的位置,簡而言之選擇未排序區間最小值與未排序區間首值進行互換。直到未排序區間 元素個數為零。

空間複雜度:因為不需要額外的空間,所以空間複雜度為O(1),是原地排序演算法。

穩定性:不穩定,因為其中涉及到元素互換,所以會導致大小相同元素的相對位置被打亂。(為什麼互換而不能插 入,因為空間複雜度為一,若像插入排序那樣將最小值插入已排序區間末尾,會導致未排序首位值被覆 蓋,造成資料丟失)

時間複雜度:最好,最壞,平均都為O(n2)。

歸併排序

基本思路:將整個陣列分開成小部分,分別排序後再合併到一起。關鍵在於合併步驟,假設要合併的陣列分別為A 和B,那麼我們利用一個臨時陣列C,兩個指標a,b初始位置分別位於A,B陣列首位,然後將a,b所指內容 進行比較,將較小資料放入C中,然後將該指標後移一位,繼續比較a,b直到某一陣列為零,然後將另 一陣列剩餘資料拷貝進C中。C中的資料即為合併完成的資料,然後拷貝回原陣列中。其實排序是在合 並步驟中完成的。

空間複雜度:不是原地排序演算法,因為合併過程中需要臨時陣列C,所以需要額外空間,空間複雜度為O(n).

穩定性:穩定,是否穩定主要看合併過程,當我們對A,B 陣列進行比較時,如果碰到相同資料,只需要將A陣列數 據放入C中就會使相同資料原相對位置不變,也就達到了穩定排序。

時間複雜度:遞迴程式碼時間複雜度,假設求解A的時間為T(a),A分解為子問題B,C 求解時間分別為T(b),T(c),那麼 T(a)=T(b)+T(c)+K,K為b與c合併的時間。假設A的長度為n,那麼將A分為兩等份的話時間為 2*T(n/2),且合併兩個有序陣列的時間複雜度為O(n),所以分析長度為n的陣列時間複雜度為:

T(n) = 2*T(n/2) + n

= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n

= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n

= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
......

= 2^k * T(n/2^k) + k * n

所以當n/2^k=1時,k=log2n,所以時間複雜度為O(nlogn).

快速排序:

基本思路:在陣列A中選擇一個隨機資料a,令quick_sort()函式可以將陣列A中大於a的數放在a的前方,小於a的數放在a的後方,那麼遞推公式為quick_sort(p.....r)=quick_sort(p.....q-1)+quick_sort(q-1.....r).截止條件為p>=r。

1,關於放置方法,若不考慮空間複雜度,則可以類似於選擇排序,設定兩個臨時陣列B C ,將A陣列的資料與a進行比較,將較大的數放在B,較小的數放在C,再將陣列B與C的資料複製回A,將a放在中間。但是這種方法並不是原地排序方法。並不常用。

2,若要原地排序,則類似於插入排序,將陣列A分為已排序區間與未排序區間,將未排序區間的數與a進行比較,若小於a,則將該數放在已排序區間末尾,這裡可以選擇將需要放置的數與被放置位置的數進行交換,從而避免插入過程中的資料搬移使得時間損耗上升,而由於交換操作會導致相同資料原位置被打亂,所以這種方法是不穩定的。

所以快排一般為原地,但並不穩定的排序方法。

空間複雜度:O(1)。

時間複雜度:由於這裡也是用的遞迴思想,那麼對於歸併排序的思想這裡同樣適用,時間複雜度為O(nlogn)。但是在極端情況下會退化為O(n2).

優化:之所以會造成極端情況,是因為分割槽點選擇不合理,合理的分割槽點選擇是要陣列資料在分割槽點兩邊資料數量差不多。分割槽點選擇方法,一個是三數取中法,即在資料區間首,中,尾各取一數,選擇中間數作為分割槽點,而如果陣列資料較大,則可以“五數取中”“十數取中”。還有一個是隨機法。

 

桶排序:

不基於比較,但是對資料又較為嚴格的要求:

1.要求資料可以很容易的分成m個桶,並且桶與桶之間有天然的大小關係,從而可以不用對桶進行排序,

2.要求資料在桶中分散式比較均勻的

3.桶排序比較適合外部排序,也就是資料儲存在外部磁碟中,資料量很大無法全部載入到記憶體中進行排序。

基本思路:將全部資料均勻的分到桶中,然後對每個桶進行快排,之後按照桶的大小將其中資料依次取出。

時間複雜度:假設有n個資料,分在m個桶中,那麼每個桶中的資料就為k=n/m個,對m個桶種

資料進行快排,時間複雜度為O(m*k log(n/m))也就是O(n log(n/m)),當m接近n時,時間複雜度也就較接近O(n)。

堆排序:

不穩定,原地,時間複雜度:O(nlogn)

基本思路:

1.建堆,就是將陣列原地組建成堆的資料結構,這其中利用到了堆化操作,堆化就是將堆中子節點與其葉子節點盡行比較,將其中最大的(或最小)的資料放到子結點處,完成後比較下一個子節點,直到所有子節點全部完成該操作。

程式碼如下:

void  heapify(int a[], int n,int i)//堆化
{
    while (true)
    {
        int maxpos = i;
        if (i*2<=n && a[i] < a[2 * i]) maxpos = 2 * i;
        if (i*2 +1<= n && a[maxpos] < a[2 * i + 1]) maxpos = 2 * i + 1;
        if (maxpos == i) break;
        swap(a, i, maxpos);
        i=maxpos;//為了將使下降的原主節點進行下一輪堆化,直到最底層的葉節點
    }
}
void  buildheap(int a[], int n)//建立堆
{
    for (int i = n / 2; i >= 1; --i)
    {
        heapify(a,n,i);
    }
}

2.排序,這個過程有點類似於刪除堆頂操作,將堆頂元素與堆底元素進行交換,這樣該陣列最大的數放在了位於n的位置,然後將剩下的n-1資料進行堆化,重複這個操作直到堆中剩下下標為1的一個元素,排序完成。

程式碼如下:

void heapsort(int a[], int n)
{
    for (int i = n; i > 1; --i)
    {
        swap(a, 1, i);
        buildheap(a, i-1);
    }
    return;
}

時間複雜度:每個節點堆化的過程都會與葉子節點進行比較與交換,所以每個節點的時間複雜度與該節點所在的高度成正比,堆化時間複雜度為O(n),排序時間複雜度為O(n/logn),所以整個堆排序的時間複雜度為O(n/logn)。

穩定度:不穩定,因為在排序過程中回進行資料交換。

為什麼快排效能比堆排序好,主要有兩個方面原因:

1.快速排序的資料訪問順序時順序訪問,但是堆排序的資料訪問是跳著訪問的,這樣對CPU快取不友好。

2.對於資料相同的陣列,堆排序的資料交換次數多於快排。

相關文章