Python - Recursion 学习笔记
- Recursion
- 基本概念
- Call Stack
- Numeric Examples
- Divide and Conquer
- 步骤
- 例题(List & String)
- 例题(Dictionary & Tuple)
- For Loop
Recursion
基本概念
Recursive function: A function that calls itself. (直接或者间接)
递归算法解决问题的特点:
(1)递归就是在过程或函数里调用自身
(2)在使用递归策略时,必须有一个明确的递归结束条件,即Base case
(3)递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。
例如斐波那契数列:
def fibonacci(n):
# Base Case
if n <= 1:
return 1
#Recursive Case
return (fibonacci(n-2) + fibonacci(n-1))
Call Stack
每一次function call都会生成一个新的call frame
例如:对于fibonacci()递归算法,其运算顺序如下
绘制Call Stack:
def skip(s):
"""Returns: copy of s
Odd (from end) skipped"""
result = ''
# 从前往后一个个看,奇数项跳过
if (len(s) % 2 = 1):
result = skip(s[1:])
#从前往后,偶数项return,递归计算后面字符
elif len(s) > 0:
result = s[0]+skip(s[1:])
return result
Numeric Examples
对于基本的数学计算,递归有一点类似于数学归纳法。已知 n=1 或 n=0(即Base Case)时的值,假设已知 n = k 时成立(即,n=k 时,方法return的是预期正确的值),可以用 n = k 的值(当Base Case有多种限制时,用自身方法表示:此时调用自身方法会进入Base Case)来表示n = k-1的值(从 k 到 k-1 运算步骤)
例题1: 从1加到n
def sum_to(n):
"""
Returns the sum of numbers 1 to n.
Example: sum_to(3) = 1+2+3 = 6,
Parameter n: the number of ints to sum
Precondition: n >= 1 is an int.
"""
if n == 1:
return 1
else:
return n + sum_to(n-1)
例题2: n的数字位数
def num_digits(n):
"""
Returns the number of the digits in the decimal representation of n.
Example: num_digits(0) = 1
Example: num_digits(1356) = 4
Parameter n: the number to analyze
Precondition: n >= 0 is an int
"""
if n//10 == 0:
return 1
else:
return 1 + num_digits(n//10)
例题3: n每位数字之和
def sum_digits(n):
"""
Returns the sum of the digits in the decimal representation of n.
Example: sum_digits(0) = 0
Example: sum_digits(345) = 12
Parameter n: the number to analyze
Precondition: n >= 0 is an int.
"""
if n //10 == 0:
return n
else:
return n%10 + sum_digits(n//10)
例题4: 返回n中数字2出现的次数
def number2(n):
"""
Returns the number of 2's in the decimal representation of n.
Example: number2(0) = 0
Example: number2(234252) = 3
Parameter n: the number to analyze
Precondition: n >= 0 is an int.
"""
if n == 0:
return 0
if n//10 == 0 and n%10 == 2:
return 1
if n//10 == 0 and n%10 != 2:
return 0
else:
return number2(n%10) + number2(n//10)
例题5:
def into(n, c):
"""
Returns the number of times that c divides n,
Example: into(5,3) = 0 because 3 does not divide 5.
Example: into(3*3*3*3*7,3) = 4.
Parameter n: the number to analyze
Precondition: n >= 1 is an int
Parameter c: the number to divide by
Precondition: c > 1 are ints.
"""
if n%c != 0:
return 0
else:
return 1 + into(n/c,c)
Divide and Conquer
步骤
- Decide what to do on “small” data
- Decide how to break up your data
- Decide how to combine your answers
例题(List & String)
- 返回数组中元素的和
def sum_list(thelist):
"""
Returns the sum of the integers in list l.
Example: sum_list([7,34,1,2,2]) is 46
Parameter thelist: the list to sum
Precondition: thelist is a list of ints
"""
if thelist == []:
return 0
if len(thelist) == 1:
return thelist[0]
left = thelist[0]
right = thelist[1:]
return left + sum_list(right)
- 返回v在list中出现的次数
def numberof(thelist, v):
"""
Returns the number of times v occurs in thelist.
Parameter thelist: The list to count from
Precondition: thelist is a list of ints
Parameter v: The value to count
Precondition: v is an int
"""
if len(thelist)==0:
return 0
if len(thelist)==1:
if thelist[0]==v:
return 1
else:
return 0
#也可以写成 return 1 if thelist[0] == v else 0
left = thelist[0]
right = thelist[1:]
return numberof(left) + numberof(right)
- 返回一个新的数组,这个新数组的元素a都被替代成b
def replace(thelist,a,b):
"""
Returns a COPY of thelist but with all occurrences of a replaced by b.
Example: replace([1,2,3,1], 1, 4) = [4,2,3,4].
Parameter thelist: The list to count from
Precondition: thelist is a list of ints
Parameter a: The value to replace
Precondition: a is an int
Parameter b: The value to replace with
Precondition: b is an int
"""
if len(thelist) == 0:
return []
if thelist[0] == a:
return [b]
else:
return[thelist[0]]
left = [thelist[0]]
right = thelist[1:]
return replace(left,a,b)+replace(right,a,b)
- 返回一个新的数组,其相邻重复的数字都被删除
def remove_dups(thelist):
"""
Returns a COPY of thelist with adjacent duplicates removed.
Example: for thelist = [1,2,2,3,3,3,4,5,1,1,1], return [1,2,3,4,5,1]
Parameter thelist: The list to modify
Precondition: thelist is a list of ints
"""
if len(thelist) <= 1:
return thelist
left = [thelist[0]]
right = thelist[1:]
if thelist[0] == thelist[1]:
return remove_dups(right)
else:
return left + remove_dups(right)
- 返回一个新的数组,删除第一次出现的元素v
def remove_first(thelist, v):
"""
Returns a COPY of thelist but with the FIRST occurrence of v removed
(if present).
Parameter thelist: the list to search
Precondition: thelist is a list of ints
Parameter v: the value to search for
Precondition: v is an int
"""
if len(thelist) == 0:
return []
if thelist[0] == v:
return thelist[1:]
else:
return = [thelist[0]] + remove_first(thelist[1:],v)
- 返回新的数组,其中奇数在前,偶数在后
def oddsevens(thelist):
"""
Returns a COPY of the list with odds at front, evens in the back.
Odd numbers are in the same order as thelist. Evens are reversed.
Example:
oddsevens([2,3,4,5,6]) returns [3,5,6,4,2].
Parameter thelist: The list to modify
Precondition: thelist is a list of ints (may be empty)
"""
if len(thelist) == 0:
return []
left = [thelist[0]]
right = thelist[1:]
if thelist[0] % 2 == 0:
return oddsevens(right) + [left]
else:
return [left] + oddsevens(right)
Remark:Reversing a String or List,在recursive call写成right + left即可
- 返回新的,被展开的数组
def flatten(thelist):
"""
Returns a COPY of thelist flattened to remove all nested lists
Examples:
flatten([[1,[2,3]],[[4],[5,[6,7]]]]) is [1,2,3,4,5,6,7]
flatten([1,2,3]) is [1,2,3]
Parameter thelist: the list to flatten
Precondition: thelist is a list of either ints or lists
"""
if len(thelist) == 0:
return []
left = thelist[0]
right = thelist[1:]
if type(left) == int:
return [left] + flatten(right)
else:
return flatten(left)+flatten(right)
- 判断是否是回文字符
def ispalindrome(s):
"""Returns: True if s is a palindrome"""
if len(s) <2:
return True
end = (s[0] == s[-1])
middle = ispalindrome(s[1:-1])
return end and middle
- 拆分(此题不再是分成left[0] + right[s:])
def split(s,delimiter):
"""Return: list of substrings of s separated by the delimiter.
This function breaks up s into several substrings according to
the delimiter (a single character separator like ',' or ':').
Example: split('ab,c „ d', ',') is ['ab', 'c ', '', ' d']
split('ab::cd :: ef', '::') is ['ab', 'cd ', ' ef']
split('ab::cd :: ef', ',') is ['ab::cd :: ef']
split('', ',') is ['']
Precondition: s is a string (possibly empty).
delimiter is a nonempty string"""
pos = s.find(delimiter)
if pos = -1:
return s
front = [s[:pos]]
back = s[pos+len(delimiter):]
return front + split(back,delimiter)
注意:上述9个例题均对list 或者 string 进行变换的递归算法。Python中List的优点:可以直接进行叠加(即,[2,3]+[4] >>> [2,3,4])因此我们可以先将Break up the data, 再通过连续不断call自己的方法来combine the answer。
但是,对于不可叠加的字典(dictionary) 我们需要做一些逻辑上的小改变。
例题(Dictionary & Tuple)
例题1: 返回字母在string s中出现的次数,并生成一个字典。
def histogram(s):
"""Return: a histogram (dictionary) of the # of letters in string s.
The letters in s are keys, and the count of each letter is the value. If
the letter is not in s, then there is NO KEY for it in the histogram.
Example: histogram('') returns {}, while histogram('abracadabra')
returns {'a':5,'b':2,'c':1,'d':1,'r':2}
Precondition: s is a string (possibly empty)."""
if len(s) == 0:
return {}
right = histogram(s[1:])
if s[0] in right:
right[s[0]] = right[s[0]] + 1
else:
right[s[0]] = 1
return right
本题的难点在于:字典不可叠加。我们不能像对list一样,将每一个元素单独提出,进行判断,并在最后进行整合相加。字典的添加必须基于一个已知的字典,并在此基础上添加key和value。
我们唯一已知的字典是当len(s) == 0
时的空字典(这里用到了python的一个List的特点,若s == ''
,则s[1:]
return ‘’ instead of Error)所以,为了先生成可以进行修改的dictionary,我们首先要将字符串 s 通过调用自己的方法,将call frame一直拆分到base case,即len(s) == 0的情况。
Call Frame
Call Frame (从上到下) | Return (从下往上) |
histogram(‘abca’) | right = {“a”: 1; “c”: 1; “b”: 1};进入判断句 ; {“a”: 2; “c”: 1; “b”: 1} |
histogram(‘bca’) | right = {“a”: 1; “c”: 1};进入判断句 ; {“a”: 1; “c”: 1; “b”: 1} |
histogram(‘ca’) | right = {“a”: 1};进入判断句 ; {“a”: 1; “c”: 1} |
histogram(‘a’) | right = {} ;进入判断句 ; {“a”:1} |
histogram(’’) | dict: {} (进入base case) |
总结:对于字典这类不可以相加的类型,resursive call需要写在前面。先生成一个base dictionary(一般为空字典),并利用字典可以添加的功能,一个一个往里面添加。
类似字典这种不能相加的类型,我们还可以想到元组(Tuple)。虽然元组本质上是可以相加的(类似数组),如(1,2)+(2,3) >>> (1,2,2,3)
,但是元组不能修改,这也在一定程度上限制了递归算法。
Tuple的语法是利用小括号()。若只想生成一位元组,例(1),我们需要写成(1,)
例题2:
def segregate(nlist):
"""Returns: A tuple segregating nlist into negative and non-negative.
This function returns a tuple (pos,rlist). The value rlist is a reordered copy
of nlist where negatives come before the non-negatives. However, ordering inside
each part (negative, non-negatives) should remain EXACTLY as it is in nlist.
The value pos indicates the first position of a non-negative number in rlist.
If there are no non-negative numbers, pos is -1.
Examples: segregate([1, -1, 2, -5, -3, 0]) returns (3, [-1, -5, -3, 1, 2, 0])
segregate([-1, -5, -3]) returns (-1, [-1, -5, -3])
Precondition: nlist is a (possibly empty) list of numbers"""
if nlist == []:
return (-1,[])
left = [nlist[0]]
right = segregate(nlist[1:])
pos = right[0]
rlist = right[1]
if left[0]>=0:
if pos < 0:
pos += 1
return (pos+1,left + rlist)
else:
return (pos,left+rlist)
正如我们在前面提到的,tuple是不可以修改的。可以想到我们要先将nlist一直拆到base case,再一步步return回上一级。所以本题的逻辑是:每一次call frame都return一个新的tuple,而此tuple(第n-1个call frame)的第0和1位的数字都是通过上一个(第n个call frame)return的tuple的0、1位提取、计算得到的。
例如我们call segregate([1, 0, -2, 3])
Call Frame (从上到下) | left;right | Return (从下到上) |
segregate([1,0,-2,3]) | [1]; (2, [0, -2, 3]) | (3, [1, 0, -2, 3]) |
segregate([0,-2,3]) | [0]; (1, [-2, 3]) | (2, [0, -2, 3]) |
segregate([-2,3]) | [-2]; (1, [3]) | (1, [-2, 3]) |
segregate([3]) | [3];(-1,[]) | (1, [3]) |
segregate([]) | (-1, []) |
For Loop
循环(For Loop & While Loop)和递归都是重复运算。二者可以相互替代,但在一定情况下,其中一种算法可能会更简单,更有效率。
For Loop 逻辑比较简单,这里不进行赘述。