go语言|二叉树递归遍历,可能有你没见过的花样玩法

简介: go语言|二叉树递归遍历,可能有你没见过的花样玩法

9ddddd890441439cb5ed1a979e45dca6.png在上一篇博客《go语言|数据结构:二叉树可视化(制作svg格式树形图)》中,已实现了用svg图片展示二叉树的树形结构。但对非满二叉树的比较复杂,需要先转成满二叉树,再获取各结点在满二叉树中的对应坐标位置,这种做法明显有个缺点就是遍历的次数比较多。


树形图的关键在于获取结点的坐标。对满二叉树的来说,它的坐标数组可以标记为:[[(0,0)], [(1,0), (1,1)], [(2,0), (2,1), (2,2), (2,3)], [(3,0),(3,1), ...], ...];如能对任意二叉树只做一次遍布,就获得这样的坐标,效率就提高了。首先,从递归遍历说起——




递归遍历


递归遍历时,看数据域和左右子树结点出现的位置顺序看,分先序、中序、后序三种方式。不管哪种方式都可以用2种方法,一种是直接打印遍历结果、一种返回遍历结果(结点的数据域数组)。如下代码,以先序遍历为例:

package main
import "fmt"
type btNode struct {
  Data   interface{}
  Lchild *btNode
  Rchild *btNode
}
type biTree struct {
  Root *btNode
}
func Build(List ...interface{}) *biTree {
  if len(List) == 0 || List[0] == nil {
    return &biTree{}
  }
  node := &btNode{Data: List[0]}
  Queue := []*btNode{node}
  for lst := List[1:]; len(lst) > 0 && len(Queue) > 0; {
    cur, val := Queue[0], lst[0]
    Queue, lst = Queue[1:], lst[1:]
    if val != nil {
      cur.Lchild = &btNode{Data: val}
      Queue = append(Queue, cur.Lchild)
    }
    if len(lst) > 0 {
      val, lst = lst[0], lst[1:]
      if val != nil {
        cur.Rchild = &btNode{Data: val}
        Queue = append(Queue, cur.Rchild)
      }
    }
  }
  return &biTree{Root: node}
}
func (bt *btNode) PreorderPrint() {
  if bt != nil {
    fmt.Print(bt.Data, " ")
    bt.Lchild.PreorderPrint()
    bt.Rchild.PreorderPrint()
  }
}
func (bt *btNode) PreorderList() []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, bt.Data)
    res = append(res, bt.Lchild.PreorderList()...)
    res = append(res, bt.Rchild.PreorderList()...)
  }
  return res
}
func main() {
  tree := Build(1, 2, 3, 4, 5, 6, 7, 8)
  fmt.Println(tree.Root.PreorderList())
  tree.Root.PreorderPrint()
}


创建函数Build()也作了改进,直接用变长参数,而非之前用数组作参数。另外,参数中如有不能连接的结点,直接放弃而非抛出错误panic()。如参数 1,nil,nil,2,3 就只返回一个结点。



深度的遍历


递归遍历核心就以下三行代码,先读出数据域,后左右递归的即先序遍历;读出放中间或最后一行就变成中序和后序遍历了。


 

fmt.Print(bt.Data, " ")                //读出数据域
    bt.Lchild.PreorderPrint()          //左子树递归
    bt.Rchild.PreorderPrint()          //右子树递归

如果把bt.Data换成其它呢,结点结构就定义了三个值,换成bt.Lchild,bt.Rchild就能遍历出左右子树结点。这样用处不大, 如果写成 bt.Lchild!=nil 或者 bt.Rchild!=nil,遍历结果就是该结点是否有左子树或右子树;写成 bt.Lchild==nil && bt.Rchild==nil 就能判断是否为叶结点。如果换作一个自定义函数呢,那就能得到自己想要的结果。也可以玩其它花样,比如累加所有结点的和,为了方便默认它们的数据域都是整数:


//相同代码部分略,见上面的代码
func (bt *btNode) SumTree() int {
  var res int
  if bt != nil {
    res += bt.Data.(int)
    res += bt.Lchild.SumTree()
    res += bt.Rchild.SumTree()
  }
  return res
}
func main() {
  tree := Build(1, 5, 26, 9, 8, 51)
  fmt.Println(tree.Root.SumTree())
}
// 输出结果 100



深度和高度的概念


深度是从根节点数到它的叶节点,高度是从叶节点数到它的根节点。即深度是从上到下计数的,而高度是从下往上计数。


二叉树的深度是指所有结点中最深的结点所在的层数。对于整棵树来说,最深的叶结点的深度就是树的深度,即树的高度和深度是相等的。但同一层的节点的深度是相同,它们的高度不一定相同。如下图,节点2和3的深度相等,但高度不相等;节点4、5、6深度也相等,但高度不全相等。

baf17268732246b89af38e904dd07fa0.png



深度的遍历

从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1。由此,需先分别求得左、右子树的深度,算法中“访问结点”的操作为:求得左、右子树深度的最大值,然后加 1 。递归代码如下:

func (bt *btNode) MaxDepth() int {
  if bt == nil {
    return 0
  }
  Lmax := bt.Lchild.MaxDepth()
  Rmax := bt.Rchild.MaxDepth()
  if Lmax > Rmax {
    return 1 + Lmax
  } else {
    return 1 + Rmax
  }
}



遍历时用上这个函数就可得到所有结点的深度了,完整测试代码以8个结点的完全二叉树为例,返回结果是:“4 3 2 1 1 2 1 1”。最大值4为根结点的尝试,也就是整个二叉树的深度或高度;四个最小值1,表示有四个叶结点。

package main
import "fmt"
type btNode struct {
  Data   interface{}
  Lchild *btNode
  Rchild *btNode
}
type biTree struct {
  Root *btNode
}
func Build(List ...interface{}) *biTree {
  if len(List) == 0 || List[0] == nil {
    return &biTree{}
  }
  node := &btNode{Data: List[0]}
  Queue := []*btNode{node}
  for lst := List[1:]; len(lst) > 0 && len(Queue) > 0; {
    cur, val := Queue[0], lst[0]
    Queue, lst = Queue[1:], lst[1:]
    if val != nil {
      cur.Lchild = &btNode{Data: val}
      Queue = append(Queue, cur.Lchild)
    }
    if len(lst) > 0 {
      val, lst = lst[0], lst[1:]
      if val != nil {
        cur.Rchild = &btNode{Data: val}
        Queue = append(Queue, cur.Rchild)
      }
    }
  }
  return &biTree{Root: node}
}
func (bt *btNode) PreorderDepth() {
  if bt != nil {
    fmt.Print(bt.MaxDepth(), " ")
    bt.Lchild.PreorderDepth()
    bt.Rchild.PreorderDepth()
  }
}
func (bt *btNode) MaxDepth() int {
  if bt == nil {
    return 0
  }
  Lmax := bt.Lchild.MaxDepth()
  Rmax := bt.Rchild.MaxDepth()
  if Lmax > Rmax {
    return 1 + Lmax
  } else {
    return 1 + Rmax
  }
}
func main() {
  tree := Build(1, 2, 3, 4, 5, 6, 7, 8)
  tree.Root.PreorderDepth()
}



叶结点的遍历

func (bt *btNode) LeafPrint() {
  if bt != nil {
    if bt.Lchild == nil && bt.Rchild == nil {
      fmt.Print(bt.Data, " ")
    }
    bt.Lchild.LeafPrint()
    bt.Rchild.LeafPrint()
  }
}
func (bt *btNode) LeafList() []interface{} {
  var res []interface{}
  if bt != nil {
    if bt.Lchild == nil && bt.Rchild == nil {
      res = append(res, bt.Data)
    }
    res = append(res, bt.Lchild.LeafList()...)
    res = append(res, bt.Rchild.LeafList()...)
  }
  return res
}

结点结构的三变量都用过了,对得到结点的坐标帮助不大,坐标需要的是结点在树中的层数和所在层数的索引号。于是尝试引入其它变量——



坐标的遍历


纵坐标遍历

//相同代码部分略,见上面的代码
func (bt *btNode) LevelPrint(y int) {
  if bt != nil {
    fmt.Print(bt.Data, ":", y, " ")
        y++
    bt.Lchild.LevelPrint(y)
    bt.Rchild.LevelPrint(y)
        y--
  }
}
func main() {
  tree := Build(1, 2, 3, 4, 5, 6, 7, 8)
  tree.Root.LevelPrint(0)
}
//输出结果 1:0 2:1 4:2 8:3 5:2 3:1 6:2 7:2

a6cd915bda0c441cbe72086eef1b4c04.png

改成返回数组形式如下: 另外也可以把y++,y--改成参数y+1,输出[0 1 2 3 2 1 2 2],结果相同。

//相同代码部分略,见上面的代码
func (bt *btNode) LevelList(y int) []int {
  var res []int
  if bt != nil {
    res = append(res, y)
    res = append(res, bt.Lchild.LevelList(y+1)...)
    res = append(res, bt.Rchild.LevelList(y+1)...)
  }
  return res
}
func main() {
  tree := Build(1, 2, 3, 4, 5, 6, 7, 8)
  fmt.Print(tree.Root.LevelList(0))
}



横坐标遍历

依葫芦画瓢,照纵坐标的方式遍历:

func (bt *btNode) IndexPrint(x int) {
  if bt != nil {
    fmt.Print(bt.Data, ":", x, " ")
    bt.Lchild.IndexPrint(x - 1)
    bt.Rchild.IndexPrint(x + 1)
  }
}

但结果不符合要求:1:0 2:-1 4:-2 8:-3 5:0 3:1 6:0 7:2

a6e76e2bd002444bbe1165c6658c3cd1.png



最底下一层最靠近中间的左右两结点横坐标是-1,1就OK了,以满二叉树为例来说比较好理解:

15a36b1a61ca4df68eddfbbafade78bd.png



从上图的规律看,每层平移的距离不一样的,不能平移正负1,移动距离应该是2的高度减一次方才对,调用时参数为(0,2的高度-2次方),完整的测试代码如下:

package main
import "fmt"
type btNode struct {
  Data   interface{}
  Lchild *btNode
  Rchild *btNode
}
type biTree struct {
  Root *btNode
}
func Build(List ...interface{}) *biTree {
  if len(List) == 0 || List[0] == nil {
    return &biTree{}
  }
  node := &btNode{Data: List[0]}
  Queue := []*btNode{node}
  for lst := List[1:]; len(lst) > 0 && len(Queue) > 0; {
    cur, val := Queue[0], lst[0]
    Queue, lst = Queue[1:], lst[1:]
    if val != nil {
      cur.Lchild = &btNode{Data: val}
      Queue = append(Queue, cur.Lchild)
    }
    if len(lst) > 0 {
      val, lst = lst[0], lst[1:]
      if val != nil {
        cur.Rchild = &btNode{Data: val}
        Queue = append(Queue, cur.Rchild)
      }
    }
  }
  return &biTree{Root: node}
}
func Pow2(x int) int { //x>=0
  res := 1
  for i := 0; i < x; i++ {
    res *= 2
  }
  return res
}
func Max(L, R int) int {
  if L > R {
    return L
  } else {
    return R
  }
}
func (bt *btNode) MaxDepth() int {
  if bt == nil {
    return 0
  }
  Lmax := bt.Lchild.MaxDepth()
  Rmax := bt.Rchild.MaxDepth()
  return 1 + Max(Lmax, Rmax)
}
func (bt *btNode) IndexPrint(x, w int) {
  if bt != nil {
    fmt.Print(bt.Data, ":", x, " ")
    bt.Lchild.IndexPrint(x-w, w/2)
    bt.Rchild.IndexPrint(x+w, w/2)
  }
}
func main() {
  tree := Build(1, 2, 3)
  tree.Root.IndexPrint(0, Pow2(tree.Root.MaxDepth()-2))
  fmt.Println()
  tree = Build(1, 2, 3, 4, 5, 6, 7)
  tree.Root.IndexPrint(0, Pow2(tree.Root.MaxDepth()-2))
  fmt.Println()
  tree = Build(1, 2, 3, 4, 5, 6, 7, 8)
  tree.Root.IndexPrint(0, Pow2(tree.Root.MaxDepth()-2))
  fmt.Println()
  tree = Build(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
  tree.Root.IndexPrint(0, Pow2(tree.Root.MaxDepth()-2))
  fmt.Println()
}
/*输出结果
1:0 2:-1 3:1 
1:0 2:-2 4:-3 5:-1 3:2 6:1 7:3 
1:0 2:-4 4:-6 8:-7 5:-2 3:4 6:2 7:6 
1:0 2:-4 4:-6 8:-7 9:-5 5:-2 10:-3 11:-1 3:4 6:2 12:1 13:3 7:6 14:5 15:7 
*/


横纵坐标联合输出

综上,把横纵坐标放一起以数组形式输出,参数多一个是变动的平移距离:

//相同代码部分略,见上面的代码
func (bt *btNode) Coordinate(x, y, w int) []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, []interface{}{bt.Data, x, y})
    res = append(res, bt.Lchild.Coordinate(x-w, y+1, w/2)...)
    res = append(res, bt.Rchild.Coordinate(x+w, y+1, w/2)...)
  }
  return res
}
func main() {
  tree := Build(1, 2, 3)
  fmt.Println(tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2)))
  tree = Build(1, 2, 3, 4, 5, 6, 7)
  fmt.Println(tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2)))
  tree = Build(1, 2, 3, nil, nil, 5, 6, 7, 8)
  fmt.Println(tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2)))
  tree = Build(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
  fmt.Println(tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2)))
  tree = Build(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
  fmt.Println(tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2)))
}
/*输出结果
[[1 0 0] [2 -1 1] [3 1 1]]
[[1 0 0] [2 -2 1] [4 -3 2] [5 -1 2] [3 2 1] [6 1 2] [7 3 2]]
[[1 0 0] [2 -4 1] [3 4 1] [5 2 2] [7 1 3] [8 3 3] [6 6 2]]
[[1 0 0] [2 -4 1] [4 -6 2] [8 -7 3] [9 -5 3] [5 -2 2] [10 -3 3] [11 -1 3] [3 4 1] [6 2 2] [12 1 3] [13 3 3] [7 6 2] [14 5 3] [15 7 3]]
[[1 0 0] [2 -8 1] [4 -12 2] [8 -14 3] [16 -15 4] [9 -10 3] [5 -4 2] [10 -6 3] [11 -2 3] [3 8 1] [6 4 2] [12 2 3] [13 6 3] [7 12 2] [14 10 3] [15 14 3]]
*/


至此,坐标遍历全部完成,再加上判断有否左右子树结点就能作图了:

package main
import "fmt"
type btNode struct {
  Data   interface{}
  Lchild *btNode
  Rchild *btNode
}
type biTree struct {
  Root *btNode
}
func Build(List ...interface{}) *biTree {
  if len(List) == 0 || List[0] == nil {
    return &biTree{}
  }
  node := &btNode{Data: List[0]}
  Queue := []*btNode{node}
  for lst := List[1:]; len(lst) > 0 && len(Queue) > 0; {
    cur, val := Queue[0], lst[0]
    Queue, lst = Queue[1:], lst[1:]
    if val != nil {
      cur.Lchild = &btNode{Data: val}
      Queue = append(Queue, cur.Lchild)
    }
    if len(lst) > 0 {
      val, lst = lst[0], lst[1:]
      if val != nil {
        cur.Rchild = &btNode{Data: val}
        Queue = append(Queue, cur.Rchild)
      }
    }
  }
  return &biTree{Root: node}
}
func Pow2(x int) int { //x>=0
  res := 1
  for i := 0; i < x; i++ {
    res *= 2
  }
  return res
}
func Max(L, R int) int {
  if L > R {
    return L
  } else {
    return R
  }
}
func (bt *btNode) MaxDepth() int {
  if bt == nil {
    return 0
  }
  Lmax := bt.Lchild.MaxDepth()
  Rmax := bt.Rchild.MaxDepth()
  return 1 + Max(Lmax, Rmax)
}
func (bt *btNode) Coordinate(x, y, w int) []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, []interface{}{bt.Data, x, y, w, bt.Lchild != nil, bt.Rchild != nil})
    res = append(res, bt.Lchild.Coordinate(x-w, y+1, w/2)...)
    res = append(res, bt.Rchild.Coordinate(x+w, y+1, w/2)...)
  }
  return res
}
func main() {
  tree := Build(1, 2, 3, 4, 5, 6, 7, 8)
  list := tree.Root.Coordinate(0, 0, Pow2(tree.Root.MaxDepth()-2))
  fmt.Println(list)
  //数组的遍历
  for _, data := range list {
    for _, d := range data.([]interface{}) {
      fmt.Print(d, " ")
    }
    /*内循环或者用:
    for i, _ := range data.([]interface{}) {
      fmt.Print(data.([]interface{})[i], " ")
    }
    */
    fmt.Println()
  }
}
/*输出结果
[[1 0 0 true true] [2 -4 1 true true] [4 -6 2 true false] [8 -7 3 false false] [5 -2 2 false false] [3 4 1 true true] [6 2 2 false false] [7 6 2 false false]]
1 0 0 true true 
2 -4 1 true true 
4 -6 2 true false 
8 -7 3 false false 
5 -2 2 false false 
3 4 1 true true 
6 2 2 false false 
7 6 2 false false 
*/


作图时的用法,以上面完整代码的输出为例:


遍历到 2 -4 1 2 true true ,表示(-4,1)处是结点2,true true就有2条子树的连线,指向坐标 (-4-2, 1+1) (-4+2, 1+1),即 (-6, 2) 和 (-2, 2),即结点4和结点;

遍历到 4 -6 2 1 true false,表示(-6,2)处是结点4,true false说明它只有左子树,指向坐标 (-7,3),即结点8;

遍历到 8 -7 3 0 false false,表示(-7,3)处是结点8,false,false说明它是叶结点。


具体如何把结点和坐标转换到二叉树svg图形格式,请参见上篇文章:


《go语言|数据结构:二叉树可视化(制作svg格式树形图)》。

BTW,以上所有代码不论是哪一个递归遍历都可以有三种顺序,比如坐标遍历的代码:

//先序
func (bt *btNode) Coordinate(x, y, w int) []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, []interface{}{bt.Data, x, y})
    res = append(res, bt.Lchild.Coordinate(x-w, y+1, w/2)...)
    res = append(res, bt.Rchild.Coordinate(x+w, y+1, w/2)...)
  }
  return res
}


换个顺序就成为中序和后序遍历:

//中序
func (bt *btNode) Coordinate(x, y, w int) []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, bt.Lchild.Coordinate(x-w, y+1, w/2)...)
    res = append(res, []interface{}{bt.Data, x, y})
    res = append(res, bt.Rchild.Coordinate(x+w, y+1, w/2)...)
  }
  return res
}
//后序
func (bt *btNode) Coordinate(x, y, w int) []interface{} {
  var res []interface{}
  if bt != nil {
    res = append(res, bt.Lchild.Coordinate(x-w, y+1, w/2)...)
    res = append(res, bt.Rchild.Coordinate(x+w, y+1, w/2)...)
    res = append(res, []interface{}{bt.Data, x, y})
  }
  return res
}

(本文完)

目录
相关文章
|
5天前
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
53 22
零值在go语言和初始化数据
|
5天前
|
JSON Java Go
Go 语言性能优化技巧
在Go语言中优化性能涉及数字字符串转换(如用`strconv.Itoa()`代替`fmt.Sprintf()`)、避免不必要的字符串到字节切片转换、预分配切片容量、使用`strings.Builder`拼接、有效利用并发(`goroutine`和`sync.WaitGroup`)、减少内存分配、对象重用(`sync.Pool`)、无锁编程、I/O缓冲、正则预编译和选择高效的序列化方法。这些策略能显著提升代码执行效率和系统资源利用率。
42 13
|
1天前
|
Cloud Native Java Go
为什么要学习Go语言?
GO logo的核心理念,即简单胜于复杂。使用现代斜体无衬线字体与三条简单的运动线相结合,形成一个类似于快速运动的两个轮子的标记,传达速度和效率。字母的圆形暗示了GO地鼠的眼睛,创造了一个熟悉的形状,让标记和吉祥物很好地搭配在一起。
13 4
|
5天前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
41 8
|
5天前
|
存储 Go
go语言中fmt格式化包和内置函数汇总
【7月更文挑战第10天】本文介绍fmt包和`Errorf`用于创建格式化的错误消息。`fmt`包还涉及一些接口,如`Formatter`、`GoStringer`、`ScanState`、`Scanner`和`Stringer`,支持自定义格式化和输入/输出处理。
17 1
|
5天前
|
Go
go语言中格式化输出的占位符
【7月更文挑战第10天】`fmt` 包在 Go 语言中用于格式化输出,包括不同类型的占位符:%v(默认格式)、%+v(带字段名的结构体)、%#v(Go语法表示)、%T(类型表示)、%%(百分号)。布尔值用%t,整数有%b、%c、%d、%o、%q、%x、%X和%U。浮点数和复数用%b、%e、%E、%f、%g、%G。字符串和字节切片用%s、%q、%x、%X。指针用%p。占位符可配合+、-、#、空格和0进行调整。宽度和精度控制输出格式,例如 %.4g 控制小数精度。Go 没有 `%u`,但无符号整数默认打印为正数。运算符包括逻辑、比较、加减、乘除、移位、按位和按位异或等。
17 1
|
7天前
|
存储 Go 索引
在go语言中自定义泛型的变长参数
【7月更文挑战第8天】在Go语言中,由于官方1.18以前的版本不支持泛型,可以通过空接口和反射模拟泛型。泛型适用于通用数据结构和函数,虽牺牲了一些性能,但提高了代码复用和类型安全性。
42 1
|
3天前
|
安全 Go
Go语言map并发安全,互斥锁和读写锁谁更优?
Go并发编程中,`sync.Mutex`提供独占访问,适合读写操作均衡或写操作频繁的场景;`sync.RWMutex`允许多个读取者并行,适用于读多写少的情况。明智选择锁可提升程序性能和稳定性。示例展示了如何在操作map时使用这两种锁。
6 0
|
3天前
|
安全 Go 开发者
Go语言map并发安全使用的正确姿势
在Go并发编程中,由于普通map不是线程安全的,多goroutine访问可能导致数据竞态。为保证安全,可使用`sync.Mutex`封装map或使用从Go 1.9开始提供的`sync.Map`。前者通过加锁手动同步,后者内置并发控制,适用于多goroutine共享。选择哪种取决于具体场景和性能需求。
6 0
|
3天前
|
存储 安全 Java
Go语言中的map为什么默认不是并发安全的?
Go语言的map默认不保证并发安全,以优化性能和简洁性。官方建议在需要时使用`sync.Mutex`保证安全。从Go 1.6起,并发读写map会导致程序崩溃,鼓励开发者显式处理并发问题。这样做的哲学是让代码更清晰,并避免不必要的性能开销。
4 0