范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

为什么你学不会递归?谈谈我的经验

  本文已收录到 GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star。技术和职场问题,请关注公众号 [彭旭锐] 进 Android 面试交流群。  前言
  大家好,我是小彭。
  今天分享到计算机科学中一个基础又非常重要的概念 —— 递归。递归是计算机中特有的概念,你很难在现实世界中找到一个恰当的例子与之关联起来。因此,对于很多初学编程的人,一开始会很难理解。
  那么,究竟什么是递归,我们为什么要使用递归?我们今天就围绕这两个问题展开。
  学习路线图:
  1. 什么是递归?
  递归(Recursion)是一种通过 "函数自己调用自己" 的方式,将问题重复地分解为同类子问题,并最终解决问题的编程技巧。
  举个例子,要求一个数 n 的阶乘 n!=n∗(n−1)∗(n−2)∗…∗2∗1 ,有  2 种思考问题的思路: 递推(一般思维):  我们从 1 开始,用 1 乘以 2 得到 2! 问题的解,用 3 乘以 2! 得到 3! 问题的解。依次类推,直到用 n 乘以 (n−1)! 得到原问题 n! 的解。这就是用递推解决问题,这是相对简单直接的思考方式;递归(计算机思维):  我们把 n! 的问题拆分为一个 (n−1)! 的问题,如果我们知道 (n−1)! 的解,那么将它乘以 n 就可以得出 n! 的解。以此类推,我们将一个 (n−1)! 的问题拆分为同类型的规模更小的 (n−2)! 子问题,直到拆分到无法拆分,可以直接得出结果 1! 问题。此时,我们再沿着拆分问题的路径,反向地根据子问题的解求出原问题的解,最终得到原问题 n! 的结果。这就是用递归解决问题。
  求 n!
  从这个例子可以看出,  递归其实是在重复地做 2 件事: 1、自顶向下拆分问题:  从一个很难直接求出结果的、规模较大的原问题开始,逐渐向下拆分为规模较小的子问题(从 n! 拆分到 (n−1)!),直到拆分到问题边界时停止拆分,这个拆分的过程就是 "递"(问题边界也叫基准情况或终止条件);2、自底向上组合结果:  从问题边界开始,逐渐向上传递并组合子问题的解(从 (n−1)! 得到 n!),直到最终回到原问题获得结果,这个组合的过程就是 "归"。
  看到这里你会不会产生一个疑问:  我们直接从问题边界 1! 一层层自底向上组合结果也可以得到 n! 的解,自顶向下拆分问题的过程显得没有必要。确实,对于对于这种原问题与子问题只是 "线性"  地减少一个问题规模的情况,确实是这样。但是对于很多稍微复杂一些的问题,原问题与子问题会构成一个树型的 "非线性"  结构,这个时候就适合用递归解决,很难用递推解决。
  举个例子,  求斐波那契数列,这个问题同时也是 LeetCode 上的一道典型例题:LeetCode · 509. 斐波那契数:该数列从 1 开始,每一项数字都是前面两项数字的和。
  LeetCode 例题
  虽然,我们可以利用递推的方式从 F(0) 和 F(1) 自底向上推导出 F(n) 的解,但是这种非线性的方式在编程语言中很难实现,而使用递归的方式自顶向下地解决问题,在编码上是很容易实现的。
  当然,这段代码中存在非常多的重复计算,最终使得整个算法的时间复杂度达到惊人的指数级 O(2n)。例如在计算 F(5)=F(3)+F(4) 和 F(6)=F(4)+F(5) 的时候,F(4) 就被重复计算 2 次,这种重复计算完全相同的子问题的情况就叫  重叠子问题  ,以后我们再专门讨论。
  用递归解决斐波那契数列
  用递归解决(无优化)  class Solution {     fun fib(N: Int): Int {         if(N == 0){             return 0         }         if(N == 1){             return 1         }         // 拆分问题 + 组合结果         return fib(N - 1) + fib(N - 2)     } } 2. 递归的解题模板1、判断当前状态是否异常,例如数组越界, n < 0   等;2、判断当前状态是否满足终止条件,即达到问题边界,可以直接求出结果; 3、递归地拆分问题,缩小问题规模; 4、组合子问题的解,结合当前状态得出最终解。 fun func(n){     // 1. 判断是否处于异常条件     if(/* 异常条件 */){         return     }     // 2. 判断是否满足终止条件(问题边界)     if(/* 终止条件 */){         return result     }     // 3. 拆分问题     result1 = func(n1)     result2 = func(n2)     ...     // 4. 组合结果     return combine(result1, result2, ...) } 3. 计算机如何实现递归?
  递归程序在解决子问题之后,需要沿着拆分问题的路径一层层地原路返回结果,并且后拆分的子问题应该先解决。这个逻辑与栈 "后进先出" 的逻辑完全吻合: 拆分问题:  就是一次子问题入栈的过程;组合结果:  就是一次子问题出栈的过程。
  事实上,这种出栈和入栈的逻辑,在编程语言中是天然支持的,不需要程序员实现。程序员只需要维护拆分问题和组合问题的逻辑,一次函数自调用和返回的过程就是一次隐式的函数出栈入栈过程。在程序运行时,内存空间中会存在一块维护函数调用的区域,称为  函数调用栈  ,函数的调用与返回过程,就天然对应着一次子问题入栈和出栈的过程:调用函数:  程序会创建一个新的栈帧并压入调用栈的顶部;函数返回:  程序会将当前栈帧从调用栈栈顶弹出,并带着返回值回到上一层栈帧中调用函数的位置。
  我们在分析递归算法的空间复杂度时,也必须将隐式的函数调用栈考虑在内。 4. 递归与迭代的区别
  递归(Recursion)和迭代(Iteration)都是编程语言中重复执行某一段逻辑的语法。
  语法上的区别在于: 迭代:  通过迭代器(for/while)重复执行某一段逻辑;递归:  通过函数自调用重复执行函数中的一段逻辑。
  核心区别在于解决问题的思路不同: 迭代:迭代的思路认为只要从问题边界开始,在所有元素上重复执行相同的逻辑,就可以获得最终问题的解(迭代的思路与递推的思路类似); 递归:递归的思路认为只要将原问题拆分为子问题,在每个子问题上重复执行相同的逻辑,最终组合所有子问题的结果就可以获得最终问题的解。
  例如, 在计算 n! 的问题中,递推或迭代的思路是从 1! 开始重复乘以更大的数,最终获得原问题 n! 的解;而递归的思路是将 n! 问题拆分为 (n-1)! 的问题,最终通过 (n-1)! 问题获得原问题 n! 的解。
  再举个例子,面试中出现频率非常高的反转链表问题,同时也是 LeetCode 上的一道典型例题:LeetCode 206 · 反转链表。假设链表为 1 → 2 → 3 → 4 → ∅,我们想要把链表反转为 ∅ ← 1 ← 2 ←3 ←4,用迭代和递归的思路是不同的: 迭代:  迭代的思路认为,只要重复地在每个节点上处理同一个逻辑,最终就可以得到反转链表,这个逻辑是:"将当前节点的 next 指针指向前一个节点,再将游标指针移动到后一个节点"。递归:  递归的思路认为,只要将反转链表的问题拆分为 "让当前节点的 next 指针指向后面整段子链的反转链表",在每个子链表上重复执行相同的逻辑,最终就能够获得整个链表反转的结果。
  这两个思路用示意图表示如下:
  示意图
  迭代题解  class Solution {     fun reverseList(head: ListNode?): ListNode? {         var cur: ListNode? = head         var prev: ListNode? = null          while (null != cur) {             val tmp = cur.next             cur.next = prev             prev = cur             cur = tmp         }         return prev     } }
  迭代解法复杂度分析: 时间复杂度:每个节点扫描一次,时间复杂度为 O(n); 空间复杂度:使用了常量级别变量,空间复杂度为 O(1)。
  递归题解  class Solution {     fun reverseList(head: ListNode?): ListNode? {         if(null == head || null == head.next){             return head         }         val newHead = reverseList(head.next)         head.next.next = head         head.next = null         return newHead     } }
  递归解法复杂度分析: 时间复杂度:每个节点扫描一次,时间复杂度为 O(n); 空间复杂度:使用了函数调用栈,空间复杂度为 O(n)。
  理论上认为迭代程序的运行效率会比递归程序更好,并且任何递归程序(不止是尾递归,尾递归只是消除起来相对容易)都可以通过一个栈转化为迭代程序。但是,这种消除递归的做法实际上是以牺牲程序可读性为代价换取的,一般不会为了运行效率而刻意消除递归。
  不过,有一种特殊的递归可以被轻松地消除,一些编译器或运行时会自动完成消除工作,不需要程序员手动消除,也不会破坏代码的可读性。 5. 尾递归
  在编程语言中,尾调用是指在一个函数的最后返回另一个函数的调用结果。如果尾调用最后调用的是当前函数本身,就是尾递归。为什么我们要专门定义这种特殊的递归形式呢?因为尾递归也是尾调用,而在大多数编程语言中,尾调用可以被轻松地消除 ,这使得程序可以模拟递归的逻辑而又不损失性能,这叫  尾递归优化 / 尾递归消除  。例如,以下 2 段代码实现的功能是相同的,前者是尾递归,而后者是迭代。
  尾递归  fun printList(itr : Iterator<*>){     if(!itr.hasNext()) {         return     }     println(itr.next())     // 尾递归     printList(itr) }
  迭代  fun printList(itr : Iterator<*>){     while(true) {         if(!itr.hasNext()) {             return         }         println(itr.next())     } }
  可以看到,使用一个  while   循环和若干变量消除就可以轻松消除尾递归。6. 总结
  到这里,相信你已经对递归的含义以及递归的强大之处有所了解。  递归是计算机科学中特有的解决问题的思路:先通过自顶向下拆分问题,再自底向上组合结果来解决问题。这个思路在编程语言中可以用函数自调用和返回实现,因此递归在编程实现中会显得非常简洁。  正如图灵奖获得者尼克劳斯·维尔特所说:"递归的强大之处在于它允许用户用有限的语句描述无限的对象。因此,在计算机科学中,递归可以被用来描述无限步的运算,尽管描述运算的程序是有限的。"
  另外,你会发现 "先拆分问题再合并结果" 的思想与 "分治思想" 相同,那么你认为递归和分治是等价的吗?这个我们下回说。
  发现一个 Google 的小彩蛋:在 Google 搜索里搜索 "递归",提示词里会显示 "您是不是要找:递归"。这就会产生递归的效果的,因为点击提示词 "递归" 后,还是会递归地显示 "您是不是要找:递归"。哈哈,应该是 Google 跟程序员开的小玩笑。
  参考资料 数据结构与算法分析 · Java 语言描述(第 1 章 · 引论、第 3 章 · 表栈和队列、第 10 章 · 算法设计技巧)—— [美] Mark Allen Weiss 著 算法导论(第 4 章 · 分治策略)—— [美] Thomas H. Cormen 等 著 算法吧 · 递归 —— liweiwei1419 著 Recursion (computer science) —— Wikipedia Divide-and-conquer algorithm —— Wikipedia Iterator —— Wikipedia Tail call —— Wikipedia

威少到来!快船变航母?!根据Woj报道,威少经纪人JeffSchwartz透露,威少正在与爵士敲定买断合同,买断完成后威少计划签约快船!Woj还透露,最近几天威少与公牛奇才和热火讨论了交易,但有机会争夺总未婚先孕的奥运冠军,嫁给二婚男还要忍受令人窒息的婆媳关系作为奥运会冠军的何雯娜,并没有像郭晶晶伏明霞一样嫁给一个尊重爱护自己的男人,而是嫁给了二婚男梁超,在参加婚前21天节目时,男方一家的表现也让不少人直呼何雯娜为什么会看上这样的男人?重庆市第37届马拉松接力赛报名开启3月4日鸣枪起跑近日,从九龙坡区体育局获悉,2023重庆市第37届马拉松接力赛暨飙讯杯九龙坡区智慧环湖跑将于3月4日上午在九龙坡区华岩寺鸣枪开跑。目前,赛事报名通道已开启,广大市民朋友可扫描文末二湖人真的会考虑签下约翰沃尔,作为替补后卫吗?NBA全明星大赛已经结束了,对于洛杉矶湖人队来说,新的挑战又来了,因为他们目前常规赛还剩下23场比赛,球队只取得了27胜32负的战绩,西部排名第13名,这很有可能导致他们无缘202大蒜咖啡,龟苓膏肠粉食物开始变鬼畜现在的食品从业者不是在做食品,是在做视频比起味道拍得好看说得好听才是好shipin而近些年出现的网红诡异食物油泼辣子新地螺蛳粉薯片臭豆腐雪糕榴莲拿铁从食品角度,很难理解但换成视频分水果汁要这样喝,清热补水还润肠车厘子与樱桃很多节日,现在大家都喜欢吃车厘子,因为它颜色好看,味道甜美,听说还不容易升血糖,能降尿酸。这么好的水果,好像除了贵,就没有别的毛病了。事实上,我们混淆了车厘子和樱桃。血人参归脾丸补中益气丸附子理中丸,脾虚怎么选?今天就和大家聊一聊人参归脾丸补中益气丸附子理中丸这三种补脾的中成药在我们生活中该如何去选用?第一人参归脾丸由人参白术茯苓甘草炙黄芪等组成。其中人参炙黄芪具有补元气健脾胃益气血的作用有一种苦叫凉茶很多人不太理解,或者很少有喝凉茶的习惯,或者为什么要喝凉茶的?因为在我们广东地区,广东处于岭南地区,南濒大海,属于亚热带气候,天气炎热,多雨潮湿,干燥自古以来是瘴疠之地。北宋太平圣积极管理三高,重享美好生活高血压高血压发病率高并发症多,是一种无法彻底根治的慢性非传染性疾病。高血压会引起多种心脑血管疾病,如脑卒中心肌梗死。高血压患者患脑卒中和心肌梗死的概率分别是常人的3。41倍和2。2春天为什么要多喝水?春天要多喝水吗?春天多喝水有什么好处?一个正常的成年人,身体的6070是水。水是我们身体中不可缺少的东西,春季天气比较干燥,又是传染病的高发季节,我们需要多喝水,及时排泄掉身体有毒不宜空腹看人看量来源生命时报胃不好血糖高易腹泻等人群,餐前最好不吃过酸过咸等刺激性食物中国农业大学食品科学与营养工程学院教授范志红近些年来,空腹不能吃频繁出现在某些食物的宣传提醒中。我简单调查统计
中兴Axon50Pro很能打1亿像素5500mAh,12512G仅3299中兴新一代旗舰定名为中兴Axon50系列,这个系列包括三款产品,也就是中兴Axon50Pro中兴Axon50S中兴Axon50Ultra。从配置实力上看,中兴Axon50Pro和中一组医药行业网站页面设计案例推荐互联网在医药产业中的应用是成功的,降低了其销售及推广费用,而且提供了许多原来所不能提供的新型服务,同时使昂贵的药品价格得以下降,复杂的服务流程得以简化。医药产业与互联网的交集已经越山东110月全省服务业发展趋稳央广网济南12月8日消息(记者刘博伦)12月8日,记者从山东省统计局了解到,今年以来,山东省进一步巩固拓展疫情防控和经济社会发展成果,服务业经济顶住压力逐步趋稳,全省规模以上服务业乌鲁木齐人民公园迎客共赏冬日美景12月5日,市民在乌鲁木齐人民公园赏冬日美景。(记者王丽丽摄)乌鲁木齐晚报全媒体讯(记者王丽丽)12月5日,乌鲁木齐人民公园开门迎客。市民走出家门,在湛蓝的天空下温暖的阳光中,在这BC省滑雪景区房价飙升!年涨幅高达45本周二,温哥华迎来了一场难得一见的大雪,朋友圈里也是有人欢喜有人忧。有人抱怨出行不便,有人则兴奋见到一个多雪的冬天今年的滑雪场不用担心缺雪了!在大家享受滑雪乐趣的时候,也许很少有人长得像梨子,宝石和红茶举世闻名,一起揭秘当地真实模样斯里兰卡是一个位于印度洋斯大渡东南外海的热带岛国,接近赤道,1972年之前称为锡兰。与印度隔海相望,两国之间的保克海峡和马纳尔湾最窄处仅只有30公里。两国在印度的东南海岸和斯里兰卡塔玛尔山城游览记文杨忠新今年夏季,我有幸来到西宁市湟中区共和镇苏尔吉村,游览了这个村的塔玛尔山城。塔玛尔山城是一座建在山岗上的古代戍边要塞,山城的东西两面为山谷,地势险要,易守难攻。城墙由黄土夯筑手绘丨中国茶那些事来源新华社中国传统制茶技艺及其相关习俗项目近日入选联合国教科文组织人类非物质文化遗产代表作名录。茶者,南方之嘉木也。中国是茶树的故乡,中国人饮茶已有千年历史。从神奇的东方树叶,到盏2022年乌镇戏剧节,依旧是一个乌托邦2022年乌镇戏剧节闭幕了。过去两个周末的乌镇看上去和往年一样,热闹拥挤,所有的人群都显得热烈而亢奋。每场演出照旧满座,举办青赛的蚌湾剧场门口永远都是排队等待入场的观众。延续自去年很好用但好贵!ANKER推出3合1磁吸魔方无线充,1000元开售苹果旗下iPhoneAPPLEWatchAirPods三大品类设备均全面配备无线充电功能,特别是iPhone121314系列更是支持MagSafe磁吸无线充电功能,摆脱线缆束缚提供数字化学习体系人人可创新,处处皆科技编者按伴随着大数据云计算人工智能技术的突飞猛进,数字化学习已经开始并将成为主流的学习形式。相对于传统的课堂式培训,数字化学习的底层逻辑发生了根本性的变化。在这样的背景下,我们要建立