一、二叉树定义
-
二叉树基础
在 Go 语言中,我们通常使用结构体来定义二叉树的节点。一个典型的二叉树节点结构体包含一个存储节点值的数据字段,以及两个指向左右子节点的指针字段
type TreeNode struct { Val int Left *TreeNode Right *TreeNode }
这里,Val 字段用于存储节点的值,Left 和 Right 分别是指向左子节点和右子节点的指针。若节点没有左子树或右子树,相应的指针则为 nil。通过这样的结构体定义,我们可以构建出复杂的二叉树结构,每个节点都如同一个小小的枢纽,连接着整个二叉树的不同部分,为后续的遍历操作提供了基础框架。
-
二叉树遍历概念
二叉树遍历是按照特定顺序访问二叉树中的所有节点,且每个节点仅访问一次。主要有三种遍历方式:前序遍历、中序遍历和后序遍历。
前序遍历的顺序是根节点 - 左子树 - 右子树。就好像我们站在树前,先观察树的顶端(根节点),然后沿着左边一路向下探索(左子树),最后再审视右边的分支(右子树)。这种遍历方式常用于复制二叉树,因为先处理根节点能确保新树结构的逐步构建,按照根节点的模样依次搭建左右子树。
中序遍历遵循左子树 - 根节点 - 右子树的顺序。想象我们在树林中漫步,先沿着左边的小路深入(左子树),走到尽头遇到一棵标志性的大树(根节点),接着再走向右边的岔路(右子树)。它在处理有序二叉搜索树时特别有用,按中序遍历输出的节点值是升序排列的,能轻松实现排序输出的功能。
后序遍历则是左子树 - 右子树 - 根节点的顺序。如同我们完成一次探险后,从树林最深处的左边角落(左子树)开始折返,经过右边区域(右子树),最后回到入口处(根节点)。常用于计算二叉树的一些属性,比如求树的高度、释放二叉树内存等操作,因为需要先处理完子树的相关信息,最后才能基于子树结果处理根节点
二、递归遍历实现
2.1前序遍历递归
在 Go 语言中,前序遍历的递归实现简洁而优雅。我们定义一个名为 PreorderTraversal 的函数,它接受二叉树的根节点作为参数。函数内部,首先判断根节点是否为空,如果为空,则直接返回,这是递归的终止条件,避免了无限循环。若根节点不为空,先将根节点的值加入结果切片 res,接着对左子树递归调用 PreorderTraversal,再对右子树进行同样的操作。
func PreorderTraversal(root *TreeNode) []int {
var res []int
if root == nil {
return res
}
res = append(res, root.Val)
res = append(res, PreorderTraversal(root.Left)...)
res = append(res, PreorderTraversal(root.Right)...)
return res
}
假设我们有如下二叉树:
1
/ \
2 3
/ \
4 5
当调用 PreorderTraversal 函数传入根节点 1 时,首先将 1 加入 res,然后递归左子树,遇到节点 2,将 2 加入 res,再递归 2 的左子树,把 4 加入 res,此时 4 的左子树为空,返回上一层,接着递归 2 的右子树,把 5 加入 res,左子树遍历完毕;再递归根节点 1 的右子树,把 3 加入 res。最终得到前序遍历结果 [1, 2, 4, 5, 3]。通过这种递归方式,代码按照根节点 - 左子树 - 右子树的顺序,有条不紊地访问二叉树的每个节点,就像沿着一条既定的路线,精准地收集节点信息。
2.2中序遍历递归
中序遍历的递归实现同样遵循特定的逻辑。定义 InorderTraversal 函数,参数为二叉树的根节点。在函数内部,先判断根节点是否为空,为空则返回。若不为空,先递归左子树,当左子树遍历完后,将根节点的值加入结果切片 res,最后再递归右子树。
func InorderTraversal(root *TreeNode) []int {
var res []int
if root == nil {
return res
}
res = append(res, InorderTraversal(root.Left)...)
res = append(res, root.Val)
res = append(res, InorderTraversal(root.Right)...)
return res
}
思路:调用 InorderTraversal 函数时,首先递归左子树,一直深入到最左边的节点 4,由于 4 的左子树为空,将 4 加入 res,然后返回上一层,把 2 加入 res,接着递归 2 的右子树,把 5 加入 res,此时左子树遍历完成,再将根节点 1 加入 res,最后递归右子树,把 3 加入 res。最终得到中序遍历结果 [4, 2, 5, 1, 3]。这种递归方式确保了按照左子树 - 根节点 - 右子树的顺序访问节点,如同在树林中漫步,先探索左边的小路,遇到大树(根节点)记录下来,再走向右边的岔路,有序地收集二叉树的节点值。
2.3后序遍历递归
后序遍历递归函数 PostorderTraversal 也以根节点为参数。函数内,先判断根节点是否为空,为空返回。然后先递归左子树,再递归右子树,最后将根节点的值加入结果切片 res。
func PostorderTraversal(root *TreeNode) []int {
var res []int
if root == nil {
return res
}
res = append(res, PostorderTraversal(root.Left)...)
res = append(res, PostorderTraversal(root.Right)...)
res = append(res, root.Val)
return res
}
思路:调用 PostorderTraversal 函数后,首先递归左子树,深入到最左边的节点 4,由于 4 的左子树为空,返回上一层,接着递归 2 的右子树,把 4 和 5 加入 res,此时左子树遍历完毕;再递归右子树,把 3 加入 res,最后将根节点 1 加入 res。最终得到后序遍历结果 [4, 5, 2, 3, 1]。通过这样的递归过程,严格按照左子树 - 右子树 - 根节点的顺序访问二叉树节点,就像完成一次探险后,从树林最深处折返,依次记录经过的节点,准确地获取后序遍历的结果
三、迭代遍历实现
在二叉树的迭代遍历中,栈被用来模拟递归调用的过程,对于不同的遍历顺序(前序、中序、后序),栈的操作规则略有不同,但核心思想是利用栈的后进先出(LIFO)特性来控制节点访问顺序。
3.1解题思路:
3.1.1前序遍历 (根-左-右):首先访问当前节点,并将其值记录,将右子节点压入栈(如果存在),再将左子节点压入栈(如果存在)。重复上述步骤直到栈为空。
3.1.2中序遍历 (左-根-右):持续向左深入并依次将沿途节点压入栈,直到不能再深入为止,开始从栈中弹出节点并记录其值,每次弹出后转向该节点的右子树,重复操作。
3.1.3后序遍历 (左-右-根):类似于前序遍历,但需要额外的逻辑或标记来确保根节点是在左右子树都被访问过后才被处理。
3.2前序遍历迭代
func PreorderTraversalIterative(root *TreeNode) []int {
var stack []*TreeNode
var res []int
if root == nil {
return res
}
stack = append(stack, root)
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
res = append(res, node.Val)
if node.Right!= nil {
stack = append(stack, node.Right)
}
if node.Left!= nil {
stack = append(stack, node.Left)
}
}
return res
}
3.3中序遍历迭代
func InorderTraversalIterative(root *TreeNode) []int {
var stack []*TreeNode
var res []int
cur := root
for cur!= nil || len(stack) > 0 {
if cur!= nil {
stack = append(stack, cur)
cur = cur.Left
} else {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
res = append(res, node.Val)
cur = node.Right
}
}
return res
}
3.4后序遍历迭代
func PostorderTraversalIterative(root *TreeNode) []int {
var stack []*TreeNode
var res []int
var prev *TreeNode
if root == nil {
return res
}
stack = append(stack, root)
for len(stack) > 0 {
node := stack[len(stack)-1]
if (node.Left == nil && node.Right == nil) ||
(node.Right == nil && prev == node.Left) ||
(prev == node.Right) {
res = append(res, node.Val)
stack = stack[:len(stack)-1]
prev = node
} else {
if node.Right!= nil {
stack = append(stack, node.Right)
}
if node.Left!= nil {
stack = append(stack, node.Left)
}
}
}
return res
}