排序算法之選擇排序

選擇排序的基本思想是每次從待排序的數(shù)據(jù)元素集合中選取最小或最大的數(shù)據(jù)元素放到數(shù)據(jù)元素集合的最前面或最后面,數(shù)據(jù)元素集合不斷縮小,當(dāng)數(shù)據(jù)元素集合為空時,選擇排序結(jié)束。常用的選擇排序有直接選擇排序和堆排序兩種。

1、直接選擇排序

直接選擇排序是從待排序的數(shù)據(jù)元素集合中選取最小的數(shù)據(jù)元素并將它與原始數(shù)據(jù)元素集合中的第一個數(shù)據(jù)元素交換位置;然后從不包括第一個位置上的數(shù)據(jù)元素的集合中選取最小的數(shù)據(jù)元素,并將它與原始數(shù)據(jù)元素集合中的第二個數(shù)據(jù)元素交換位置;如此重復(fù),直到數(shù)據(jù)元素集合中只剩下一個數(shù)據(jù)元素為止。
例如對無序表{56,12,80,91,20}采用簡單選擇排序算法進(jìn)行排序,具體過程為:

  1. 第一次遍歷時,從下標(biāo)為 1 的位置即 56 開始,找出關(guān)鍵字值最小的記錄 12,同下標(biāo)為 0 的關(guān)鍵字 56 交換位置:
  1. 第二次遍歷時,從下標(biāo)為 2 的位置即 56 開始,找出最小值 20,同下標(biāo)為 2 的關(guān)鍵字 56 互換位置:
  1. 第三次遍歷時,從下標(biāo)為 3 的位置即 80 開始,找出最小值 56,同下標(biāo)為 3 的關(guān)鍵字 80 互換位置:
  1. 第四次遍歷時,從下標(biāo)為 4 的位置即 91 開始,找出最小是 80,同下標(biāo)為 4 的關(guān)鍵字 91 互換位置:
1.1、 直接選擇排序的代碼實現(xiàn)
void SelectSort() {
    int arr[] = {56, 12, 80, 91, 20};
    int len = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < len; ++i) {
        int small = I;
        for (int j = i + 1; j < len; ++j) {
            if (arr[j] < arr[small])
                small = j;//記住最小元素的下標(biāo)
        }
        if (small != i) {//當(dāng)前最小元素的下標(biāo)不等于i時,交換兩者的值
            int temp = arr[i];
            arr[i] = arr[small];
            arr[small] = temp;
        }
    }
}
1.2、直接選擇排序的效率分析

在直接選擇排序中,第1次排序要進(jìn)行n-1(n表示數(shù)據(jù)元素的個數(shù))次比較,第二次排序要進(jìn)行n-2次比較,如此類推第n-1次排序需要比較1次,所以比較的從此次數(shù)為:
比較次數(shù)=(n-1)+(n-2)+···+1=n(n-1)/2

因此直接選擇排序算法的時間復(fù)雜度為O(n^2)。直接選擇排序算法的空間復(fù)雜度為 O(1)

2、堆排序

在直接選擇排序中,待排序的數(shù)據(jù)元素集合構(gòu)成一個線性結(jié)構(gòu),要從有n個數(shù)據(jù)元素的線性結(jié)構(gòu)中選出一個最小的數(shù)據(jù)元素比較n-1次。如果能把待排序的數(shù)據(jù)元素集合構(gòu)成一個完全二叉樹結(jié)構(gòu),則每次選擇出一個最小或最大的數(shù)據(jù)元素只需要比較完全二叉樹的深度值次,即lbn次,則排序算法的時間復(fù)雜度為O(nlbn)。這就是堆排序的基本思想。

2.1 堆的定義

堆分為最大堆(也稱大頂堆或大根堆)和最小堆(也稱小頂堆或小根堆)兩種。

最大堆是指在數(shù)組a中存放n個數(shù)據(jù)元素,數(shù)組下標(biāo)從0開始,如果當(dāng)數(shù)組下標(biāo)2i+1<n時有a[i]\geqa[2i+1],當(dāng)數(shù)組下標(biāo)2i+2<n時有a[i]\geqa[2i+2],則這樣的數(shù)據(jù)結(jié)構(gòu)稱為最大堆。如果把有n個數(shù)據(jù)元素的數(shù)組a中的元素看做一個完全二叉樹的n和結(jié)點,則a[0]對應(yīng)這完全二叉樹的樹根,a[1]對應(yīng)著樹根的左孩子結(jié)點,a[2]對應(yīng)著樹根的右孩子結(jié)點,a[3]對應(yīng)著a[1]的左孩子結(jié)點,a[4]對應(yīng)著a[1]的右孩子結(jié)點,如此等等。在此基礎(chǔ)上,只需要再調(diào)整所有非葉結(jié)點的數(shù)組元素,使之滿足條件:a[i]\geqa[2i+1]和a[i]\geqa[2i+2],這這樣的完全二叉樹就是一個最大堆。
如下圖所示是同一個數(shù)據(jù)序列的完全二叉樹和最大堆的表示圖例。

最小堆是指在數(shù)組a中存放的n和數(shù)據(jù)元素,數(shù)組的下標(biāo)從0開始,如果數(shù)組小標(biāo)2i+1<n時有a[i]\leqa[2i+1],當(dāng)數(shù)組下標(biāo)2i+2<n時有a[i]\leqa[2i+2],這樣的數(shù)據(jù)結(jié)構(gòu)稱為最小堆。
根據(jù)前面對于堆的定義可以推知堆有如下兩個性質(zhì)。

  1. 最大堆的根結(jié)點是堆中值最大的數(shù)據(jù)元素,最小堆的根結(jié)點是堆中最小的數(shù)據(jù)元素,
  2. 對于最大堆,從根結(jié)點到每個葉結(jié)點的路徑上,數(shù)據(jù)元素組成的序列都是遞減有序的;對于最小堆,從根結(jié)點到每個葉結(jié)點的路徑上,數(shù)據(jù)元素組成的序列都是遞增有序的。
2.2 創(chuàng)建堆

要進(jìn)行堆排序,首先要創(chuàng)建堆。按非遞減序列排序時,要創(chuàng)建最大堆。設(shè)數(shù)組a中存放了n個數(shù)據(jù)元素,若把數(shù)組a中這n個數(shù)據(jù)元素看做是一個完全二叉樹的n個結(jié)點,則這棵有n個結(jié)點的完全二叉樹采用了順序存儲結(jié)構(gòu)。但是完全二叉樹還不一定滿足最大堆的定義。要讓一棵完全二叉樹滿足最大堆的定義,需要從完全二叉樹的葉結(jié)點端開始逐個結(jié)點進(jìn)行調(diào)整,使它們滿足最大堆的定義。
在一棵順序存儲結(jié)構(gòu)存儲的完全二叉樹中,所有葉結(jié)點都滿足最大堆的定義。對于第1個非葉結(jié)點ai,由于其左孩子結(jié)點a[2i+1]和右孩子結(jié)點a[2i+2]都已經(jīng)是最大堆,所以只需要首先找出a[2i+1]結(jié)點和a[2i+2]結(jié)點的較大者,然后比較這個較大者結(jié)點和a[i]結(jié)點。如果a[i]結(jié)點大于或等于這個較大的結(jié)點,則以a[i]結(jié)點為根結(jié)點的完全二叉樹已經(jīng)滿足最大堆的定義;否則對換a[i]結(jié)點和這個較大的結(jié)點,對換后,以a[i]結(jié)點為根結(jié)點的完全二叉樹滿足最大堆的定義。按照這樣的方法,再調(diào)整第2棵非葉結(jié)點a[i-1],第3棵非葉結(jié)點a[i-2],直到最后調(diào)整根結(jié)點a[0]。當(dāng)根結(jié)點調(diào)整完成后,這課二叉樹就是一個最大堆。
對于一個數(shù)組序列{10,50,32,5,76,9,40,88}對應(yīng)的完全二叉樹如下所示。

  1. 首先調(diào)整第1個非葉結(jié)點(即下標(biāo)為i=(n-1)/2=3的數(shù)組元素),交換88和5的位置。


  2. 調(diào)整第2個非葉結(jié)點(即下標(biāo)為i=2的數(shù)組元素),交換40和32的位置。


  3. 調(diào)整第3個非葉結(jié)點(即下標(biāo)為i=1的數(shù)組元素),交換88和50的位置。


  4. 跳幀下標(biāo)為0的數(shù)據(jù)元素即根結(jié)點。在調(diào)整根結(jié)點時,將引起數(shù)據(jù)元素88和數(shù)據(jù)元素76的上移,數(shù)據(jù)元素10最終下移存放在數(shù)據(jù)元素76的位置。


當(dāng)完全二叉樹中某一個非葉結(jié)點ai的左孩子結(jié)點a[2i+1]和右孩子結(jié)點a[2i+2]都已經(jīng)是最大堆后,調(diào)整非也結(jié)點a[i]使之滿足最大堆的函數(shù)代碼實現(xiàn)如下:

//調(diào)整非葉結(jié)點a[i]使之滿足最大堆
void CreateHeap(int a[],int len, int i) {
    int h = i;//要創(chuàng)建堆的二叉樹根結(jié)點下標(biāo)
    int j = 2 * h + 1;//根結(jié)點的左孩子結(jié)點下標(biāo)
    int temp = a[h];
    int flag = 0;
    while (j < len && flag != 1) {
        //尋找左右孩子結(jié)點中的較大者,j為其下標(biāo)
        if (j < len - 1 && a[j] < a[j + 1]) {
            j++;
        }
        //左右孩子結(jié)點的較大者和a[h]結(jié)點比較
        if (temp > a[j]) {
            flag = 1;
        } else {
            a[h] = a[j];
            h = j;
            j = 2 * h + 1;
        }
        a[h] = temp;
    }
};
//初始化創(chuàng)建最大堆:從第一個非葉結(jié)點a[i](i=(len-2)/2)開始到根結(jié)點a[0]位置循環(huán)調(diào)用CreateHeap函數(shù)。
void InitCreateHeap(int a[],int len){
    for (int k = (len-2)/2; k > 0; k--) {
        CreateHeap(a,len,k);
    }
};
2.3 堆排序的實現(xiàn)

堆排序的過程:首先把有n個元素的數(shù)組a初始化創(chuàng)建為最大堆,然后循環(huán)執(zhí)行如下過程直到數(shù)組為空為止。① 把堆頂a[0]元素(為最大元素)和當(dāng)前最大堆的最后一個元素交換;② 最大堆元素個數(shù)減1;③由于第①步后根結(jié)點不再滿足最大堆的定義,所以調(diào)整根結(jié)點使之滿足最大堆的定義。
堆排序算法代碼如下:

void HeapSort(){
    int arr[] = {56, 12, 80, 91, 20};
    int len = sizeof(arr) / sizeof(arr[0]);
    InitCreateHeap(arr,len);
    for (int i = len-1; i >0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        CreateHeap(arr,i,0);
    }
    for (int k = 0; k < len; ++k) {
        printf("%d ", arr[k]);
    }
}

如下圖所示是數(shù)組序列{10,50,32,5,76,9,40,88}的堆排序的排序過程圖。

  1. 堆排序算法是基于完全二叉樹的排序。把一個完全二叉樹調(diào)整為堆,以及每次堆頂元素交換后進(jìn)行調(diào)整的時間復(fù)雜度為O(lbn),所以堆排序算法的時間復(fù)雜度為O(lbn)
  2. 堆排序算法的空間復(fù)雜度為O(1)
  3. 堆排序算法是一種不穩(wěn)定的排序方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。