<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>阅读感想 on 有点稳</title><link>https://blog.nicelylit.net/tags/%E9%98%85%E8%AF%BB%E6%84%9F%E6%83%B3/</link><description>Recent content in 阅读感想 on 有点稳</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Mon, 14 Jun 2021 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.nicelylit.net/tags/%E9%98%85%E8%AF%BB%E6%84%9F%E6%83%B3/index.xml" rel="self" type="application/rss+xml"/><item><title>判定、构造和证明</title><link>https://blog.nicelylit.net/posts/%E5%88%A4%E5%AE%9A%E6%9E%84%E9%80%A0%E5%92%8C%E8%AF%81%E6%98%8E/</link><pubDate>Mon, 14 Jun 2021 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E5%88%A4%E5%AE%9A%E6%9E%84%E9%80%A0%E5%92%8C%E8%AF%81%E6%98%8E/</guid><description>&lt;h2 id="引子"&gt;引子
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;最近学习近世代数基础时，短短的一章，一共出现了32个定义、10个性质和30个定理。每一个性质和定理都要经过证明，并非一个顺畅的过程，特别是证明庞加莱定理的那个引理的证明，理解起来颇费精力。学习过程中最为愉快的是认识了同构。同构的定义将置换群与抽象群联系在了一起，从而构造同构的方法与构造对称群的子群的方法被联系在了一起，形成一个完美的闭环。&lt;/p&gt;
&lt;p&gt;课程中的性质和定理无非是一些判定，涉及到群、子群、陪集、正规子群、同态、同构的判定。课程中的定义和证明涉及到构造，包括正规子群、自同构的构造。任何一条都够抽象，很容易让人陷入到去找具体例子当中去，从而忘记了为什么要讨论当下的这条内容，这点与设计软件时，总是钻入下层甚至更底层有些类似。避免这样的状况发生的办法核心是一条，回到因果链的原始状态，找到那个大的因，并且从这个因上，感受到意义和价值。如果感受不到意义和价值，那放弃了，大概也没什么可惜的。例如，群论的产生的大的因是一元高次方程是否可根式求解的问题。我已经记不清为什么一年前会突然开始对这个问题非常感兴趣，但我很清楚，我迄今仍就对这个问题充满兴趣，虽然根式解的答案我已经很清楚，但了解其论证过程依旧能让我感到意义，并且激发我进一步的兴趣。&lt;/p&gt;
&lt;p&gt;主动做一件事情上让人觉得有意义，一定是过程中诱发了别样的生命体验，不一定舒适，但也不会太痛苦。学习近世代数诱发我体验到的是看清生命和思维的局限性，能够看清人生道路的边界，不至于走出去，偏离了终极目标。具体到思维上，经过此番学习，至少我对判定、构造和证明有了更加清晰的认识。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;判定、断定、断言是对所感知事物的描述和叙说，其结果称作是一个陈述或者一个命题。这个思维过程是基于感知和记忆来展开的，过程的结果以记忆的形式对内留存，或以语音和文字的形式对外留存。所形成的记忆并不局限于大脑，还包括身体各个部位的神经和肌肉，以致产生思维过程的本体，从而形成经验。当我们断言产生思维过程的本体与肉体同一的时候，会有超出经验的观察，被人们称作超验或者先验；可当我们断言本体跨越多个肉体的时候，只需要留存经验的概念来解释一切的观察。不论是超验还是经验都是基于本体可感知、可观察和可断言而谈的。可感知的事物在思维过程中被对象化、抽象化、符号化，从而脱离了感知，只留存为了记忆。&lt;/p&gt;
&lt;p&gt;构造、搭建、创造、创作是将经验运用在新的感知对象上，从而产生新的概念和感知对象的过程。在运用经验时，运用者必须意图明确并且能力充分，否则作用会产生偏差。明确意图与做判定的过程类似，要基于感知抽取记忆对象和符号，对将要进行的过程，全面清晰地描述。虽然使用描述这个字眼，但并不意味着一定要用嘴说或者用手写，而是通过念头来完成。明确意图意味着要将念头聚焦到与一个对象相关的一系列记忆上，也就是要专注。能够专注才能意图明确。能力是践行经验的要素。能力充分与否只有运用者才能够全面的认识到，因为从运用者的肉身到肉身以外的一切可感知的事物所具有的局限性就是运用者的能力。具备认知局限性的能力本身也是一种能力。可见能力必然是有边界的，否则所谓的创造也就没有了意义。人们追逐能力的提升，与追求出世间的自由是天然矛盾的，因为出世间的自由是没有边界的。回到世间的追求上，追求创造力的提升，也就是在追求局限性的缩小，也就要提升感知力、专注力和行动力。&lt;/p&gt;
&lt;p&gt;论述、论证、证明是命题（陈述）之间的推演，不产生新的概念和可感知对象。理想中推导和演绎的过程不需要感知力和行动力，这大概也是人们追求人工智能的一个基础假设，感知力和行动力对应到如今的计算机模型上，就是外部设备的功能。这样的假设显然与常人的经验是相悖的，具备与运用推导和演绎的能力往往需要强大的感知力和行动力（对于计算机，人们只好不停地加缓存，头疼于对信息不一致的问题）。现实中的论证过程，不可能是在所有的命题都具足的前提下进行的，这就要求论证者做判断的过程快速准确，若相信本体跨越多个肉体，那么论证者必然是一个经验丰富的跨越者。命题不具足的时候，往往还需要构造新的对象与符号，这又要求创造力。当然没有人会否认论证过程需要高度的专注，甚至从专注上升到了信念。&lt;/p&gt;</description></item><item><title>编译器前端回顾（下）</title><link>https://blog.nicelylit.net/posts/%E7%BC%96%E8%AF%91%E5%99%A8%E5%89%8D%E7%AB%AF%E5%9B%9E%E9%A1%BE%E4%B8%8B/</link><pubDate>Sun, 09 Aug 2020 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E7%BC%96%E8%AF%91%E5%99%A8%E5%89%8D%E7%AB%AF%E5%9B%9E%E9%A1%BE%E4%B8%8B/</guid><description>&lt;p&gt;编译器前端的程序主干是字符串的匹配，可真正的目的是翻译，将源代码转换为目标码。若将匹配流程比作骨架，那么翻译方法就是血肉。设计翻译方法，在于解决三个问题：如何表示源代码中的语义？如何表示目标码？如何将源代码到目标代码的翻译步骤添加到程序的主干流程当中？&lt;/p&gt;
&lt;p&gt;语义是藏在文字符号本身之上的一种可被解释与理解的特性。比方说，符号&amp;gt;，符号本身只是个开口向左的V，而藏在符号之上的特性是大于，用于体现符号左右两侧的量的大小关系；如果两个连在一起，还表示右移。源代码中藏有的语义可分两类，一类体现了目标码的计算特性，另一类体现了源代码的控制复杂特性。第一类在翻译中会通过不同的形式表示，最终翻译为目标码。第二类在翻译中辅助程序做安全检查和组织目标码结构。体现目标码的计算特性的语句类型有算术表达式、逻辑表达式、比较表达式、函数调用和控制流。体现源代码的控制复杂特性的语句类型主要是类型声明，以及一些附加的限定规则。&lt;/p&gt;
&lt;p&gt;在上下文无关文法的框架下，一篇源代码中的字符流，会被转换为文法符号流，语义特性可以作为文法符号的某个属性。语法制导定义中，依据属性对其他符号属性的依赖的不同，将属性分作了两类，继承属性和综合属性。继承属性值的计算依赖于产生式中当前符号左侧的其他文法符号的属性值。综合属性值的计算依赖于产生式中右侧的文法符号的属性值。上下文无关文法通过产生式将源代码映射为了一棵语法分析树，分析树上的结点是文法符号。继承属性值的计算依赖于父结点和兄弟结点的属性值。如果是L属性的语法制导定义，那么规定只依赖于左侧的兄弟结点。综合属性值的计算依赖于孩子结点的属性值。目标码是语法分析树文法结点的主属性，可以表示为综合属性，或者直接依照分析过程生成到文件中或者数组、字符串中。&lt;/p&gt;
&lt;p&gt;目标码要运行在解释器上，而解释器的类型是多样的，可能是物理机器，也可能是软件虚拟机，一种语言若要兼容多种运行环境，那么设计一种中间代码能够让前端的程序得以复用。特别是当有M种语言要运行在N种解释器上时，可以让编写编译器的数量从M*N降低到M+N。&lt;/p&gt;
&lt;p&gt;中间代码要尽可能地接近解释器上运行的指令。一条指令通常包含一个指令名，可以理解为操作符，以及不超过两个的操作数。如果一条指令有两个操作符，那么一定能够拆成两条指令。如果一条指令有多于两个操作数，那么也一定能够拆成两条以上的指令。比如，函数的调用就是个很好的例子。每个参数都能单独拆成一条单操作数指令，函数调用本身也是一条单操作数指令。这种一个操作符和两个操作数的表示方法称作三地址代码，再考虑到指令本身相对其他指令的序列编号，即地址，也就有了中间代码的四元表示。&lt;/p&gt;
&lt;p&gt;三地址码的另一种等价形式是语法树，中间结点是操作符，叶子结点是操作数。如果考虑子表达式的复用，更一般的形式是有向无环图。语法树不同于语法分析树，语法树与三地址码等价，而语法分析树同语法推导的分析过程等价。&lt;/p&gt;
&lt;p&gt;在语法制导定义中添加构造语法树的语义动作，伴随符号的匹配和产生式的规约，执行属性求值、语法树的构造与翻译。在产生式中引入表记的技术能够被用来回填无条件跳转指令中缺失的地址信息。设计语义动作可以依据以下的步骤进行：一、找出关联的几个属性；二、找出哪些是综合属性，哪些是继承属性；三、写出该有哪些语义动作；四、确定语义动作的顺序；五、校验。&lt;/p&gt;</description></item><item><title>编译器前端回顾（上）</title><link>https://blog.nicelylit.net/posts/%E7%BC%96%E8%AF%91%E5%99%A8%E5%89%8D%E7%AB%AF%E5%9B%9E%E9%A1%BE%E4%B8%8A/</link><pubDate>Sun, 17 May 2020 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E7%BC%96%E8%AF%91%E5%99%A8%E5%89%8D%E7%AB%AF%E5%9B%9E%E9%A1%BE%E4%B8%8A/</guid><description>&lt;p&gt;在设计编译器的前端时，程序驱动的主干是字符串的匹配，所回答的基本问题是_&lt;strong&gt;输入文本&lt;/strong&gt;&lt;em&gt;是否能够匹配**&lt;em&gt;预定义规则&lt;/em&gt;**，因而返回值是&lt;/em&gt;&lt;strong&gt;是或否&lt;/strong&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;
&lt;p&gt;观察单个的产生式，像是一个变量的定义，也像是一个函数的定义，像是一个类的定义，总之像是一种定义，就是拿一个符号来代表一组固定序列的符号。文字上人们称其为替换，逻辑上人们称其为抽象。看到产生式的左边，将其替换（展开）为右边的操作称作推导。看到产生式右边，将其替换（合并）为左边的操作称作归约。&lt;/p&gt;
&lt;p&gt;观察单个产生式的左边和右边的非终结符，存在一种特殊的形式：递归，即产生式右边的非终结符里包含产生式左边的符号。对于某个非终结符如果存在一个递归的展开（A → Aa ），那么一定还要有一个非递归的展开（A → b ），否则推导无法结束。容易联想到，在数学递推公式中等于在说，必须给定初始条件，否则推导无法结束。此外，这也往往作为检查递归程序是否正确的重要一点。&lt;/p&gt;
&lt;p&gt;根据左边的符号在右边出现的位置不同，分三种情况，开头、中间和结尾。出现在开头，被称作左递归。出现在结尾，被称作尾递归。出现在中间，就是普通的递归。左递归是设计文法时要避免的，含有左递归的文法无法进行自上而下的分析。消除左递归需要引入新的非终结符并利用空串，添加新的产生式。普通递归和尾递归在文法设计时没有特殊之处，可在设计解析器的时候，尾递归往往可以进行优化，即返回当前调用的上下文，清除当前栈帧，而进行新的调用，减少调用栈的层次。这样的优化虽然对执行本身起到了好处，但却给调试工具引入了麻烦。&lt;/p&gt;
&lt;p&gt;观察两个产生式之间的关系，有三种：并、嵌套和不相关。并指的是两个产生式具有相同的左端。嵌套指的是一个产生式的左端出现在另一个产生式的右端中。不相关是指不存在并和嵌套关系的关系。并的关系是对称的偏序关系，嵌套的关系是不对称的偏序关系。文法推导的过程中需要做两个选择，其一，存在并关系的产生式中选哪一个；其二，一个产生式的多个嵌套产生式中选哪一个。这也是文法产生二义性的来源。第一个选择可以就输入符号的情况加以选择，在自上而下的分析方法中表现为向前看几个符号的问题，在自下而上的分析方法中表现为不同项集之间的状态转移问题，此外，还有二义文法中利用优先级和结合性来额外处理的问题。第二个选择可以规定从左到右展开还是从右到左展开，从左到右的方法称作LL，从右到左的方法称作LR。第一个L是指的从左到右扫描输入文本，第二个是指的推导的展开次序。自下而上的分析方法虽然是归约式的，但其逆过程等价于一个从右到左的推导过程，因而常常被称作LR的。&lt;/p&gt;
&lt;p&gt;产生式模型的构建，将设计语言的问题转换为了设计产生式的问题，如何设计非终结符，如何规定非终结符的右端，选择设计多少个产生式，这是一个相对开放而又复杂的问题，并不在编译原理课程的讨论范围内。编译原理中关注和解决的问题是如何在给定了产生式集合的情况下，生成一个自动机，高效地完成匹配任务。&lt;/p&gt;
&lt;p&gt;编译原理中介绍了两种自动机，一种是不带栈的有穷状态自动机，另一种是带栈的有穷自动机。不带栈的有穷状态自动机通常被称作有穷状态机，通常包含一个状态转换的驱动程序、一个状态转换表、输入字符串和输出。带栈的有穷状态机通常被称作下推自动机，除了包含有穷状态机中的四种东西外，还包含一个不限大小的栈。虽然驱动程序和状态转换表两种自动机都有，但内容相似却不同，下推自动机的驱动程序中不只是移动输入和转移状态，还包括弹栈和入栈的操作。Lex可以用来将一组正则表达式生成一个有穷状态机，被用作词法分析器。Yacc可以用来将一组产生式生成一个下推自动机，被用作语法分析。正则文法能够表达的三种操作都能够用上下文无关无法表达，反之却不可以。如何选择哪些部分应该用正则表达，哪些部分应该用产生式表达，基本原则是如果能用正则就尽量用正则，如果不能则用产生式，例如括号、语句块、条件分支、循环、嵌套、递归等。&lt;/p&gt;
&lt;p&gt;若输入文本有错，报告错误的一个原则是，尽可能多、准确、快速地报告错误。若要达到多的目标，就要用到恐慌模式的思想，即碰到一个错误，记录下来，不要退出程序，消化错误，继续向前解析。采用的办法往往是丢弃掉一些输入，找到较为清晰的同步词法单元。若要达到准确的目标，就要用到短语层次的恢复策略，为不同的状态转移动作设计专门的报错例程。若要达到快速的目标，主要还是在于保证匹配算法本身的效率，与报告错误的处理关系不是太大，但也要尽量减小错误恢复的空间开销。&lt;/p&gt;</description></item><item><title>滚动硬币的启示</title><link>https://blog.nicelylit.net/posts/%E6%BB%9A%E5%8A%A8%E7%A1%AC%E5%B8%81%E7%9A%84%E5%90%AF%E7%A4%BA/</link><pubDate>Tue, 23 Jan 2018 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E6%BB%9A%E5%8A%A8%E7%A1%AC%E5%B8%81%E7%9A%84%E5%90%AF%E7%A4%BA/</guid><description>&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;script src="https://nicelylit.net/wp-content/themes/jeremysworld/js/coin-path.js?ver=6.1.9" id="coin-path-js"&gt;&lt;/script&gt;
&lt;p&gt;&lt;canvas id="coinPath" style="margin:auto;display:block;" width="400" height="400"&gt;您的浏览器不支持Canvas API&lt;/canvas&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;blockquote&gt;
 &lt;p&gt;人心在把自己的能力施用于简单的观念时，其作用约可分为三种：第一，它可以把几个简单观念合成一个复合观念，因而造成一切复杂观念。第二，它可以把两个观念（不论是简单的或复杂的）并列起来，同时观察，可是并不把它们结合为一；这样，它就得到它的一切关系观念。第三，它可以把连带的其他观念排斥于主要观念的真正存在以外；这便叫做抽象作用，这样就造成一切概括的观念，这就分明表示出，人类的能力同其作用方式，在物质世界方面同理性世界方面，都是一样的。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;虽然数学和物理中经典的例子不停地向我们展示这样的技巧，只是当遇到新问题时，我们的思维和注意力容易被复杂的细节和对象带偏，从而抓不住本质的对象。如果我们在遇到新问题难以找到方向时，不妨想一下约翰·洛克给我们的启示，我们一定不会太过迷茫。&lt;/p&gt;</description></item><item><title>图灵停机问题的两种符号表述</title><link>https://blog.nicelylit.net/posts/%E5%9B%BE%E7%81%B5%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98%E7%9A%84%E4%B8%A4%E7%A7%8D%E7%AC%A6%E5%8F%B7%E8%A1%A8%E8%BF%B0/</link><pubDate>Sat, 09 Dec 2017 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E5%9B%BE%E7%81%B5%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98%E7%9A%84%E4%B8%A4%E7%A7%8D%E7%AC%A6%E5%8F%B7%E8%A1%A8%E8%BF%B0/</guid><description>&lt;p&gt;年中在读《量子计算与量子信息原理》的时候，写了一篇《&lt;a class="link" href="https://blog.nicelylit.net/posts/%E5%9B%BE%E7%81%B5%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98%E7%9A%84%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E8%AE%BA%E8%BF%B0/" &gt;图灵停机问题的一个简单论述&lt;/a&gt;》的阅读笔记，对大学时没学明白的图灵停机问题有了一些基本认识。今天在读《复杂》的时候，里面采用了另外一种等价的说法，在符号形式上看起来略有差异，如今记录一下加深印象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回顾量子一书中的描述&lt;/strong&gt;，先假设存在算法A具有判定任意算法是否能够停止运算的能力，用符号表述也就是&lt;/p&gt;
&lt;p&gt;对于算法T输入mh，可以停止运算时，A回答是 A(T(mh))=是&lt;/p&gt;
&lt;p&gt;对于算法T输入mf，不能停止运算时，A回答否 A(T(mf))=否&lt;/p&gt;
&lt;p&gt;现在构造算法B，对于A输出为是的，B算法不能停止运算 B(A(T(mh))=不&lt;/p&gt;
&lt;p&gt;对于A输出为否的，B算法可以停止运算 B(A(T(mf))=停&lt;/p&gt;
&lt;p&gt;此时，如果将算法B作为算法A和算法B的输入就会推出矛盾&lt;/p&gt;
&lt;p&gt;根据A算法具备的能力，那么以下的两个代入应该成立 A(B(A(T(mh)))=否 A(B(A(T(mf)))=是&lt;/p&gt;
&lt;p&gt;根据B算法具备的能力，那么以下的代入应该成立 B(A(B(A(T(mh))))=停 B(A(B(A(T(mf))))=不&lt;/p&gt;
&lt;p&gt;矛盾出现了，对于输入mh，既能停止运算，又不能停止运算；对于输入mf，既不能停止运算，有能停止运算。所以B不存在，A也不存在。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复杂一书中的描述如下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;H算法（图灵机）可以判定任意M算法对于任意输入I的停机行为，即 对于M(I)可以停止，H(M, I)=是 对于M(I)不可停止，H(M, I)=否&lt;/p&gt;
&lt;p&gt;如果将M算法作为M的输入，那么根据前面的假设H也能够得到是与否的结论。不过，这时，我们去构造一个算法H&amp;rsquo;，对于M(M)可以停止的时候，H&amp;rsquo;算法不可停止；相反则课停止，即 对于M(M)可以停止，H&amp;rsquo;(M,M)=不 对于M(M)不可停止，H&amp;rsquo;(M,M)=停&lt;/p&gt;
&lt;p&gt;这时问H&amp;rsquo;(H&amp;rsquo;,H&amp;rsquo;)会如何呢？ 根据H&amp;rsquo;构造的逻辑，H&amp;rsquo;(H&amp;rsquo;)如果可以停止，那么H&amp;rsquo;(H&amp;rsquo;,H&amp;rsquo;)不能停止，而如果它不能停止，又是可停止的，于是得到矛盾。&lt;/p&gt;
&lt;p&gt;这两种叙述看起来有许多不同，但本质的两个方面是相同的。其一，算法本身可以作为算法的输入，这点观察不是显而易见的，并且也不那么容易理解。这点观察有个简单的等价问题就是理发师问题，此外，还有就是罗素的集合悖论以及哥德尔证明数学完备性的基础。拓展到计算机中，所有学过计算机软件理论的人都感到学习编译原理的难度要高过操作系统、计算机网络等，主要原因大概就是因为编译过程的输入、处理规则和输出都是程序，非常反直觉。其二，无矛盾是理性推理的基石，这点学过一些基础数学证明的人都曾会有些体会。&lt;/p&gt;</description></item><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><item><title>图灵停机问题的一个简单论述</title><link>https://blog.nicelylit.net/posts/%E5%9B%BE%E7%81%B5%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98%E7%9A%84%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E8%AE%BA%E8%BF%B0/</link><pubDate>Sun, 21 May 2017 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E5%9B%BE%E7%81%B5%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98%E7%9A%84%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E8%AE%BA%E8%BF%B0/</guid><description>&lt;p&gt;图灵停机问题问的是“能否设计一个算法，在判定任意的一段算法对给定的一些输入在有限步骤后停止运算时，返回是，而在判定任意的一段算法对给定的另外一些输入无法在有限步骤后停止运算时，返回否“。图灵的回答是这样的算法无法被设计出来。&lt;/p&gt;
&lt;p&gt;问题中所谓的算法等价于图灵机。&lt;/p&gt;
&lt;p&gt;假设存在算法A，具备这样的能力。设想对任意的算法T，对于输入mh，能够在有限步骤后停止运算，那么，此时算法A对于(T, mh)回答是。同样的算法T，对于输入mf，无法在有限步骤后停止运算，那么此时算法A对于(T, mf)回答否。&lt;/p&gt;
&lt;p&gt;这时，我们构造算法B，对于算法A判定回答是的输入mh，算法B不能在有限步骤后停止运算，而对于算法A判定回答否的输入mf，算法B在有限步骤后停止运算。如果算法A存在，那么一定能够设计出算法B。&lt;/p&gt;
&lt;p&gt;根据A具备的能力，算法B在输入为mf时，有限步骤后停止运算，A应该输出是，然而根据B的构造，凡是A输出是的输入，B应该无法在有限步骤后停止运算。这与前面说的“B在输入为mf时，有限步骤后停止运算”相矛盾。因此算法B不存在，因而算法A也不存在。&lt;/p&gt;
&lt;p&gt;有人说设计这类算法的困难之处在于给出出现无穷循环的条件。&lt;/p&gt;
&lt;p&gt;还有人从图灵停机问题上看出了&lt;a class="link" href="http://blog.sciencenet.cn/blog-2322490-991454.html" target="_blank" rel="noopener"
 &gt;人的不确定本质&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我个人好奇的两个问题是，其一，人脑的计算能力和图灵机是否等价；其二，逻辑推理的最为锐利的武器就是推导出矛盾，因为我们观测到的现实的具象的某一个存在都应是无矛盾的，然而思维本身却不是无矛盾的，矛盾如何形式化，形式化后对解决现有逻辑无法解决的问题有帮助吗，我们有能力将具象的某一个存在也变成一个矛盾的综合体吗。&lt;/p&gt;
&lt;p&gt;本质的好奇在于思维和宇宙是什么关系。宇宙是否自洽，具象的存在是否无矛盾。是思维囊括了宇宙还是宇宙滋养了思维。如果宇宙自洽，具象无矛盾的，那么思维的能力似乎要强于宇宙。如果宇宙不自洽，那么思维能力只是宇宙的小部分。我说这段话的时候，自洽与非自洽已经在假设将宇宙给了某种定性，或许宇宙就像思维一样，是一个自洽和非自洽的综合体。我们的思维倾向于更加的符合某种逻辑性，而宇宙恰好是那种逻辑性的表述。&lt;/p&gt;</description></item><item><title>音和声</title><link>https://blog.nicelylit.net/posts/%E9%9F%B3%E5%92%8C%E5%A3%B0/</link><pubDate>Thu, 08 Jan 2015 00:00:00 +0000</pubDate><guid>https://blog.nicelylit.net/posts/%E9%9F%B3%E5%92%8C%E5%A3%B0/</guid><description>&lt;p&gt;老子说“有无相生，难易相成，长短相形，高下相盈，音声相和，前后相随。”意在说明事物中普遍存在的对立统一的辩证关系，朴素确也深刻。这其中有无、难易、长短、高下、前后等五组都容易理解，大概是因为视觉比听觉更让人记忆清楚，容易想象。音和声的对立关系直觉中却难以建立，我想了许久才有些明白。&lt;/p&gt;
&lt;p&gt;声音在现代汉语中常常组合在一起表示耳朵的感知，准确一点是声波带动耳膜的振动感。实际上，音和声被用来描述不同对象。百科中的解释都太含糊，查音的时候，解释是声，而查声时候，又解释是音。有一种解释说音是有节奏的声，言外之意似乎是说音是一种特殊的声，说明的还是统一性，而难以体现音与声的对立关系。还有一种解释说音与心相通，声与耳相通，说得有些道理，却还不够直白。&lt;/p&gt;
&lt;p&gt;音和声在现代汉语中虽被组合在一起用，却也还有很多分开的时候。音的词组相对较多，像音阶、音律、辅音、高音、低音、乐音、噪音等，在这些词组中，替换成声不合适。声表本意的词组相对较少，我能想到的是声带、声波，这两个词组中也不可被替换为音。看这些词组的规律，音多用来表示具有特定名称的听觉感知，从对立的角度来讲声自然就表示相反的状态，可以说是非特定名称的听觉感知。好像还是不够具体，静下心来，闭上眼睛，听，左边传来喝水的声音，同时左前方传来打哈欠的声音，不到一秒右边倒水的声音，点击鼠标的声音，关门的声音，全部都分辨得清清楚楚。试想，我们的视觉感知有这种能力吗，有，放在眼前的东西，是长是短，是大是小一目了然，为什么，因为有了对比，脱离了小的物体，就无法说大，没有短，也就自然显不出长。音和声是同样的道理，假想世界上只有水流声，一个频率一种音色，关门声是这个频率这个音色，甚至咳嗽声也是这个频率这个音色，那么耳朵必定单调极了，只有小提琴没有长笛的世界不会快乐。&lt;/p&gt;</description></item></channel></rss>