一看就会,一写就废?详解递归

欢迎关注 代码宇宙,每天9点半,不见不散

目录

    • 前言
    • 什么是递归
    • 例题
    • 递归解法
    • 代码
    • 复杂度分析
            • 时间复杂度O(m+n)。
            • 空间复杂度:O(m+n)。
    • 相关题目

前言

递归解法总是给人一种“只可意会不可言传”的感觉,代码一看就懂,自己动手一写就呆住了,很难受。究其原因,一是我们练习不够,二是理解不够。

什么是递归

递归的例子在平时生活中很容易见到,比如:

一看就会,一写就废?详解递归_第1张图片
开个玩笑
什么是递归呢?函数在运行时调用自己,这个函数就叫递归函数,调用的过程叫做递归。

比如定义函数 f(x)=x+f(x-1)f(x)=x+f(x−1):

def f(x):
    return x + f(x-1)

如果代入 f(2)f(2):
返回 2+f(1)2+f(1);
调用 f(1)f(1);
返回 1+f(0)1+f(0);
调用 f(0)f(0);
返回 0+f(-1)0+f(−1)
……

这时程序会无休止地运行下去,直到崩溃。

如果我们加一个判断语句 x > 0:

def f(x):
    if x > 0:
        return x + f(x-1)
    else:  # f(0) = 0
        return 0

这次计算
f(2)=2+f(1)=2+1+f(0)=2+1+0=3f(2)=2+f(1)=2+1+f(0)=2+1+0=3

我们从中总结两个规律:

递归函数必须要有终止条件,否则会出错;

递归函数先不断调用自身,直到遇到终止条件后进行回溯,最终返回答案。

例题

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

递归解法

我们可以如下递归地定义在两个链表里的 merge 操作(忽略边界情况,比如空链表等):

也就是说,两个链表头部较小的一个与剩下元素的 merge 操作结果合并。
根据以上规律考虑本题目:

终止条件:当两个链表都为空时,表示我们对链表已合并完成。

如何递归:我们判断 l1 和 l2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果。(调用递归)

一看就会,一写就废?详解递归_第2张图片

代码

Python:


class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        if not l1: return l2  # 终止条件,直到两个链表都空
        if not l2: return l1
        if l1.val <= l2.val:  # 递归调用
            l1.next = self.mergeTwoLists(l1.next,l2)
            return l1
        else:
            l2.next = self.mergeTwoLists(l1,l2.next)
            return l2

Java:


class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        }
        else if (l2 == null) {
            return l1;
        }
        else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        }
        else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }

    }
}

C++:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == NULL) {
            return l2;
        }
        if (l2 == NULL) {
            return l1;
        }
        if (l1->val <= l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        }
        l2->next = mergeTwoLists(l1, l2->next);
        return l2;
    }
};

复杂度分析

如何计算递归的时间复杂度和空间复杂度呢?其中时间复杂度可以这样计算:

给出一个递归算法,其时间复杂度 O(T),通常是递归调用的数量记作 R 和计算的时间复杂度的乘积(表示为O(S))的乘积:O(T) = R * O(s)

时间复杂度O(m+n)。

m 和 n 为 l1 和 l2 的元素个数。递归函数每次去掉一个元素,直到两个链表都为空,因此需要调用 R=O(m + n)R=O(m+n) 次。而在递归函数中我们只进行了 next 指针的赋值操作,复杂度为 O(1),故递归的总时间复杂度为 O(T) = R * O(1) = O(m+n)

空间复杂度:O(m+n)。

对于递归调用
self.mergeTwoLists()
当它遇到终止条件准备回溯时,已经递归调用了 m+nm+n 次,使用了 m+nm+n 个栈帧,故最后的空间复杂度为O(m+n)。

相关题目

以下是一些基础但很经典的题目,值得我们好好练习:
反转字符串(https://leetcode-cn.com/problems/reverse-string/)

汉诺塔问题(https://leetcode-cn.com/problems/hanota-lcci/solution/)

两两交换链表中的节点(https://leetcode-cn.com/problems/swap-nodes-in-pairs/)

二叉树的最大深度(https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)

如有问题,欢迎讨论~

文章推荐(公众号:代码宇宙,阅读)

一看就会:最大自序和状态压缩算法
坚持做一件事,究竟难在哪里?
有趣的多线程和无趣的线程锁
做技术,如何使自己在重复性业务中持续提升?
Openresty 配合 redis 实现无感知灰度发布系统(基础篇)
「纯手打」2万字长文从0开始Spring Boot(上)

一看就会,一写就废?详解递归_第3张图片

你可能感兴趣的:(算法)