算法導論-順序統計-快速求第i小的元素

目錄                                                              

一、問題的引出-求第i個順序統計量html

二、方法一:以指望線性時間作選擇ios

三、方法二(改進):最壞狀況線性時間的選擇c++

四、完整測試代碼(c++)git

五、參考資料github

內容                                                               

一、問題的引出-求第i個順序統計量                                        

   什麼是順序統計量?及中位數概念算法

   在一個由元素組成的集合裏,第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 }
View Code

三、方法二(改進):最壞狀況線性時間的選擇                       

      相比於上面的隨機選擇,咱們有另外一種相似的算法,它在最壞狀況下也能達到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 }

該算法在最壞狀況下運行時間爲Θ(n)

代碼實現(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 }
View Code

四、完整測試代碼(c++)                                                  

完整源碼下載地址Github

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 
View Code

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 }
View Code

五、參考資料                                                                    

【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

相關文章
相關標籤/搜索