1. 程式人生 > >分治法-線性時間選擇

分治法-線性時間選擇

線性時間選擇問題:

給定線性序集中n個元素和一個整數k,1≤k≤n,要求找出這n個元素中第k小的元素。

1、隨機劃分線性選擇

基本思想:

  • 對劃分出的子陣列之一進行遞迴處理。
  • 子陣列的選擇與劃分元和k相關。
#include <iostream>
#include <ctime>

using namespace std;

int a[] = {5, 7, 3, 4, 8, 6, 9, 1, 2};

template<class Type>
void Swap(Type &x, Type &y);

inline int Random(int x, int y);

template<class Type>
int Partition(Type a[], int p, int r);

template<class Type>
int RandomizedPartition(Type a[], int p, int r);

template<class Type>
Type RandomizedSelect(Type a[], int p, int r, int k);

int main() {
    for (int i = 0; i < 9; i++) {
        cout << a[i] << " ";
    }
    cout << endl;
    cout << RandomizedSelect(a, 0, 8, 3) << endl;
}

template<class Type>
void Swap(Type &x, Type &y) {
    Type temp = x;
    x = y;
    y = temp;
}

inline int Random(int x, int y) {
    srand((unsigned) time(0));
    int ran_num = rand() % (y - x) + x;
    return ran_num;
}

template<class Type>
int Partition(Type a[], int p, int r) {
    int i = p, j = r + 1;
    Type x = a[p];

    while (true) {
        while (a[++i] < x && i < r);
        while (a[--j] > x);
        if (i >= j) {
            break;
        }
        Swap(a[i], a[j]);
    }
    a[p] = a[j];
    a[j] = x;
    return j;
}

template<class Type>
int RandomizedPartition(Type a[], int p, int r) {
    int i = Random(p, r);
    Swap(a[i], a[p]);//隨機獲得的一個位置元素,與所要劃分的子陣列起始位置元素交換
    return Partition(a, p, r);
}

template<class Type>
Type RandomizedSelect(Type a[], int p, int r, int k) {
    if (p == r) {
        return a[p];
    }
    int i = RandomizedPartition(a, p, r);//劃分元位置i
    int j = i - p + 1;//左子陣列a[p:i]的元素個數
    if (k <= j) {
        return RandomizedSelect(a, p, i, k);
    } else {
        //由於已知道子陣列a[p:i]中的元素均小於要找的第k小元素
        //因此,要找的a[p:r]中第k小元素是a[i+1:r]中第k-j小元素。
        return RandomizedSelect(a, i + 1, r, k - j);
    }
}

註釋:

1、利用隨機函式產生劃分基準,將陣列a[p:r]劃分成兩個子陣列a[p:i]和a[i+1:r],使a[p:i]中的每個元素都,不大於,a[i+1:r]中的每個元素

2、計算a[p:i]中元素個數j=i-p+1。

3、如果k<=j,則a[p:r]中第k小元素在子陣列a[p:i]中。

4、如果k>j,則第k小元素在子陣列a[i+1:r]中,要找的a[p:r]中第k小元素是a[i+1:r]中第k-j小元素。

5、在最壞的情況下,找到最小元素時,總是在最大元素處劃分,這是時間複雜度為O(n^2)。但平均時間複雜度與n呈線性關係,為O(n)。

 

 2、利用中位數線性時間選擇

問題分析:

  • 如果能線上性時間內找到一個劃分基準,使得劃分得到的兩個子陣列的長度都至少為原陣列長度的ε(0<ε<1)。
  • 那麼就可以在最壞情況下用O(n)時間完成選擇任務。
  • ε=0.9,則T(n)≤T(0.9n)+O(n),由此得,T(n)=O(n)?

例子:

按遞增順序,找出下面29個元素的第18個元素:8,31,60,33,17,4,51,57,49,35,11,43,37,3,13,52,6,19,25,32,54,16,5,41,7,23,22,46,29.

(1) 把前面25個元素分為6(=ceil(29/5

))組 

     (8,31,60,33,17),(4,51,57,49,35),(11,43,37,3,13),(52,6,19,25,32),(54,16,5,41,7),(23,22,46,29)

(2) 提取每一組的中值元素,構成集合{31,49,13,25,16,29}。

(3) 遞迴地使用演算法求取該集合的中值,得到m=29。

(4) 根據m=29, 29個元素劃分為3個子陣列:

  • P={8,17,4,11, 3,13,6,19, 25,16,5,7,23,22}
  • Q={29}
  • R={31,60,33,51,57,49,35,43,37,52,32,54,41,46}

(5) 由於|P|=14,|Q|=1,k=18,所以放棄P,Q,使k=18-14-1=3R遞迴地執行本演算法。

(6) 將R劃分成3(ceil(14/5))組:

     {31,60,33,51,57},{49,35,43,37,52},{32,54,41,46}

(7) 求取這3組元素的中值元素分別為:{51,43,46},這個集合的中值元素是43。

(8) 根據43R劃分成3組:

     {31, 33, 35,37,32, 41},{43},{60, 51,57, 49, 52,54, 46}

(9)  因為k=3第一個子陣列的元素個數大於k,所以放棄後面兩個子陣列,以k=3第一個子陣列遞迴呼叫本演算法。

(10)  將這個子陣列分成5個元素的一組:

      {31,33,35,37,32}{41},取其中值元素為33

(11)  根據33,把第一個子陣列劃分成

      {31,32},{33},{35,37}

(12)  因為k=3,而第一、第二個子陣列的元素個數之和3所以33為所求取的第18個小元素。

 

template<class Type>
void BubbleSort(Type a[], int p, int r);

template<class Type>
Type Select(Type a[], int p, int r, int k);

template<class Type>
void BubbleSort(Type a[], int p, int r) {
    //記錄一次遍歷中是否有元素的交換
    bool exchange;
    for (int i = p; i <= r - 1; i++) {
        exchange = false;
        for (int j = i + 1; j <= r; j++) {
            if (a[j] < a[j - 1]) {
                Swap(a[j], a[j - 1]);
                exchange = true;
            }
        }
        //如果這次遍歷沒有元素的交換,那麼排序結束
        if (false == exchange) {
            break;
        }
    }
}

template<class Type>
Type Select(Type a[], int p, int r, int k) {
    if (r - p < 75) {
        BubbleSort(a, p, r);
        return a[p + k - 1];
    }
    //(r-p-4)/5相當於n-5
    for (int i = 0; i <= (r - p - 4) / 5; i++) {
        //將元素每5個分成一組,分別排序,並將該組中位數與a[p+i]交換位置
        //使所有中位數都排列在陣列最左側,以便進一步查詢中位數的中位數
        BubbleSort(a, p + 5 * i, p + 5 * i + 4);
        Swap(a[p + 5 * i + 2], a[p + i]);
    }
    //找中位數的中位數
    Type x = Select(a, p, p + (r - p - 4) / 5, (r - p - 4) / 10);
    int i = Partition(a, p, r, x);
    int j = i - p + 1;
    if (k <= j) {
        return Select(a, p, i, k);
    } else {
        return Select(a, i + 1, r, k - j);
    }
}

int main() {
    int a[100];
    srand((unsigned) time(0));
    for (int i = 0; i < 100; i++) {
        a[i] = Random(0, 500);
        cout << "a[" << i << "]:" << a[i] << " ";
    }
    cout << endl;
    cout << "第83小元素是" << Select(a, 0, 99, 83) << endl;
    //重新排序,對比結果
    BubbleSort(a, 0, 99);
    for (int i = 0; i < 100; i++) {
        cout << "a[" << i << "]:" << a[i] << " ";
    }
    cout << endl;
}

 劃分原理分析:

1、將全部的數劃分為兩個部分,小於基準的在左邊,大於等於基準的在右邊。

2、在上述情況下,找出的基準x至少比3 ⌊(n-5)/10⌋ 個元素大

推導:3 ⌊(n-5)/10⌋

     (1)因為在剩餘一半的組中⌊n/5-1⌋*(1/2),每一組中有2個元素小於本組的中位數,有⌊n/5-1⌋*(1/2)*2= ⌊n/5-1⌋個小於基         準。

     (2)在 n/5 ⌉箇中位數中1/2*⌊n/5-1⌋=⌊(n-5)/10⌋個小於基準x

     (3)因此,總共有,3 ⌊(n-5)/10⌋個元素小於基準x,同理,基準x也至少比3 ⌊(n-5)/10⌋個元素小。

3、當n≥75時,3 ⌊(n-5)/10⌋≥n/4,所以按此基準劃分所得的2個子陣列長度都至少縮短1/4。

演算法複雜度分析:

  • n<70,演算法所用的計算時間不超過一個常數C1
  • 分組求中位數的for迴圈執行時間為O(n)
  • 中位數x為基準對陣列進行劃分,需要O(n)時間
  • 設:對n個元素的數組調selectT(n)
  1. 則找出中位數的中位數x,至多需要T(n/5)
  2. 已證明劃分得到的子陣列長度不超過3n/4
  3. 無論對哪個子陣列調select多需T(3n/4)時間
  4. 因此,T(n)≤2*O(n)+T(n/5)+T(3n/4)

演算法分析:

  • 分組大小固定為5,以70為是否遞歸呼叫的分界
  • 這兩點保證了T(n)的遞迴式中2個自變數之和n/5+3n/4=19n/20=εn0<ε<1
  • 這是使T(n)=O(n)的關鍵之