数据结构与算法碎碎念之运用递归处理问题

运用递归处理问题

参考自《300分钟搞定数据结构与算法》 [中]苏勇 著

递归的基本性质:函数调用其本身
  • 递归是把大规模的问题不断变小,再进行推导的过程
  • 特点:可以使一个看似复杂的问题变得简洁和易于理解
运用递归算法的例子

一个递归算法的经典案例——汉诺塔问题

汉诺塔(又称:河内塔)是根据一个传说形成的数学问题:
有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:
1.每次只能移动一个圆盘;
2.大盘不能叠在小盘上面。
提示:可将圆盘临时置于 B 杆,也可将从 A 杆移出的圆盘重新移回 A 杆,但都必须遵循上述两条规则。
问:如何移?最少要移动多少次?
——维基百科

这一经典问题的解法,其基本思想就是递归
假设有 A、B、C 三个塔,A 塔有N盘,目标是把这些盘全部移到 C 塔。那么先把 A 塔顶部的 N-1块盘移动到 B 塔,再把 A 塔剩下的大盘移到 C,最后把 B 塔的N-1块盘移到 C。
如此递归地使用下去, 就可以求解。

Python实现

# 将A塔的n个圆盘移动到C塔
def tower_of_hanoi(A, B, C, n):
	# 判断是否还有盘可移动
	if n > 0:
		# 先把 A 塔顶部的 N-1块盘移动到 B 塔
		tower_of_hanoi(A, ,C, B, n-1)
		# 再把 A 塔剩下的大盘移到 C
		move(A, C)
		# 最后把 B 塔的N-1块盘移到 C
		tower_of_hanoi(B, A, C, n-1)

另一个递归算法的案例——展开Python的嵌套列表

将如下嵌套列表展开并保存为一个列表
[4, 5, [6, 7], [[8, 9], [10, 11], 12]]
展开结果为:[4, 5, 6, 7, 8, 9, 10, 11, 12]

递归思想解决思路
我们可以对给定列表中的元素逐个进行考虑,其结果有两种可能:

  1. 如果该元素不是列表,则直接加到结果列表中
  2. 如果该元素是一个列表,则还需对其进行展开方可添加到结果列表中

如此递归地对元素进行展开并添加至结果列表中, 就可以求解。

Python实现

def spread_list(spreadlist):
	# 结果列表
	res = []
	# 遍历列表中每一个元素
	for meb in spreadlist:
		# 判断元素类型
		if not isinstance(meb, list):
			# 如果该元素不是列表,则直接加到结果列表中
			res.append(meb)
		else:
			# 如果该元素是一个列表,则还需递归展开方可添加到结果列表中
			res.extend(spread_list(meb))
	# 返回结果
	return res
算法的基本思想

通过上面汉诺塔以及展开嵌套列表两个例子,我们总结递归算法的基本思想如下

  • 递归算法是一种自顶向下分析并解决问题的方法
  • 其通常把规模大的问题转化为规模小的相似的子问题,结合子问题的解来得出结果。
  • 在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。

另外我们在解决问题时还需特别注意的一点是:解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。

算法的一般格式

综上,我们总结出编写递归算法的一般格式如下

def recursion_arithmetic(参数):
	# 判断当前情况是否非法,若非法则立即返回
	if  illegality:
		return

	# 判断是否满足递归结束的条件,若满足则做相应求解等操作
	if satisfy condition:
		do something
	
	# 递归调用自身,缩小问题规模并返回子问题的解
	result1 = recursion_arithmetic(参数1)
	result2 = recursion_arithmetic(参数2)

	# 整合结果,对子问题的解以及当前数据进行分析,得出最终答案
	return combine(result1,result2)
Leetcode题目解析

接下来我们通过对Leetcode上面的题目分析来进一步认识利用递归算法求解问题

91.解码方法(难度:中等)
一条包含字母 A-Z 的消息通过以下方式进行了编码:

'A' -> 1
'B' -> 2
...
'Z' -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1

输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。

示例 2

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

递归思想解决思路
要运用递归来解决问题,首先我们应该思考对于某个问题,我们能否能将问题的规模变小呢,并且对我们要解决的问题产生帮助呢?
对于示例 2来说,给定编码为226,若不看最后一个字符6,对于22来说,若我们能求出对于22的解码有n种可能,那在这n种可能的基础上,加一个字符6在待解码的字符串22后面,只是在解码所得串后面加上字符F而已,解码方法依旧只有n种。
继续分析,对于6来说,若其前面一个字符如果是1或者2的话,那么它就可以构成16、26,所以我们还需再往前看一个字符,对于示例 2来说,6前面的字符是2,构成了26,若这个时候我们能求出26其前面的字符串解码方式有k种,那在这k种可能的基础上加上字符26,相当于在解码所得串后面加上字符Z而已,解码方法依旧只有k种。
所以总的解码个数就是n+k。对于示例 2的解码方式按此方法分析为2+1=3种。

Python实现

class Solution:
    def numDecodings(self, s: str) -> int:
    	# 递归终止条件:若字符数少于1,则只有一种解码情况
        if len(s) <= 1:
            return 1

		# 统计解码方法
        count = 0

		# 取出当前元素及当前元素的前一个元素
        curr = s[-1]
        prev = s[-2]

		# 若当前元素不为0,解码方法与前面n-1个元素解码方法相同
        if not curr == '0':
            count = self.numDecodings(s[:-1])

		# 若当前元素与其前一个元素构成的数字在10~26以内,则解码方法还需加上前面n-2个元素的解码方法
        if prev == '1' or (prev == '2' and curr <= '6'):
            count += self.numDecodings(s[:-2])
          
        # 返回总的解码方法
        return count

该解法只为演示运递归思想解决问题,事实上针对该问题有时间复杂度为O(1)的最优解法

时间复杂度分析
  1. 迭代法
  2. 公式法

迭代分析法
迭代法是一种较为直观的分析递归算法时间复杂度的方法,下面通过对汉诺塔的例子时间复杂度的分析来进行说明。
问题的规模大小为n,假设递归函数的运行时间为T(n),对函数内部代码的执行情况进行分析,第一条执行语句为if,其对n的大小进行了一次判断,需占用1个单位的执行时间,接下来两次调用了递归函数,每一次调用问题的规模都比当前问题的规模减少了1,因此这里有2T(n-1)的执行时间,并假设nove操作需占用1个单位执行时间,因此我们可以得出以下表达式(if以及move执行规模为O(1)):

T(n) = 2 * T(n - 1) + O(1)

对上式进行分析,当没有盘子的时候,我们仅需进行一次if判断,所以T(0) = 1,对上式进行展开如下

T(n) = 2 * T(n - 1) + O(1)
T(n) = 2 (2 * T(n - 2) + 1) + 1 = 2^2 * T(n - 2) + (2 + 1)
T(n) = 2 (2 (2 * T(n - 3) + 1) + 1) + 1 = 2^3 * T(n - 3) + (4 + 2 + 1)

T(n) = 2^k *T(n - k) + (2^k - 1)

当n = k时,上式可化为T(n) = 2 * 2^n - 1 => O(n) = 2^n

公式法
计算递归函数复杂度最方便的工具
当递归函数的时间执行函数满足如下关系式时,可利用公式法求解:

T(n) = a * T(n/b) + f(n)
f(n)是指每次递归完毕后,额外的计算执行时间

当参数a,b都确定时,只看递归部分时间复杂度就是O(n^logb(a))

运用公式法求解递归函数的执行时间需分以下三种情况

  1. 当递归部分的执行时间O(n^logba) > f(n) 时,最终的时间复杂度就是O(n^logba)
  2. 当递归部分的执行时间O(n^logba) < f(n) 时,最终的时间复杂度就是f(n)
  3. 当递归部分的执行时间O(n^logba) = f(n) 时,最终的时间复杂度是O(n^logb(a))log(n)

对于情况1、2的解释为:时间复杂度取最耗时部分
一个利用公式法分析时间复杂度的例子
假设有时间执行函数:T(n) = 2 * T(n/4) + 1,分析如下:
a = 2, b = 4, f(n) = 1
代入公式,得到nlog4(2) = n^(1/2)
当n > 1时,n^(1/2) > 1,则时间复杂度就是:O(n^(1/2))

递归思想是一种重要的算法思想,很多算法都有其影子,如:二叉树遍历、归并排序、快速排序等,需重点掌握
欢迎关注我的博客:博客主页

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值