分治法-线性时间选择

线性时间选择问题:

给定线性序集中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)的关键之

猜你喜欢

转载自blog.csdn.net/qq_27437197/article/details/85347025