binary-tree
本系列是algorithm-pattern的学习笔记。主要目的是为了合理刷题,积累算法模板,有套路的进行leetcode刷题。安利一下该仓库,作者总结归纳的不错,适合作为leetcode刷题的参考资料。
模板能够帮助我们,以不变应万变;但模板不是教条主义,要具体问题具体分析,能够根据模板举一反三。
本篇blog是二叉树篇,主要内容是二叉树的遍历模板的积累,4种遍历方式,2种遍历思想。
ps:本篇blog也可以在我的GitHub主页进行查看,同时4种遍历方式的snippet也放在了我的gist主页,易于大家自取积累snippet。
classification
二叉树的遍历主要有两种,DFS和BFS。
对于DFS而言,能够以root结点的访问顺序细分为前序遍历、中序遍历和后序遍历。DFS的一个遍历特点是左子树优先右子树访问。
以一个简单的二叉树为例,展示三种遍历的结果。
graph TB
A((root))
B((left))
C((right))
A-->B
A-->C
前序遍历-preOrder:
graph LR
A((root))
B((left))
C((right))
A-->B
B-->C
style A fill: #76BA99
中序遍历-inOrder:
graph LR
A((root))
B((left))
C((right))
B-->A
A-->C
style A fill: #76BA99
后序遍历-postOrder:
graph LR
A((root))
B((left))
C((right))
B-->C
C-->A
style A fill: #76BA99
对于三种遍历而言,从逻辑的实现上又能够分为递归遍历和非递归遍历。
而BFS遍历本质是层序遍历,就像排队一样,一层结点遍历完再遍历下一层结点。以上图二叉树为例,BFS遍历结果如下。
层序遍历-levelOrder:
graph LR
A((root))
B((left))
C((right))
A-.-B
B-->C
下面就分别总结一下4种遍历方式的模板,以一颗简单的二叉树结构为例。
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
preOrder
递归遍历:
func preOrder(root *TreeNode) {
ans := make([]int, 0)
ans = append(ans, root.Val)
preOrder(root.Left)
preOrder(root.Right)
}
非递归遍历:利用stack存储已访问结点,用于原路返回
func preOrder(root *TreeNode) []int {
if root == nil {
return nil
}
ans := make([]int, 0)
stack := make([]*TreeNode, 0)
for root != nil || len(stack) > 0 {
for root != nil {
// root->left->right
ans = append(ans, root.Val)
// push root
stack = append(stack, root)
root = root.Left
}
// pop top of stack
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
root = node.Right
}
return ans
}
inOrder
递归遍历:
func inOrder(root *TreeNode) {
ans := make([]int, 0)
inOrder(root.Left)
ans = append(ans, root.Val)
inOrder(root.Right)
}
非递归遍历:利用stack存储已访问结点,用于原路返回
func inOrder(root *TreeNode) []int {
if root == nil {
return nil
}
ans := make([]int, 0)
stack := make([]*TreeNode, 0)
for root != nil || len(stack) > 0 {
for root != nil {
// push root
stack = append(stack, root)
root = root.Left
}
// pop top of stack
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// left->root->right
ans = append(ans, node.Val)
root = node.Right
}
return ans
}
postOrder
递归遍历:
func postOrder(root *TreeNode) {
ans := make([]int, 0)
postOrder(root.Left)
postOrder(root.Right)
ans = append(ans, root.Val)
}
非递归遍历:同样是利用stack存储已访问结点,但需要一个标识lastVisit,确认右子结点是否已经弹出,保证root结点在right结点之后弹出
func postOrder(root *TreeNode) []int {
if root == nil {
return nil
}
ans := make([]int, 0)
stack := make([]*TreeNode, 0)
var lastVisit *TreeNode
for root != nil || len(stack) > 0 {
for root != nil {
// push root
stack = append(stack, root)
root = root.Left
}
// read top of stack
node := stack[len(stack)-1]
if node.Right != nil || node.Right != lastVisit {
root = node.Right
} else {
// pop top of stack
stack = stack[:len(stack)-1]
// left->right->root
ans = append(ans, node.Val)
// marking current node pop
lastVisit = node
}
}
return ans
}
top-down and bottom-up
同样的对于DFS有三种不同的遍历方式,但也有两种遍历思想,自顶向下和自底向上。
- top-down:将最终结果通过指针参数传递
- bottom-up:先递归返回结果,最后将其合并,也就是分而治之
以前序遍历示例两种不同的遍历思想。
top-down:
func preOrder(root *TreeNode) []int {
ans := make([]int, 0)
dfs(root, &ans)
return ans
}
func dfs(root *TreeNode, ans *[]int) {
if root == nil {
return
}
// root->left->right
*ans = append(*ans, root.Val)
dfs(root.Left, ans)
dfs(root.Right, ans)
}
其实本质是上面所展示的前序遍历的递归方式,结果是通过一个指针进行传参存储的。
bottom-up:
func preOrder(root *TreeNode) []int {
ans := dfs(root)
return ans
}
func dfs(root *TreeNode) []int {
ans := make([]int, 0)
if root == nil {
return ans
}
// divid
left := dfs(root.Left)
right := dfs(root.Right)
// conquer
ans = append(ans, root.Val)
ans = append(ans, left...)
ans = append(ans, right...)
return ans
}
自底向上,分而治之,核心是每个部分都能够独立解决,最后将每个部分的结果进行归并汇总,就是整个问题的结果。
分而治之的思想应用是十分广泛的,比如快速排序,归并排序等;这里只是简单引入一下该思想,后面同样会对于分治法进行模板总结。
levelOrder
层序遍历:利用queue存储每层结点,保证结点访问的层序性
func levelOrder(root *TreeNode) [][]int {
if root == nil {
return nil
}
ans := make([][]int, 0)
queue := make([]*TreeNode, 0)
// push root
queue = append(queue, root)
// level by level
for len(queue) > 0 {
list := make([]int, 0)
// length of current level
l := len(queue)
for i := 0; i < l; i++ {
// pop head of queue
node := queue[0]
queue = queue[1:]
list = append(lsit, node.Val)
// push children of node
if node.Left != nil {
queue = append(queue, ndoe.Left)
}
if node.Right != nil {
queue = append(queue, node.RIght)
}
}
// add current level to ans
ans = append(ans, list)
}
return ans
}
summary
本篇blog主要是对二叉树的4种遍历方式进行了模板总结。DFS包括了前序遍历、中序遍历和后序遍历,而BFS本质就是层序遍历。
而DFS的实现又能够分为递归实现和非递归实现。同时也引入了自顶向下和自底向上两种思想。
不断进行模板总结,知识类比细化,对于个人而言,是一个有效的学习算法的方式。在下篇blog将对4种遍历方式进行实践,通过leetcode感受模板的快乐。
欢迎大家评论转载,多多发表自己的意见或建议。