【数据结构与算法(八)】——递归和循环

版权声明:小白会努力的,欢迎大佬们在评论区吐槽! https://blog.csdn.net/Laon_Chan/article/details/80334241

第八天打卡
查找、排序;二分查找、归并排序、快速排序;回溯法;递归–栈;动态规划;贪婪算法;位运算(与、或、异或、左移、右移)

递归和循环

如果没有条件要求限制,尽量多使用递归方法编程。但是在前一天的“函数调用栈”中,说到了函数调用的过程,递归是函数自己调用自己,这样每一次函数调用都需要时间和空间的消耗,都要在内存中分配空间用来保存参数、返回地址和临时变量,而且往栈中压入数据和弹出数据都需要时间。所以递归也是有缺点的,虽然它看起来代码简洁。
还有就是递归的本质是把一个问题分解成为两个或多个小问题。
递归还容易导致栈溢出?递归太深会导致栈溢出,参考前一天的“函数调用栈”的图。栈的容量是有限的,如果不断递归,就不断需要开辟新的栈帧,需要新的内存,递归里用到的局部变量存储在堆栈中,堆栈的访问效率高,速度快,但空间有限,递归太多变量需要一直入栈而不出栈,导致需要的内存空间(占用了又不释放)大于堆栈的空间,栈空间是2M(操作系统给划分的),堆空间内存空间,最终导致栈空间不够,就是栈溢出了。
对于函数调用栈这一点我总是似懂非懂,很不自信,总感觉那里不太对,所以还请知道的大佬能帮忙指正(●'◡'●)
貌似Java没有栈溢出这个问题存在

这里写图片描述

//递归方法计算1+2+……+n
int AddFrom1ToN(int n)
{
    return n <= 0 ? 0 : AddFrom1ToN(n - 1);
}

//循环方法计算1+2+……+n
int (int n)
{
    int result = 0;
    for (int i = 0; i < n; i++)
        result += n;
    return result;
}

题目

求斐波那契数列的第n项

写一个函数,输入n,求斐波那契数列的第n项。斐波那契数列定义:

        f(n)=0,n=0
        f(n)=1,n=1
        f(n)=f(n-1)+f(n-2),n>1

思路:

1、最传统的做法,效率很低

long long Fibonacci(unsigned int n)
{
    //unsigned 有可能小于0吗?
    if (n <= 0)
        return 0;
    if (n == 1)
        return 1;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

以上的做法时间复杂度很大,因为重复计算的次数太多了(看下图)。这里只是n=10而已,如果是n=100,那需要调用函数的次数就更多了,这样很容易导致溢出问题,而且时间效率也很低(课本的例题都是用这个例子这种做法讲递归)
这里写图片描述

2、循环的做法,避免重复计算

从下往上的做法【感觉这本书总是逆着我的习惯思路来的,之前数组、链表插入元素的时候也是,一般情况下都是想着从前面开始,然而高效的思路总是从后往前】,先计算f(2),之后由f(1)和f(2)计算f(3),由f(3)和f(4)计算f(5)……依次类推,知道计算f(n),这样比如说在计算f(4)的时候,已经由f(2)和f(3)了,每个f(i)只需要被计算依次,所以函数被调用的次数,也就是n-1次,比指数型递增的次数少了很多。

//时间复杂度O(n)
//改进版的斐波那契数列,提高了时间效率和空间效率
int Fibonacci(unsigned int n)
{
    if (n < 2)
        return n;
    long long fibMinusOne = 1;
    long long fibMinusTwo = 0;
    long long fibN = 0;
    for (unsigned int i = 2; i <= n; i++)
    {
        fibN = fibMinusOne + fibMinusTwo;
        fibMinusTwo = fibMinusOne;
        fibMinusOne = fibN;
    }

    return fibN;
}

青蛙跳台阶问题(Fibonacci的应用)

一只青蛙依次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。

思路:

把n级台阶时的跳法看成n的函数,记为f(n)。当n>2时,第一跳只跳一个台阶,这种跳法的总数就等于f(n-1)的次数了;第二种是第一跳时跳两个台阶,这种跳法的总种类数就等于f(n-2)的总的种类数了;所以f(n)=f(n-1)+ f(n-2),可以看到这也是一个Fibonacci数列问题了,其中f(1)=1;f(2)=2;n>2,f(n)=f(n-1)+ f(n-2),只要稍稍改一下上面的代码就可以了

//青蛙跳问题
int FrogJump(unsigned int n)
{
    if (n < 3)
        return n;
    long long fibMinusOne = 2;
    long long fibMinusTwo = 1;
    long long fibN = 0;
    for (unsigned int i = 3; i <= n; i++)
    {
        fibN = fibMinusOne + fibMinusTwo;
        fibMinusTwo = fibMinusOne;
        fibMinusOne = fibN;
    }

    return fibN;
}

测试用例:
1、普通正常值:3、6、9等等
2、边界值测试:0、1、2
3、性能测试:输入较大的值400、500、1000等
很多现实应用问题实际上都是斐波那契数列问题,所以注意对现实应用问题进行数学问题,回归到常见的数学问题上,用解决数学问题的方法解决应用问题

n个骰子的点数——抽象建模能力的考察

把n个骰子扔在地上,所有骰子朝上一面的点数的和为s。输入n,输出s的所有可能的值出现的概率。

建模的步骤:
第一步是要选择合理的数据结构来描述问题
第二步是要分析模型中的内在规律,并要求会用编程语言描述这种规律

思路:

确定s∈[6,6n]之间的整数,投出来情况有6n种(每个骰子有6种情况,这只是投出来的情况,并不是和的种树)。对于这个问题,我们需要先计算出每个s出现的次数,之后除以总的次数就可以求出每个s出现的概率了。

1、基于递归求骰子点数,时间效率低

  • 统计每个点数出现的次数,建立模型:要想求出n个骰子的点数和s,可以把n个骰子分为两堆,其中一堆只有一个骰子,另一堆有n-1个骰子。第一个堆有6种s【1~6】结果;对于第二个堆,同样进行分堆,第一个堆只有一个骰子,第二个堆有n-2个骰子,同样,第一个堆有6种s结果;对于第二个堆还是继续分堆……我们每次把上一轮那个单独骰子的点数和这一轮单独骰子的点数相加,接着再加剩下的n-2个骰子的点数和。
  • 定义一个长度为6n-n+1【为什么?因为最大的s值是6n,最小的s值是n,所以一共有这么多种s结果】,之后讲和为s的点数出现的次数存到数组第s-n个元素里【为什么是s-n?因为第0个元素的s代表n,第一个元素的s代表n+1,……以此类推】
#define MAX_VALUE 6
void printProbability(int n)
{
    if (n < 1)
        return;

    int maxSum = MAX_VALUE * n;
    //定义一个大小带有变量的数组的做法
    int* pProbabilities = new int[maxSum - n + 1];
    for (int i = 0; i < maxSum - n + 1; i++)
        pProbabilities[i] = 0;  //为什么要先赋值??不赋值的话默认不也是0吗。。

    //i表示第一个堆的骰子的点数。其实这个调用只是一个触发点
    for (int i = 1; i <= MAX_VALUE; i++)
        Probability(n,n,i,pProbabilities);

    int total = pow((double)MAX_VALUE, n);//一共有这么多种情况,为了计算概率
    for (int i = 0; i < maxSum-n+1; i++)
    {
        double radio = (double)pProbabilities[i] / total;
        cout << i;
        cout << " : ";
        cout << radio << endl;
    }

}

//参数:1、真正的骰子个数;2、这一轮计算时的骰子个数;3、和s;4、存放和s个数的数组
void Probability(int n, int current, int sum, int* pProbabilities)
{
    //如果只计算一个骰子,那就直接在传过来的当前的sum上的对应个数上加1
    //为什么?这其实就是表示第二个堆n-1的sum已经计算完了
    if (current == 1)
        pProbabilities[sum - n]++;
    else
        for (int i = 1; i <= MAX_VALUE; i++)
            //sum+i是第一个堆和第二个堆的第一堆的点数的和,接下来要计算第一个堆的第二个堆
            Probability(n, current - 1, sum + i, pProbabilities);
}

2、基于循环求骰子点数,时间性能好

使用两个数组。在一轮循环中,第一个数组中的第n个元素表示骰子和为n出现的次数。在下一轮循环中,我们加上一共新的骰子,此时和为n的骰子出现的次数应该等于上一轮循环中骰子点数和为n-1、n-2、n-3、n-4、n-5、n-6的次数的综合,所以我们把另一个数组的第n个数字设为前一个数组对应的第n-1、n-2、n-3、n-4、n-5、n-6个数字之和。

void PrintProbability(int n)
{
    if (n < 1)
        return;
    int* pProbabilities[2]; //定义两个数组
    //分别给两个数组指定大小,又是带参数
    pProbabilities[0] = new int[MAX_VALUE*n - n + 1];
    pProbabilities[1] = new int[MAX_VALUE*n - n + 1];
    //又是一个赋值的过程,突然想到,就好像int i=0;一样,如果不给i赋初始值,那就不能拿i进行计算
    for (int i = 0; i < MAX_VALUE*n - n + 1; i++) {
        pProbabilities[0][i] = 0;
        pProbabilities[1][i] = 0;
    }

    int flag = 0;
    //只有一个骰子的时候,出现的可能sum=1~6
    for (int i = 1; i <= MAX_VALUE; i++)
        pProbabilities[flag][i] = 1;
    //从有两个骰子开始使用循环计算
    for (int k = 2; k <= n; k++) {
        //为新一轮不可能的和s赋值为0,比如当有两个骰子时,和就不可能为0、1
        for (int i = 0; i < k; i++)
            pProbabilities[1 - flag][i] = 0;
        //当前计算的是有k个骰子,所以一共s的范围是k~6*k
        //每次循环计算中i表示的是和sum
        for (int i = k; i <= MAX_VALUE * k; i++) {
            pProbabilities[1 - flag][i] = 0;
            //j<=i限制是因为有可能和i在2~6之间
            //所以计算这一轮和i的个数的时候根本就没有需要加所谓的前6个,因为前面没有6个和
            //比如当骰子数k=2计算和i=3的个数的时候那就只需要加上前一轮的i-1、i-2,也就是只有一个骰子,sum=1、2的时候
            for (int j = 1; j <= i && j <= MAX_VALUE; j++)
                pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
        }
        flag = flag - 1;    //两个数组角色互换,之后进入下一轮计算
    }//到这里就已经计算完了,注意每次一轮新的循环的时候,flag的数组是上一轮计算好的数组,这也是最后退出循环时的得到的目标数组,1-flag的数组是作为新一轮循环的需要计算的数组

    double total = pow(MAX_VALUE, n);
    for (int i = n; i <= MAX_VALUE * n; i++) {
        double ratio = (double)pProbabilities[flag][i] / total;
        cout << i;
        cout << " : ";
        cout << ratio << endl;
    }

    delete[] pProbabilities[0];
    delete[] pProbabilities[1];
}

测试用例:
1、功能测试:(正常应用)1、2、3、4
2、特殊输入测试:0
3、性能测试:输入较大的数字 11

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页