zoukankan      html  css  js  c++  java
  • 由BNF解释如何用递归表示循环

          正巧老夫最近在做C99语法分析的工作,看了<华丽的递归...>的帖子,里面用几个简单的双向递归模拟循环,突然有点儿心得,贴出来分享一下,有错误的地方还请各位看官指正。

          先看看3种不同的BNF的产生式,由这三个产生式可以回答二个问题

          1 什么样的递归可以用不带栈的循环表示 ?

          2 怎么用递归表示循环 ?

       1                   2                   3
    A : Aa A : aA A : aAb
    | a | a | c

           式1是左递归, 可以产生诸如 a, aa, aaa, aaaa...这样的表达式。说白了,可以用一个简单的"循环" a+ 来表示;式2是尾递归, 也同样产生a, aa, aaa, aaaa...这样的表达式,当然也可以用一个"循环" a+ 来表示。式1和式2都能产生同样的表达式,且都能用循环表示(不同点在于结合性,一个自左向右,一个自右向左,但这不是本文的重点)

          下面看看式3,这东东的递归不在头也不在尾而是在中间,可以产生诸如acb, aacbb, aaacbbb这样表达式,初看上去好像也能用某种"循环"来表示,比如a*cb*。但问题是acb,aacbb,aaacbbb中都有一个"特点",c左边的a要和c右边b"数量一致"。这里的"数量一致"可以换成其他说法,比如"匹配"。你把a换成左括号"(",把b换成右括号")" ,aacbb就变成了((c)),这个"数量一致"就变成了"括号匹配"。显然对于式3,你必须要给它一个栈或者其他数据结构去记住c左边到底遇到多少个a,所以仅仅用"循环"是没有办法模拟这种具有"嵌套"的递归结构。

          那么到此为止,第一个问题实际上就清楚了。

           1 什么样的递归可以用不带栈的循环表示 ?

              一般左递归和尾递归可以很顺利的转换为一个简单的循环。

          但是在命令式的语言中(函数式的老夫尚不涉及,不敢胡说),左递归对应的代码片段是有问题,一进入A()就立刻调用A(),实际上死循环了,所以左递归一般比较少存在。那么就只剩下尾递归了。由于尾递归可以很容易的变成循环结构(有些地方编译器都可以自己给优化),因此第二个问题也清楚了,"

    void A()
    {
    A();
    a();
    }

         2 怎么用递归表示循环 ?

            可以用尾递归表示循环

          当然一般情况下,我们没有必要干这个傻事儿,典型的吃饱了撑的。但是从理解问题本质出发,如果能多用递归的思想解决问题,确实蛮有意思的。依稀记得某大牛曾有句话 "To iterate is human,to recurse divine"。随着工作年限的增加,确实觉得这句话越来越有一定道理了。

          还是以"正整数分解为数目最少的平方数之和"为例,首先用循环的思想,最朴素的是先看看正整数M能不能分解为1个平方数之和,能不能分解成2个,3个,4个......当然越小越好。

    大概是  

    void compute(int n)
    {
    for (int i = 1; i < n; i++)
    //do something for i 能不能分解成1个,2个,3个....
    }

         现在要把它变成递归形式,显然是函数compute每调用一次,就相当于以前循环一次。for语句里有2个变量控制循环,一个是i,一个是n, 在"递归"的方式中,它们应该是函数的参数,在函数间传递,模拟以前的循环间传递。于是compute的接口就增加了2个参数,如果compute还有其他形参,可以原封不动的放在前面。

         这里有个巧合,前面的n和for循环里的n其实是一个,所以3个参数就变成了2个参数。

    void compute( int n, int i)

         循环的还有2个重要结构,一个是终止条件,一个是递增步长,再加上上面说的2个控制变量,可以按下面的模板进行转换

    void compute( int n, int i)      // 增加2个形参,表示控制循环的变量
    {
    // 循环终止条件 for语句 i < n 部分

    // do something for i 这部分原封不动

    // 通过函数调用表示步长递增 for语句 i++部分
    }

      最终

    void compute( int n, int i)  // 增加2个形参,表示控制循环的变量
    {
      if (i >= n) return; // for语句 i < n 部分

      // do something for i // 这部分依然原封不动

      compute(n, i + 1); // for语句 i++部分
    }

    整个过程比较机械化,可以不怎么动脑子,把 do something for i 合进来后,修正下函数返回值。

    // n能否为m个平方数之和
    int compute(int n, int m)
    {
      if (m >= n)  return m;
    //n 能否为 m个平方数 之和
    return isSquareSum(n, m, n, 1) ? m : compute(n, m + 1);
    }

        isSquareSum也是用递归来模拟循环,实际上isSquareSum最开始只有前2个参数,后面2个同compute一样,也是用来模拟循环的,实际上,看到这种模式可以先别管后面的形参,不然很容易搞糊涂。

        原始的isSquareSum 是这样的

    bool isSquareSum(int n, int m)
    {
    //是否完全平方数
      if (m == 1) return isSqaure(n); // something
      bool _result = false; // something

      for (int i = 1; i * i < n; i ++)
      {
        _result = isSquareSum(n - i * i, m - 1); // something
        if (_result) return _result;          // something
      }
      return _result;
    }

         isSquareSum本身已经是一个递归了,将"n能否为m个平方数之和" 递归表示为" n - 一个平方数 能否为 m - 1个平方数之和","n - 一个平方数"

    再次用for 循环来穷举,1,4,9,16.... 这个循环还可以再次用上面的模板来改造成递归,先增加2个控制参数v,k,然后写终止条件,然后写调用函数。

    bool isSquareSum(int n, int m, int v, int i) //增加控制参数
    {
    if ( i * i >= v) return false; // 终止条件
    return isSquareSum(n, m, v, i + 1); // 递增
    }

    然后再把something部分给搬过来

    bool isSquareSum(int n, int m, int v, int i)
    {
      if (m == 1) return isSqaure(n);

      if ( i * i >= v) return false;
      
      return
    isSquareSum(n - i * i, m - 1, v , i) ? true : isSquareSum(n, m, v, i + 1);
    }

         这样也就形成了键盘农夫所说的双向递归,左边的递归前面的参数,右边的递归后面的参数。最后是完整的代码,isSqaure部分是偷大懒了,老夫也是一时心血来潮,毕竟不是专门做算法的。话说老夫今日虚火,昨个三黄片吃多了,折腾了大半宿,今天此番活动活动脑筋,竟然精神了很多,奇哉,奇哉啊

    int data[17] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};

    bool isSqaure(int i)
    {
      bool result[18] = { false, true, false, false, true, false, false, false, false,
                  true, false,false, false, false,false, false, true, false};
    return result[i];
    }

    //n 能否为 m个平方数 之和
    bool isSquareSum(int n, int m, int v, int i)
    {
    if (m == 1) return isSqaure(n);

    if ( i * i >= v) return false;

    return isSquareSum(n - i * i, m - 1, v , i) ? true : isSquareSum(n, m, v, i + 1);
    }

    // 正整数分解为数目最少的平方数之和
    // n 正整数
    // m 起始平方数个数
    //
    int compute(int n, int m)
    {
    if (m >= n) return m;
    return isSquareSum(n, m, n, 1) ? m : compute(n, m + 1);
    }


    int leastSquareSum(int n)
    {
    return compute(n, 1);
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    int len = sizeof(data) / sizeof(int);
    for(int i = 0; i < len; i++)
    printf("%d least square sum is %d\n", data[i], leastSquareSum( data[i]));
    system("pause");
    }

    Powered by Zoundry Raven

  • 相关阅读:
    我孤独吗?我软弱吗?
    DataGrid 中的 HyperLinkColumn 如何传递多个参数?(未整理)
    C# 获取 MAC地址!
    (文本)文件操作
    ioninfinitescroll实现上拉分页加载更多
    前端面试题:防抖的实现
    vue 传送门功能,实现模态窗口
    前端面试题:节流的实现( 减少一段时间的触发频率)
    vue中的render函数
    ionic5实现tab栏切换效果
  • 原文地址:https://www.cnblogs.com/quixotic/p/2276836.html
Copyright © 2011-2022 走看看