Python数据结构与算法_11_递归

递归是解决问题的一种方法,它将问题不断地分成更小的子问题,直到子问题可以用普通的方法解决。通常情况下,递归会使用一个不停调用自己的函数来进行。

引例

现存在一个数字列表(numlist),计算数字列表各数字之和。

第一种方法,通过循环实现:

def listsum(numlist):
    nSum = 0
    for i in numlist:
        nSum = nSum + i

	return nSum

第二种方法,通过递归实现: 递归的逻辑不是循环,而是将问题分解成更小、更容易解决的子问题。

def listsum(numlist):
    if len(numlist) == 1:
        return numlist[0]
    else:
        return numlist[0] + listsum(numlist[1:])
  • 其中,数字列表 numList 的各元素总和等于列表中的第一个元素( numList[0] )加上其余元素之和,而其余元素则是 numList[1:] 列表的各元素。可以说,numList[1:] 是将第一个元素剔除后得到的新列表。
  • 代码中第 2 行检查列表是否只包含一个元素。这个检查非常重要,同时也是该函数的退出语句。对于长度为 1 的列表,其元素之和就是列表中的数。这同时也是算法停止递归的条件,停止条件通常是小到足以直接解决的问题。
  • 代码中第 5 行 listsum 函数通过调用自己来进行下一步操作,所以将 listsum 函数称为递归函数,每一次递归都改变了问题的状态并向停止条件靠近。

总结递归:一系列递归调用其实就是一系列的简化操作。每一次递归调用都是在解决一个更小的问题,如此进行下去,直到问题本身不能再简化为止。所以当问题无法再简化时,我们开始拼接所有子问题的答案,以此解决最初的问题。


实例

给定一个整数,将整数转换成任意进制(2~16进制)的字符串。 例如,将十进制整数10转换成十进制字符串"10" ,或者转换成二进制字符串"1010" 。

(1)首先考虑问题的最简情况:即数字小于10的情况。假设进制为八,一个小于十的数字是7,那么在八进制下十进制的7转换后为字符“7”。用代码表示则为:

convertString = "0123456789ABCDEF" #一个最大能表示十六进制的字符映射表
return convertString[7]

(2)若一个小于十的数字是9,那么在八进制下十进制的9转换后为字符串“11”。用代码表示为:

convertString = "0123456789ABCDEF"
return convertString[9//8] + convertString[9%8]

以上述两个分析步骤,可以总结出代码:

def toStr(n, base):
    convertString = "0123456789ABCDEF"
    if n < base:
        return convertString[n]
    else:
        return toStr(n//base, base) + convertString[n%base]

其中 base 表示转换后的进制,n 表示为初始整数。当 整数n 小于进制时,参考情况(1)直接得到转换后的字符。

当 整数n 大于进制时,参考情况(2),将整数拆分为 单数位 后分别转换成对应字符再拼接。其中大于 个位数 的部分,如 十位、百位数 则利用了递归函数进行进制转换。就是将拆分成单个数字的十位、百位数进行单独进制转换,再把每个数位转换后得到的字符串再拼接起来。

在这个过程中,n 所代表的就是问题的最简单情况,这种情况下问题本身不能再进行简化。

所以整个算法包含三个组成部分:

  1. 将原来的整数分成一系列仅有单数位的数;
  2. 将单数位的数字转换成字符(串);
  3. 连接每一步得到的字符(串),从而得出整个字符串。

递归的实质——栈

引例

上个实例中,已经得到的代码如下:

def toStr(n, base):
    convertString = "0123456789ABCDEF"
    if n < base:
        return convertString[n]
    else:
        return toStr(n//base, base) + convertString[n%base]

假设不拼接递归调用 toStr 的结果和 convertString 的查找结果,而是在进行递归之前把字符压入 中,那么所有递归调用结束后,只需执行出栈操作和拼接操作,最终得到字符串便是转换后的结果。

rStack = Stack()
def toStr(n, base):
    convertString = "0123456789ABCDEF"
    if n < base:
        rStack.push(convertString[n])
    else:
        rStack.push(convertString[n%base]) #余数入栈
        toStr(n//base, base)
class Stack:
    # 定义一个列表/构造一个栈
    def __init__(self):
        self.items = []
        print("你创造了一个栈!")

    def isEmpty(self):
        return self.items == []

    def push(self, item):
        self.items.append(item)
        print("你给栈顶加了个%s" % item)

    def pop(self):
        return self.items.pop()


rStack = Stack()


def toStr(n, base):
    convertString = "0123456789ABCDEF"
    if n < base:
        rStack.push(convertString[n])
    else:
        rStack.push(convertString[n % base])  # 余数入栈
        toStr(n // base, base)


toStr(19, 2)

while rStack.isEmpty() == False:
    print(rStack.pop(), end=' ')

程序运行结果如下:
Python数据结构与算法_11_递归_第1张图片
上面这个过程,我们使用栈替换了最初的拼接操作,目的是便于理解递归的整个过程。我们发现,将整个程序运行过程想象成一个栈,则每次递归的过程都像是“入栈”,递归到无可递归,则是到达了“栈顶”。然后返回每一次递归的结果,这个过程则像“出栈”。

有了这种思想,再来看最开始的引例:

def listsum(numlist):
    if len(numlist) == 1:
        return numlist[0]
    else:
        return numlist[0] + listsum(numlist[1:])

程序的目的是计算数字列表中各元素之和。

假设给定列表为[1, 2, 3],则程序分析过程如下:

numlist = [1, 2, 3]
第一次递归发生于:return 1 + listsum([2, 3]) 
	假定 递归=入栈 则此步意味着执行listsum([2, 3])并“入栈”

numlist = [2, 3]
第二次递归发生于:return 2 + listsum([3])
	listsum([3])执行并入栈

listnum([3])的执行结果为3。

出栈:
	第一次出栈:"return 2+3"
	第二次出栈:"return 1+(return 2+3)"

则总过程结束后得到6

函数调用栈、栈帧

事实上在所有程序语言进行的编程中,当一个函数被调用时,计算机使用的正是“栈”来存储函数的所有数据,称为 “函数调用栈”。在程序运行过程中, 不管是函数执行还是函数调用, 这个栈都非常关键, 它的主要作用:

  • 保存函数的局部变量
  • 向被调用函数传递参数
  • 返回函数的返回值
  • 保存函数的返回地址(返回地址是指函数调用结束后,程序应该继续执行的指令地址)

每个函数在执行过程中都需要使用一块栈内存用来保存上述这些值,我们称这块“栈内存”为函数的“栈帧(stack frame)”。

当发生函数调用时,因为调用者还没有执行完成,其栈内存中保存的数据还有用,所以被调用函数不能覆盖调用者的栈帧,只能把被调用函数的栈帧“push”到栈上,等被调用函数执行完成后再将其栈帧从栈上 “pop” 出去。这样,函数调用栈的大小会随着函数调用的层级增加而生长,也会随着函数的返回而缩小。

在函数递归调用时,首次函数因为进行了递归调用自己,所以算处在一种未执行完成的状态,故而后面一次次递归调用所产生的“栈桢”会一次次“push”到函数调用栈上。栈桢限定了函数所用变量的作用域,尽管反复调用相同的函数,但是每一次调用都会为函数的局部变量创建新的作用域。

总结栈帧:

  • 栈帧是一块因函数运行而临时开辟的空间
  • 每调用一次函数便会创建一个独立栈帧
  • 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值
  • 当函数运行完毕栈帧将会销毁

知道了递归的实质之后,再来分析最开始的程序:

def toStr(n, base):
    convertString = "0123456789ABCDEF"
    if n < base:
        return convertString[n]
    else:
        return toStr(n//base, base) + convertString[n%base]

以 toStr(10, 2) 为例:

toStr(10, 2)
	n=10
	base=2
	return toStr(10//2,2)+convertString[10%2]

toStr(5, 2)
	n=5
	base=2
	return toStr(5//2,2)+convertString[5%2]

toStr(2, 2)
	n=2
	base=2
	return toStr(2//2,2)+convertString[2%2]

调用 toStr(2//2, 2) 将返回值“1”放在栈的顶端。之后,这个返回值被用来替换对应的函数调用 toStr(2//2, 2) 并生成表达式“1 + convertString[2%2]”。这一表达式会作为 toStr(5//2, 2) 的返回值,并将字符串“10”留在栈顶。后面的过程则以此类推。所以我们是利用了函数调用栈来取代了引例中显式使用的栈,还可以认为利用栈中的返回值特性取代了累加过程。

你可能感兴趣的:(数据结构与算法,python,算法,数据结构)