- 代码随想录之二叉树系列遍历问题算法
- 1.遍历方式分类
- 2.统一递归遍历方法论
- 3.迭代遍历方法论
- 4.统一迭代遍历方法论
- 5.二叉树的先序遍历
- 5.1递归(统一递归遍历)
- 5.2递归(统一匿名函数递归遍历)
- 5.3栈+迭代(迭代遍历)
- 5.4栈+迭代(统一迭代遍历)
- 5.5栈+迭代(另类写法)
- 6.二叉树的中序遍历
- 6.1递归(统一递归遍历)
- 6.2递归(统一匿名函数递归遍历)
- 6.3栈+迭代(迭代遍历)
- 6.4栈+迭代(统一迭代遍历)
- 6.5栈+迭代(另类写法)
- 7.二叉树的后序遍历
- 7.1递归(统一递归遍历)
- 7.2递归(统一匿名函数递归遍历)
- 7.3栈+迭代(迭代遍历)
- 7.4栈+迭代(统一迭代)
- 7.5栈+迭代(另类写法)
- 8.二叉树的层序遍历
- 8.1队列+迭代法
- 9.二叉树的层序遍历II
- 9.1队列+迭代法
- 10.二叉树的右视图
- 10.1队列+迭代法
- 11.二叉树的层平均值
- 11.1队列+迭代法
- 12.N叉树的层序遍历
- 12.1队列+迭代法
- 13.在每个树行中找最大值
- 13.1队列+迭代法
- 14.填充每个节点的下一个右侧节点指针
- 14.1队列+迭代法
- 15.二叉树的最大深度
- 16.二叉树的最小深度
2.统一递归遍历方法论二叉树的遍历分为两类,一类是深度优先遍历,一类是广度优先遍历。
*二叉树的深度优先遍历有三种方式,先序(先根次序)、中序(中根次序)和后序(后根次序)遍历。
(1)先序遍历:按照“根结点 > 左孩子 > 右孩子”的顺序进行访问
(2)中序遍历按照“左孩子 > 根结点 > 右孩子”的顺序进行访问
(3)后序遍历按照“左孩子 > 右孩子 > 根结点”的顺序进行访问
因为树的定义本身是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。
若采用非递归的方法遍历二叉树,需要采用栈去模拟实现。
* 二叉树的广度优先遍历有以下遍历:层序优先遍历
广度优先算法的方式是按层次从上到下,从左到右的逐层访问,不难想到,可以利用一个队列来实现。
基本思想如下:
(1)首先把二叉树的根节点送入队列;
(2)队首的节点出队列并访问之,然后把它的右子节点和左子节点分别入队列;
(3)重复上面两步操作,直至队空。
这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。
主要是对递归不成体系,没有方法论,每次写递归算法 ,都是靠玄学来写代码,代码能不能编过都靠运气。
本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。
这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
1.确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
好了,我们确认了递归的三要素,接下来就来练练手:
以下以前序遍历为例:
1.确定递归函数的参数和返回值:
因为要打印出前序遍历节点的数值,所以参数里需要传入数组放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数没有返回值类型,代码如下:
func traversal(cur *TreeNode, vec *[]int) {}
2.确定终止条件:
在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if cur == nil {
return
}
3.确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了
*vec = append(*vec, cur.Val)//中 traversal(cur.Left, vec)//左 traversal(cur.Right, vec)//右
那么前序遍历写出来之后,中序和后序遍历就不难理解了,代码如下:
中序遍历:
func traversal(cur *TreeNode, vec *[]int) {
if cur == nil {
return
}
traversal(cur.Left, vec)//左
*vec = append(*vec, cur.Val)//中
traversal(cur.Right, vec)//右
}
后序遍历:
func traversal(cur *TreeNode, vec *[]int) {
if cur == nil {
return
}
traversal(cur.Left, vec)//左
traversal(cur.Right, vec)//右
*vec = append(*vec, cur.Val)//中
}
3.迭代遍历方法论
前序遍历(迭代法)
为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?
我们在"栈与队列:匹配问题都是栈的强项"中提到了,递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
此时大家应该知道我们用栈也可以是实现二叉树的前后中序遍历
我们先看一下前序遍历。
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
动画如下:
不难写出前序遍历的如下代码: (注意代码中空节点不入栈)
func preorderTraversal(root *TreeNode) (vals []int) {
stack := []*TreeNode{}
if root == nil {
return
}
stack=append(stack,root)
for len(stack) != 0 {
node := stack[len(stack)-1] // 中
stack = stack[:len(stack)-1]
vals = append(vals, node.Val)
// 右(空节点不入栈)
if node.Right != nil {
stack = append(stack, node.Right)
}
// 左(空节点不入栈)
if node.Left != nil {
stack = append(stack, node.Left)
}
}
return vals
}
中序遍历(迭代法)
此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。
此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?
其实还真不行!
但接下来,再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
1.处理:将元素放进vals数组中
2.访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
动画如下:
中序遍历,可以写出如下代码:
func inorderTraversal(root *TreeNode) []int {
vals := make([]int, 0)
stack := []*TreeNode{}
cur := root
for cur != nil || len(stack) != 0 {
// 指针来访问节点,访问到最底层
if cur != nil {
stack = append(stack, cur)// 将访问的节点放进栈
cur = cur.Left// 左
} else {
cur = stack[len(stack)-1]// 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
stack = stack[:len(stack)-1]
vals = append(vals, cur.Val)// 中
cur = cur.Right // 右
}
}
return vals
}
后序遍历(迭代法)
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:
func postorderTraversal(root *TreeNode) []int {
vals := make([]int, 0)
stack := []*TreeNode{}
if root == nil {
return vals
}
stack = append(stack, root)
for len(stack) != 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
vals = append(vals, node.Val)
// 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if node.Left != nil {
stack = append(stack, node.Left)
}
// 空节点不入栈
if node.Right != nil {
stack = append(stack, node.Right)
}
}
vals =reverse(vals) // 将结果反转之后就是左右中的顺序了
return vals
}
func reverse(vals []int) []int {
left := 0
right := len(vals)-1
for left < right {
vals[left], vals[right] = vals[right], vals[left]
left++
right--
}
return vals
}
总结
4.统一迭代遍历方法论此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
上面这句话,可能一些同学不太理解,建议自己亲手用迭代法,先写出来前序,再试试能不能写出中序,就能理解了。
那么问题又来了,难道 二叉树前后中序遍历的迭代法实现,就不能风格统一么(即前序遍历 改变代码顺序就可以实现中序 和 后序)?
当然可以,这种写法,还不是很好理解,我们将在下一篇文章里重点讲解,
迭代法中序遍历
此时我们用递归的方式实现了二叉树前中后序的遍历。
也用栈实现了二叉树前后中序的迭代遍历(非递归)。
之后我们发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。
实践过的同学,也会发现使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。
其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
重头戏来了,接下来介绍一下统一写法。
我们以“递归中序遍历”为例,提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
func inorderTraversal(root *TreeNode) []int {
vals := make([]int, 0)
stack := []*TreeNode{}
if root != nil {
stack = append(stack, root)
}
for len(stack) != 0 {
cur := stack[len(stack)-1]
if cur != nil {
stack = stack[:len(stack)-1]// 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if cur.Right!=nil{
stack =append(stack,cur.Right)/ 添加右节点(空节点不入栈)
}
stack =append(stack,cur) // 添加中节点
stack =append(stack,nil)// 中节点访问过,但是还没有处理,加入空节点做为标记。
if cur.Left!=nil{
stack =append(stack,cur.Left)// 添加左节点(空节点不入栈)
}
} else {// 只有遇到空节点的时候,才将下一个节点放进结果集
stack = stack[:len(stack)-1] // 将空节点弹出
cur = stack[len(stack)-1]// 重新取出栈中元素
stack = stack[:len(stack)-1]
vals = append(vals, cur.Val) // 加入到结果集
}
}
return vals
}
看代码有点抽象我们来看一下动画(中序遍历):
动画中,result数组就是最终结果集。
可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集
迭代法前序遍历
迭代法前序遍历代码如下: (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)
func preorderTraversal(root *TreeNode) (vals []int) {
stack := []*TreeNode{}
if root != nil {
stack = append(stack, root)
}
for len(stack) != 0 {
cur := stack[len(stack)-1]
if cur != nil {
stack = stack[:len(stack)-1]
// 右
if cur.Right!=nil{
stack =append(stack,cur.Right)
}
// 左
if cur.Left!=nil{
stack =append(stack,cur.Left)
}
stack =append(stack,cur)// 中
stack =append(stack,nil)
} else {
stack = stack[:len(stack)-1]
cur = stack[len(stack)-1]
stack = stack[:len(stack)-1]
vals = append(vals, cur.Val)
}
}
return vals
}
迭代法后序遍历
后续遍历代码如下: (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)
func postorderTraversal(root *TreeNode) []int {
vals := make([]int, 0)
stack := []*TreeNode{}
if root != nil {
stack = append(stack, root)
}
for len(stack) != 0 {
cur := stack[len(stack)-1]
if cur != nil {
stack = stack[:len(stack)-1]
stack =append(stack,cur)// 中
stack =append(stack,nil)
if cur.Right!=nil{// 右
stack =append(stack,cur.Right)
}
if cur.Left!=nil{// 左
stack =append(stack,cur.Left)
}
} else {
stack = stack[:len(stack)-1]
cur = stack[len(stack)-1]
stack = stack[:len(stack)-1]
vals = append(vals, cur.Val)
}
}
return vals
}
总结
5.二叉树的先序遍历 5.1递归(统一递归遍历)此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。
但是统一风格的迭代法并不好理解,而且想在面试直接写出来还有难度的。
所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法
func preorderTraversal(root *TreeNode) (vals []int) {
traversal(root, &vals)
return vals
}
func traversal(cur *TreeNode, vec *[]int) {
if cur == nil {
return
}
*vec = append(*vec, cur.Val)//中
traversal(cur.Left, vec)//左
traversal(cur.Right, vec)//右
}
func preorderTraversal(root *TreeNode) (vals []int) {
traversal(root, vals)
return vals
}
//必须传入指针型的
func traversal(cur *TreeNode, vec []int) {
if cur == nil {
return
}
vec = append(vec, cur.Val)
traversal(cur.Left, vec)
traversal(cur.Right, vec)
}
5.2递归(统一匿名函数递归遍历)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func main() {
var tree TreeNode = TreeNode{
Val: 1,
Left: nil,
Right: &TreeNode{
Val: 2,
Left: &TreeNode{
Val: 3,
Left: nil,
Right: nil,
},
Right: nil,
},
}
fmt.Println(preorderTraversal(&tree))//&tree是个地址
}
/
func levelOrderBottom(root *TreeNode) [][]int {
tmp := levelOrder(root)
res := [][]int{}
for i := len(tmp) - 1; i >= 0; i-- {
res = append(res, tmp[i])
}
return res
}
func levelOrder(root *TreeNode) [][]int {
ret := [][]int{}
que := []*TreeNode{}
if root != nil {
que = append(que, root)
}
for len(que) != 0 {
vec := []int{}
n := len(que)
// 这里一定要使用固定大小n,不要使用i
node := que[0]
que = que[1:]
vec = append(vec, node.Val)
if node.Left != nil {
que = append(que, node.Left)
}
if node.Right != nil {
que = append(que, node.Right)
}
}
ret =append(ret,vec)
}
return ret
}
10.二叉树的右视图
10.1队列+迭代法
func rightSideView(root *TreeNode) []int {
res := []int{}
if root == nil {
return res
}
queue := []*TreeNode{root}
for len(queue) > 0 {
n := len(queue)
for i := 0; i < n; i++ {
if queue[i].Left != nil {
queue = append(queue, queue[i].Left)
}
if queue[i].Right != nil {
queue = append(queue, queue[i].Right)
}
}
res = append(res, queue[n-1].Val)
queue = queue[n:]
}
return res
}
这一题是按层序遍历的变种题。按照层序把每层的元素都遍历出来,然后依次取每一层的最右边的元素即可。用一个队列即可实现。
func averageOfLevels(root *TreeNode) []float64 {
ret := [][]int{}
que := []*TreeNode{}
if root != nil {
que = append(que, root)
}
for len(que) != 0 {
vec := []int{}
n := len(que)
// 这里一定要使用固定大小n,不要使用i
node := que[0]
que = que[1:]
vec = append(vec, node.Val)
if node.Left != nil {
que = append(que, node.Left)
}
if node.Right != nil {
que = append(que, node.Right)
}
}
ret = append(ret, vec)
}
average := []float64{}
for i := 0; i < len(ret); i++ {
var sum float64
for j := 0; j < len(ret[i]); j++ {
sum = sum + float64(ret[i][j])
}
sum = sum / float64(len(ret[i]))
average = append(average,sum)
}
return average
}
12.N叉树的层序遍历
12.1队列+迭代法
func levelOrder(root *Node) [][]int {
}
func levelOrder(root *Node) [][]int {
ret := [][]int{}
que := []*Node{}
if root != nil {
que = append(que, root)
}
for len(que) != 0 {
vec := []int{}
n := len(que)
for i := 0; i < n; i++ {
node := que[0]
que = que[1:]
vec = append(vec, node.Val)
for i:=0; i
que =append(que,node.Children[i])
}
}
ret =append(ret,vec)
}
return ret
}
13.在每个树行中找最大值
13.1队列+迭代法
func largestValues(root *TreeNode) []int {
tmp := levelOrder(root)
res := []int{}
for i := 0; i < len(tmp); i++ {
sort.Ints(tmp[i])
res = append(res, tmp[i][len(tmp[i])-1])
}
return res
}
func levelOrder(root *TreeNode) [][]int {
ret := [][]int{}
que := []*TreeNode{}
if root != nil {
que = append(que, root)
}
for len(que) != 0 {
vec := []int{}
n := len(que)
// 这里一定要使用固定大小n,不要使用i
node := que[0]
que = que[1:]
vec = append(vec, node.Val)
if node.Left != nil {
que = append(que, node.Left)
}
if node.Right != nil {
que = append(que, node.Right)
}
}
ret =append(ret,vec)
}
return ret
}
14.填充每个节点的下一个右侧节点指针
14.1队列+迭代法
type Node struct {
Val int
Left *Node
Right *Node
Next *Node
}
func connect(root *Node) *Node {
que := []*Node{}
if root != nil {
que = append(que, root)
}
for len(que) != 0 {
n := len(que)
var nodePre *Node
var node *Node
// 这里一定要使用固定大小n,不要使用i
if i == 0 {
nodePre = que[0] // 取出一层的头结点
que = que[1:]
node = nodePre
} else {
node = que[0]
que = que[1:]
nodePre.Next = node // 本层前一个节点next指向本节点
nodePre = node
}
if node.Left != nil {
que = append(que, node.Left)
}
if node.Right != nil {
que = append(que, node.Right)
}
}
node.Next = nil // 本层最后一个节点指向NULL
}
return root
}
在此
16.二叉树的最小深度在此



