递归与分治

来源:互联网 发布:淘宝海鲜店铺排名 编辑:程序博客网 时间:2024/05/16 13:07

递归与分治

算法总体思想

  对这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。

  将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

递归的概念

  直接或间接地调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数

  由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。

分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

示例:

例1 阶乘函数

阶乘函数可递归地定义为:

n! = 1      n = 0  (边界条件)

n! = n(n-1)!   n > 0  (递归方程)

边界条件递归方程是递归函数的二个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。

实现:

#include <iostream>
using namespace std;

// factorial implement by recursive
longfactorial_recursive(long n)
{
    if (n == 0)
        return 1;
    return n * factorial_recursive(n-1);
}

// factorial implement by loop
longfactorial_loop(long n)
{
    long result = 1;
    for (int i = n; i >0; -- i)
        result *= i;
    return result;
}

int main()
{
    for (int i = 0; i < 10; i ++ ) {
        cout << i << "!" << " = "
             <<factorial_recursive(i)
             << ","
             << factorial_loop(i)
             << endl;
    }
    return 0;
}

 

例2 Fibonacci数列

无穷数列11235813213455……,称为Fibonacci数列。它可以递归地定义为:

F(n) = 1                n = 0 (边界条件)

F(n) = 1            n = 1 (递归方程)

F(n) = F(n - 1) +F(n - 2)      n > 2 (递归方程)

实现:

#include <iostream>
using namespace std;

// fibonacci implement by recursive
longfibonacci_recursive(long n)
{
    if (n <= 1 )
        return 1;

    return fibonacci_recursive(n - 1)
            + fibonacci_recursive(n - 2);
}

// fibonacci implement by loop
longfibonacci_loop(long n)
{
    if (n == 0 || n ==1)
        return 1;

    long f1 = 1;
    long f2 = 1;
    long result = 0;
    for (long i =1; i < n ; ++ i) {
        result = f1 + f2;
        f1 = f2;
        f2 = result;
    }
    return result;
}

int main()
{
    cout << "fibonacci implement by recursive: " << endl;
    for (long i =0; i <= 20; ++ i)
        cout <<fibonacci_recursive(i) << " " ;
    cout << endl << endl;

    cout << "fibonacci implement by loop: " << endl;
    for (long i =0; i <= 20; ++ i)
        cout << fibonacci_loop(i)<< " " ;
    cout << endl;
    return 0;
}

 

3 Ackerman函数

当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数

Ackerman函数A(nm)定义如下:

A(1,0) = 2

A(0,m) = 1 m >= 0

A(n,0) = n + 2 n >= 2

A(n,m) = A(A(n-1,m),m-1) n,m >= 1

2例中的函数都可以找到相应的非递归方式定义:

n! = 1 * 2 * 3 * ... * (n - 1) * n

本例中的Ackerman

函数却无法找到非递归的定义。

A(nm)的自变量m的每一个值都定义了一个单变量函数:

M = 0时,A(n,0)=n+2

M = 1时,A(n,1)=A(A(n-1,1),0) = A(n-1,1)+2,和A(1,1)=2A(n,1)=2*n

M = 2时,A(n,2) = A(A(n-1,2),1)=2A(n-1,2),和A(1,2)=A(A(0,2),1)=A(1,1)=2,故A(n,2)=2^n

M = 3时,类似的可以推出

M = 4时,A(n,4)

的增长速度非常快,以至于没有适当的数

学式子来表示这一函数。

实现:

#include <iostream>
using namespace std;

// ackerman implement
long ackerman(long n,long m)
{
    if (n == 1 && m ==0)
        return (long)2;
    if (n == 0)
        return 1;
    if (m == 0)
        return n + 2;

    return ackerman( ackerman(n-1,m) , m-1);
}

int main()
{
    cout << "m = 0 : " << endl;
    cout << "ackerman(1,0) = " << ackerman(1,0) << endl;
    cout << "ackerman(2,0) = " << ackerman(2,0) << endl;
    cout << "ackerman(3,0) = " << ackerman(3,0) << endl;
    cout << "ackerman(4,0) = " << ackerman(4,0) << endl;

    cout << "m = 1 : " << endl;
    cout << "ackerman(1,1) = " << ackerman(1,1) << endl;
    cout << "ackerman(2,1) = " << ackerman(2,1) << endl;
    cout << "ackerman(3,1) = " << ackerman(3,1) << endl;
    cout << "ackerman(4,1) = " << ackerman(4,1) << endl;

    cout << "m = 2 : " << endl;
    cout << "ackerman(1,2) = " << ackerman(1,2) << endl;
    cout << "ackerman(2,2) = " << ackerman(2,2) << endl;
    cout << "ackerman(3,2) = " << ackerman(3,2) << endl;
    cout << "ackerman(4,2) = " << ackerman(4,2) << endl;

    cout << "m = 3 : " << endl;
    cout << "ackerman(1,3) = " << ackerman(1,3) << endl;
    cout << "ackerman(2,3) = " << ackerman(2,3) << endl;
    cout << "ackerman(3,3) = " << ackerman(3,3) << endl;
    cout << "ackerman(4,3) = " << ackerman(4,3) << endl;

    return 0;
}

4排列问题

设计一个递归算法生成n个元素{r1,r2,,rn}的全排列。

R={r1,r2,,rn}是要进行排列的n个元素,

Ri=R-{ri}

集合X中元素的全排列记为perm(X)

(ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。R的全排列可归纳定义如下:

n=1时,perm(R)=(r),其中r是集合R

中唯一的元素;

n>1时,perm(R)(r1)perm(R1)(r2)perm(R2),…,(rn)perm(Rn)构成。

#include <iostream>
#include <vector>
#include <iterator>
using namespace std;


/* 使用递归实现
*
递归产生所有前缀是list[0:k-1]
*
且后缀是list[k,m]的全排列的所有排列
*
调用算法perm(list,0,n-1)则产生list[0:n-1]的全排列
*/

template <class T>
voidperm_recursion(T list[],int k,int m)
{
    // 产生list[k:m]的所有排列
    if (k == m) {
        for (int i =0; i <= m; i ++)
            cout << list[i]<< " ";
        cout << endl;
    }
    else {
    // 还有多个元素,递归产生排列
       for (int i = k; i <= m;++ i) {
            swap(list[k],list[i]);
            perm_recursion(list,k+1,m);
            swap(list[k],list[i]);
        }
    }
}

// 非递归实现(可参照STL next_permutation源码)
template <class T>
void perm_loop(Tlist[],int len)
{
    int i,j;
    vector<int> v_temp(len);

    // 初始排列
    for(i = 0; i < len ; i++)
        v_temp[i] = i;

    while (true) {
        for (i = 0; i < len; i ++ )
            cout << list[v_temp[i]]<< " ";
        cout << endl;

        // 从后向前查找,看有没有后面的数大于前面的数的情况,若有则停在后一个数的位置。
       for(i = len - 1;i > 0 &&v_temp[i] < v_temp[i-1] ; i--);
        if (i == 0)
            break;
        // 从后查到i,查找大于 v_temp[i-1]的最小的数,记入j
       for(j = len - 1 ; j > i&& v_temp[j] < v_temp[i-1] ; j--);
        // 交换 v_temp[i-1] v_temp[j]
       swap(v_temp[i-1],v_temp[j]);

        // 倒置v_temp[i]v_temp[n-1]
       for(i = i,j =len - 1 ; i < j;i++,j --) {
            swap(v_temp[i],v_temp[j]);
        }
    }
}


int main()
{
    int list[] = {0,1,2};
    cout << "permutation implement by recursion: " << endl;
    perm_recursion(list,0,2);
    cout << endl;

    cout << "permutation implement by loop: " << endl;
    perm_loop(list,3);
    cout << endl;
    return 0;
}

 

5整数划分问题

将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥1k≥1

正整数n的这种表示称为正整数n的划分。正整数n的不同划分个数称为正整数n的划分数,记作p(n)

例如正整数6有如下11种不同的划分,所以p(6) = 11

6

5+1

4+24+1+1

3+33+2+13+1+1+1

2+2+22+2+1+12+1+1+1+1

1+1+1+1+1+1

前面的几个例子中,问题本身都具有比较明显的递归关系,因而容易用递归函数直接求解。

在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:在正整数n的所有不同划分中,将最大加数n1不大于m的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。

(1) q(n,1)=1,n >= 1;当最大加数n1不大于1时,任何正整数n只有一种划分形式,

n = 1 + 1 + 1 + … +1.

(2) q(n,m) = q(n,n),m >= n; 最大加数n1实际上不能大于n。因此,q(1,m)=1(3) q(n,n)=1 + q(n,n-1); 正整数n的划分由n1=n的划分和n1 ≤ n-1的划分组成。

(4) q(n,m)=q(n,m-1)+q(n-m,m),n > m >1;正整数n的最大加数n1不大于m的划分由 n1 = m的划分和n1 ≤ m-1的划分组成。

前面的几个例子中,问题本身都具有比较明显的递归关系,因而容易用递归函数直接求解。

在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量:将最大加数n1不大于m的划分个数记作q(n,m)。可以建立q(n,m)的如下递归关系。

q(n,m) = 1 n = 1, m = 1

q(n,m) = q(n,n) n = 1, m = 1

q(n,m) = 1 + q(n,n-1) n = m

q(n,m) = q(n,m-1) + q(n-m,m) n > m > 1

正整数n的划分数p(n) = q(n,n)

实现:

<iostream>
using namespace std;

//
int __int_partition(int n,int m)
{
    if (n < 1 || m <1)
        return 0;
    if (n == 1 || m ==1)
        return 1;
    if (n < m)
        return __int_partition(n,n);
    if (n == m)
        return __int_partition(n,m - 1) + 1;
    return __int_partition(n,m - 1) +__int_partition(n - m,m);
}
int integer_partition(int n)
{
    return __int_partition(n,n);
}

int main()
{
    for (int i = 1; i < 7; ++ i) {
        cout << "integer_patition("
             << i
             << ") = "
             <<integer_partition(i)
             << endl;
    }
    return 0;
}

 

6 Hanoi塔问题

a,b,c3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:

规则1:每次只能移动1个圆盘;

规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上;

规则3:在满足移动规则12的前提下,可将圆盘移至a,b,c中任一塔座上。

实现:

递归小结

优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。

缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。

解决方法:在递归算法中消除递归调用,使其转化为非递归算法。

1、采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。

2、用递推来实现递归函数。

3、通过变换能将一些递归转化为尾递归,从而迭代求出结果。

后两种方法在时空复杂度上均有较大改善,但其适用范围有限。

分治法的适用条件

分治法所能解决的问题一般具有以下几个特征:

1、该问题的规模缩小到一定的程度就可以容易地解决;

2、该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质

3、利用该问题分解出的子问题的解可以合并为该问题的解;

4、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。(这条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然也可用分治法,但一般用动态规划较好。)

分治法的基本步骤

divide-and-conquer(P)

{

if (|P| <= n0) adhoc(P); // 解决小规模的问题

divide P into smaller subinstances P1,P2,...,Pk//分解问题

for (i=1,i<=k,i++)

yi=divide-and-conquer(Pi); //递归的解各子问题

return merge(y1,...,yk); //将各子问题的解合并为原问题的解

}

人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。即将一个问题分成大小相等的k个子问题的处理方法是行之有效的。这种使子问题规模大致相等的做法是出自一种平衡(balancing)子问题的思想,它几乎总是比子问题规模不等的做法要好。

分治法的复杂性分析

一个分治法将规模为n的问题分成k个规模为nm的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用mergek个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P| = n的问题所需的计算时间,则有:

通过迭代法求得方程的解:

二分搜索技术

给定已按升序排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x

分析:

1、该问题的规模缩小到一定的程度就可以容易地解决;

2、该问题可以分解为若干个规模较小的相同问题;

3、分解出的子问题的解可以合并为原问题的解;

4、分解出的各个子问题是相互独立的。

很显然此问题分解出的子问题相互独立,即在a[i]的前面或后面查找x是独立的子问题,因此满足分治法的第四个适用条件。

二分搜索实现:

#include <iostream>
using namespace std;

// 查找成功返回value索引,查找失败返回-1
template <class T>
intbinary_search(T array[],const T& value,int left,int right)
{
    while (right >= left) {
        int m = (left + right) / 2;
        if (value == array[m])
            return m;
        if (value < array[m])
            right = m - 1;
        else
            left = m + 1;
    }
    return -1;
}

int main()
{
    int array[] = {0,1,2,3,4,5,6,7,8,9};

    cout << "0 in array position: " << binary_search(array,0,0,9) << endl;
    cout << "9 in array position: " << binary_search(array,9,0,9) << endl;
    cout << "2 in array position: " << binary_search(array,2,0,9) << endl;
    cout << "6 in array position: " << binary_search(array,6,0,9) << endl;
    cout << "10 in array position: " << binary_search(array,10,0,9) << endl;

    return 0;
}

算法复杂度分析:

每执行一次算法的while循环,待搜索数组的大小减少一半。因此,在最坏情况下,while循环被执行了O(logn)次。循环体内运算需要O(1) 时间,因此整个算法在最坏情况下的计算时间复杂性为O(logn)

大整数的乘法

请设计一个有效的算法,可以进行两个n位大整数的乘法运算

小学的方法:O(n^2)效率太低

分治法:

X = a b;

Y = c d;

X = a*2^(n/2) + b Y = c*2^(n/2) + d

X*Y = a*c*2^n + (a*d + b*c)*2^(n/2) + b*d

算法复杂度分析:

T(n) = O(1) n = 1

T(n) = 4T(n/2) + O(n) n > 1

T(n) = O(n^2) 没有改进

为了降低时间复杂度,必须减少乘法的次数

(1)X*Y = a*c*2^n + ((a-b)(d-c)+ac+bd)*2^(n/2) + b*d

(2)X*Y = a*c*2^n + ((a+b)(d+c)-ac-bd)*2^(n/2) + bd

细节问题:两个XY的复杂度都是O(nlog3),但考虑到a+b,d+c可能得到n+1位的结果,使问题的规模变大,故不选择第2种方案。

算法复杂度分析:

T(n) = O(1) n = 1

T(n) = 3T(n/2) + O(n) n > 1

T(n) = O(n^log3) = O(n^1.59) 较大的改进

小学的方法:O(n^2)û效率太低

分治法: O(n^1.59)较大的改进

更快的方法??如果将大整数分成更多段,用更复杂的方式把它们组合起来,将有可能得到更优的算法。

Strassen矩阵乘法

对于两个n*n的矩阵A,B,求其乘积

传统方法:O(n^3)

AB的乘积矩阵C中的元素C[i,j]定义为

若依此定义来计算AB的乘积矩阵C,则每计算C的一个元素C[i][j],需要做n次乘法和n-1次加法。因此,算出矩阵C的个元素所需的计算时间为O(n^3)

分治法:

使用与上例类似的技术,将矩阵ABC中每一矩阵都分块成4个大小相等的子矩阵。由此可将方程C=AB重写为:

由此可得:

算法复杂度分析

T(n) = O(1) n = 2

T(n) = 8T(n/2) + O(n^2) n > 2

T(n) = O(n^3)

为了降低时间复杂度,必须减少乘法的次数。

算法复杂度分析

T(n) = O(1) n = 2

T(n) = 7*T(n/2) + O(n^2) n > 2

T(n) = O(n^log7) = O(n^2.81) 较大的改进

更快的方法??

HopcroftKerr已经证明(1971),计算2个2×2矩阵的乘积,7次乘法是必要的。因此,要想进一步改进矩阵乘法的时间复杂性,就不能再基于计算2×2矩阵的7次乘法这样的方法了。或许应当研究3×3或5×5矩阵的更好算法。

Strassen之后又有许多算法改进了矩阵乘法的计算时间复杂性。目前最好的计算时间上界是 O(n^2.376)

是否能找到O(n^2)的算法?

 

棋盘覆盖

在一个2k×2k个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2L型骨牌不得重叠覆盖。

棋盘示例(k = 2)和四种L型骨牌示例

 

分析

k>0时,将2^k×2^k棋盘分割为42^(k-1)×2^(k-1)子棋盘所示。

特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1

算法复杂度

实现

 

#include <iostream>
#include <vector>
#include <cmath>
#include <iterator>
using namespace std;

void__chessboard_cover(vector<vector<int> >& cheb,
                        int tx,int ty,
                        int dx,int dy,
                        int size,
                        int& tile);
/* 棋盘覆盖主函数
* cheb:
棋盘数组
* dx:
特殊方格的横坐标
* dy:
特殊方格的纵坐标
*/

voidchessboard_cover(vector<vector<int> >& cheb,int dx,int dy)
{
    int tile = 1;
    __chessboard_cover(cheb,0,0,dx,dy,cheb.size(),tile);
}
/* 棋盘覆盖辅助函数
* cheb:
棋盘数组
* tx:
起始横坐标
* ty:
起始纵坐标
* dx:
特殊方格的横坐标
* dy:
特殊方格的横坐标
* size:
棋盘大小
* tile:
骨牌编号
*/

void__chessboard_cover(vector<vector<int> >& cheb,
                        int tx,int ty,
                        int dx,int dy,
                        int size,
                        int& tile)
{
    if (size == 1)
        return ;
    int t = tile ++ ; // L骨牌号
    int s = size / 2; // 分割棋盘

    //
覆盖左上角子棋盘
    if (dx < tx + s&& dy < ty + s) {
        // 特殊方格在此子棋盘中
       __chessboard_cover(cheb,tx,ty,dx,dy,s,tile);
    }
    else {
        // 此棋盘中无特殊方格,t号骨牌覆盖下角方格
       cheb[tx + s - 1][ty + s - 1] = t;
        // 覆盖其余方格
       __chessboard_cover(cheb,tx,ty,tx + s - 1, ty + s -1,s,tile);
    }

    // 覆盖右上角子棋盘
    if (dx >= tx + s&& dy < ty + s) {
        // 特殊方格在此棋盘中
       __chessboard_cover(cheb,tx + s,ty,dx,dy,s,tile);
    }
    else {
        // tL型骨牌覆盖左下角
       cheb[tx + s][ty + s - 1] = t;
        __chessboard_cover(cheb,tx +s,ty,tx + s,ty + s - 1,s,tile);
    }

    // 覆盖左下角子棋盘
    if (dx < tx + s&& dy >= ty + s) {
        // 特殊方格在此棋盘中
       __chessboard_cover(cheb,tx,ty + s,dx,dy,s,tile);
    }
    else {
        // tL型骨牌覆盖右上角
       cheb[tx + s - 1][ty + s] = t;
        __chessboard_cover(cheb,tx,ty +s,tx + s - 1,ty +s,s,tile);
    }

    // 覆盖右下角子棋盘
    if (dx >= tx + s&& dy >= ty + s) {
        // 特殊方格在此棋盘中
       __chessboard_cover(cheb,tx + s,ty + s,dx,dy,s,tile);
    }
    else {
        // tL型骨牌覆盖左上角
       cheb[tx + s][ty + s] = t;
        __chessboard_cover(cheb,tx + s,ty+ s,tx + s,ty + s,s,tile);
    }
}
int main()
{
    int k = 2;
    int size = pow (2,k);
    vector<vector<int> >cheb(size);
    for (size_t i= 0 ;i <cheb.size(); ++i) {
        cheb[i].resize(size);
    }

    for (int i = 0; i < size; ++ i) {
        for (int j =0;j < size; ++ j) {
            int dx = i;
            int dy = j;
            cout << "dx = " << dx << " , dy =" << dy << endl;
            cheb[dx][dy] = 0;
            chessboard_cover(cheb,dx,dy);

            for (size_t i = 0;i < cheb.size(); ++ i) {
                copy(cheb[i].begin(),cheb[i].end(),ostream_iterator<int>(cout," "));
                cout << endl;
            }
            cout << endl;
        }
    }
    return 0;
}

线性时间选择

给定线性序集中n个元素和一个整数k1 ≤ k ≤ n,要求找出这n个元素中第k小的元素。

思想

// 在数组apr区间内找到第k小的元素

template<class Type>

Type RandomizedSelect(Type a[],int p,int r,int k)

{

if (p == r)

return a[p]; // 如果pr相等,第n小都是a[p]

// 数组a[p:r]被随机分成两个部分,a[p:i]a[i+1:r]

// 使得a[p:i]中的元素都小于a[i+1:r]中的元素。

int i = RandomizedPartition(a,p,r);

j = i - p + 1;

if (k <= j)

return RandomizedSelect(a,p,i,k);

else

return RandomizedSelect(a,i+1,r,k-j);

}

在最坏情况下,算法randomizedSelect需要O(n^2)计算时间(在找最小元素的时候,总在最大元素处划分),但可以证明,算法randomizedSelect可以在O(n)平均时间内找出n个输入元素中的第k小元素。

如果能在线性时间内找到一个划分基准,使得按这个基准所划分出的2个子数组的长度都至多为原数组长度的ε倍(0<ε<1是某个正常数),那么就可以在最坏情况下用O(n)时间完成选择任务。

例如,若ε=9/10,算法递归调用所产生的子数组的长度至少缩短1/10。所以,在最坏情况下,算法所需的计算时间T(n)满足递归式T(n)T(9n/10)+O(n)。由此可得T(n)=O(n)

步骤

第一步,将n个输入元素划分成én/5ù个组,每组5个元素,只可能有一个组不是5个元素。用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共én/5ù个。

第二步,递归调用select来找出这én/5ù个元素的中位数。如果én/5ù是偶数,就找它的2个中位数中较大的一个。以这个元素作为划分基准。

分析

伪代码

Type Select(Type a[], int p, int r, int k)

{

if (r - p < 75) {

// 问题的规模足够小,用某个简单排序算法对数组a[p:r]排序;

return a[p + k - 1];

}

for (int i = 0; i <= ( r - p - 4 ) / 5 ; i ++ ) {

a[p + 5 * i]a[p + 5 * i + 4]的第3小元素与a[p+i]交换位置;

}

// 找中位数的中位数,r - p - 4即上面所说的n - 5

Type x = Select(a, p, p + (r - p - 4 ) / 5, (r - p - 4) / 10);

// 数据n根据x划分开来

int i = Partition(a,p,r,x);

j = i - p + 1;

if (k <= j)

return Select(a,p,i,k);

else

return Select(a,i+1,r,k-j);

}

算法复杂度分析

上述算法将每一组的大小定为5,并选取75作为是否作递归调用的分界点。这2点保证了T(n)的递归式中2个自变量之和n/5+3n/4=19n/20=εn0<ε<1。这是使T(n)=O(n)的关键之处。当然,除了575之外,还有其他选择。

实现

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

/* 线性时间查找
* arr:
数据存储数组
* start:
开始查找点
* end:
结束查找点
* n:
查找第n(n = 1,2,3,...,end-start+1)
*/

template <class T>
T linear_time_select(vector<T>& arr,int start,int end,int n)
{
    if (end - start < 75) {
        sort (arr.begin() +start,arr.begin() + end + 1);
        return arr[start + n - 1];
    }

    for (int i = 0;i < (end - start - 4) / 5; ++ i) {
        sort (arr.begin() + start + 5 * i,arr.begin() +start +5 * i + 5);
        swap (*(arr.begin() + start + 5 * i + 2),*(arr.begin() +start + i));
    }
    // 找到中位数的中位数
    Tmedian = linear_time_select(arr,start,
                                  start +(end - start - 4) / 5 - 1,
                                  (end -start - 4) /10 + 1);

    // 数据 arr根据 median 划分开来
    int middle =__partition_by_median(arr,start,end,median);
    int distance = middle - start + 1; // 中位数的位置与start的距离
    if (n <= distance)
        // 在前半截
       return linear_time_select(arr,start,middle,n);
    else
        // 在后半截
    returnlinear_time_select(arr,middle + 1,end,n - distance);

}

// arr按照值median划分开来,并返回界限的位置
template <class T>
int __partition_by_median(vector<T> &arr,int start,int end,T median)
{
    while (true) {
        while (true) {
            if (start == end)
                return start;
            else if (arr[start] <median)
                ++ start;
            else
                break;
        }
        while (true) {
            if (start == end)
                return end;
            else if (arr[end] >median) {
                -- end;
            }
            else
                break;
        }
        swap(arr[start],arr[end]);
    }
}
int main()
{
    vector<int> arr;
    const int c =2000;
    for (int i = 0;i < c; ++ i) {
        arr.push_back(i);
    }
    // 随机排列
   random_shuffle(arr.begin(),arr.end());

    for (int i = 1; i < c+1; ++ i) {
        cout << "find the " << i << "element,position is "
            <<linear_time_select(arr,0,c-1,i) << endl;
    }
    return 0;
}

循环赛日程表

题目表述:

设有n = 2 ^ k个运动员要进行网球循环赛,设计一个满足以下要求的比赛日程表:

(1)每个选手必须与其他n-1个选手各赛一次;

(2)每个选手一天只能赛一次;

(3)循环赛一共进行n-1天。

按分治策略,将所有的选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归地用对选手进行分割,直到只剩下2个选手时,比赛日程表的制定就变得很简单。这时只要让这2个选手进行比赛就可以了。

实现

#include <iostream>
#include <vector>
#include <cmath>
#include <iterator>
#include <iomanip>
using namespace std;
void__table(vector<vector<int> >& arr,int start,int end);
voidround_match_table(vector<vector<int> >& arr)
{
    int count = arr.size();
    for (int i = 0;i < count;++ i) {
        arr[0][i] = i + 1;
    }
    __table(arr,0,count-1);
}
void__table(vector<vector<int> >& arr,int start,int end)
{
    if (end - start + 1 == 2) {
        arr[1][start] = arr[0][end];
        arr[1][end] = arr[0][start];
        return ;
    }
    int half = (end - start + 1) / 2;
    // 左上角
   __table(arr,start,start + half -1 );
    // 右上角
   __table(arr,start + half,end);
    // 左下角
    for (int i =0;i < half; ++ i){
        for (int j = start; j <= end; ++ j) {
            arr[i + half][j-half] =arr[i][j];
        }
    }
    // 右下角(其实左下角和右下角可以在上一个循环中实现的,
    //
但是为了算法结构清晰,因此分为两个循环)
    for (int i =0;i < half; ++ i){
        for (int j = start; j < end; ++j) {
            arr[i + half][j + half] =arr[i][j];
        }
    }
}
int main()
{
    int k = 4;
    int size = pow(2,k);
    vector<vector<int> >arr(size);
    for (int i = 0; i < size; ++ i) {
        arr[i].resize(size);
    }

    round_match_table(arr);

    for (int i = 0;i < size; ++ i) {
        for (int j =0;j < size; ++ j) {
            cout << std::setw(3) <<arr[i][j];
        }
        cout << endl;
    }
    return 0;
}

Gray码问题

实现

#include <iostream>
#include <vector>
#include <iterator>
#include <cmath>
using namespace std;

/* gray code
* rows:
行数(2^n)
* cols:
列数(n)
* arr: rows
行,cols列的存储数组
*/


void gray_code(int rows,int cols,vector<vector<int> >& arr)
{
    // 第一行,递归结束
    if (rows == 1)
        return ;

    // 确定第一列,前半部分为0,后半部分为1
    for (int i =0; i < rows / 2; ++ i) {
        arr[i][cols - 1] = 0;
        arr[rows - i - 1][cols - 1] = 1;
    }

    // 递归完成rows列数据第cols
   gray_code(rows / 2, cols - 1,arr);

    // 对称复制
    for (int k = rows /2; k < rows; ++k) {
        for (int j =0;j < cols - 1; ++ j) {
            arr[k][j] = arr[rows - k - 1][j];
        }
    }
}

int main()
{
    const int cols =3;
    int rows = pow(2,cols);
    vector<vector<int> >arr(rows);
    for (size_t i = 0;i < arr.size(); ++ i)  {
        arr[i].resize(cols);
    }
    gray_code(rows,cols,arr);

    // output
    for (size_t i = 0;i < arr.size();++ i) {
       copy(arr[i].rbegin(),arr[i].rend(),ostream_iterator<int>(cout," "));
        cout << endl;
    }
    return 0;
}

归并排序

实现


*/

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <cstdio>
using namespace std;


template <class T>
voidmerge(vector<T>& arr,int start ,int middle,int end)
{
    int n1 = middle - start + 1;
    int n2 = end - middle;
    vector<T> left(n1);
    vector<T> right(n2);
    int i,j,k;

    for (i = 0;i < n1; ++ i)
        left[i] = arr[start + i];
    for (j = 0;j < n2; ++ j)
        right[j] = arr[middle + j + 1];

    i = j = 0;
    k = start;
    while (i < n1 && j < n2) {
        if (left[i] < right[j])
            arr[k ++] = left[i ++];
        else
            arr[k ++] = right[j ++];
    }
    while (i < n1)
        arr[k ++] = left[i ++ ];
    while (j < n2)
        arr[k ++] = right[j ++];
}

template <class T>
voidsort(vector<T>& arr,int start,int end)
{
    // getchar();
    if (start < end)
    {
        int middle = (start + end) / 2;
        sort(arr,start,middle);
        sort(arr,middle + 1,end);
        merge(arr,start,middle,end);
    }
}

int main()
{
    const int length =20;
    vector<int> arr(length);
    for (size_t i = 0;i < arr.size(); ++ i)
        arr[i] = i;
   random_shuffle(arr.begin(),arr.end());

   copy(arr.begin(),arr.end(),ostream_iterator<int>(cout," "));
    cout << endl;

    sort(arr,0,length - 1);

   copy(arr.begin(),arr.end(),ostream_iterator<int>(cout," "));
    cout << endl;

    return 0;
}

 

 

原创粉丝点击