为什么你学不会递归?谈谈我的经验_递归算法很难理解(2)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新HarmonyOS鸿蒙全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img

img
img
htt

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注鸿蒙)
img

正文

  • 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(n)O(n);
  • 空间复杂度:使用了常量级别变量,空间复杂度为 O(1)O(1)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)O(n);
  • 空间复杂度:使用了函数调用栈,空间复杂度为 O(n)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 跟程序员开的小玩笑。

参考资料

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
[外链图片转存中…(img-3I0tIbnQ-1713609205196)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值