相互递归(2)

  版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址

  http://www.cnblogs.com/Colin-Cai/p/10920847.html 

  作者:窗户

  QQ/微信:6679072

  E-mail:6679072@qq.com

  本章继续上一章,说明一下这个问题:

  所有的相互递归都可以被转化为一般的递归,从而最终可以用lambda演算来完成。

 

  假设有以下对于$f_1, f_2, ... f_n$的相互递归:

  $f_{1} = F_{1}(f_{1}, f_{2}, ... f_{n})$

  $f_{2} = F_{2}(f_{1}, f_{2}, ... f_{n})$

  ...

  $f_{n} = F_{n}(f_{1}, f_{2}, ... f_{n})$

  如果我们定义一个高阶函数(算子)f,满足

  $f_{1} = f(1)$

  $f_{2} = f(2)$

  ...

  $f_{n} = f(n)$

  代入上式,得到

  $f(1) = F_{1}(f(1), f(2), ... f(n))$

  $f(2) = F_{2}(f(1), f(2), ... f(n))$

  ...

  $f(n) = F_{n}(f(1), f(2), ... f(n))$

  于是以上就是一个对于f的普通递归(f递归到f)。

  从而,我们就知道了,任何递归都可以转化为到自身的普通递归

  

  然而,对于lambda演算,因为自身没有名字,那又如何递归呢?

  我们就用非负整数的最大公约数为例子,还是用Scheme,一步步来。

  我们记$gcd(a_1,a_2, ..., a_n)$是$a_1,a_2, ..., a_n$的最大公约数。

  最大公约数的递归其实很简单:

  (1)$gcd(0, a) = a$

  (2)如果a不等于0,那么 $gcd(a, b) = gcd(b\%a, a)$,此处%是取余数

  (3)$gcd(a_1, a_2, ... a_n) = gcd(a_1, gcd(a_2, ... a_n))$

  

  其中第一条、第二条连续使用就是著名的欧几里得算法,或者称辗转相除法。

  而第三条则用于缩减最大公约数求解的个数,之前我在文章《汉诺塔——各种编程范式的解决》提到,递归可求解的真谛在于缩小问题处理的规模以达到降阶,以上第二、三条则是可以达到降阶的效果。

  于是上述三条再加上$gcd() = 0$ 和 $gcd(0) = 0$ 这两条边界条件,用Scheme描述递归如下:

(define gcd
 (lambda s
  (if (null? s)
   0
   (if (zero? (car s))
    (apply gcd (cdr s))
    (if (null? (cdr s))
     (car s)
     (if (null? (cddr s))
      (gcd (remainder (cadr s) (car s)) (car s))
      (gcd (car s) (apply gcd (cdr s)))
     ))))))

  

  为了实现匿名递归,也就是我们最终希望在lambda演算中递归,我们需要考虑以下函数

(define g
 (lambda (f)
  (lambda s
   (if (null? s)
    0
    (if (zero? (car s))
     (apply f (cdr s))
     (if (null? (cdr s))
      (car s)
      (if (null? (cddr s))
       (f (remainder (cadr s) (car s)) (car s))
       (f (car s) (apply f (cdr s)))
      )))))))

  我们此处好好想一想,会发现,$g(gcd) = gcd$

  也就是gcd是函数g的不动点。

  其实不动点在其他函数中一样存在,比如$f(x) = x^2$的不动点是0,

  只是这里的函数是高阶函数(算子),似乎挺拗口。

  假如有个函数Y(当然,这个Y也是一个算子)可以找到算子的不动点,比如使得$g(Y(g)) = Y(g)$,那么Y(g)就是我们本来想要实现的gcd,

  于是我们就通过lambda演算实现了匿名递归。 

  那么这样的Y存在吗?

 

  幸运的是,Y函数是存在的,有个学名叫Y combinator,我们知道美国有个孵化器公司叫这个名字,实际上就是取这个的意义。这个早在Church创建lambda验算体系的时候就已经发现,而且至关重要,否则就不知道怎么递归了。

  Scheme下,Y combinator可以如下

(lambda (f)
 ((lambda (g) (g g))
  (lambda (x) (f (lambda s (apply (x x) s))))))

  

  因为gcd函数可以表示为Y(g)的形式,

  于是,我们的gcd就可以形式如下

(define gcd
 ((lambda (f)
   ((lambda (g) (g g))
    (lambda (x) (f (lambda s (apply (x x) s))))))
  (lambda (f)
   (lambda s
    (if (null? s)
     0
     (if (zero? (car s))
      (apply f (cdr s))
      (if (null? (cdr s))
       (car s)
       (if (null? (cddr s))
        (f (remainder (cadr s) (car s)) (car s))
        (f (car s) (apply f (cdr s)))
       ))))))))

  

  于是,我们发现gcd的定义过程中,只用到了lambda演算,从而lambda演算统一了一切!

 

  靠谱吗?那么我们用上述定义的如此诡异的gcd随便运算一下几组最大公约数

  (display (gcd 225 150 165))

  得到

  15

转载于:https://www.cnblogs.com/Colin-Cai/p/10920847.html

<think>我们正在讨论循环与递归是否可以相互转化的问题。根据引用内容,递归和迭代(循环)是解决问题的两种方法,它们在某些情况下可以互相转换。例如,引用[2]提到如何用循环计算阶乘,而引用[3]则讨论了将递归调用转换为while循环实现。引用[4]则用斐波那契数列的例子说明了递归和循环的实现。 核心观点:循环和递归在理论上是可以相互转换的,因为图灵完备的语言中,递归和循环具有相同的计算能力。但是,转换的难易程度和效率可能因问题而异。 具体分析: 1. **递归转循环**: 递归通常通过栈来实现,因为每次递归调用都会在调用栈中增加一层。因此,将递归转化为循环时,我们可以显式地使用一个栈来模拟递归调用的过程。例如,对于树的深度优先遍历(递归实现),我们可以用循环和一个栈来模拟同样的过程。 但是,有些递归是尾递归(即递归调用是函数体中最后执行的语句,且返回值不需要参与其他运算),这类递归可以直接转换为循环而不需要栈,因为编译器可以进行尾递归优化(如引用[2]中提到的阶乘例子,但注意原阶乘例子不是尾递归,需要调整)。 例如,尾递归形式的阶乘函数: ```javascript function factorialTail(n, acc = 1) { if (n <= 1) return acc; return factorialTail(n-1, n * acc); // 尾递归 } ``` 可以转换为循环: ```javascript function factorialLoop(n) { let acc = 1; for (let i = n; i > 1; i--) { acc = acc * i; } return acc; } ``` 2. **循环转递归**: 循环也可以转化为递归。通常,循环中的状态变量可以转化为递归函数的参数。循环体中的操作则转化为递归函数体中的操作,而循环的终止条件则对应递归的基准条件(base case)。 例如,一个简单的循环: ```javascript function sumLoop(n) { let s = 0; for (let i=1; i<=n; i++) { s += i; } return s; } ``` 转化为递归: ```javascript function sumRec(n, i=1, s=0) { if (i > n) return s; // 基准条件 return sumRec(n, i+1, s+i); // 递归调用 } ``` 3. **转换的限制与注意事项**: - **栈溢出**:递归深度过大可能导致栈溢出(如引用[3]中提到的递归调用产生循环)。而循环通常不会出现此问题。 - **效率**:递归的函数调用开销通常比循环大,因为每次调用都需要保存上下文、创建新的栈帧等。但尾递归优化(TCO)可以消除这种开销(如引用[2]中提到的,但并非所有语言都支持)。 - **可读性**:某些问题(如树遍历、分治算法)用递归表达更直观,而迭代(循环)表达可能更复杂。反之,简单的重复操作用循环更清晰。 4. **转换的通用方法**: - 对于非尾递归,通常需要引入一个显式的栈来保存状态。例如,汉诺塔问题的递归解法转换为循环时,需要用一个栈来模拟递归调用的参数。 - 对于尾递归,可以直接将递归条件转化为循环条件,递归体转化为循环体。 结论:循环和递归在编程中可以相互转换,但转换的难易程度和效率取决于具体问题以及递归的形式(是否为尾递归)。在转换时,需要考虑栈的使用、空间开销和代码可读性。 相关问题: 1. 什么是尾递归?尾递归优化如何工作? 2. 如何将汉诺塔问题的递归解法转换为循环解法? 3. 在哪些情况下递归比循环更合适?反之呢?</think>### 循环与递归相互转化原理 在编程中,**循环和递归本质上是等价的**,两者可以相互转换。这种等价性源于图灵完备性理论——任何递归算法都能用循环实现,反之亦然[^1][^3]。以下是核心转换原理: #### 一、递归转循环的通用方法 1. **显式栈模拟递归调用** 递归隐式使用调用栈存储状态,循环可显式用栈数据结构模拟: ```javascript // 递归版阶乘 function factorialRec(n) { if (n <= 1) return 1; return n * factorialRec(n-1); } // 循环+栈实现 function factorialLoop(n) { const stack = []; while (n > 1) { // 模拟递归调用 stack.push(n); n--; } let result = 1; while (stack.length) { // 模拟返回过程 result *= stack.pop(); } return result; } ``` **转换关键**: - `stack.push()` 对应递归调用前的参数保存 - `stack.pop()` 对应递归返回时的状态恢复[^3] 2. **尾递归的特殊优化** 尾递归可直接转为循环(无栈): ```javascript // 尾递归形式 function tailRec(n, acc=1) { if (n <= 1) return acc; return tailRec(n-1, acc*n); // 尾调用 } // 直接转为循环 function tailToLoop(n) { let acc = 1; while (n > 1) { acc *= n; n--; } return acc; } ``` > 编译器优化原理:尾递归调用位置在函数末尾,可直接复用当前栈帧[^2] #### 二、循环转递归的核心策略 1. **循环变量转为递归参数** ```javascript // 循环求和 function sumLoop(n) { let total = 0; for (let i=1; i<=n; i++) { total += i; } return total; } // 递归实现 function sumRec(n, i=1, total=0) { if (i > n) return total; // 终止条件 ≈ 循环结束条件 return sumRec(n, i+1, total+i); // i++和累加操作 } ``` 2. **状态机模式转换** 复杂循环可建模为状态机: ```javascript // 循环遍历二叉树 function traverseLoop(root) { const stack = [root]; while (stack.length) { const node = stack.pop(); if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } } // 递归实现 function traverseRec(node) { if (!node) return; traverseRec(node.left); // 状态转移1 traverseRec(node.right); // 状态转移2 } ``` #### 三、转换的实践约束 | 维度 | 递归 → 循环 | 循环 → 递归 | |--------------|--------------------------------------|--------------------------------------| | **空间效率** | 需额外栈空间$O(n)$ | 调用栈深度可能超限 | | **可读性** | 复杂递归逻辑可能降低可读性 | 分治/回溯问题更直观[^3] | | **适用场景** | 树遍历、回溯算法、分治策略 | 线性迭代、固定次数的重复操作 | | **编译器支持**| 依赖尾调用优化(TCO) | 所有语言均支持 | > **关键结论**: > 1. 任何递归都可转为循环(显式栈实现) > 2. 尾递归可无损转为循环(无需栈) > 3. 循环转递归时需警惕栈溢出风险 > 4. 选择依据:问题结构 > 性能需求 > 代码可维护性[^1][^4] --- ### 相关问题 1. 尾递归优化在JavaScript/Java/Python中的支持程度有何差异? 2. 如何证明循环和递归的计算等价性(图灵完备角度)? 3. 哪些经典算法用递归实现比循环更简洁(如DFS、快速排序)? 4. 当递归深度达到$10^6$级别时,循环实现需要如何设计栈结构避免内存溢出?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值