从递归到非递归

跟風遠走 提交于 2021-01-14 00:37:07

递归确实是一种优雅强大的技术, 但是好多代码库都偏爱使用迭代,即使使用递归, 也都往往对递归调用的最大栈深度提前做预估或限制等。可能考虑递归性能一般低于迭代。有些问题,我们可能先是使用递归解决, 然后再转变成对应的迭代版本, 练习递归到迭代的转换,也有助于我们理解问题的递归结构。

树是典型的递归定义数据结构, 对应的操作也是递归的实现,如二叉树的遍历:

type node struct {
     link [2]*node
     data rune
}
func preOrder(root *node) {
    if  root != nil  {
         fmt.Printf("%c ", root.data)
         preOrder(root.link[0])  //left subtree
         preOrder(root.link[1])  //right subtree
    }
}
func inOrder(root *node) {
    if  root != nil  {
         inOrder(root.link[0])  //left subtree
         fmt.Printf("%c ", root.data)
         inOrder(root.link[1])  //right subtree
    }
}
func postOrder(root *node) {
    if  root != nil  {
         postOrder(root.link[0])  //left subtree
         postOrder(root.link[1])  //right subtree
         fmt.Printf("%c ", root.data)
    }
}

前序遍历和中序遍历都包含一个尾递归,尾递归比较容易消除, 剩下的递归调用可以通过模拟系统栈,保存恢复原来的调用参数。 preorder, 尾部调用消掉,仅需把调用的实参(root.link[1]) 传给 形参(root), 然后跳转都函数入口位置:

func preOrder(root *node) {
call:
	if root != nil {
		fmt.Printf("%c ", root.data)
		preOrder(root.link[0])
		root = root.link[1]
		goto call
	}
}

消除标签和goto, 转换成循环:

func preOrder(root *node) {
	for root != nil {
		fmt.Printf("%c ", root.data)
		preOrder(root.link[0])
		root = root.link[1]
	}
}

第一个递归调用需要使用stack,保存恢复参数模拟系统栈行为:

func preOrder(root *node) {
	var stack []*node
calling:
	for root != nil {
		fmt.Printf("%c ", root.data)
		stack = append(stack, root) // push node to stack
		root = root.link[0]
		goto calling
	}
	if len(stack) != 0 {
		root = stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		root = root.link[1]
		goto calling
	}
}

最后的if 语句块是模拟一个stack frame 被弹掉后的处理逻辑:从第一个递归调用返回后的逻辑,可以假想有一个returning 标签, golang 不允许定义不使用的标签,这里省略了。 重新整理以下代码的控制结构,消掉标签和goto :

func preOrder(root *node) {
	var stack []*node
	for {
		for root != nil {
			fmt.Printf("%c ", root.data)
			stack = append(stack, root) // push node to stack
			root = root.link[0]
		}
		if len(stack) == 0 {
			break
		}
		root = stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		root = root.link[1]
	}
}

中序的过程类似,最后结果:

func inOrder(root *node) {
	var stack []*node
	for {
		for root != nil {
			stack = append(stack, root)
			root = root.link[0]
		}
		if len(stack) == 0 {
			break
		}
		root = stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		fmt.Printf("%c ", root.data)
		root = root.link[1]
	}
	fmt.Println()
}

后序的情况比较麻烦, 没有尾递归, 观察原来的递归函数:

func postOrder(root *node) {
    if  root != nil  {
         postOrder(root.link[0])  //left subtree
         postOrder(root.link[1])  //right subtree
         fmt.Printf("%c ", root.data)
    }
}

在发起递归调用的过程中,第一个调用root.link[0] 变身为root,蒙头往下钻之前, 当前stack frame 0 对应的root.link[1] 先于root需要先压栈保存,到stack frame 1时,root.link[0].link[1] 先于root.link[0] 保存, 这个过程一直持续到root == nil, 栈刚刚保存了整颗树的最左节点:

func postOrder(root *node) {
	var stack []*node
calling:
	if root != nil {
		if root.link[1] != nil {
			stack = append(stack, root.link[1])
		}
		stack = append(stack, root)
		root = root.link[0]
		goto calling
	}
	....
	....
}

这是一直生成stack frame, 往下calling 的过程, root == nil 后, 需要弹出节点,作出决策,如果弹出节点没有右孩子或者右子树已经处理完成,可以访问它, 然后当前stack frame 可以被弹掉,继续往上返回(对应从第二个递归调用返回);如果弹出节点存在右孩子且右子树没被处理,跳转到函数入口处,重复calling标签的逻辑(对应从第一个递归调用返回)。

func postOrder(root *node) {
	var stack []*node
calling:
	if root != nil {
		if root.link[1] != nil {
			stack = append(stack, root.link[1])
		}
		stack = append(stack, root)
		root = root.link[0]
		goto calling
	}
returning:

	if len(stack) != 0 {
		root = stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		if root.link[1] != nil &&
			len(stack) != 0 &&
			stack[len(stack)-1] == root.link[1] {
			stack = stack[:len(stack)-1] //pop right child
			stack = append(stack, root)  //push node
			root = root.link[1]
			goto calling
		} else {
			fmt.Printf("%c ", root.data)
			goto returning
		}
	}
	fmt.Println()
}

消除标签和goto后:

func postOrder(root *node) {
	var stack []*node

	for {
		for root != nil {
			if root.link[1] != nil {
				stack = append(stack, root.link[1])
			}
			stack = append(stack, root)
			root = root.link[0]
		}

	 if len(stack) != 0 {
			root = stack[len(stack)-1]
			stack = stack[:len(stack)-1]
		        if root.link[1] != nil &&
				len(stack) != 0 &&
				stack[len(stack)-1] == root.link[1] {
				stack = stack[:len(stack)-1] //pop right child
				stack = append(stack, root)  //push node
				root = root.link[1]
		} else {
				fmt.Printf("%c ", root.data)
				root = nil
			}
		} else {
			break
		}
	}
	fmt.Println()
}

转换后的迭代版本复杂,不是那么直觉,难于记忆,发现Robert Sedgewick的方法很好,直觉,易于记忆:

It is also useful to consider nonrecursive implementations that use an explicit pushdown stack. For simplicity, we begin by considering an abstract stack that can hold items or trees, initialized with the tree to be traversed. Then, we enter into a loop, where we pop and process the top entry on the stack, continuing until the stack is empty. If the popped entity is an item, we visit it; if the popped entity is a tree, then we perform a sequence of push operations that depends on the desired ordering: • For preorder, we push the right subtree, then the left subtree, and then the node. • For inorder, we push the right subtree, then the node, and then the left subtree. • For postorder, we push the node, then the right subtree, and then the left subtree.

package main

import (
	"fmt"
)

const (
	PreOrder = iota
	InOrder
	PostOrder
)

type node struct {
	link [2]*node
	data rune
}

func newNode(data rune) *node {
	return &node{
		data: data,
	}
}

func lvlOrder(root *node) {
	var queue []*node
	queue = append(queue, root)
	for len(queue) != 0 {
		head := queue[0]
		queue = queue[1:]
		fmt.Printf("%c ", head.data)
		if head.link[0] != nil {
			queue = append(queue, head.link[0])
		}
		if head.link[1] != nil {
			queue = append(queue, head.link[1])
		}
	}
	fmt.Println()
}

func treeOrder(root *node, order int) {
	var stack []interface{}
	stack = append(stack, root)

	stackOps := [3]func(top *node){
		PreOrder: func(top *node) {
			if top.link[1] != nil {
				stack = append(stack, top.link[1])
			}
			if top.link[0] != nil {
				stack = append(stack, top.link[0])
			}
			stack = append(stack, top.data)
		},
		InOrder: func(top *node) {
			if top.link[1] != nil {
				stack = append(stack, top.link[1])
			}
			stack = append(stack, top.data)
			if top.link[0] != nil {
				stack = append(stack, top.link[0])
			}
		},
		PostOrder: func(top *node) {
			stack = append(stack, top.data)
			if top.link[1] != nil {
				stack = append(stack, top.link[1])
			}
			if top.link[0] != nil {
				stack = append(stack, top.link[0])
			}
		},
	}
	for len(stack) != 0 {
		top := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		switch top := top.(type) {
		case *node:
			stackOps[order](top)
		case rune:
			fmt.Printf("%c ", top)
		}
	}
	fmt.Println()
}

func preOrder(root *node) {
	treeOrder(root, PreOrder)
}

func inOrder(root *node) {
	treeOrder(root, InOrder)
}

func postOrder(root *node) {
	treeOrder(root, PostOrder)
}

func newTree() *node {
	var root *node
	root = newNode('e')
	root.link[0] = newNode('d')
	root.link[1] = newNode('h')
	b := newNode('b')
	root.link[0].link[0] = b
	b.link[0] = newNode('a')
	b.link[1] = newNode('c')
	root.link[1] = newNode('h')
	root.link[1].link[0] = newNode('f')
	root.link[1].link[0].link[1] = newNode('g')
	return root
}

func main() {
	root := newTree()
	fmt.Print("preOrder: ")
	preOrder(root)
	fmt.Print("inOrder: ")
	inOrder(root)
	fmt.Print("postOrder: ")
	postOrder(root)
	fmt.Print("lvlOrder: ")
	lvlOrder(root)
}

不管是data item 还是 tree link 都统统压栈, 更接近对真实系统栈的模拟, 系统栈可以看成是一个泛型的栈 stack<T: machine word size>,前序,中序,后续只是访问顺序的差别,结构上是一致的。

类似这种二元递归结构的还有汉诺塔,快速排序都可以用这种方式解决。

汉诺塔描述的是有n个大小各不相同的盘子,要从from 柱子 借助 aux 辅助柱子 搬到 to 柱子,一次搬动一个,小号盘子需要先于大号盘子被挪动,递归版本:

package main

import (
	"fmt"
)

func hanoiR(n int, from, aux, to rune) {
	if n != 0 {
		hanoiR(n-1, from, to, aux)
		fmt.Printf("move disk %d from %c to %c\n", n, from, to)
		hanoiR(n-1, aux, from, to)
	}
}
func main() {
	hanoiR(3, 'f', 'a', 't')
}

结构类似中序遍历,迭代版本:

package main
import (
	"fmt"
)
func hanoi(n int, from, aux, to rune) {
	var stack []interface{}
	type (
		nonprint struct {
			n    int
			from rune
			aux  rune
			to   rune
		}
		print struct {
			n    int
			from rune
			to   rune
		}
	)
	stack = append(stack, nonprint{n, from, aux, to})
	for len(stack) != 0 {
		top := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		switch top := top.(type) {
		case nonprint:
			if top.n != 1 {
				stack = append(stack, nonprint{top.n - 1, top.aux, top.from, top.to})
			}
			stack = append(stack, print{top.n, top.from, top.to})
			if top.n != 1 {
				stack = append(stack, nonprint{top.n - 1, top.from, top.to, top.aux})
			}
		case print:
			fmt.Printf("move disk %d from %c to %c\n", top.n, top.from, top.to)
		}
	}
}

func main() {
	hanoi(3, 'f', 'a', 't')
}

快速排序的主要活动都是分区函数完成,分区函数每次将枢纽元素调正到正确的位置上,然后递归处理枢纽的左右两边:

func partition(nums []int) int {
	pivot := nums[0]
	i := 0
	len := len(nums)
	j := len
	for {
		i++
		j--
		for nums[i] < pivot && i < len-1 {
			i++
		}
		for pivot < nums[j] {
			j--
		}
		if i >= j {
			break
		}
		nums[i], nums[j] = nums[j], nums[i]
	}
	nums[0], nums[j] = nums[j], nums[0]
	return j
}

func qsortR(nums []int) {
	if len(nums) <= 1 {
		return
	}
	i := partition(nums)
	qsortR(nums[0:i])
	qsortR(nums[i+1:])
}

结构上类似前序遍历,非递归的实现:

func qsort(nums []int) {
	var stack [][]int
	stack = append(stack, nums)
	for len(stack) != 0 {
		top := stack[len(stack)-1]
		stack = stack[:len(stack)-1]
		if len(top) <= 1 {
			continue
		}
		i := partition(top)
		stack = append(stack, top[:i])
		stack = append(stack, top[i+1:])
	}
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!