日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

BFPTR算法详解+实现+复杂度证明

發布時間:2023/11/30 编程问答 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 BFPTR算法详解+实现+复杂度证明 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

BFPTR算法是由Blum、Floyed、Pratt、Tarjan、Rivest這五位牛人一起提出來的,其特點在于可以以最壞復雜度為O(n)O(n)O(n)地求解top?ktop-ktop?k問題。所謂top?ktop-ktop?k問題就是從一個序列中求解其第k大的問題。

top?ktop-ktop?k問題有許多解決方法,其他解決方法總結可以看我之前的一篇博客:傳送門

這個算法的介紹更詳細的可以參照算法導論9.3節 Selection in worst-case linear time,這里記錄一下我對這個算法的理解、實現以及復雜度的證明。

下面詳細的介紹一下BFPTR算法:

算法思想

宏觀來看,BFPTR算法是快速排序思想的一種應用:

  • 每次我們找到樞紐元素,然后按照樞紐元素將數組劃分為兩部分,就可以得到左邊x個元素都是比樞紐元素小的,右邊y個元素都是比樞紐元素大的

  • 然后根據我們想要查找的k決定到哪邊進行遞歸查找

    假設我們想要查找第k小,如果x==y,則樞紐元素就是需要查找的元素,如果x>k,則到左邊繼續查找,如果x<k,則去右邊的區間查找第k-x個元素

  • 典型的分治思想。問題的關鍵在于如何查找樞紐以及如何劃分,如果這兩步做的不夠好那么將會導致子問題的規模不能夠很快的縮小,從而使復雜度退化為O(n2)O(n^2)O(n2)

    關于如何劃分,主要就是快速排序的劃分思想,我在另一篇博客中已經詳細介紹了各種劃分方法的優劣,如果有興趣可以移步:傳送門。

    而BFPTR算法的精髓就在于如何選擇樞紐以保證復雜度不會退化。

    選擇樞紐

  • 將數組五個五個劃分為┌n/5┐\ulcorner n/5 \urcornern/5個小塊,求出每個塊的中位數
  • 遞歸求出這些中位數的中位數,將這個數當做樞紐,如何做到這點呢?我們BFPTR不就是求第k大嗎?我們遞歸地調用BFPTR就可以了。
  • 我在網上仔細研究了其他人實現BFPTR的代碼,個人覺得他們的代碼并沒有按照算法原本的思想操作(如果我理解的有問題歡迎大家批評指正),他們的做法都是將第一步劃分的中位數再遞歸地劃分為五個塊,求取中位數,直到最后不足五個元素直接返回中位數作為樞紐。代碼如下:

    void GetPovit(T* a,int l,int r) {int x; //將區間分割為[x,x+5)int cnt=0; //有多少個中位數for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]); //將當前區間的中位數放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return;GetPovit(a,l,l+cnt); }

    雖然這樣仍然可以解決問題,但是我覺得這樣并不是BFPTR算法的思想,我們可以參見算法導論中對這一步驟的描述:

    其中第三步中的遞歸地選擇這些中位數中的中位數,這顯然不是再將其劃分為五個五個的小塊求取中位數。因為我們再將其劃分為五個五個的小塊是不能保證最后選出來的是中位數的,如果這樣可行的話就會有一種求中位數的方法是這樣分塊遞歸了,但是現實是我們現在做的正是這個算法。所以我認為他們的做法是不對的,可行的原因也只是這是類似于一種隨機選取樞紐的操作。同樣佐證這一點的是后面對于復雜度的分析,如果使用上面網上的做法那么是無法進行復雜度分析的。

    我自己的做法是對于選出來的中位數調用BFPTR算法得到中位數。

    實現代碼

    首先是主體部分:

    int BFPTR(T* a,int l,int r,int k) {if(r-l == 1) return l; //返回找到的數字//五個一組的中位數的中位數的位置int idx = GetPovit(a,l,r);T povit = a[idx];swap(a[l],a[idx]);int i=l,j=r;while(i<j){do ++i; while(i+1 < r && a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}swap(a[l],a[j]);int num=j-l+1; //povit在當前序列中排第幾if(k == num) return j;else if(num > k) return BFPTR(a,l,j,k);else return BFPTR(a,j+1,r,k-num); }

    這里的劃分方法有一點點不同于我快速排序文章中的方法,主要是這里我將樞紐放在了中間,因為這樣我們有可能直接返回樞紐而不必一定要將區間劃分為1才返回。我認為可以一定程度上降低復雜度。
    這里我返回的是需要查找的數在數組中的位置,覺得位置包含了更多的信息,我們知道位置以后可以直接得到查詢的數字,可是返回查詢的數字無法直接得到位置。

    下面是最重要的獲取樞紐的操作:

    void InsertSort(T* a,int l,int r) {//插入排序int mid=(l+r)>>1; //獲得中位數就足夠了for(int i=l+1;i<=mid;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;} }int BFPTR(T* a,int l,int r,int k);T GetPovit(T* a,int l,int r) {int x; //將區間分割為[x,x+5)int cnt=0; //有多少個中位數for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]); //將當前區間的中位數放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return l;return BFPTR(a,l,l+cnt,cnt/2); }

    其中上面是一個簡單的插入排序,下面是將數組劃分為五個五個的小段,求取中位數以后放在數組的前面,然后再調用BFPTR函數得到中位數的中位數。之所以放在數組前面是因為這樣可以原地操作而不用占用其他的空間。

    完整的測試代碼會附在最后,也會附上網上大家的代碼供大家測試。

    復雜度分析

    對于一個規模為n的輸入,其最壞的時間復雜度為T(n)=T(n5)+Θ(n)+T(7n10)+Θ(n)T(n)=T(\frac{n}{5})+\Theta(n)+T(\frac{7n}{10})+\Theta(n)T(n)=T(5n?)+Θ(n)+T(107n?)+Θ(n)

    其中T(n5)+Θ(n)T(\frac{n}{5})+\Theta(n)T(5n?)+Θ(n)為求解其中位數的中位數的復雜度。我們首先將其劃分為┌n/5┐\ulcorner n/5 \urcornern/5個小塊,然后求解每個小塊的中位數是常數操作,因此總共是Θ(n)\Theta(n)Θ(n),前面的T(n5)T(\frac{n}{5})T(5n?)是BFPTR遞歸求解中位數的復雜度

    最后的Θ(n)\Theta(n)Θ(n)是劃分的復雜度

    比較難理解的是為什么是T(7n10)T(\frac{7n}{10})T(107n?)。我們可以先看一張圖(算法導論上盜下來的)


    這里的箭頭表示的含義是箭屁股的元素小于箭腦袋的元素。那么白元素的含義就很明顯啦,即就是每個五個元素小塊里面的中位數,其中的x表示中位數中的中位數。

    由箭頭關系我們不難看出,右下方的灰色區域的元素全部小于x,同理左上方區域的元素全部大于x,那么我們以x為樞紐元素將數組劃分以后數組的兩邊至少都有這些元素。這些元素最少有多少呢?

    白色區域大體和灰色區域的個數相等,因此我們計算灰色區域的個數。總共有┌n/5┐\ulcorner n/5 \urcornern/5列,因為我們求取的是最小有多少,因此我們取下界└n/5┘\llcorner n/5 \lrcornern/5,灰色區域占到其中的一半,每一列有三個元素,因此總共有3?┌└n/5┘/2┐3*\ulcorner \llcorner n/5 \lrcorner/2 \urcorner3?n/5/2,即最少3n10\frac{3n}{10}103n?的元素在一邊。

    我們考慮最壞的時間復雜度,因此另一邊最多7n10\frac{7n}{10}107n?個元素,而我們總落入這一邊。因此我們每次劃分以后都需要再進行T(7n10)T(\frac{7n}{10})T(107n?)的遞歸。

    接下來就是解遞歸式:T(n)=T(n5)+T(7n10)+Θ(n)T(n)=T(\frac{n}{5})+T(\frac{7n}{10})+\Theta(n)T(n)=T(5n?)+T(107n?)+Θ(n)
    對于這樣的遞歸式我們沒有什么好方法,只能選擇代入法進行證明。假設T(k)=ckT(k)=ckT(k)=ck,那么我們需要證明
    c?n5+c?7n10+Θ(n)<=cnc*\frac{n}{5}+c*\frac{7n}{10}+\Theta(n)<=cn c?5n?+c?107n?+Θ(n)<=cn

    即就是
    c?n10>=Θ(n)c*\frac{n}{10}>=\Theta(n) c?10n?>=Θ(n)

    對于足夠大的ccc,上式成立。
    當處于基本情況時,對于足夠大的ccc,上式也成立。

    因此,BFPTR的復雜度為O(n)O(n)O(n),證畢。

    為什么是5?

    首先,我們肯定是要劃分成奇數塊的,偶數塊求中位數不好求。
    其次,最小可以選擇的奇數是3,但是如果是3的話可以證明其復雜度就不是線性的了T(n)=T(n3)+Θ(n)+T(7n10)+Θ(n)T(n)=T(\frac{n}{3})+\Theta(n)+T(\frac{7n}{10})+\Theta(n)T(n)=T(3n?)+Θ(n)+T(107n?)+Θ(n)
    也可以選擇7,但是選擇7的話常數會更大,因此我們選擇將數組劃分為長度為5的小區間

    測試與分析

    雖然這個算法的復雜度為O(n)O(n)O(n)的,但是我將按自己思路實現的代碼和按網上思路實現的代碼都對規模為1e81e81e8規模的數據進行了測試,發現BFPTR算法不是很快。原因可能是BFPTR算法的常數比較大,而網上的思路作為一種模擬的隨機性快速選擇算法常數比較小。但是我認為如果要使用隨機性算法的話直接使用隨機數常數會更小。關于隨機性快速選擇算法和模擬的隨機性算法以及BFPTR到底哪一個更快,可以看我的另一篇博客:傳送門

    BFPTR算法代碼

    #include <iostream> #include <fstream> #include <ctime>using namespace std;typedef double T;T* CreatList(int &n) {ifstream in("TestFile");in >> n;T* ret = new T[n];for(int i=0;i<n;++i){in>>ret[i];}in.close();return ret; }void Show(T* a,int n) {for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl; }void InsertSort(T* a,int l,int r) {//插入排序int mid=(l+r)>>1; //獲得中位數就足夠了for(int i=l+1;i<=mid;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;} }int BFPTR(T* a,int l,int r,int k);T GetPovit(T* a,int l,int r) {int x; //將區間分割為[x,x+5)int cnt=0; //有多少個中位數for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]); //將當前區間的中位數放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return l;return BFPTR(a,l,l+cnt,cnt/2); }int BFPTR(T* a,int l,int r,int k) {if(r-l == 1) return l; //返回找到的數字//五個一組遞歸求取中位數int idx = GetPovit(a,l,r);T povit = a[idx];swap(a[l],a[idx]);int i=l,j=r;while(i<j){do ++i; while(i+1 < r && a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}swap(a[l],a[j]);int num=j-l+1; //povit在當前序列中排第幾if(k == num) return j;else if(num > k) return BFPTR(a,l,j,k);else return BFPTR(a,j+1,r,k-num); }void Query(T* a,int n) {int k;while(true){cout<<"請輸入k:";cin>>k;if(cin.fail() == true)break;if(k>n || k<1){//檢查輸入的有效性cout<<"請輸入1~"<<n<<"之間的數字"<<endl;continue;}clock_t S,E;S=clock();int idx=BFPTR(a,0,n,k);E=clock();cout<<"區間第"<<k<<"大的數字為:"<<a[idx]<<endl;cout<<"這次查詢花費時間"<<(double)(E-S)/CLOCKS_PER_SEC<<"s"<<endl;}cin.clear();cin.ignore(); }int main() {int n;T* a=CreatList(n);Query(a,n);delete[] a;return 0; }

    模擬隨機化選擇的代碼:

    #include <iostream> #include <fstream> #include <ctime>using namespace std;typedef double T;T* CreatList(int &n) {ifstream in("TestFile");in >> n;T* ret = new T[n];for(int i=0;i<n;++i){in>>ret[i];}in.close();return ret; }void Show(T* a,int n) {for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl; }void InsertSort(T* a,int l,int r) {//插入排序for(int i=l+1;i<r;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;} }void GetPovit(T* a,int l,int r) {int x; //將區間分割為[x,x+5)int cnt=0; //有多少個中位數for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]); //將當前區間的中位數放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return;GetPovit(a,l,l+cnt); }int BFPTR(T* a,int l,int r,int k) {if(r-l == 1) return l; //返回找到的數字GetPovit(a,l,r); //五個一組遞歸求取中位數T povit=a[l];int i=l-1,j=r;while(i<j){do ++i; while(a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}if(j-l+1>=k) return BFPTR(a,l,j+1,k);else return BFPTR(a,j+1,r,k-j+l-1); }void Query(T* a,int n) {int k;while(true){cout<<"請輸入k:";cin>>k;if(cin.fail() == true)break;if(k>n || k<1){//檢查輸入的有效性cout<<"請輸入1~"<<n<<"之間的數字"<<endl;continue;}clock_t S,E;S=clock();int idx=BFPTR(a,0,n,k);E=clock();cout<<"區間第"<<k<<"大的數字為:"<<a[idx]<<endl;cout<<"這次查詢花費時間"<<(double)(E-S)/CLOCKS_PER_SEC<<"s"<<endl;}cin.clear();cin.ignore(); }int main() {int n;T* a=CreatList(n);Query(a,n);delete[] a;return 0; }

    總結

    以上是生活随笔為你收集整理的BFPTR算法详解+实现+复杂度证明的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。