對排序算法的初步探究
初學排序算法,我覺得只需要掌握算法的精髓,沒必要把所有算法都實現一遍,下面我會實現一些經典的排序算法。(均采用C++實現)
學習的排序算法包含:
1》插入排序(直接插入排序、希爾排序)
2》選擇排序(簡單選擇排序、堆排序)
3》交換排序(快速排序、冒泡排序)
4》歸並排序
5》基數排序
我認為初學者掌握基本的排序算法的思想即可,其他排序算法基於特定的數據結構和存儲結構,遇到具體的實例再學習即可。
下面就開始學習了。
插入排序
插入排序:把一個數插入到一個有序的序列中,並要求插入後此數據序列仍然有序。這種排序思想就是插入排序。
直接插入排序:把余下元素一個一個插入有序表中,每次都從有序表的最後一個元素開始比較,若是小於該元素:data<a[i],則再與前一個元素比較data[i-1],...直到找到合適位置,最後把該位置及其以後的元素都向後移動:a[j]=a[j-1],騰出一個位置讓新元素插入。這也是最簡單的插入排序算法。
下面是直接插入排序的簡單實現:
void insert_sort(int *a,int n) { if(a == NULL || n <= 1) return; for(int i=1;i<n;i++) { /* 存放當前無序的初始序號, *初始先將數組的第一個元素構成有序序列, *將數組的後面的序列看成無序序列, *從數組的第二個元素開始做插入排序 *註意:有序序列已經是從大到小排好的 */ int j=i; int temp=a[i];while(j && temp<a[j-1]) //一直找到在有序序列中的位置,記錄在j中 { a[j]=a[j-1]; //不斷將數組元素往後移動,騰出位置 j--; } a[j]=temp; //插入temp,即存放的無序序列中的第一個值 } }
交換插入排序:可以在比較的過程中,直接交換前後位置的元素,一步一步地把插入的元素交換到合適的位置。
void insert_sort(int *a,int n) { if(a == NULL || n <= 1) return; for(int i=1;i<n;i++) { int j=i; //一邊插入一邊比較交換排序,不需要元素後移騰出空間。 while(j && temp<a[j-1]) { int temp=a[i]; a[j] = a[j-1]; a[j-1] = temp; j--; } } }
折半插入排序:在直接插入中,我們每次都是把一個元素插入到一個有序的序列中。為了使找到位置更高效,我們可以借鑒二分查找的方法,減少比較次數,快速找到合適的插入位置。這就是折半插入的來歷。
void binary_insert_sort(int *a,int n) { if(a == NULL || n <= 1) return; int low,high,mid; for(int i=1;i<n;i++) { low = 0; high = i-1; while(low <= high) //采用左閉右閉的區間 { mid = low + (high - low)>>1; //不寫成/2是為了防止溢出 if(a[i]>a[mid]) //不斷的判斷條件,來改變low,mid,high low = mid + 1; else high = mid - 1; } //循環結束的low值就是這個元素插入的位置,至於為什麽,自己畫個程序的流程就明白了。值就是這個元素插入的位置,至於為什麽,自己畫個程序的流程就明白了。 int temp = a[i]; //不斷的將元素後移 for(int j=i;j>low;j--) a[j] = a[j-1]; a[low] = temp; } }
希爾排序
我們知道當一個序列基本有序時,直接插入會變得很高效。因為此時只需少量的移動元素,操作集中在元素的比較上。基於這種想法,我們就試圖把一個序列在進行直接插入前調整得盡量有序。這就是希爾排序(Shell Sort)的核心思路。(Shell只是算法發明者的名字,無特殊含義)
那到底該怎麽做呢?
希爾排序一反以前的做法,插入為何只在相鄰的元素之間進行,不相鄰的同樣可以進行。於是,希爾排序也被形象地稱為”跳著插“。
那應該隔幾個元素進行插入呢?
這就說到希爾排序的另一個名字:縮小增量排序(Diminishing Increment Sort)。實際上這個增量序列或者說步長序列是可以提前指定的。不同的序列可以極大地影響到算法效率,至於最好的步長序列貌似還在研究。不過可以肯定的是最後的一個步長一定是1。因為最後一次是直接插入。
先來看一種最簡單、也最常用的步長序列:n/2、n/2/2、... 1 (n是待排序的元素個數)。也是就說,初始步長是n/2,以後每次減半,直到步長為1。
利用這種步長序列,舉一個例子:開始序列中加下劃線的字體表示每一趟待排序的數字。
原序列: 21 12 4 9 9 5 78 1 (n=8)
下標: 0 1 2 3 4 5 6 7
第一趟:步長step=4,0、4號元素直接插入排序
開始 21 12 4 9 9 5 78 1
結束 9 12 4 9 21 5 78 1
第二趟:步長step=2, 0、2、4、6號元素直接插入排序
開始 9 12 4 9 21 5 78 1
結束 4 12 9 9 21 5 78 1
第三趟:步長step=1,0、1、2、3、4、5、6、7、8號元素直接插入排序(顯然這是整體直接插入排序)
開始 4 12 9 9 21 5 78 1
結束 1 4 5 9 9 12 21 78
如何理解每一趟排序:
- 步長序列的長度決定了排序的趟數。
- 每一趟排序,並不是所有元素都參與。參與排序的只能是下標為 0、step、2*step...的元素。
- 插入排序時,采用何種策略。如直接插入、交換插入、折半插入、表插入或者表折半插入,這可任意選擇。
- 最後一趟排序的步長一定是1。由第二點可知,最後一趟,全體元素都參與排序。
排序結果中紅色9出現在了黑色9的前面,表明希爾排序是不穩定的。
希爾排序的簡單實現:
void ShellSort(int *a,int n) { if(a == NULL || n <= 1) return; for(int step = n/2;step >= 1;step = step/2) //規定步長 { //下面采用交換插入排序算法 for(int i=step;i<n;i+=step) for(int j=i;j>0;j-=step) if(a[j]<a[j-step]) swap(a[j],a[j-step]); } }
插入排序算法的個人感受:整體來說,直接插入排序的算法時間復雜度最壞情況下為O(n^2),空間復雜度可以忽略不計。而采用改進後的插入排序算法(包括希爾排序)只是減少元素移動的個數,提高排序的效率。
選擇排序(select sort)
簡單選擇排序
經過一趟排序,可以從n-i+1(i=1,2...)個記錄中選取關鍵字最小的記錄作為有序序列中第i個記錄。也就是說,每一趟排序,都會排好一個元素的最終位置。
最簡單的是簡單選擇排序。
簡單選擇排序(Simple Selection Sort,也叫直接選擇排序)
簡單選擇排序的思想:在每一趟排序中,通過n-i次關鍵字的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i個記錄交換,以此確定第i個記錄的最終位置。簡單說,逐個找出第i小的記錄,並將其放到數組的第i個位置。
下面是簡單交換排序的算法實現:
void SimpleSelectSort(int *a,int n) { if(a == NULL && n <= 1) return; int index; //用來記錄關鍵字最小的數組元素的下標 for(int i=0;i<n;i++) { index = i; for(int j=i+1;j<n;j++) //遍歷數組的後面的元素找到關鍵字最小的元素的下標 { if(a[j]<a[index]) index = j; } if(i != index) //當最小關鍵字的元素的下標和當前的元素不同的時候進行交換 { int temp = a[i]; a[i] = a[j]; a[j] = temp; } } }
堆排序
堆排序(Heap Sort):使用堆這種數據結構來實現排序。
先看下堆的定義:
最小堆(Min-Heap)是關鍵碼序列{k0,k1,…,kn-1},它具有如下特性:
ki<=k2i+1,
ki<=k2i+2(i=0,1,…)
簡單講:孩子的關鍵碼值大於雙親的。
同理可得,最大堆(Max-Heap)的定義:
ki>=k2i+1,
ki>=k2i+2(i=0,1,…)
同樣的:對於最大堆,雙親的關鍵碼值大於兩個孩子的(如果有孩子)。
堆的特點:
- 堆是一種樹形結構,而且是一種特殊的完全二叉樹。
- 其特殊性表現在:它是局部有序的,其有序性只體現在雙親節點和孩子節點的關系上(樹的每一層的大小關系也是可以體現的)。兄弟節點無必然聯系。
- 最小堆也被稱為小頂堆(根節點是最小的),最大堆也被稱為大頂堆(根節點是最大的)。我們常利用最小堆實現從小到大的排序,最大堆實現從大到小的排序。
這裏只講“堆”這種數據結構的應用之一-------堆排序,而不展開講“堆”這種數據結構的內部實現的細節。
堆排序的基本原理:大體來說就是根據數組元素建立一個最小堆(從小到大排序)或者最大堆(從大到小排序),然後不斷的返回堆頂的元素,刪除堆頂的元素後堆內部的結構會發生一些改變,改變結構是為了滿足堆的性質,從而不斷輸出堆頂的元素就是排好序的序列。
堆排序步驟:
- 先建好堆。
- 不斷地刪除堆頂即可(刪除前記得打印堆頂元素),直到只剩下一個元素。
堆排序的簡單實現如下所示:
void HeapSort(int *a,int n) { if(a == NULL && n <= 1) return; createHeap(a,n); //創建堆 while(n > 1) //不斷刪除堆頂的元素,刪除前打印。即是堆排序的順序。 { cout<<a[0]; deleteAt(a,n,0); } cout<<a[0]; }
具體的原理以及實現的細節可以參考:http://blog.csdn.net/zhangxiangdavaid/article/details/30069623
交換排序:兩兩比較待排序記錄的關鍵碼,若是逆序,則交換,直到無逆序。
其中最簡單的交換排序是:冒泡排序。冒泡排序法很穩定,但是不夠高效,時間復雜度是O(n^2)。
效率比較高的交換排序法有快速排序。
冒泡排序(Bubble Sort,也叫起泡排序):
不斷地比較相鄰的記錄,若是不滿足排序要求,則交換。
交換時,可從前向後,也可從後向前。
看一個從前向後的排序過程:
原序列 12 3 45 33 6
下標 0 1 2 3 4
第一趟:
3 12 45 33 6 (3,12交換)
3 12 45 33 6 (12,45不用交換)
3 12 33 45 6 (45,33交換)
3 12 33 6 45 (45,6交換)
第二趟:
3 12 33 6 45 (3,12不用交換)
3 12 33 6 45 (12,33不用交換)
3 12 6 33 45 (33,6交換)
第三趟:
3 12 6 33 45 (3,12不用交換)
3 6 12 33 45 (12,6交換)
第四趟:
3 6 12 33 45 (3,6不用交換)
結束。
冒泡排序法的實現(未經優化):
void swap(int &a,int& b) //交換函數 { int temp = a; a = b; b = temp; } void swap(int& a,int& b) //交換函數也可以這樣寫,可以自己驗證下 { if(a!=b) { a^=b; b^=a; a^=b; } } /*冒泡排序法 *從左到右:遍歷的數組下標從0--n-1、0--n-2、...、0--1 *可以看作是將最大元素冒泡到最右邊 *從右到左:遍歷的數組下標從n-1--0、n-1--1、...、n-1--n-2 *可以看作是將最大元素冒泡到最左邊 */ void BubbleSort1(int *a,int n) //冒泡排序法,從左往右 { for(int i=1;i<n;i++) for(int j=0;j<n-i;j++) { if(a[j]>a[j+1]) swap(a[j],a[j+1]); } } void BubbleSort2(int *a,int n) //冒泡排序法,從右往左 { for(int i=1;i<n;i++) for(int j=n-1;j>=i;j--) { if(a[j]>a[j-1]) swap(a[j],a[j-1]); } }
現在可以進行一下優化:
如果在某趟排序的時候沒有進行數據的交換,那麽就表明已經是有序排列的了,此時就不需要進行排序了,結束程序就可以了。
改進的冒泡排序如下:
void swap(int& a,int& b) { a ^= b; b ^= a; a ^= b; } void BuubbleSort(int* a,int n) { if(a == NULL && n >= 1) return; bool flag = true; //設置一個標誌位,當標誌位為true才會循環 while(flag == true) { for(int i=1;i<n;i++) for(int j=0;j<n-i;j++) { if(a[j]>a[j+1]) { swap(a[j],a[j+1]); } else flag = false; //當某一趟循環中沒有數據交換的時候,就會將標誌位置false } } }
再度優化:下一趟排序向右(或向左)的最遠位置,只是簡單的減一嗎?可否更高效?
可以更加高效,記錄上一趟排序時交換元素的最遠距離,下一趟排序最遠只到這個位置即可。
下面是改進後:
void swap(int& a,int& b) { a ^= b; b ^= a; a ^= b; } void BuubbleSort(int* a,int n) { if(a == NULL && n >= 1) return; bool flag = true; //仍然設置標誌位 int j = n-1; //初始的循環上限 while(flag) { for(int i=0;i<j;i++) { if(a[i]>a[i+1]) { swap(a[i],a[i+1]); j = i; //每次記錄交換的下標 ,最後一次的j值就是最遠交換的下標,也就是下次的循環上限 } else flag = false; //如果某一趟遍歷中沒有數據的交換,那麽就表明已經是排好序的了,可以結束程序了 } } }
快速排序(Quick Sort)也是一種交換排序,它在排序中采取了分治策略。
快速排序的主要思想:
- 從待排序列中選取一元素作為軸值(也叫主元)。
- 將序列中的剩余元素以該軸值為基準,分為左右兩部分。左部分元素不大於軸值,右部分元素不小於軸值。軸值最終位於兩部分的分割處。
- 對左右兩部分重復進行這樣的分割,直至無可分割。
- 如何選擇軸值?(軸值不同,對排序有影響嗎?)
- 如何分割?
對排序算法的初步探究