<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tail-Recursion on 有点稳</title><link>https://blog.nicelylit.net/tags/tail-recursion/</link><description>Recent content in Tail-Recursion on 有点稳</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 20 Aug 2017 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.nicelylit.net/tags/tail-recursion/index.xml" rel="self" type="application/rss+xml"/><item><title>求数组的子数组之和的最大值</title><link>https://blog.nicelylit.net/posts/%E6%B1%82%E6%95%B0%E7%BB%84%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84%E4%B9%8B%E5%92%8C%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC/</link><pubDate>Sun, 20 Aug 2017 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E6%B1%82%E6%95%B0%E7%BB%84%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84%E4%B9%8B%E5%92%8C%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC/</guid><description>&lt;p&gt;——尾递归的应用&lt;/p&gt;
&lt;p&gt;这是《编程之美》中2.14节提出的一个问题，问题的描述为&lt;/p&gt;
&lt;p&gt;一个有N个整数元素的一维数组(A[0], A[1], &amp;hellip;, A[n-2], A[n-1])，这个数组有很多子数组，那么子数组之和的最大值是什么？&lt;/p&gt;
&lt;p&gt;以往遇到这种问题的第一种思维模式也像书中的第一种解法一样去枚举出所有的子数组，对每个子数组内部元素求和，然后找出所有和中最大的。这是一种分解问题的思路，但所分解的子问题已经偏离了原始的问题。比如第一步的分解就是枚举所有的子数组，单就这步就已经是\(O(n^2)\)了，而后的两个子问题，一个是内部元素求和，另一个是找出所有和中最大的，这两个问题与原问题也不具有太多的共性，因而能够由此想到最优的算法极为困难。&lt;/p&gt;
&lt;p&gt;书中的解法二用了递归分解子问题的思路，却没有给出适当的语言描述，使用了循环。用循环去描述，对思考者而言不是一种好的语言，正如我在《&lt;a class="link" href="https://blog.nicelylit.net/posts/%E5%B0%BE%E9%80%92%E5%BD%92%E7%9A%84%E5%90%AF%E7%A4%BA/" &gt;尾递归的启示&lt;/a&gt;》一篇中写的那样，循环是一种机器友好的语言。这篇文章，我将会用纯粹地过程调用的方式来完成这个问题的解答，最终得到的优化算法与《编程之美》中提到的最后一种解法是等价的。当然递归子问题的分解与解法二也不太一样。&lt;/p&gt;
&lt;p&gt;对于原问题，可以分解为以下两个子问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求不包含A[0]的所有子数组之和的最大值；&lt;/li&gt;
&lt;li&gt;求包含A[0]的所有子数组之和的最大值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原问题的解就是两个子问题解的较大者。显然，第一个子问题是原问题的递归，不包含A[0]的所有子数组，意味着(A[1], A[2], &amp;hellip;, A[n-1])的所有子数组。第二个问题是一个新的子问题，但比原问题要简单的子问题。假设我们记原问题为sub_arr_max，数组A为arr，数组的起始坐标为s，结束坐标为e，第二个子问题为max_of_all_subs，那么以上的分解，很容易翻译为以下的代码&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def sub_arr_max(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接下来是如何解决子问题max_of_all_subs。先回顾这个子问题的含义，这个子问题是说所有包含arr[s]元素的子数组元素和中，最大的值。这个子问题可以进一步分解为两个子问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求不包含arr[e]的数组中所有包含arr[s]的最大值；&lt;/li&gt;
&lt;li&gt;求数组arr[s..e]所有元素的和。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个问题的分解中，第一个子问题又一次是原问题的递归，而第二个子问题是一个更为简单的递归问题。假设记第二个子问题为sum_of_all，那么以上的问题分解可以立刻翻译为&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def max_of_all_subs(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;解决子问题sum_of_all是一个常见的线性递归，可以表述为，当s &amp;gt; e时，&lt;/p&gt;
&lt;p&gt;\(sum\_of\_all (arr, s, e) = \) \(sum\_of\_all(arr, e, e-1) + arr[e]\)&lt;/p&gt;
&lt;p&gt;直接翻译以上的递归式就能得到下面的解法&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def sum_of_all(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以上就是一个完整可以运行的解法，并且很容易调试错误，作为寻找优化算法的起点。为了不偏离本文的核心要义，且不去分析这样的解法中有多少的重复计算和空间浪费。直接将三个过程依次改造为尾递归的形式，这个改造过程中，不仅会找到最优的解法，并且能够洞察原问题中的一些特殊性，算法分析也附带会变得清楚。&lt;/p&gt;
&lt;h2 id="第一步改造sum_of_all"&gt;第一步：改造sum_of_all
&lt;/h2&gt;&lt;p&gt;sum_of_all方法是一个线性递归的过程，我们可以在参数中增加一个变量res来记录从第一个元素arr[s]开始累加到s[e]的结果，每次递归调用只需要增加s即可。这个改造过程与《尾递归的启示》中提到的阶乘的改造过程几乎是相同的。一种写法如下：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def tsum_of_all(arr, s, e, res):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;为了区分与前面算法的不同，重命名为tsum_of_all，t代表tail recursion的意思。有了新方法，在max_of_all_subs中可以这样调用&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def max_of_all_subs(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;或者&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def max_of_all_subs(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;两种写法等价。这里之所以要写出这两种，其实是想提醒，新增变量的初始值一定要注意，0是个很特殊的值，有些情形下并不能初始为0，第二步的改造中就会遇到这样的问题。&lt;/p&gt;
&lt;h2 id="第二步改造max_of_all_subs"&gt;第二步：改造max_of_all_subs
&lt;/h2&gt;&lt;p&gt;max_of_all_subs求解了arr[s], arr[s..s+1], arr[s..s+2], .., arr[s..e]中，所有数组各自求和中最大的那个。用动态规划的思路，就是从小的开始求解，arr[s]显然就是自己，而后的每一个，比如arr[s..s+i]，等于arr[s..s+i-1]的最大解和arr[s..s+i-1]的和加arr[s+i]两者比较的较大者。在求arr[s..s+i-1]时，可以保存两个结果，一个是arr[s..s+i-1]的最大解，记为res，另一个是arr[s..s+i-1]的和，记为ss，即sum_of_all问题已经融入到了问题的迭代中。改造后的算法如下&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def tmax_of_all_subs(arr, s, e, res, ss):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;为了区分与前面算法的不同，重命名为tmax_of_all_subs。当在sub_arr_max中使用这个方法时候，需要注意的是，初始值res不能为0，理由是如果数组中的元素都是负数，那么所有的子数组和都无法比0大，造成错误。因而调用时候要从第一个元素开始&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def sub_arr_max(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第三步改造sub_arr_max"&gt;第三步：改造sub_arr_max
&lt;/h2&gt;&lt;p&gt;回顾改造尾递归的两个要素之一是从小问题开始，寻找合适的变量来记录中间结果。如果数组是一个元素，直接返回结果，如果是两个元素，我们可以记录一个元素时候的结果，然后同新加入元素之后的结构进行比较。最开始自顶向下考虑问题时，我们从A[0]元素开始划分，A[1..N-1]同原问题有着同样的结构，而从自底向上考虑时，可以反过来想，考虑到第i个元素时A[0..i-1]同原问题有着同样结构的子问题，而A[0..i]需要解决的是max_of_all_subs的问题。这时我们能够改写出一个tmax_of_all_subs的一个对称写法。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def tmax_of_all_subs(arr, s, e, res, ss):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在sub_arr_max中最终比较的两个对象，其一是与原问题有着同样结构子问题的值，我们可以用一个变量去记录这个值，其二是tmax_of_all_subs问题，我们不需要每次去重算，只需要保存arr[s..e-1]中的最大值，能比较的是保存的最大值+arr[e]和arr[e]的比较中较大的作为新的保存值。为什么保存的arr[s..e-1]中最大值可以加arr[e]？因为这个最大值一定包含arr[e-1]，维护了tmax_of_all_subs这个问题求值的结构。如同在第二步中省去sum_of_all一样，这个问题中我们一样可以省去max_of_all_subs。由此就得到了最终的解法&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def tsub_arr_max(arr, s, e):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对照《编程之美》上的循环解法，容易看出，这两种写法运用了相同的思路，然而整个的思考过程却是不同的，对我而言，改写尾递归的方式更容易些。&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关阅读&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="http://mjm1990.com/article/%E5%B0%BE%E9%80%92%E5%BD%92%E7%9A%84%E5%90%AF%E7%A4%BA" target="_blank" rel="noopener"
 &gt;尾递归的启示&lt;/a&gt;?⭐️⭐️⭐️⭐️⭐️&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>尾递归的启示</title><link>https://blog.nicelylit.net/posts/%E5%B0%BE%E9%80%92%E5%BD%92%E7%9A%84%E5%90%AF%E7%A4%BA/</link><pubDate>Sat, 19 Aug 2017 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E5%B0%BE%E9%80%92%E5%BD%92%E7%9A%84%E5%90%AF%E7%A4%BA/</guid><description>&lt;p&gt;——读《计算机程序的构造和解释》第一章第二小节所想&lt;/p&gt;
&lt;p&gt;尾递归是指在过程调用中，递归调用过程本身的操作始终是过程的最后一步。举例来讲，计算阶乘的方法，根据定义，直接翻译成递归形式为&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个递归中，最后一步操作是乘法，而非调用过程本身，因而不是尾递归。转换成尾递归的形式如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;factorial_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;factorial_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;factorial_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用尾递归的好处，可以从两方面来看，一则是同非尾递归过程比较的优势，另一则是同程序设计语言中专门的循环结构（也可以称作迭代结构）比较的优势。&lt;/p&gt;
&lt;p&gt;不论是同非尾递归过程比较还是专门的循环结构比较，都先需要明确的是过程调用意味着什么，过程到底是什么。&lt;/p&gt;
&lt;p&gt;初学程序设计的人会说，过程不就是一个函数么，接收一些输入，返回一些输出，有什么可以深究的呢。过程的概念初听，理解起来非常容易，但能清晰地想清楚如何利用好过程以及明白过程带来的意义并非显而易见。&lt;/p&gt;
&lt;p&gt;了解清楚过程，要从两个方面来看待，一方面是过程在如何影响我们设计和组织程序，另一方面是过程在如何指挥计算机执行任务。&lt;/p&gt;
&lt;p&gt;好的过程设计，会明确过程的输入和输出，过程的命名也明确意味着要执行的任务，整个过程如同黑盒子一般，除了依赖输入和输出之外，不会依赖其它的任何东西。物理世界中，人们生产的无数优秀的生活用品、科技产品都具有这个特性。比如水龙头，接水管的口径是明确的，操作水龙头闭合和打开的阀门是明确的，输出口的口径也是明确的，进而水流的速度也是可调控的。再比如白炽灯，螺纹接口的尺寸和样式是明确的，电灯消耗的电功率是明确的，输入的电压范围是明确的，电灯的材料是明确的，进而发光的亮度也是可调控的。物理世界中的物品，人们很容易注意到他们的依赖，也就是使用条件或者说输入和输出。当场景切换到计算机软件，逻辑的世界中，模块就如同物品，过程就是模块的基本构件。有人会觉得模块听起来像是静态的，过程听起来像是动态的，怎么能说是一回事。仔细想想，其实现实中的物品又有什么是完全静态的呢，不论水龙头还是白炽灯。从自然语言上来看，所有的动词又何尝不是一个名词呢，我们给一种动作起了个名字就是动词所表达的含义。逻辑的世界中，过程封装了一系列的指令用于去完成某个任务，而过程本身对设计者而言意味着一条新的计算机指令。软件本身就是一个复杂的过程，优秀的计算机软件都有着这样的设计。&lt;/p&gt;
&lt;p&gt;于计算机而言，一个过程有外部的环境，有内部的环境。设计良好的过程对外部的依赖应该仅局限于输入的参数提供的内容，暴露给外部环境的应该仅局限于输出的返回值。内部的环境不应该修改外部环境的任何内容，返回结果也应该始终稳定前后一致。过程的嵌套调用给环境的维护带来了一些开销。写过汇编语言的人知道每个调用指令执行时都要先将当前的寄存器状态压栈，然后才会加载新过程的指令。每个线程都会有各自的栈用来保存这些状态。高级语言中，也有类似的情形，当函数调用开始之后，实际参数替换了形式参数后，实际参数也都需要保存，以免出现调用返回之后找不到符号的问题。调用层次较多的非尾递归程序因为这样的原因，就需要更多的空间去保存调用前的参数状态，从而造成空间的浪费。然而尾递归却不用有这个忧虑，编译器通常会做优化，如果发现是尾递归，那么实际参数不会再被使用，调用之前的状态也就不必保存。&lt;/p&gt;
&lt;p&gt;非尾递归除了有明显的空间浪费外，往往还会有明显的重复计算，由于参数的保存，仅限于参数，并不会根据具体的问题，保存一些已经计算过的中间状态。这些重复计算在树形结构的调用中往往是巨大的。尾递归由于没有系统栈帮着保存状态，就要求程序设计者设计合理的参数来保存计算过程中的必要的中间状态，从而既减少了重复计算，又减少了空间浪费。&lt;/p&gt;
&lt;p&gt;拿前面阶乘的例子来看，product就是用来保存中间状态的参数，counter是用来控制迭代次数的参数。尾递归实现中通过增加这两个参数，将空间开销由O(n)减到了O(1)。阶乘的例子由于是线性递归，所以不存在重复计算，但即便如此，非尾递归的实现中也需要先展开再规约，而尾递归的实现中只需要一遍就能得到结果，计算量也会减少一半。&lt;/p&gt;
&lt;p&gt;很多的问题的递归解法，呈现出树形递归的特点，即原问题的解决依赖于多个相同子问题的解决，比如换零钱的问题。&lt;/p&gt;
&lt;p&gt;假设只有50美分、25美分、10美分、5美分和1美分的面值，如果将给定数额的钱用前面提到的五种面额兑换，那么有多少种换法？&lt;/p&gt;
&lt;p&gt;这个问题跟求组合数的问题非常类似，可以将原问题分解为两个子问题。第一，完全不用50美分兑换，用剩下的四种面值兑换给定额度的钱；第二，用一个50美分兑换，对于除去50美分剩下的钱再用五种面值去兑换。这两个子问题彼此互斥，所以求和就是原问题的解。用程序语言描述即为&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rec_count_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个解法显然不是尾递归，最后一步操作是加法，不过非尾递归的解法往往非常容易辅助在脑海中产生求解问题的分解结构。分解的结构是棵二叉树，树的叶子节点要么是0，要么是1，因而叶子的数目与问题的结果是同量级的，这其中的重复计算量是巨大的。当amount是100时，问题的结果是292；当amount是1000时，问题的结果是801,451。虽然amount只增长了10倍，但结果却增长了2700多倍，这是超线性的增长。此外，这棵树不太平衡，如果记rcc(amount, kinds_of_coins-1)为左子树，另外一个为右子树，那么由于kinds_of_coins很快就减到了0，所以左边的深度较浅，最左边的深度只有kinds_of_coins+1；右边由于50美分减得很快，深度也较浅，最右边的深度为int(amount/50)+1，最深的部分在中间为amount+kinds_of_coins+1，所以整棵树像是一串葡萄一样。最大的深度也意味空间开销与amount+kinds_of_coins是同量级的。&lt;/p&gt;
&lt;p&gt;将这个问题改造成尾递归的形式，需要两点技巧，第一，尾递归一般意味着自底向上地先求解最终问题依赖的子问题的结果，因而需要在参数中增加适当的变量，开辟适当的空间，用于存储中间子问题结果，空间的大小越小，不仅省空间，往往同时也会提升运行效率；第二，当问题中有两个以上的输入时，就需要找对迭代的方法和维度，这点往往对节省空间起着至关重要的作用。&lt;/p&gt;
&lt;p&gt;如果用表格记录所有可能子问题的结果，那么表格一定只有(amount+1)*kinds_of_coins的大小，共有kinds_of_coins行，amount+1列。在求解表格中任何一个格子中的值时，都只依赖于前一行同列（kinds_of_coins-1, amount）与同行前面某列（kinds_of_coins, amount-first_denomination(kinds_of_coins)）的值，如果我们逐行地填充表格，就不必要保存着整张表，而只需要一行的存储空间。这样的空间开销不会比非尾递归大，时间上要远好。想清楚了第一点技巧，还要想清第二点，也就是说，起始时候，amount从0开始，kinds_of_coins从1开始计算，到底是先固定amount为0不变，将所有的kinds_of_coins都算出来呢，还是相反将kinds_of_coins固定为1，而去计算出对应所有amount的值。其实在选择存储空间大小时候，就已经潜在回答了这个问题，我们选择了一行，即为后者。明白了改造尾递归的两个方面，改起算法来也不会非常困难。尾递归的解法为&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tail_rec_count_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;trcc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trcc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kinds_of_coins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ck&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kinds_of_coins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;trcc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kinds_of_coins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ck&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;remain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;first_denomination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remain&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;remain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;trcc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kinds_of_coins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;尾递归相比于专门的循环结构，既保持了原始递归分解的形状，又拥有了动态规划自底向上的实质。这两个特点是极其优秀的，人们思考问题时最自然的方式是自顶向下的分解，几乎所有的问题都如此，否则大型系统的构造无从谈起。从原始递归转换为尾递归时，设计者可以专注于寻找适当的存储空间，放在参数中即可，而专门的循环结构往往会干扰设计者的思路。循环结构中循环控制变量，会让人不停地去重复在大脑中运行循环，而不是去思考寻找适当的存储空间和解决小的子问题。我非常同意《计算机程序的构造和解释》一书中作者对尾递归存在意义的评价：“有了一个尾递归的实现，我们就可以利用常规的过程调用机制表述迭代，这也会使各种复杂的专用迭代结构变成不过是一些语法糖衣了”。面对复杂的算法，优先写出递归解法，再改造成尾递归形式，然后再用专用的循环结构去改造不失为一种好的策略。实际上，循环结构是机器友好的，并不是人类友好的，循环天然拥有着一种自底向上的特质，只适合于做，不适合于想。从尾递归改造成循环结构难度要小于从原始递归转变为尾递归。从原始递归转变为尾递归，需要思路上的转变，而从尾递归转变为循环结构，如同找了一些同义词，从新表达一个段落而已。&lt;/p&gt;
&lt;p&gt;然而，值得注意的是并不是所有的语言都适合写成尾递归的形式，很多解释型的语言并没有对尾递归做优化，比如上面所用的&lt;a class="link" href="https://stackoverflow.com/questions/13591970/does-python-optimize-tail-recursion" target="_blank" rel="noopener"
 &gt;Python语言&lt;/a&gt;。编译型的语言多会对尾递归做优化，比如C、Java。我尝试在Python上运行tail_rec_count_change(1000)，结果抛出了RuntimeError: maximum recursion depth exceeded的异常，而在Java的虚拟机上运行，则会给出正常的结果。由此，尾递归适合用于辅助我们思考较为复杂的算法问题，最终实现时要改造为循环结构的迭代形式。&lt;/p&gt;
&lt;h2 id="后记"&gt;后记
&lt;/h2&gt;&lt;p&gt;除了换零钱的例子外，求解组合数、求幂的O(log n)算法、求数组的子数组之和的最大值也都是良好的练习，相应的解法可以&lt;a class="link" href="https://github.com/jeremy1990/small-problems/tree/master/tail-recursion-examples" target="_blank" rel="noopener"
 &gt;参考这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关阅读&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="http://mjm1990.com/article/%E6%B1%82%E6%95%B0%E7%BB%84%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84%E4%B9%8B%E5%92%8C%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC" target="_blank" rel="noopener"
 &gt;求数组的子数组之和的最大值&lt;/a&gt;?⭐️⭐️⭐️⭐️&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>