递归改写成非递归的两种套路 Python实现

树的遍历 所有遍历方式,这一篇就够了
生成随机二叉树并彩色打印

喜欢的话,记得点赞和收藏哟!

1 模拟系统调用栈

编译器使用堆栈传递函数参数、保存返回地址等、这里我们把子问题的参数都压入栈中,通过顺序判断参数对应的过程是否执行来模拟返回地址,即下步应该做什么。

例子1:汉诺塔问题(属于中序遍历)
汉诺塔递归解
def hanoi(n, a, b, c):
    """
    常规的汉诺塔问题,可以直接从a到c
    :param n: 数量
    :param a: 起始位置
    :param b: 辅助位置
    :param c: 目标位置
    :return: 总的移动次数
    """
    num = 0
    if n == 0: return 0
    part1 = hanoi_original(n-1, a, c, b)
    print("move %s from %s to %s" % (n, a, c))
    part2 = hanoi_original(n-1, b, a, c)
    num = part1 + 1 + part2
    return num
汉诺塔非递归解
def hanoiStack(n, a, b, c):
    stack = []
    while n > 0 or len(stack)>0:
        while n > 0:
            # 问题(n,a,b,c)可以转为3个子问题,1汉诺塔(n-1,a,c,b),2移动(打印)(n,a,c)和3汉诺塔(n-1,b,a,c)
            # 压入子问题1的三个子问题,子问题1的解决转化为三个新的子问题,相当于子问题1已经解决了,所以用True来表示,False表示子问题未解决。
            params = [[True, n-1, a, c, b], [False, n, a, c], [False, n-1, b, a, c]]
            stack.append(params)
            n -= 1
            a, b, c = a, c, b
        finish = stack.pop()
        # 当
        if not finish[1][0]:
            print("move %s from %s to %s" % (finish[1][1], finish[1][2], finish[1][3]))
            finish[1][0] = True
        if not finish[2][0]:
            n = finish[2][1]
            finish[2][0] = True
            if n > 0:
                stack.append(finish)
                a, b, c = finish[2][2:]
优化1 压栈的时候两个汉诺塔子问题的参数分别压入,打印移动过程的子问题不入栈
def hanoiStack(n, a, b, c):
    stack = []
    while n > 0 or len(stack) > 0:
        while n > 0:
            paramsLeft = [True, n-1, a, c, b]
            paramsRight = [False, n-1, b, a, c]
            stack.append(paramsRight)
            stack.append(paramsLeft)
            n -= 1
            c, b = b, c
        finishLeft = stack.pop()
        finishRight = stack.pop()
        if not finishRight[0]:
        	# 如果子问题3是False,先解决子问题2,再把子问题3转为它自己的子问题,False改为True.
            print("move %s from %s to %s" % (finishRight[1]+1, finishRight[3], finishRight[4]))
            finishRight[0] = True
            n, b, a, c = finishRight[1:]
            a, b, c = b, a, c
            if n > 0:
                stack.append(finishRight)
                stack.append(finishLeft)
优化2 可以发现paramsLeft参数就是打酱油的,去掉
def hanoiOriginalStack3(n, a, b, c):
    stack = []
    while n > 0 or len(stack) > 0:
        while n > 0:
            # paramsLeft = [True, n-1, a, c, b]
            paramsRight = [False, n-1, b, a, c]
            stack.append(paramsRight)
            # stack.append(paramsLeft)
            n -= 1
            c, b = b, c
        # finishLeft = stack.pop()
        finishRight = stack.pop()
        if not finishRight[0]:
            print("move %s from %s to %s" % (finishRight[1]+1, finishRight[3], finishRight[4]))
            finishRight[0] = True
            n, b, a, c = finishRight[1:]
            a, b, c = b, a, c
            if n > 0:
                stack.append(finishRight)
                # stack.append(finishLeft)
例子2 归并排序(后序遍历)
合并函数(L到mid有序,mid+1到R有序,排序L到R)
def mergeList(lyst, L, mid, R):
    help = []
    left, right = L, mid+1
    for i in range(L, R+1):
        if right > R or left <= mid and lyst[left] <= lyst[right]:
            help.append(lyst[left])
            left += 1
        else:
            help.append(lyst[right])
            right += 1
    for i in help:
        lyst[L] = i
        L += 1
归并排序递归解(使用了嵌套函数,外层函数只要传入要排序的列表就好了)
def mergeSort(lyst):
    def process(lyst, L, R):
        if L >= R:
            return
        mid = L + ((R - L) >> 1)
        process(lyst, L, mid)
        process(lyst, mid + 1, R)
        mergeList(lyst, L, mid, R)
    L, R = 0, len(lyst)-1
    process(lyst, L, R)
    return lyst
归并排序非递归解(这个例子中没有像汉诺塔那个例子里用True或False来表示每个子问题是否解决,而是通过把解决的子问题参数置为1的方式,两种方式本质一样,这种从模拟系统栈的角度更好理解)
def mergeSortStack(lyst):
    L, R = 0, len(lyst)-1
    stack = []
    while L < R or len(stack) > 0:
        while L < R:
            mid = L + ((R - L) >> 1)
            pushargs = [(L, mid), (mid+1, R), (L, mid, R)]
            stack.append(pushargs)
            R = mid
        last = stack[-1]
        if last[1] != 1:
            L = last[1][0]
            R = last[1][1]
            if L == R:
                last[1] = 1
        elif last[2] != 1:
            mergeList(lyst, last[2][0], last[2][1], last[2][2])
            last[2] = 1
        else:
            stack.pop()
            if len(stack) > 0:
            	# 把第一个不是1的子问题参数置为1
                if stack[-1][0] != 1:
                    stack[-1][0] = 1
                elif stack[-1][1] != 1:
                    stack[-1][1] = 1
                else:
                    stack[-1][2] = 1
    return lyst
优化1 减少压栈的参数
def mergeSortStack1(lyst):
    L, R = 0, len(lyst) - 1
    stack = []
    while L < R or len(stack) > 0:
        while L < R:
            mid = L + ((R - L) >> 1)
            pushargsLeft = [True, L, mid]
            pushargsRight = [False, mid + 1, R]
            stack.append(pushargsRight)
            stack.append(pushargsLeft)
            R = mid
        popLeft = stack.pop()
        popRight = stack.pop()
        if not popRight[0]:
            L, R = popRight[1:]
            popRight[0] = True
            if L < R:
                stack.append(popRight)
                stack.append(popLeft)
                continue
        if popRight[0]:
            mergeList(lyst, popLeft[1], popLeft[2], popRight[2])
    return lyst
优化2 pushargsLeft像是在打酱油,mergeList函数用到了pushargsLeft的参数,把L放到pushargsRight中就可以不压入pushargsLeft了,改对应参数
def mergeSortStack2(lyst):
    L, R = 0, len(lyst) - 1
    stack = []
    while L < R or len(stack) > 0:
        while L < R:
            mid = L + ((R - L) >> 1)
            pushargsRight = [False,L, mid+1, R]
            stack.append(pushargsRight)
            R = mid
        popRight = stack.pop()
        if not popRight[0]:
            L, R = popRight[2], popRight[3]
            popRight[0] = True
            if L < R:
                stack.append(popRight)
                continue
        if popRight[0]:
            mergeList(lyst, popRight[1], popRight[2]-1, popRight[3])
    return lyst

这里小总结一下,用归纳法的归并排序时间复杂度常数项<递归函数<上面的非递归。
用这种思路改写非递归,套路性很强,通过比较可以发现,不管是中序遍历类型的递归还是后续遍历类型的递归,在这个套路里只是对应非递归子问题处理的位置不同而已。下面是这个套路的代码模板

def ...:
    stack = []
    while condition or len(stack) > 0:
        while condition:
            params = ...
            stack.append(params)
            condition = ...                   
        last = stack[-1]
        if last[1] != 1: (使用True,False的方式就是if not last[1][0]:)
            params = ... Or do something
            last[1] = 1
        elif last[2] != 1:
            params = ... Or do something
            last[2] = 1
        elif ...:
        	...
        ...
        else:
            stack.pop()
            if len(stack) > 0:
            	# 把第一个不是1的子问题参数置为1
                if stack[-1][0] != 1:
                    stack[-1][0] = 1
                elif stack[-1][1] != 1:
                    stack[-1][1] = 1
                elif ...:
                    ...
                ...
                else:
                    stack[-1][.] = 1
    return something

2 模拟递归的调用过程

不把所有子问题的参数都压入栈中,只压入下步需要解决的子问题的参数。
这个思路的出发点是:如果有一个复杂的问题(这里解释一下什么是复杂问题和简单问题。复杂问题,比如大于1层的汉诺塔,超过两个数的合并排序,不能直接解决。简单问题,打印汉诺塔某一步的移动过程,只有一层的汉诺塔问题,少于两个数的归并排序问题,已经有序的两部分合并的问题,可以直接处理),用归简法的思想,把复杂的问题分解成若干个子问题,这样做的好处可能是,有的子问题规模比原来的问题小,有的子问题是简单问题,可以直接解决。
实现思路是,如果一个问题是复杂问题那么把它放入栈中(可以处理的简单问题就直接处理了,处理不了的复杂问题放入栈中待处理),压入表示待解决的问题,那么从栈中弹出就是要解决这个复杂问题,而一般的解决思路就是(递归的)分解问题,解决简单问题,把复杂问题继续放入栈中,循环这个过程(不会一直这样无限循环下去,因为复杂问题不断分解后,问题规模会不断变小直到成为简单问题)。

假设A问题可以分解为A1,A2,A3三个子问题解决。模拟系统栈的方式则是[(A1,A2,A3),(A11,A12,A12),(A111,A112,A113)…],
而模拟调用过程的方式则是[A,A1,A11,A111…]。
按照这个思路看代码。

汉诺塔非递归解

用这个观点考虑4层汉诺塔问题。
(4,left,mid,right) 是个复杂问题, 压入栈 [(4,left,mid,right)]
弹出(4,left,mid,right),是个复杂问题没有解决,所以还压回栈内(这里可以优化,如果子问题还是复杂问题就不弹出原来的问题,而直接压入子问题)。把这个复杂问题分解为 (3,left,right,mid) 复杂问题, (4,left,right) 把4从left移动到右,简单问题, (3,mid,left,right) 复杂问题。
虽然子问题2是简单问题,但是要等子问题1解决后才能解决,而子问题1是复杂问题,按照前面的思路,压入栈。
重复这个过程直到子问题变成了简单问题(也就是递归函数达到了basecase边界条件)
这时候需要分析弹出的问题和栈顶的问题之间的关联。弹出的问题是栈顶的问题的子问题1,对于栈顶的问题来说,它的子问题1已经解决了。所以每弹出一个问题,只需要解决子问题2和子问题3即可,子问题2是简单问题,可以直接解决,子问题3通常是复杂问题压入栈。

def hanoiOriginalStack4(n, a, b, c):
    stack = []
    params = [n, a, b, c]
    while stack or params[0] > 0:
    	# params[0]即n, n>0是复杂问题,压入栈(把n=1作为边界)
        while params[0] > 0:
            stack.append(params)
            # 子问题1的参数,n>0压入栈
            params = [params[0] - 1, params[1], params[3], params[2]]
        # 按照上面的讨论, 弹出问题的子问题1已经解决
        finish = stack.pop()
        # 子问题2打印某一步移动过程是个简单问题
        print("move %s from %s to %s" % (finish[0], finish[1], finish[3]))
        # 子问题3的参数,会进入到上面的循环来判断是否是复杂问题
        params = [finish[0]-1, finish[2], finish[1], finish[3]]
归并排序非递归解

归并排序的子问题1:排序左边部分(复杂问题),子问题2:排序右边部分(复杂问题),子问题3:排序两个有序列表(简单问题)有的问题会没有子问题1,但都会有子问题2的。没有子问题1的时候,要解决的就只有子问题2,和子问题3.这时候把子问题2压入栈。弹出的问题说明达到了边界条件成为了简单问题或者子问题1,子问题2都解决了,只要解决子问题3就好了,而子问题3是个简单问题。

def mergeSort6(lyst):
    stack = []
    L, R = 0, len(lyst) - 1
    params=(L, R)
    while stack or params[0] < params[1]:
    	# 排序的列表最左边元素下标小于最右边元素下标,复杂问题
        while params[0] < params[1]:
            stack.append(params)
            mid = params[0] + ((params[1]-params[0])>>1)
            if params[0] < mid:
            	# 子问题1的参数
                params = (params[0], mid)
            else:
            	# 没有子问题1的时候,才子问题2的参数送入循环
                params = (mid+1, params[1])
        L,  R = stack.pop()
        mid = L + ((R - L) >> 1)
        # 子问题3,
        mergeList(lyst, L, mid, R)
        if stack and L == stack[-1][0]:
            params = (R + 1, stack[-1][1])
        # else:
        #     pass
    return lyst

前序遍历类型的递归(比如快速排序)改写成非递归,比较简单,这里没有给出改写的例子。
用这个思路来理解树的前序遍历,中序遍历和后序遍历就很自然了。

欢迎留言讨论。
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值