選擇排序之簡單選擇排序和堆排序
選擇排序的思想非常直接,不是要排序么?那好,我就從所有序列中先找到最小的,然后放到第一個位置。之后再看剩余元素中最小的,放到第二個位置……以此類推,就可以完成整個的排序工作了。可以很清楚的發現,選擇排序是固定位置,找元素。相比於插入排序的固定元素找位置,是兩種思維方式。
常見的選擇排序有:簡單選擇排序和堆排序。
簡單選擇排序
簡單選擇排序的思想是,從第一位置開始,逐漸向后,選擇后面的無序序列中的最小值放到該位置。很簡單,直接上代碼吧:
#include <stdio.h> #include <stdbool.h> void select_sort(int value[],int n) { int i = 0; for(i = 0;i < n - 1;i++) { int j = i; int min_index = i;//記錄最小值的下標 for(j = i + 1;j < n;j++) { if(value[j] < value[min_index]) { min_index = j; } } if(min_index != i)//將最小值放在正確的位置 { int temp = value[i]; value[i] = value[min_index]; value[min_index] = temp; } } } int main() { int value[] = {8,6,3,7,4,5,1,2,10,9}; int n = 10; select_sort(value,n); printf("排序結果為:\n"); int i = 0; for(;i < n;i++) { printf("%d ",value[i]); } printf("\n"); return 0; }
直接選擇排序和冒泡排序很像,不同的是冒泡排序要比較每兩個相鄰的元素並且交換(如果需要),而直接選擇排序則只需要將選擇出來的最小值與它應該所在的位置上的數進行交換即可。在這里也給出冒泡排序的代碼:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <stdbool.h> void bubble_sort(int value[],int n) { int i = 0; for(;i < n - 1;i++)//n-1趟 { int j = 0; bool tag = false; for(;j < n-i-1;j++)//依次進行兩兩比較 { if(value[j] > value[j+1]) { tag = true;//存在交換 int temp = value[j]; value[j] = value[j + 1]; value[j + 1] = temp; } } if(!tag)//不存在交換,說明已經有序,退出循環 break; } printf("進行了%d趟排序\n",i); } int main() { int value[] = {8,6,3,7,4,5,1,2,10,9}; int n = 10; bubble_sort(value,n); printf("排序結果為:\n"); int i = 0; for(;i < n;i++) { printf("%d ",value[i]); } printf("\n"); return 0; }
通過比較冒泡排序和簡單選擇排序的代碼就可以看到,兩者的比較操作都是在內層循環中進行的,比較操作的時間復雜度都是O(n2)。但是對於交換操作,冒泡排序是在內層循環,而簡單選擇排序是在外層循環中,所以對於交換操作,冒泡排序最好的情況是0次,最差的情況是n(n-1)/2(大概呀),簡單選擇排序的最好情況是0次,最差情況是n-1次。所以簡單選擇排序的效率要高於冒泡排序。
堆排序
堆的定義
若將和此序列對應的一維數組(即以一維數組作此序列的存儲結構)看成是一個完全二叉樹,則堆的含義表明,完全二叉樹中所有非終端結點的值均不大於(或不小於)其左、右孩子結點的值。
排序時我們通常使用大頂堆,小頂堆主要用於優先隊列。
對於一棵滿二叉樹或者完全二叉樹,我們可以用線性存儲結構(數組)來進行存儲。
比如對於完全二叉樹:
可以用數組:[0,1,2,3,4,5,6,7,8,9]來表示,節點之間的父子關系是:對於i節點,左孩子為2i+1,右孩子為2i+2.比如對於0號節點,他的孩子就是2*0+1=1,和2*0+2=2。所以用數組就可以表示完全二叉樹。之所以我們平時用鏈式結構表示二叉樹,是因為,如果二叉樹不是完全二叉樹,那么如果用數組表示,我們就必須為空節點留出存儲位置。比如:如果上圖沒有2號節點和其子節點,如果我們仍用數組存儲,我們依然需要長度為10的數組來表示這棵樹,否則節點和其子節點之間2i+1的關系就會破壞,也就沒有了節點之間的關系。這造成了存儲空間的浪費。
調整操作
對於完全二叉樹:
顯然不滿足大頂堆的定義,那我們怎么調整,使其滿足大頂堆的條件呢?
很簡單,就是找出6、5、21(也就是本節點和它的兩個孩子之間)三者之間的最大值,也就是21,然后交換6和21(也就是本節點和最大節點)即可。如下圖:
頂元素篩下操作
對於一個滿足大頂堆的完全二叉樹來說,樹根就是最大的元素,而我們排序,最大的元素應當放在數組的最后,所以我們要將最后一個元素的樹根元素進行交換,將樹根元素放在最后,這樣該元素就到了它應該在的位置。這個過程叫做頂元素篩下。如果我們反復進行頂元素篩下操作,那么最大值就會依次被放在正確的位置,也就完成了排序工作。但是要注意的是,沒篩選一個頂元素,就會使原有二叉樹不再滿足大頂堆的定義,這時候要進行調整。比如:
有大頂堆:
這是后,根元素10,為最大元素,我們進行頂元素篩下,也就是將最后一個元素4和10進行交換
這個時候10就到了正確的位置。但是我們發現完全二叉樹已經不滿足大頂堆了!這個時候要進行調整(注意,此時,調整的二叉樹應該是除了10以外的,因為10是剛剛篩下的,而且已經在正確的位置上了)。調整要從根節點開始:
此時我們發現,雖然9、4、8三個節點滿足了大頂堆。但是由於4和9的交換,導致4、7、5不滿足大頂堆,因此還要繼續調整,調整到什么時候呢?調整到葉子節點!(注意調整時已經不包括10這個節點了,所以5這個節點現在就是葉子節點了)。
此時已經到了最后一個非葉子節點,再次進行調整(如果需要)后,整棵樹就滿足大頂堆了。
調整完后,我們就要進行下一輪頂元素篩下和調整了。
接下來就不寫了。
總結一下就是,對於已經創建好的大頂堆,我們進行頂元素篩下操作,也就是將頂元素和最后一個元素進行交換,然后從頂元素開始,沿着交換路徑進行調整,直到葉子節點,整棵樹就滿足大頂堆了。然后就可以進行下一輪篩下了,反復進行,就可以達到排序的目的。需要注意的是,調整的過程,針對的二叉樹是不包含已經篩下的節點的,還要注意最后一個非葉子節點很可能沒有右孩子節點,這一點編程的時候要進行判斷,因為實際數組中那個位置很可能是我們剛剛篩下的那個元素,而調整時,是不包括已經篩下的元素的!!
建立大頂堆
利用頂元素篩下操作進行排序,前提是我們要先建立一個大頂堆。大頂堆的建立實際上也是一個類似頂元素篩下的調整過程,所不同的是,頂元素篩下的調整是對根元素來一次直到葉子節點的調整,而創建堆的過程是從最后一個非葉子節點到根節點,對這其中所有的節點都進行一次直到葉子節點的調整(如果需要)。
比如有完全二叉樹:
從我們要對從最后一個非葉子節點開始向上到根節點中,所有的節點進行調整,也就是一次對4、1、8、5、3節點進行一次直到葉子節點的調整。
對4號節點,也就是4元素:
對3號節點,也就是1元素:
對2號節點,也就是8元素:
對1號節點,也就是5元素:
注意,此時新的起點5和它葉子之間整好滿足大頂堆所以不需要繼續調整,如果碰到不滿足的要繼續調整,直到葉子節點。
對於0號節點,也就是3元素:
此時新的起點3,和它的葉子節點8、2並不滿足大頂堆,所以要繼續調整:
新的起點是葉子節點了,結束。
此時已經對從最后一個非葉子節點到根節點中所有的節點都調整了,此時,整棵樹已經滿足大頂堆了。
需要注意的就是,對每個節點的調整都要直到葉子節點為止(當然,如果這個過程中,已經滿足條件了,就不需要直到葉子節點了)
源代碼:
#include <stdio.h> /** * 求一個節點的父親節點的下標 * 參數為該節點的下標 * */ int get_parent(int index) { return (index - 1) >> 1; //按位右移比除法效率高 } /** * 獲取左子節點的下標 * 參數為該節點的下標 * */ int get_left(int index) { return (index << 1) + 1; //按位左移比乘法效率高 } /** * 獲取右子節點的下標 * 參數為該節點的下標 * */ int get_right(int index) { return (index << 1) + 2; } /** * 求最后一個非葉子節點的下標 * 數組下標從0開始 * 參數len為數組的長度 * */ int get_ln_leaf_i(int len) { //下標從0開始的數組,長度為len,則最后一個節點的下標為len-1,最后一個非葉子節點就是最后一個節點的父親節點 return get_parent(len - 1); } /** * 求數組中三個數的最大值的下標 * 參數為三個數的下標 * 返回值為最大值下標 * */ int get_max_index(int value[],int a,int b,int c) { int max_index; if(value[a] > value[b]) max_index = a; else max_index = b; if(value[max_index] < value[c]) max_index = c; return max_index; } /** * 從下標begin_index開始調整,使得以begin_index節點為根的樹滿足堆的要求 * 參數value表示待排序的數組,len是數組的長度,begin_index是開始調整的下標,ln_leaf_i是最后一個非葉子節點的下標 * */ static void adjust(int value[],int len,int begin_index,int ln_leaf_i) { do{ int left = get_left(begin_index); int right = get_right(begin_index); int max_index; //注意可能沒有右孩子 if(right >= len)//說明起始沒有右孩子 { if(value[begin_index] > value[left]) break; max_index = left; } else max_index = get_max_index(value,begin_index,left,right); if(max_index == begin_index)//如果本節點比兩個孩子都大,說明已經滿足堆的條件了,結束 break; //否則,交換該節點與最大值節點 //用異或的方式交換效率比較高 value[begin_index] ^= value[max_index]; value[max_index] ^= value[begin_index]; value[begin_index] ^= value[max_index]; //更新起始節點,繼續 begin_index = max_index; }while(begin_index <= ln_leaf_i);//直到最后一個非葉子節點 } /** * 初始化堆 * */ void init_heap(int value[],int len) { /** * 初始化堆的過程就是: * 從最后一個非葉子節點開始,向樹根, * 對這其中所有的節點,都進行依次adjust操作 * */ int ln_leaf_i = get_ln_leaf_i(len);//最后一個非葉子節點 int i = ln_leaf_i; for(;i >= 0;i--) { adjust(value,len,i,ln_leaf_i); } } void heap_sort(int value[],int len) { init_heap(value,len); printf("創建的堆為:\n"); int j = 0; for(;j < len;j++) { printf("%d ",value[j]); } printf("\n"); int i = len-1;//最后一個元素的下標 for(;i > 0;i--) { //交換最后一個節點和樹根元素 //用異或的方式交換效率比較高 value[0] ^= value[i]; value[i] ^= value[0]; value[0] ^= value[i]; /**從根節點開始進行調整 * 注意由於數組的最后一個元素已經在正確的位置了 * 所以只對[0...i-1]這個數組進行調整就好 * */ if(i == 1)//當只有一個元素時,就不需要篩下啦 break; int ln_leaf_i = get_ln_leaf_i(i);//這里i就是剩下的數組的長度 adjust(value,i,0,ln_leaf_i); } } int main() { int value[] = {3,5,8,1,4,10,2,7,6,9}; int n = 10; heap_sort(value,n); printf("排序結果為:\n"); int i = 0; for(;i < n;i++) { printf("%d ",value[i]); } printf("\n"); return 0; }
10月11日更新,修改了堆排序函數,使用了遞歸,思路更加清晰,簡潔
#include <stdio.h> /** * 獲取左子節點的下標 * 參數為該節點的下標 * */ int get_left(int index) { return (index << 1) + 1; //按位左移比乘法效率高 } /** * 獲取右子節點的下標 * 參數為該節點的下標 * */ int get_right(int index) { return (index << 1) + 2; } /** * 求數組中三個數的最大值的下標 * 參數為三個數的下標 * 返回值為最大值下標 * */ int get_max_index(int value[],int a,int b,int c) { int max_index; if(value[a] > value[b]) max_index = a; else max_index = b; if(value[max_index] < value[c]) max_index = c; return max_index; } //交換兩個數 void swap(int *a,int *b) { *a^=*b; *b^=*a; *a^=*b; } /** * 從下標begin開始調整,使得以begin節點為根的樹滿足堆的要求 * 參數value表示待排序的數組,len是數組的長度,begin是開始調整的下標 * * 遞歸調用,遞歸出口為:沒有左右孩子時,或者有孩子但是不需要調整時 * */ static void adjust(int value[],int len,int begin) { int left = get_left(begin);//左孩子下標 int right = get_right(begin);//右孩子下標 int max = begin;//記錄最大值的下標 if(right < len)//說明左右孩子都存在 { max = get_max_index(value,begin,left,right);//獲得最大值下標 } else//說明不存在右孩子 { if(left < len)//說明存在左孩子 { if(value[begin] < value[left]) max = left; } //else的情況是左右節點都不存在的情況,這時也是遞歸出口 } if(max == begin)//說明不需要調整,也是一個遞歸出口 return; swap(&value[begin],&value[max]);//調整 adjust(value,len,max);//遞歸調用 } /** * 初始化堆 * */ void init_heap(int value[],int len) { //初始化堆,從最后一個非葉子節點開始調整,也就是調用adjust,直到根結點 int i = (len - 1) >> 1;//最后一個非葉子節點的下標 for(;i >= 0;i--) { adjust(value,len,i); } } void heap_sort(int value[],int len) { init_heap(value,len); printf("創建的堆為:\n"); int j = 0; for(;j < len;j++) { printf("%d ",value[j]); } printf("\n"); int i = len-1;//最后一個元素的下標 for(;i > 0;i--) { swap(&value[0],&value[i]);//頂元素篩下 adjust(value,i,0);//從根節點開始調整,注意無序的數組長度逐漸減小 } } int main() { int value[] = {3,5,8,1,4,10,2,7,6,9}; int n = 10; heap_sort(value,n); printf("排序結果為:\n"); int i = 0; for(;i < n;i++) { printf("%d ",value[i]); } printf("\n"); return 0; }
堆排序的時間復雜度
如果初始建立的堆已經是有序的,此時就不需要堆進行頂元素篩下和調整。最壞的情況,每次頂元素篩下后,都需要進行直到葉子節點的調整,那么此次調整實際上就形成從根到葉子的一條路徑,路徑長度為樹的高度,對於滿樹,樹高為:log2(n+1),對於完全二叉樹是該中向上取正。所以最壞情況,每次調整都需要O(log2n),要進行n-1次頂元素的篩下,雖然每次調整n在逐漸減小,但應該可以證明,整個篩下排序過程需要O(nlog2n).
創建堆需要O(n),從書上看到了,不會證明。
所以最壞情況下堆排序的時間復雜度是O(nlog2n),這一點比那個牛掰的快排序都好。
參考文獻
http://www.cnblogs.com/luchen927/archive/2012/02/27/2367108.html
http://www.cnblogs.com/mengdd/archive/2012/11/30/2796845.html
最后附上word和源碼的鏈接
鏈接:http://pan.baidu.com/s/1skMrit3 密碼:dzu2
如果你覺得對你有用,請點贊吧~~