分段做事+返回值:双枪破递归recursion 2019-11-20(未经允许禁止转载)

1.啥是递归

函数自己调用自己是递归

def func(x):
    # 自己调用自己
    func(x-1)

在上面的代码中,函数func在自己的函数体中调用了自己,就是递归

2.递归怎么写

递归双枪:分段做事 + 返回值

以def recursion(n)为例:

  • A. 首先必须明确,递归函数需要返回什么值

  • B. 再者明确函数recursion(n) 要做什么事——找分段函数!:

    寻找有出口的 + 可递推的关于recursion(n)取值的分段函数。注意,必须是有出口的 + 可递推的分段,如图所示:
    递归的核心-分段函数

写成代码形式就是一个 if else 语句:

# 出口段
if n meet some situation:
    recursion(n) = 现成值  # 递归出口 ----> 出口一定是现成值
    # 当一个值是现成的,当然是直接拿啊,还递归干啥

# 递归段
else:
    recursion(n) = recursion(n-1)  # 没到出口,递归继续 ----> 可递推才能递归

下面实战

例1:

写一个函数,输入正整数n,求出斐波那契数列第n项的值

思路
def fib(n):

  • 1.明确函数返回值。这里很简单,fib(n)返回斐波那契数列第n项的值
  • 2.分段做事

关于fib(n)取值的分段函数的结构:

if n == 1 or n == 2:
    fib(n) = 1
else:
    fib(n) = fib(n-1) + fib(n-2)

那么,基于上面的分段函数和返回值,这个求fib(n)函数的递归写法很容易就可以写出来了:

# 1.首先明确fib(n)返回值是斐波那契数列第n项
def fib(n):
    # 2.递归出口是n为1或者2
    if n == 1 or n == 2:
        return 1
    # 如果不是递归出口
    else:
        # 3.借助问题递推式,继续递归缩小问题规模,直到递归出口
        return fib(n-1) + fib(n-2)

利用中间过程持久化来优化递归

在上面斐波那契数列的递归代码中, return fib(n-1) + fib(n-2) 这行进行了2次自我调用: fib(n-1) 一次,fib(n-2) 一次。实际上,在这两次自我调用的过程中,产生了大量的重复计算,这点通过画递归树可以很好理解。如果我们把计算过的值存起来,下次再用的时候就可以直接取了,这是常见的优化思路,即中间过程持久化

这里可以通过共享的字典来记录中间值

result_dict = {
    1: 1,
    2: 1,
}
# 1.首先明确fib(n)返回值是斐波那契数列第n项
def fib(n):
    global result_dict
    # 2.递归出口是n在字典内,可以直接取值,不用通过fib(n-1) + fib(n-2)递归计算了
    if n in result_dict:
        return result_dict[n]
    # 如果不是递归出口,即n不在字典内
    else:
        # 3.借助问题递推式,递归求出fib(n),并加入字典
        result_dict[n] = fib(n-1) + fib(n-2)
        return result_dict[n]

例2(leetcode687):

给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点

注意:两个节点之间的路径长度由它们之间的边数表示

示例 1:
输入:

          5
         / \
        4   5
       / \   \
      1   1   5

输出:
2
因为5-5-5是最长路径,共有2条边,长度为2

思路

一棵树中,这样所谓的最长路径,肯定经过某棵子树的根节点
因此,马上有一个等式:每一条符合要求的路径的长度 = 某一节点的最长左支 + 某一节点的最长右支,但这个等量关系并不是递推式,因为等号左右并非同一形式,因此也不能作为我们目的分段函数的其中一段

怎么办?不方,仔细看其实可以发现,
节点node的最长左支 = 【1 + max(node.left的最长左支, node.left的最长右支)】or 【0】,仅当node.val != node.left.val时取0。这显然是一个有出口的、可递推的分段函数
同理,
节点node的最长右支 = 【1 + max(node.right的最长左支, node.right的最长右支)】or 【0】,仅当node.val != node.right.val时取0。同样是一个有出口的、可递推的分段函数

找到了有出口的、可递推的分段函数,就找到了递归的突破口

于是,定义一个函数,接收一个结点,返回以这个结点为根的左右支中最长一支的长度,即返回max(node的最长左支, node的最长右支)

在返回最长一支的长度之前,肯定计算出了node的最长左支和node的最长右支,这两项加起来,就是经过node的最长路径。不断递归,也就能不断计算出整棵树中的每一个结点为根时的最长路径,取其中最大的即为所求

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def __init__(self):
        self.max_val = 0

    def longestUnivaluePath(self, root: TreeNode) -> int:
        self.getLongestOneSide(root)
        return self.max_val

    # 返回结点root左右支最长一支的长度,并同时在函数体中操作self.max_val
    def getLongestOneSide(self, root) -> int:
        if not root:
            return       
        
        # 左支
        left = 0
        if root.left:
            # 如果根结点和左孩子结点的值相同,通过递归更新left
            if root.left.val == root.val:
                left = 1 + self.getLongestOneSide(root.left)
            # 如果根结点和左孩子结点的值不同,不改变left,同时递归获取root.left的左右支的最长支
            else:
                self.getLongestOneSide(root.left)
        
        # 右支
        right = 0
        if root.right:
            if root.right.val == root.val:
                right = 1 + self.getLongestOneSide(root.right)
            else:
                self.getLongestOneSide(root.right)

        # 将root结点的左右支最长和与self.max_val比较,取其大
        self.max_val = max(self.max_val, left+right)
        # 返回root结点最长的一侧
        return max(left, right)

可以看到,在这题中,不断递归的函数getLongestOneSide的返回值并不直接就是需要我们求的最长路径长度,而是返回左右支中最长的一支的长度。需要我们求的最长路径长度,在函数返回之前,就进行了比较和结算。这提示我们,递归不必一定要在返回所求;目标所求也可以在函数体的某一部分进行结算,即“第一枪就击中目标,不必一定得第二枪击中”

例3

给定一个链表和正整数k,从链表头开始k个为一组,进行组内链表翻转。若最后一组不足k个结点,则该组无需翻转

思路
根据双枪
定义一个函数reverseKGroup,接收参数head,翻转从head开始的第一组然后对剩下的链表进行递归
1.返回翻转后的链表头
2.找分段函数。显然,当一组不足k个结点时,无需翻转,直接返回现成的该组的第一个结点;否则,只要一组有k个结点,就要递归下去

于是代码如下:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

# 这就是组内翻转啊。。。,k个为一组,一组要翻转k - 1次。
# 定义函数reverseKGroup,接收参数head, 翻转从head开始的第一组,完成后返回翻转后的链表头
class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        check = head
        count = 0
        # 将check指向一组的末尾
        while check and count < k-1:
            count += 1
            check = check.next

        now_head = head
        # 分段函数。check是否为None是分段依据
        if check:
            # 组内翻转k-1次
            for i in range(k-1):
                # 这里开始三步登基噢
                new_head = head.next
                head.next = new_head.next
                new_head.next = now_head
                now_head = new_head
            head.next = self.reverseKGroup(head.next, k)
        # 出口
        else:
            pass
        return now_head

leetcode#### 50. Pow(x, n)
编写一个函数,返回x的n次方,n可以是负数

思路:
暴力法,x连乘n次,时间复杂度O(n)
二分法,【x的n次方 = x的n//2次方 * x的n//2次方 or x的n//2次方 * x的n//2次方 * x】 。注意,这个等式的等号两边是同型的,这就是递推式。有了递推式,出口是啥?当n==0时,返回1

class Solution:
    def myPow(self, x: float, n: int) -> float:
        # 二分

        # 递归出口
        if n == 0:
            return 1.0
        
        negtive = n < 0
        n = abs(n)
        temp = self.myPow(x, n//2)
        if n % 2 == 0:
            return temp*temp if not negtive else 1/(temp*temp)
        else:
            return temp*temp*x if not negtive else 1/(temp*temp*x)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351