一、問題的引出-求第i個順序統計量html
二、方法一:以指望線性時間作選擇ios
三、方法二(改進):最壞狀況線性時間的選擇c++
四、完整測試代碼(c++)git
五、參考資料github
什麼是順序統計量?及中位數概念算法
在一個由元素組成的集合裏,第i個順序統計量(order statistic)是該集合第i小的元素。例如,最小值是第1個順序統計量(i=1),最大值是第n個順序統計量(i=n)。一個中位數(median)是它所在集合的「中點元素」。當n爲奇數時,中位數是惟一的;當n爲偶數時,中位數有兩個。問題簡單的說就是:求數組中第i小的元素。數組
那麼問題來了:如何求一個數組裏第i小的元素呢?dom
常規方法:能夠首先進行排序,而後取出中位數。因爲排序算法(快排,堆排序,歸併排序)效率能作到Θ(nlogn),因此,效率達不到線性; 在本文中將介紹兩種線性的算法,第一種指望效率是線性的,第二種效率較好,是在最壞狀況下能作到線性效率。見下面兩個小節;ide
這是一種分治算法:以快速排序爲模型:隨機選取一個主元,把數組劃分爲兩部分,A[p...q-1]的元素比A[q]小,A[q+1...r]的元素比A[q]大。與快速排序不一樣,若是i=q,則A[q]就是要找的第i小 的元素,返回這個值;若是i < q,則說明第i小的元素在A[p...q-1]裏;若是i > q,則說明第i小的元素在A[q+1...r]裏;而後在上面獲得的高區間或者低區間裏進行遞歸求取,直到找到第i小的元素。函數
下面是在A[p...q]中找到第i小元素的僞碼:
1 RandomSelect(A,p, q,k)//隨機選擇統計,以指望線性時間作選擇 2 { 3 if (p==q) return A[p]; 4 int pivot=Random_Partition(A,p,q);//隨機選擇主元,把數組進行劃分爲兩部分 5 int i=pivot-p+1; 6 if (i==k )return A[pivot]; 7 else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k小的數不在主元左邊,則在右邊遞歸選擇 8 else return RandomSelect(A,p,pivot-1,k);//第k小的數不在主元右邊,則在左邊遞歸選擇 9 }
在最壞狀況下,數組被劃分爲n-1和0兩部分,而第i個元素老是落在n-1的那部分裏,運行時間爲Ө(n^2);可是,除了上述很小的機率狀況,其餘狀況都能達到線性;在平均狀況下,任何順序統計量均可以在線性時間Θ(n)內獲得。
實現代碼(c++):
1 //template<typename T>使用模板,可處理任意類型的數據 2 template<typename T>//交換數據 3 void Swap(T &m,T &n) 4 { 5 T tmp; 6 tmp=m; 7 m=n; 8 n=tmp; 9 } 10 11 /***********隨機快速排序分劃程序*************/ 12 template<typename T> 13 int Random_Partition(vector<T> &A,int p,int q) 14 { 15 //隨機選擇主元,與第一個元素交換 16 srand(time(NULL)); 17 int m=rand()%(q-p+1)+p; 18 Swap(A[m],A[p]); 19 //下面與常規快排劃分同樣 20 T x=A[p]; 21 int i=p; 22 for (int j=p+1;j<=q;j++) 23 { 24 if (A[j]<x) 25 { 26 i=i+1; 27 Swap(A[i],A[j]); 28 } 29 } 30 Swap(A[p],A[i]); 31 return i; 32 } 33 /***********隨機選擇統計函數*************/ 34 template<typename T> 35 T RandomSelect(vector<T> &A,int p,int q,int k)//隨機選擇統計,以指望線性時間作選擇 36 { 37 if (p==q) return A[p]; 38 int pivot=Random_Partition(A,p,q);//隨機選擇主元,把數組進行劃分爲兩部分 39 int i=pivot-p+1; 40 if (i==k )return A[pivot]; 41 else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k小的數不在主元左邊,則在右邊遞歸選擇 42 else return RandomSelect(A,p,pivot-1,k);//第k小的數不在主元右邊,則在左邊遞歸選擇 43 }
相比於上面的隨機選擇,咱們有另外一種相似的算法,它在最壞狀況下也能達到O(n)。它也是基於數組的劃分操做,並且利用特殊的手段保證每次劃分兩邊的子數組都比較平衡;與上面算法不一樣之處是:本算法不是隨機選擇主元,而是採起一種特殊的方法選擇「中位數」,這樣能使子數組比較平衡,避免了上述的最壞狀況(Ө(n^2))。選出主元后,後面的處理和上述算法一致。
那麼問題又來了,這種特殊的手段是什麼呢?
如上圖所示:
1) 將輸入數組的n個元素劃分爲n/5組,每組(上圖中的每列爲一組)5個元素,且至多隻有一個組有剩下的n%5個元素組成
2) 首先對每組中的元素(5個)進行插入排序,而後從排序後的序列中選擇出中位數(圖中黃色數)。
3) 對第2步中找出的n/5箇中位數,遞歸調用SELECT以找出其中位數x(圖中紅色數)。(若是有偶數箇中位數取較小的中位數)
這三個步驟就能夠選出一個很好的主元,下面的處理和方法一一致(遞歸)
OK! 下面是完整的算法步驟:
1) 將輸入數組的n個元素劃分爲n/5組,每組(上圖中的每列爲一組)5個元素,且至多隻有一個組有剩下的n%5個元素組成
2) 首先對每組中的元素(5個)進行插入排序,而後從排序後的序列中選擇出中位數(圖中黃色數)。
3) 對第2步中找出的n/5箇中位數,遞歸調用SELECT以找出其中位數x(圖中紅色數)。(若是有偶數箇中位數取較小的中位數)
4) 調用PARTITION過程,按照中位數x對輸入數組進行劃分。肯定中位數x的位置k。
5) 若是i=k,則返回x。不然,若是i<k,則在地區間遞歸調用SELECT以找出第i小的元素,若干i>k,則在高區找第(i-k)個最小元素。
大體僞碼:
1 WorseLinearSelect(vector<T> &A,int p,int q,int k) 2 { 3 // 將輸入數組的n個元素劃分爲n/5(上取整)組,每組5個元素, 4 // 且至多隻有一個組有剩下的n%5個元素組成。 5 if (p==q) return A[p]; 6 7 int len=q-p+1; 8 int medianCount=1; 9 if (len>5) 10 medianCount = len%5 >0 ? len/5 + 1 : len/5; 11 vector<T> medians(medianCount);//存放每組的中位數 12 13 // 尋找每一個組的中位數。首先對每組中的元素(至多爲5個)進行插入排序, 14 // 而後從排序後的序列中選擇出中位數。 15 int m=p; 16 for (int j=0,m=p;j<medianCount-1;j++) 17 { 18 medians[j] = GetMedian(A,m,m+4); 19 m+=5; 20 } 21 medians[medianCount-1] = GetMedian(A,m,q); 22 //對第2步中找出的n/5(上取整)箇中位數,遞歸調用SELECT以找出其中位數pivot。 23 //(若是是偶數去下中位數) 24 int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2); 25 //調用PARTITION過程,按照中位數pivot對輸入數組進行劃分。肯定中位數pivot的位置r。 26 int r = partitionWithPivot(A,p,q,pivot); 27 int num = r-p+1; 28 //若是num=k,則返回pivot。不然,若是k<num,則在地區間遞歸調用SELECT以找出第k小的元素, 29 //若干k>num,則在高區找第(k-num)個最小元素。 30 if(num==k) return pivot; 31 else if (num>k) return WorseLinearSelect(A,p,r-1,k); 32 else return WorseLinearSelect(A,r+1,q,k-num); 33 }
代碼實現(c++):
1 template<typename T>//插入排序 2 void insertion_sort(vector<T> &A,int p,int q) 3 { 4 int i,j; 5 T key; 6 int len=q-p+1; 7 for (j=p+1;j<=q;j++) 8 { 9 i=j-1; 10 key=A[j]; 11 while (i>=p&&A[i]>key) 12 { 13 A[i+1]=A[i]; 14 i--; 15 } 16 A[i+1]=key; 17 } 18 } 19 /* 20 * 利用插入排序選擇中位數 21 */ 22 template<typename T> 23 T GetMedian(vector<T> &A,int p,int q) 24 { 25 insertion_sort(A,p,q);//插入排序 26 return A[(q-p)/2 + p];//返回中位數,有兩個中位數的話返回較小的那個 27 } 28 /* 29 * 根據指定的劃分主元pivot來劃分數組 30 * 並返回主元的順序位置 31 */ 32 template<typename T> 33 int partitionWithPivot(vector<T> &A,int p,int q,T piovt) 34 { 35 //先把主元交換到數組首元素 36 for (int i=p;i<q;i++) 37 { 38 if (A[i] == piovt) 39 { 40 Swap(A[i],A[p]); 41 break; 42 } 43 } 44 //常規的快速排序劃分程序 45 // 46 T x=A[p]; 47 int i=p; 48 for (int j=p+1;j<=q;j++) 49 { 50 if (A[j]<x) 51 { 52 i=i+1; 53 Swap(A[i],A[j]); 54 } 55 } 56 Swap(A[p],A[i]); 57 return i; 58 } 59 /* 60 * 最壞狀況下線性時間選擇算法 61 * 此算法依然是創建在快速排序的劃分算法基礎之上的 62 * 可是與randomizedSelect算法的不一樣指之處,就是次算法的本質 63 * 是保證了每次劃分選擇的劃分主元必定是一個較好的主元,算法先對數組5個一組進行分組 64 * 而後選擇每組的中位數,再遞歸的選擇各組中位數中的中位數做爲數組的劃分主元,以此保證劃分的平衡性 65 * 選擇中位數的時候必須使用遞歸調用的方法才能下降時間複雜度 66 * 從而保證在最壞狀況下都獲得一個好的劃分 67 * 最壞狀況下時間複雜度爲O(n) 68 */ 69 template<typename T> 70 T WorseLinearSelect(vector<T> &A,int p,int q,int k) 71 { 72 // 將輸入數組的n個元素劃分爲n/5(上取整)組,每組5個元素, 73 // 且至多隻有一個組有剩下的n%5個元素組成。 74 if (p==q) return A[p]; 75 76 int len=q-p+1; 77 int medianCount=1; 78 if (len>5) 79 medianCount = len%5 >0 ? len/5 + 1 : len/5; 80 vector<T> medians(medianCount);//存放每組的中位數 81 82 // 尋找每一個組的中位數。首先對每組中的元素(至多爲5個)進行插入排序, 83 // 而後從排序後的序列中選擇出中位數。 84 int m=p; 85 for (int j=0,m=p;j<medianCount-1;j++) 86 { 87 medians[j] = GetMedian(A,m,m+4); 88 m+=5; 89 } 90 medians[medianCount-1] = GetMedian(A,m,q); 91 //對第2步中找出的n/5(上取整)箇中位數,遞歸調用SELECT以找出其中位數pivot。 92 //(若是是偶數去下中位數) 93 int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2); 94 //調用PARTITION過程,按照中位數pivot對輸入數組進行劃分。肯定中位數pivot的位置r。 95 int r = partitionWithPivot(A,p,q,pivot); 96 int num = r-p+1; 97 //若是num=k,則返回pivot。不然,若是k<num,則在地區間遞歸調用SELECT以找出第k小的元素, 98 //若干k>num,則在高區找第(k-num)個最小元素。 99 if(num==k) return pivot; 100 else if (num>k) return WorseLinearSelect(A,p,r-1,k); 101 else return WorseLinearSelect(A,r+1,q,k-num); 102 }
Select.h
1 #ifndef SELECT_HH 2 #define SELECT_HH 3 template<typename T> 4 class Select 5 { 6 public: 7 T RandomSelect(vector<T> &A,int p,int q,int k);//指望線性時間作選擇 8 T WorseLinearSelect(vector<T> &A,int p,int q,int k);//最壞狀況線性時間的選擇 9 private: 10 void Swap(T &m,T &n);//交換數據 11 int Random_Partition(vector<T> &A,int p,int q);//隨機快排分劃 12 void insertion_sort(vector<T> &A,int p,int q);//插入排序 13 T GetMedian(vector<T> &A,int p,int q); 14 int partitionWithPivot(vector<T> &A,int p,int q,T piovt);//根據指定主元pivot來劃分數據並返回主元的順序位置 15 }; 16 17 template<typename T>//交換數據 18 void Select<T>::Swap(T &m,T &n) 19 { 20 T tmp; 21 tmp=m; 22 m=n; 23 n=tmp; 24 } 25 26 /***********隨機快速排序分劃程序*************/ 27 template<typename T> 28 int Select<T>::Random_Partition(vector<T> &A,int p,int q) 29 { 30 //隨機選擇主元,與第一個元素交換 31 srand(time(NULL)); 32 int m=rand()%(q-p+1)+p; 33 Swap(A[m],A[p]); 34 //下面與常規快排劃分同樣 35 T x=A[p]; 36 int i=p; 37 for (int j=p+1;j<=q;j++) 38 { 39 if (A[j]<x) 40 { 41 i=i+1; 42 Swap(A[i],A[j]); 43 } 44 } 45 Swap(A[p],A[i]); 46 return i; 47 } 48 /***********隨機選擇統計函數*************/ 49 template<typename T> 50 T Select<T>::RandomSelect(vector<T> &A,int p,int q,int k)//隨機選擇統計,以指望線性時間作選擇 51 { 52 if (p==q) return A[p]; 53 int pivot=Random_Partition(A,p,q);//隨機選擇主元,把數組進行劃分爲兩部分 54 int i=pivot-p+1; 55 if (i==k )return A[pivot]; 56 else if (i<k) return RandomSelect(A,pivot+1,q,k-i);//第k小的數不在主元左邊,則在右邊遞歸選擇 57 else return RandomSelect(A,p,pivot-1,k);//第k小的數不在主元右邊,則在左邊遞歸選擇 58 } 59 60 template<typename T>//插入排序 61 void Select<T>::insertion_sort(vector<T> &A,int p,int q) 62 { 63 int i,j; 64 T key; 65 int len=q-p+1; 66 for (j=p+1;j<=q;j++) 67 { 68 i=j-1; 69 key=A[j]; 70 while (i>=p&&A[i]>key) 71 { 72 A[i+1]=A[i]; 73 i--; 74 } 75 A[i+1]=key; 76 } 77 } 78 /* 79 * 利用插入排序選擇中位數 80 */ 81 template<typename T> 82 T Select<T>::GetMedian(vector<T> &A,int p,int q) 83 { 84 insertion_sort(A,p,q);//插入排序 85 return A[(q-p)/2 + p];//返回中位數,有兩個中位數的話返回較小的那個 86 } 87 /* 88 * 根據指定的劃分主元pivot來劃分數組 89 * 並返回主元的順序位置 90 */ 91 template<typename T> 92 int Select<T>::partitionWithPivot(vector<T> &A,int p,int q,T piovt) 93 { 94 //先把主元交換到數組首元素 95 for (int i=p;i<q;i++) 96 { 97 if (A[i] == piovt) 98 { 99 Swap(A[i],A[p]); 100 break; 101 } 102 } 103 //常規的快速排序劃分程序 104 // 105 T x=A[p]; 106 int i=p; 107 for (int j=p+1;j<=q;j++) 108 { 109 if (A[j]<x) 110 { 111 i=i+1; 112 Swap(A[i],A[j]); 113 } 114 } 115 Swap(A[p],A[i]); 116 return i; 117 } 118 /* 119 * 最壞狀況下線性時間選擇算法 120 * 此算法依然是創建在快速排序的劃分算法基礎之上的 121 * 可是與randomizedSelect算法的不一樣指之處,就是次算法的本質 122 * 是保證了每次劃分選擇的劃分主元必定是一個較好的主元,算法先對數組5個一組進行分組 123 * 而後選擇每組的中位數,再遞歸的選擇各組中位數中的中位數做爲數組的劃分主元,以此保證劃分的平衡性 124 * 選擇中位數的時候必須使用遞歸調用的方法才能下降時間複雜度 125 * 從而保證在最壞狀況下都獲得一個好的劃分 126 * 最壞狀況下時間複雜度爲O(n) 127 */ 128 template<typename T> 129 T Select<T>::WorseLinearSelect(vector<T> &A,int p,int q,int k) 130 { 131 // 將輸入數組的n個元素劃分爲n/5(上取整)組,每組5個元素, 132 // 且至多隻有一個組有剩下的n%5個元素組成。 133 if (p==q) return A[p]; 134 135 int len=q-p+1; 136 int medianCount=1; 137 if (len>5) 138 medianCount = len%5 >0 ? len/5 + 1 : len/5; 139 vector<T> medians(medianCount);//存放每組的中位數 140 141 // 尋找每一個組的中位數。首先對每組中的元素(至多爲5個)進行插入排序, 142 // 而後從排序後的序列中選擇出中位數。 143 int m=p; 144 for (int j=0,m=p;j<medianCount-1;j++) 145 { 146 medians[j] = GetMedian(A,m,m+4); 147 m+=5; 148 } 149 medians[medianCount-1] = GetMedian(A,m,q); 150 //對第2步中找出的n/5(上取整)箇中位數,遞歸調用SELECT以找出其中位數pivot。 151 //(若是是偶數去下中位數) 152 int pivot = WorseLinearSelect(medians,0,medianCount-1,(medianCount+1)/2); 153 //調用PARTITION過程,按照中位數pivot對輸入數組進行劃分。肯定中位數pivot的位置r。 154 int r = partitionWithPivot(A,p,q,pivot); 155 int num = r-p+1; 156 //若是num=k,則返回pivot。不然,若是k<num,則在地區間遞歸調用SELECT以找出第k小的元素, 157 //若干k>num,則在高區找第(k-num)個最小元素。 158 if(num==k) return pivot; 159 else if (num>k) return WorseLinearSelect(A,p,r-1,k); 160 else return WorseLinearSelect(A,r+1,q,k-num); 161 } 162 #endif
main.cpp
1 #include <iostream> 2 #include <vector> 3 #include <time.h> 4 using namespace std; 5 #include "Select.h" 6 #define N 10 //排序數組大小 7 #define K 100 //排序數組範圍0~K 8 ////打印數組 9 void print_element(vector<int> A) 10 { 11 int len=A.size(); 12 for (int i=0;i<len;i++) 13 { 14 std::cout<<A[i]<<" "; 15 } 16 std::cout<<std::endl; 17 } 18 int main() 19 { 20 Select <int> s1; 21 int a[10]={23,4,34,345,3,21,45,246,98,50}; 22 vector<int> vec_int(a,a+10); 23 cout<<"原始數組"<<endl; 24 print_element(vec_int); 25 // 指望線性時間作選擇測試 26 cout<<"指望線性時間作選擇測試"<<endl; 27 for(int i=1;i<=N;i++) 28 { 29 int kMin=s1.RandomSelect(vec_int,0,N-1,i); 30 cout<<"第"<<i<<"小的數是:"<<kMin<<endl; 31 } 32 //最壞狀況線性時間的選擇測試 33 cout<<"最壞狀況線性時間的選擇測試"<<endl; 34 for(int i=1;i<=N;i++) 35 { 36 int kMin=s1.WorseLinearSelect(vec_int,0,N-1,i); 37 cout<<"第"<<i<<"小的數是:"<<kMin<<endl; 38 } 39 system("PAUSE"); 40 return 0; 41 }
【1】http://blog.csdn.net/xyd0512/article/details/8279371
【2】http://blog.chinaunix.net/uid-26822401-id-3163058.html
【3】http://www.tuicool.com/articles/mqQBfm
【4】http://www.cnblogs.com/Anker/archive/2013/01/25/2877311.html