1.排序算法簡要比較
| 名稱 | 數據對象 | 穩定性 | 時間復雜度 | 空間復雜度 | 描述 | ||
|---|---|---|---|---|---|---|---|
| 平均 | 最壞 | ||||||
| 插入排序 | 數組、鏈表 | √ | ![]() |
O(1) | (有序區,無序區)。把無序區的第一個元素插入到有序區的合適的位置。對數組:比較得少,換得多。 | ||
| 直接選擇排序 | 數組 | × | ![]() |
O(1) | (有序區,無序區)。在無序區里找一個最小的元素跟在有序區的后面。 對數組:比較得多,換得少。 | ||
| 鏈表 | √ | ||||||
| 堆排序 | 數組 | × | O(nlogn) | O(1) | (最大堆,有序區)。從堆頂把根卸出來放在有序區之前,再恢復堆。 | ||
| 歸並排序 | 數組、鏈表 | √ | O(nlogn) | O(n) +O(logn) , 如果不是從下到上 | 把數據分為兩段,從兩段中逐個選最小的元素移入新數據段的末尾。可從上到下或從下到上進行。 | ||
| 快速排序 | 數組 | × | O(nlogn) | ![]() |
O(logn) ,O(n) | (小數,樞紐元,大數)。 | |
| Accum qsort | 鏈表 | √ | O(nlogn) | ![]() |
O(logn) ,O(n) | (無序區,有序區)。把無序區分為(小數,樞紐元,大數),從后到前壓入有序區。 | |
| 決策樹排序 | √ | O(logn!) | O(n!) | O(n) <O(logn!) <O(nlogn) | |||
| 計數排序 | 數組、鏈表 | √ | O(n) | O(n+m) | 統計小於等於該元素值的元素的個數 i,於是該元素就放在目標數組的索引 i位。(i≥0) | ||
| 桶排序 | 數組、鏈表 | √ | O(n) | O(m) | 將值為 i 的元素放入i 號桶,最后依次把桶里的元素倒出來。 | ||
| 基數排序 | 數組、鏈表 | √ | 一種多關鍵字的排序算法,可用桶排序實現。 | ||||
- 均按從小到大排列
- n 代表數據規模
- m 代表數據的最大值減最小值
2.冒泡排序:
冒泡排序基本思路:
冒泡排序的主要思想就是通過一趟排序,將數組中的最大值轉移到的數組最后一個元素。一個有n個元素的數組,那么經過(n-1)趟排序以后,就能夠完成排序。因為如果(n-1)個元素的位置確定了,那么最后的那一個元素的位置也就確定了。還有需要注意的是每一趟排序都能確定數組中的一個元素位置,因此經過一趟排序以后,需要進行排序比較的元素就減少一個。
在每一趟排序過程中,都是從數組元素第一個開始,依次比較相鄰的兩個數,將小數放在前面,大數放在后面。即在第一趟:首先比較第1個和第2個數,將小數放前,大數放后。然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續,直至比較最后兩個數,將小數放前,大數放后。至此第一趟結束,將最大的數放到了最后。在第二趟:仍從第一對數開始比較(因為可能由於第2個數和第3個數的交換,使得第1個數不再小於第2個數),將小數放前,大數放后,一直比較到倒數第二個數(倒數第一的位置上已經是最大的),第二趟結束,在倒數第二的位置上得到一個新的最大數(其實在整個數列中是第二大的數)。如此下去,重復以上過程,直至最終完成排序。由於在排序過程中總是小數往前放,大數往后放,相當於氣泡往上升,所以稱作冒泡排序。
冒泡排序的改進
冒泡排序法存在的不足及改進方法:
第一,在排序過程中,執行完當前的第k趟排序后,可能數據已全部排序完備,但是程序無法判斷是否完成排序,會繼續執行剩下的(n-1-k)趟排序。為了解決這一不足,可以設置一個標志位flag,將其初始值設置為非0,表示被排序的數組是一個無序的數組。每一次排序開始前設置flag值為0,表示假設已經完成排序,如果這一趟排序過程中都沒有數據發生交換,則flag的值就為0;但是當這一趟排序過程中存在數據交換時,修改flag為非0,表示這一趟仍然有數據沒有完成排序。在新一輪排序開始時,檢查此標志,若此標志為0,表示上一次沒有做過交換數據,則表示經過上一趟的排序,已經完成了對數組的排序,排序結束;否則繼續進行排序;
根據上述思路進行改進的冒泡排序代碼實現如下:
View Code
void bubbleSortImproved(int arry[],int len) { int flag=1;//表明數組是無序的 for(int i=0;i<len-1;i++)//排序len-1趟 { if(flag)//數組無序則需要繼續進行排序 { flag=0;//出事設置flag=0表明已排序完成 for(int j=0;j<len-1-i;j++)//每次比較前從arry[0]到arry[len-1-i]的元素,隨着趟數增加,需要比較的元素越來越少 { if(arry[j]>arry[j+1])//如果一個數比相鄰的后面一個數大,則交換位置 { swap(arry,j,j+1); flag=1;//如果還有元素交換,則flag=0 } } } else//如果數組已經有序,則跳出循環,無需進行排序 break; } }
第二,當排序的數據比較多時排序的時間會明顯延長。改進方法:快速排序:具體做法:任意選取某一記錄(通常取第一個記錄),比較其關鍵字與所有記錄的關鍵字,並將關鍵字比它小的記錄全部放在它的前面,將比它大的記錄均存放在它的后面,這樣,經過一次排序之后,可將所有記錄以該記錄所在的分界點分為兩部分,然后分別對這兩部分進行快速排序,直至排序完。也就是說,利用快速排序過程中的Partition()方法返回的index將需要排序的元素分為兩段,在index前面的數據都比index位置的數據小,在index后面的元素都比index位置的數據大。然后對前后兩段數據進行冒泡排序,這樣縮小的冒泡排序的數據量。
局部冒泡排序算法對冒泡排序的改進:
在冒泡排序中,一趟掃描有可能無數據交換,也有可能有一次或多次數據交換,在傳統的冒泡排序算法及近年來的一些改進的算法中,只記錄一趟掃描有無數據交換的信息,對數據交換發生的位置信息則不予處理。為了充分利用這一信息,可以在一趟全局掃描中,對每一反序數據對進行局部冒泡排序處理,稱之為局部冒泡排序。局部冒泡排序與冒泡排序算法具有相同的時間復雜度,並且在正序和逆序的情況下,所需的關鍵字的比較次數和移動次數完全相同。由於局部冒泡排序和冒泡排序的數據移動次數總是相同的,而局部冒泡排序所需關鍵字的比較次數常少於冒泡排序,這意味着局部冒泡排序很可能在平均比較次數上對冒泡排序有所改進,當比較次數較少的優點不足以抵消其程序復雜度所帶來的額外開銷,而當數據量較大時,局部冒泡排序的時間性能則明顯優於冒泡排序。
3.快速排序
參考前面寫的博文:
冒泡排序與快速排序代碼實現
View Code
#include<iostream> #include<stdlib.h> using namespace std; void swap(int arry[],int a,int b) { int temp=arry[a]; arry[a]=arry[b]; arry[b]=temp; } //冒泡排序 void bubble_sort(int arry[],int len) { for(int i=0;i<len-1;i++)//需要遍歷(len-1)趟 for(int j=0;j<len-1-i;j++)//每遍歷一趟減少確定一個數字,所以減少一個比較數字 { if(arry[j]>arry[j+1]) swap(arry,j,j+1); } } //快速排序2,不交換,直接賦值。 int partition2(int arry[],int start,int end) { int pivot=arry[start]; while(start<end) { while(start<end&&arry[end]>pivot) end--; arry[start]=arry[end];//逆向找出第一個小於pivot的值,將其移到start這一邊 while(start<end&&arry[start]<pivot) start++; arry[end]=arry[start];//正向找出第一個大於pivot的值,將其移到end這一邊 } arry[start]=pivot; return start; } //快速排序1,交換數組中的元素。 int partition(int arry[],int start,int end) { int pivot=arry[start]; while(start<end) { while(start<end&&arry[end]>pivot) end--; swap(arry,start,end); while(start<end&&arry[start]<pivot) start++; swap(arry,start,end); } arry[start]=pivot; return start; } //快速排序 void qsort(int arry[],int start,int end,int len) { int pivotkey=partition(arry,start,end); if(start<end) { qsort(arry,start,pivotkey-1,len); qsort(arry,pivotkey+1,end,len); } } void print(int arry[],int len) { for(int i=0;i<len;i++) cout<<arry[i]<<" "; cout<<endl; } void test_bubble() { int arry[]={3,8,7,1,9}; int len=sizeof(arry)/sizeof(int); print(arry,len); bubble_sort(arry,len); print(arry,len); } void test_qsort() { int arry[]={3,8,7,1,9}; int len=sizeof(arry)/sizeof(int); print(arry,len); qsort(arry,0,len-1,len); print(arry,len); } void main() { test_bubble(); test_qsort(); system("pause"); }
4.堆排序
堆排序的主要思路:
- 一般情況下都使用數組實現堆的結構,以大頂堆為例,因為是使用數組,元素下表從0開始,所以要求arry[i]>arry[i*2+1],arry[i]>arry[i*2+2]。表示父結點大於其左右孩子結點的值。
- 所以堆排序的第一步是根據其實數組元素,構建一個堆。構建堆就需要調整堆中的元素,從第一個非葉子結點開始調整,第一個非葉子結點的位置是t=(len/2-1)。因為堆是一棵滿二叉樹的結構,所以從0到t的所有結點都是非葉子結點。
- 在調整堆結構過程中,我們需要比較父節點與其左右子結點的值,可以先求出左右子結點的最大值,即c=max(arry[leftChild],arry[rightChild]),然后將這個最大值與父節點arry[parent]進行比較,如果父節點小於子節點,那么交換父節點與子節點的值(子節點中較大的那個元素),如果父節點大與子節點,則表明滿足堆結構要求,不需要交換。在調整的過程中可能會出現一種情況,比如父節點A與子節點B進行交換,此時A結點是B結點的子節點,但是A結點又是CD兩個結點的父節點,因此我們還需要調整ACD三個元素位置,一直調整到結點沒有子節點為止。
- 在堆排序中,首先是構建一個最大堆,此處需要用到buildHeap(int arry[],int len)這個函數,而這個函數中又調用了adjustHeap(int arry[],int parent,int len),所以可以將build看成是由adjustheap組成的。在構建完成以后,我們交換首元素與末元素的位置,交換以后我們知道首元素不符合最大堆的定義,所以需要調整首元素的位置,也就是adjustHeap(arry,0,len-i-1)。
- 我們可以發現adjustHeap(int arry[],int parent,int len)的時間復雜讀是o(logn),而需要調用o(n)次這個方法啊,所以時間復雜度時o(nlogn)
堆排序的代碼實現
c++實現
View Code
#include<iostream> #include<stdlib.h> #include<stack> using namespace std; //函數聲明 void PrintArry(int arry[],int len);//打印數組 void swap(int arry[],int i,int j);//交換數組元素 void AdjustHeap(int arry[],int parent,int len);//調整堆,使其滿足堆的要求 void BuildHeap(int arry[],int len);//初始化堆 void HeapSort(int arry[],int len);//堆排序 void PrintArry(int arry[],int len) { for(int i=0;i<len;i++) cout<<arry[i]<<" "; cout<<endl; } void swap(int arry[],int i,int j) { int temp=arry[i]; arry[i]=arry[j]; arry[j]=temp; } void AdjustHeap(int arry[],int parent,int len) { int c=parent*2+1;//parent的左孩子結點,這里+1是因此頭結點從0開始 while(c<len)//如果左孩子小於堆長度 { if(c+1<len&&arry[c]<arry[c+1])//如果右孩子小於對長度並且右孩子大於左孩子,則獲取右孩子的坐標 c++; if(arry[parent]>=arry[c])//如果父節點比孩子結點大則退出循環 { break; } else { swap(arry,parent,c);//如果孩子結點比父節點大,則交換 //因為孩子結點也可能是父結點,所以接下來繼續調整孩子結點,直到父節點大於兩個孩子結點或者沒有孩子結點為止。 parent=c; c=parent*2+1; } } } //初始化堆 void BuildHeap(int arry[],int len) { int begin=len/2-1;//從第一個非葉子結點開始調整,直到最后的根結點,根結點是arry[0] for(int i=begin;i>=0;i--) { AdjustHeap(arry,i,len); } } void HeapSort(int arry[],int len) { //創建初始化堆,用數組實現堆的結構。 BuildHeap(arry,len); //在初始化堆以后數組首元素arry[0]是最大的,交換首元素與堆的末尾元素, //這樣就完成了對一個數排序,堆的長度-1 for(int i=0;i<len;i++) { swap(arry,0,len-1-i);//交換堆頂元素與堆末尾元素 AdjustHeap(arry,0,len-1-i);//交換以后繼續調整整個堆的結構 } } void main() { int arry[]={1,3,6,2,7,0,8,5}; int len=sizeof(arry)/sizeof(int); PrintArry(arry,len); HeapSort(arry,len); PrintArry(arry,len); system("pause"); }
java實現(ps:2012-10-8)
View Code
public class HeapSortDemo { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub int arry[]={1,3,6,2,7,0,8,5}; int len=arry.length; printArry(arry,len);//打印數組 //testswap(arry);//測試swap方法,該方法交換元素不需要創建臨時變量。 heapSort(arry,len); printArry(arry,len);//打印數組 } public static void testswap(int arry[]) { System.out.println(arry[1]+" "+arry[2]); swap(arry,1,2); System.out.println(arry[1]+" "+arry[2]); } //不創建臨時變量交換兩個元素的值 public static void swap(int arry[],int i,int j) { arry[i]=arry[i]^arry[j]; arry[j]=arry[i]^arry[j]; arry[i]=arry[i]^arry[j]; } //堆排序 public static void heapSort(int arry[],int len) { buildHeap(arry,len);//建立最大堆 for(int i=0;i<len;i++) { swap(arry,0,len-1-i);//在構建完最大堆以后,數組首元素就是最大值,將其與最后一個元素交換位置,此時最后一個元素已排好序。 adjustHeap(arry,0,len-i-1);//交換首元素以后需要重新調整首元素的位置。所以這里adjustHeap(arry,0,len-1-i),其中0就表示首元素 } } //由底自頂構建堆 public static void buildHeap(int arry[],int len) { int nonleaf=len/2-1;//如果一個滿二叉樹有len個節點,那么從后往前找第一個非葉子節點應該是在第len/2個,但是因為此處第一個為0,所以len/2要-1 for(int i=nonleaf;i>=0;i--) { adjustHeap(arry,i,len);//從第一個父節點,也就是第一個非葉子結點開始調整 } } public static void printArry(int arry[],int len) { for(int i=0;i<len;i++) System.out.print(arry[i]+" "); System.out.println(); } //由頂自底調整堆 public static void adjustHeap(int arry[],int parent,int len) { int left=parent*2+1;//左節點 while(left<len) { if(left+1<len&&arry[left]<arry[left+1])//如果存在右節點,並且左節點小於右節點,那么就應該將右節點與父節點進行比較 left++;//此時left代表右節點 if(arry[parent]>=arry[left])//如果父節點大於葉子節點,則退出循環,表示已經構建好堆 break; else { swap(arry,parent,left); parent=left; left=parent*2+1; } } } }
此處使用異或方法交換兩個元素的值存在一些bug,建議還是使用常規的交換元素方法。
5.歸並排序
參考文獻:白話經典算法系列之五 歸並排序的實現
5.1歸並排序思路
- 歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。分治的思想類似於快速排序,不過它比快速排序多了一步merge。首先考慮下如何將將二個有序數組A/B合並的問題。這個非常簡單,設定兩個指針分別指向A和B的首地址,然后比較這兩個指針所指向的數,哪一個數小就將其保存到臨時數組T中,然后指針往前移動一個位置。比如指針pa指向數組A的首地址,pb指向數組B的首地址,指針pt指向臨時數組的首地址。加入pa指向的數小於pb指向的數,則將pa所指向的數存放在pt所指的位置上,然后pa和pt都往前移動一格。重復上述操作,直到pa和pb有一個指向數組的末尾元素。此時將另外一個指針沒有指向末尾元素的數組中的元素全部都保存到臨時數組中。這樣就完成了兩個有序數組的歸並操作。
- 解決了上面的合並有序數列問題,再來看歸並排序,其的基本思路就是將一個數組分成二組,分為A和B,如果這二組組內的數據都是有序的,那么就可以很方便的將這二組數據進行歸並。那么如何讓這二組組內數據有序了?可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,可以認為這個小組組內已經達到了有序,然后再合並相鄰的二個小組就可以了。這樣通過先遞歸的分解數列,再合並數列就完成了歸並排序。
5.2代碼示例
View Code
void printArry(int arry[],int len) { for(int i=0;i<len;i++) { cout<<arry[i]<<" "; } cout<<endl; } //歸並兩段相鄰的有序數組 void MergeArry(int arry[],int start,int mid,int end,int temp[]) { int i=start;//第一段有序數組的起始位置 int j=mid+1;//第二段有序數組的起始位置 int k=0;//臨時數組的坐標 while(i<=mid&&j<=end)//兩個起始指針都沒有走到數組末尾 { if(arry[i]<arry[j])//如果第一段中的元素比第二段中的元素小 temp[k++]=arry[i++];//將第一段中的元素賦值給臨時數組 else temp[k++]=arry[j++]; } //有一段數組沒有遍歷到末尾,進行賦值 while(i<=mid) temp[k++]=arry[i++]; while(j<=end) temp[k++]=arry[j++]; //將臨時數組中的元素重新賦值給原始數組的指定位置 for(i=0;i<k;i++) arry[start+i]=temp[i]; } //歸並排序,傳入原始數組,排序的起始與末尾位置以及臨時數組 void MergeSort(int arry[],int start,int end,int temp[]) { if(start<end) { int mid=(start+end)/2;//數組重點 MergeSort(arry,start,mid,temp);//遞歸調用,排序前半段arry[start...mid] MergeSort(arry,mid+1,end,temp);//遞歸調用,排序后半段arry[mid+1,end] MergeArry(arry,start,mid,end,temp);//歸並上述兩段有序數組。 } } //歸並排序,傳入參數為數組與數組長度 void MergeSort(int arry[],int len) { //創建臨時數組,在這里創建臨時數組比在后面創建臨時數組的開銷要小,后面所有遞歸調用用的都是同一個臨時數組 int *temp=new int[len]; int start=0; int end=len-1; //調用重載方法的歸並排序 MergeSort(arry,start,end,temp); } void main() { int arry[]={3,5,3,6,4,7,5,7,4}; int len=sizeof(arry)/sizeof(int); printArry(arry,len); MergeSort(arry,len); printArry(arry,len); system("pause"); }
注意點:
在這里我們看到我們merge的分段是arry[start...mid]跟arry[mid+1...end],我們不能分成arry[start...mid-1]跟arry[mid...end],這是因為mid=(start+end)/2,假如start=1,end=2,則mid=1,此時調用MergeSort(arry,start=1,end=3,temp);會就變成了MergeSort(arry,mid=1,end=3,temp)的死循環了。

