Golang实现通用二叉树的原理与应用

Golang实现通用二叉树的原理与应用 我想知道是否有成员了解在Go语言中实现泛型二叉树的正确方法?

我尝试使用一个Btree接口和两个实现该接口的结构体(empty和node),这种方法似乎可行,直到我需要为Btree接口添加需要额外泛型参数的函数签名。我的解决方案是:将功能移出Btree接口,创建接受Btree的泛型函数。以下是我正在尝试的功能:

btree.go

package btree

type Btree[T any] interface {
	isEmpty() bool
	Display(func(...any) (int, error))
	Add(T, func(T, T) int) Btree[T]
	Find(T, func(T, T) int) Btree[T]
	Filter(func(T) bool) Btree[T]
}
//由于额外的泛型参数,将Fold移出Btree
func Fold[T any, U any](bt Btree[T], f func(U, T) U, acc U) U {
	if bt.isEmpty() {
		return acc
	} else {
		n := bt.(node[T])
		return Fold(n.right, f, Fold(n.left, f, f(acc, n.data)))
	}
}

func mapb_aux[T any, U any](acc *Btree[U], bt Btree[T], f func(T) U, c func(U, U) int) {
	if !bt.isEmpty() {
		n := bt.(node[T])
		mapb_aux(acc, n.left, f, c)
		*acc = (*acc).Add(f(n.data), c)
		mapb_aux(acc, n.right, f, c)
	}
}
//由于额外的泛型参数,将MapB移出Btree
func MapB[T any, U any](bt Btree[T], f func(T) U, c func(U, U) int) Btree[U] {
	acc := CreateEmptyBtree[U]()
	mapb_aux(&acc, bt, f, c)
	return acc
}

func CreateEmptyBtree[T any]() Btree[T] {
	return empty[T]{}
}

empty.go

package btree

type empty[T any] struct{}

func (empty[T]) isEmpty() bool {
	return true
}

func (empty[T]) Display(f func(...any) (int, error)) {
	f("Empty")
}

func (e empty[T]) Add(data T, f func(T, T) int) Btree[T] {
	return node[T]{data, e, e}
}

func (e empty[T]) Find(_ T, f func(T, T) int) Btree[T] {
	return e
}

func (e empty[T]) Filter(_ func(T) bool) Btree[T] {
	return e
}

node.go

package btree

type node[T any] struct {
	data  T
	left  Btree[T]
	right Btree[T]
}

func (node[T]) isEmpty() bool {
	return false
}

func (n node[T]) Display(f func(...any) (int, error)) {
	n.left.Display(f)
	f(n.data)
	n.right.Display(f)
}

func (n node[T]) Add(data T, f func(T, T) int) Btree[T] {
	ans := f(n.data, data)
	if ans < 0 {
		return node[T]{n.data, n.left.Add(data, f), n.right}
	} else if ans > 0 {
		return node[T]{n.data, n.left, n.right.Add(data, f)}
	}
	return n
}

func (n node[T]) Find(data T, f func(T, T) int) Btree[T] {
	ans := f(n.data, data)
	if ans < 0 {
		return n.left.Find(data, f)
	} else if ans > 0 {
		return n.right.Find(data, f)
	}
	return n
}

func join_branches[T any](left, right Btree[T]) Btree[T] {
	if right.isEmpty() {
		return left
	}
	n := right.(node[T])
	return node[T]{n.data, join_branches(left, n.left), n.right}
}

func (n node[T]) Filter(f func(T) bool) Btree[T] {
	if f(n.data) {
		return join_branches(n.left, n.right).Filter(f)
	}
	return node[T]{n.data, n.left.Filter(f), n.right.Filter(f)}
}

main.go

package main

import (
	"fmt"
	"math/rand"
	btree "packs/MyPacks/Btree"
	"strconv"
	"time"
)

func createRandomIntSlice(s []int) []int {
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] })
	return s
}

func compare_ints(i, j int) int {
	if i < j {
		return -1
	} else if i > j {
		return 1
	}
	return 0
}

func compare_strings(i, j string) int {
	if i < j {
		return -1
	} else if i > j {
		return 1
	}
	return 0
}

func main() {
	bt := btree.CreateEmptyBtree[int]()
	for _, e := range createRandomIntSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
		bt = bt.Add(e, compare_ints)
	}
	bt.Display(fmt.Println)
	fmt.Println(bt)
	fmt.Println("\n\nFind")
	bt.Find(4, compare_ints).Display(fmt.Println)
	fmt.Println(bt.Find(4, compare_ints))
	fmt.Println("\n\nFilter")
	bt.Filter(func(i int) bool { return i%2 == 0 }).Display(fmt.Println)
	fmt.Println(bt.Filter(func(i int) bool { return i%2 == 0 }))
	fmt.Println("\n\nFold")
	fmt.Println(btree.Fold(bt, func(acc, e int) int { return acc + e }, 0))
	bts := btree.MapB(bt, func(i int) string { return strconv.Itoa(i) }, compare_strings)
	bts.Display(fmt.Println)
}

在Go语言中以这种方式创建泛型二叉树是否存在任何问题?


更多关于Golang实现通用二叉树的原理与应用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

你好 @G4143

为什么你要从一个接口开始?为什么不将 BTree 定义为一个带有方法的结构体?

更多关于Golang实现通用二叉树的原理与应用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果你使用接口来表示B树,那么可以让一个节点或空结构体实现该B树接口,这样就可以避免使用指针。

感谢您分享您的二叉树解决方案。

我在考虑或许可以修改我的二叉树,将大部分功能移到任何接口或结构之外。目前只是在尝试,看看有哪些可能性。

所有观点都很好,但我更关心的是,如果我尝试一些稍微偏离常规Go语言路径的做法,类型系统和泛型能实现什么。我将把库的编写留给更有才华的人。

实际上,我对Go语言类型系统的包容性感到有些惊讶。例如……在Go中创建一个通用的惰性序列很容易。

func main() {
    fmt.Println("hello world")
}

好的,那么我可能不是讨论这个问题的合适人选,因为我对指针没有任何问题。

我构建通用二叉树的方法确实使用了指针,而且我认为这种方法非常直接明了。)

因此,我不确定“摆脱指针”是否是一个足够充分的理由,以至于要通过接口而不是使用指针来表达一棵树。接口的用途是定义一组可以具有多种实现的通用函数(例如,io.Reader 在 bufio、os 等包中都有实现)。

话虽如此,我觉得您的方法可读性很好,尽管在这里使用接口的原因似乎不太寻常。

关于 Fold 和 MapB,由于它们需要与 BTree 无关的额外类型参数,那么也许让它们存在于接口之外也是可以的。可以将这些函数视为辅助函数。无论如何,它们都是 btree 包 API 的一部分,仅凭这一点就使它们成为了 BTree 功能的一部分。

你的二叉树实现相当不错。泛型的使用方式符合预期,但仍可从一些小的改动中受益。

实现数据结构时,主要需要考虑的是你的代码将如何以及在何处被使用。在这个案例中,它将被打包成一个库,人们会用它来插入和移除元素。你需要不断问自己“我怎样才能更好地帮助我的用户?”,以下是我的一些建议:

  • 在代码和README中都注明你的代码不是并发安全的
  • 添加大量的单元测试,至少为每个公共方法的每个边界情况(困难的输入)添加一个测试
  • 为每个公共方法(Btree接口的方法)添加基准测试,并将其结果添加到README中
  • 再次强调,在README中添加一个关于如何使用此数据结构的示例(代码)
  • 添加一些模糊测试,向你的用户证明此代码经过了大量随机数据的测试(我也在尝试这个,关于模糊测试可以私信我寻求帮助)

根据经验,你应该花更多时间在“我怎样才能更好地帮助我的用户?”的头脑风暴和规划上,而不是实际编写代码的时间。

现在,谈谈你发布代码的实际方面:

  • 你创建了 var data node[T] 以便立即返回 data,创建泛型类型零值的另一种方法是 *new(node[T])
  • 你使用了空结构体来定义一个空的二叉树;这是不必要的,你应该能够直接使用一个类型为 Btree 的空对象
  • node 结构体不应包含 left 和 right Btree 字段,而应包含 left 和 right node 字段;node 类型不应知道 Btree 类型的存在
  • nodeBtree 的所有方法都应使用指针接收器
  • 方法 node.display 执行深度优先搜索,并在遍历的每个节点上调用 f(n.data);一个更好的名字是 DFSDepthFirstSearch
  • 与其拥有一个比较两个类型 T 对象的 node.compare 方法,不如让 node[T any] 的类型为 node[T constraints.Ordered],请查看 constraints package - golang.org/x/exp/constraints - pkg.go.dev
  • 类似地,你可以定义你自己的 Comparable 接口,并让 T 的类型为 Comparable - 这将 Compare 的责任交给了你的用户:
type Comparable interface {
   Compare(other Comparable) int
}
  • node 的 filterjoin_branches 需要更好的文档(注释),如果不阅读它们的代码,你无法理解它们的作用;根据经验,如果你仅通过阅读方法签名就能理解一个方法的作用,那么你可以不加任何注释
  • filterjoin_branches 都是递归的(这通常意味着它们可以写得更高效,并且对于大数据来说成本会很高),并且它们重新分配内存(它们复制了用于节点及其子树的内存)← 也许你可以从算法角度稍微改进一下它们
  • 在编写库时,人们实际上会阅读你的代码,因此格式很重要。使用类似 gofumpt 的工具(启用了许多 linter);同时使用空行分隔代码的逻辑块(注意下面我在这三个逻辑块之间添加了 2 个空行:输入验证、计算结果、返回结果):
func join_branches[T any](left, right Btree[T]) Btree[T] {
    if right.isEmpty() {
       	return left
    }

    n, _ := right.get()

    return node[T]{n.data, join_branches(left, n.left), n.right, n.compare}
}
  • 上一点比看起来更重要,为你所有的方法添加空行
  • node.find 方法不需要 else if 命令,而是这样做:
func (n node[T]) find(data T) Btree[T] {
    ans := n.compare(n.data, data)

    if ans < 0 {
        return n.left.find(data)
    }

    if ans > 0 {
        return n.right.find(data)
    }

    return n
}
  • node.display 方法接受一个函数参数 f:display(f func(...any) (int, error));将 f 的定义改为 display(f func(T) (int, error)) 更有意义,因为你只会用 f(n.data) 调用它
  • Btree 接口应该是私有的(并只命名为 tree),而其方法是公共的(它们的名字以大写字母开头);这是因为你希望用户只从你的包中导入一样东西,即一个 btree 对象;有点复杂:用户的利益在于定义他们自己的 tree 接口,并且只调用你的包来获取一个实现他们接口的对象 ← 这样,如果你改变了你的实现(比如重命名接口中的一个函数),对他们现有代码的损害最小;阅读一下 Golang 的“接受接口,返回结构体”原则
  • CreateEmptyBtree 方法应重命名为 New,它将返回一个空对象;empty 结构体应重命名为 btree(并使其所有方法都有指针接收器);因此,你最终会拥有方法 new.New() *btree,并且是 *btree 对象实现了 tree 接口
  • 将函数 DisplayBtreeAddDataBtreeFindDataBtreeFilterBtree 重命名为:DisplayAddFindFilter。使它们成为 btree 结构体的公共方法(btree 结构体是你当前的 empty 结构体,将其重命名为 btree
  • Fold 是做什么的?它执行 DFS 并为遍历的每个值调用 f 吗?如果是这样,你可以通过拥有一个 DFS 方法来简化它,f 是一个闭包,覆盖一个切片,你将每个遍历节点的值添加到该切片中
  • 函数 mapBtree_aux 有一个 if 来检查树是否为空,那个 if 应该直接在那里返回;模式应该是:
method() {
     if condition {
          return
     }

     doStuff
}

我想在此分享我关于Go语言中泛型和二叉树的最新思考。

这个版本有一个优点……它确实隐藏了实现细节。

btree.go

package btree

type Btree[T any] interface {
	isEmpty() bool
	get() (node[T], error)
	display(f func(...any) (int, error))
	add(T) Btree[T]
	find(T) Btree[T]
	filter(func(T) bool) Btree[T]
}

func CreateEmptyBtree[T any](compare func(T, T) int) Btree[T] {
	return empty[T]{compare}
}

func DisplayBtree[T any](bt Btree[T], f func(...any) (int, error)) {
	bt.display(f)
}

func AddDataBtree[T any](bt Btree[T], data T) Btree[T] {
	return bt.add(data)
}

func FindDataBtree[T any](bt Btree[T], data T) Btree[T] {
	return bt.find(data)
}

func FilterBtree[T any](bt Btree[T], filter func(T) bool) Btree[T] {
	return bt.filter(filter)
}

func mapBtree_aux[T, U any](acc *Btree[U], bt Btree[T], m_f func(T) U) {
	if !bt.isEmpty() {
		n, _ := bt.get()
		mapBtree_aux(acc, n.left, m_f)
		*acc = AddDataBtree(*acc, m_f(n.data))
		mapBtree_aux(acc, n.right, m_f)
	}
}

func MapBtree[T, U any](bt Btree[T], mapb func(T) U, compare func(U, U) int) Btree[U] {
	acc := CreateEmptyBtree(compare)
	mapBtree_aux(&acc, bt, mapb)
	return acc
}

func Fold[T, U any](bt Btree[T], f func(U, T) U, acc U) U {
	if bt.isEmpty() {
		return acc
	} else {
		n, _ := bt.get()
		return Fold(n.right, f, Fold(n.left, f, f(acc, n.data)))
	}
}

node.go

package btree

type node[T any] struct {
	data    T
	left    Btree[T]
	right   Btree[T]
	compare func(T, T) int
}

func (node[T]) isEmpty() bool {
	return false
}

func (n node[T]) get() (node[T], error) {
	return n, nil
}

func (n node[T]) display(f func(...any) (int, error)) {
	n.left.display(f)
	f(n.data)
	n.right.display(f)
}

func (n node[T]) add(data T) Btree[T] {
	ans := n.compare(n.data, data)
	if ans < 0 {
		return node[T]{n.data, n.left.add(data), n.right, n.compare}
	} else if ans > 0 {
		return node[T]{n.data, n.left, n.right.add(data), n.compare}
	}
	return n
}

func (n node[T]) find(data T) Btree[T] {
	ans := n.compare(n.data, data)
	if ans < 0 {
		return n.left.find(data)
	} else if ans > 0 {
		return n.right.find(data)
	}
	return n
}

func join_branches[T any](left, right Btree[T]) Btree[T] {
	if right.isEmpty() {
		return left
	}
	n, _ := right.get()
	return node[T]{n.data, join_branches(left, n.left), n.right, n.compare}
}

func (n node[T]) filter(f func(T) bool) Btree[T] {
	if f(n.data) {
		return join_branches(n.left, n.right).filter(f)
	}
	return node[T]{n.data, n.left.filter(f), n.right.filter(f), n.compare}
}

empty.go

package btree

import "errors"

type empty[T any] struct {
	compare func(T, T) int
}

func (empty[T]) isEmpty() bool {
	return true
}

func (empty[T]) get() (node[T], error) {
	var data node[T]
	return data, errors.New("Empty carries no data")
}

func (empty[T]) display(f func(...any) (int, error)) {
	f("Empty")
}

func (e empty[T]) add(data T) Btree[T] {
	return node[T]{data, e, e, e.compare}
}

func (e empty[T]) find(_ T) Btree[T] {
	return e
}

func (e empty[T]) filter(_ func(T) bool) Btree[T] {
	return e
}

main.go

package main

import (
	"fmt"
	"math/rand"
	btree "packs/MyPacks/Btree"
	"strconv"
	"time"
)

func compare_int(i, j int) int {
	if i < j {
		return -1
	} else if i > j {
		return 1
	}
	return 0
}

func compare_string(i, j string) int {
	if i < j {
		return 1
	} else if i > j {
		return -1
	}
	return 0
}

func createRandomIntSlice(s []int) []int {
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] })
	return s
}

func main() {
	bt := btree.CreateEmptyBtree(compare_int)
	for _, e := range createRandomIntSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
		bt = btree.AddDataBtree(bt, e)
	}
	btree.DisplayBtree(bt, fmt.Println)
	fmt.Println("\n\nFind 4")
	btree.DisplayBtree(btree.FindDataBtree(bt, 4), fmt.Println)
	fmt.Println("\n\nFilter even")
	btree.DisplayBtree(btree.FilterBtree(bt, func(i int) bool { return i%2 == 0 }), fmt.Println)
	fmt.Println("\n\nMap int to string")
	bts := btree.MapBtree(bt, strconv.Itoa, compare_string)
	btree.DisplayBtree(bts, fmt.Println)
	fmt.Println("\n\nFold")
	fmt.Println(btree.Fold(bt, func(acc, e int) int { return acc + e }, 0))
	fmt.Println("\n\nFold big test, fold btree into btree")
	bt2 := btree.CreateEmptyBtree(compare_int)
	for _, e := range createRandomIntSlice([]int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}) {
		bt2 = btree.AddDataBtree(bt2, e)
	}
	btf := btree.Fold(bt2, func(acc btree.Btree[int], e int) btree.Btree[int] { return btree.AddDataBtree(acc, e) }, bt)
	btree.DisplayBtree(btf, fmt.Println)
}

在Go语言中实现泛型二叉树,你的方法基本正确,但有几个关键问题需要注意和改进。以下是具体分析和示例代码:

主要问题

1. 类型断言不安全

// 当前代码中的类型断言可能panic
n := bt.(node[T])  // 如果bt是empty[T]会panic

2. 接口设计可以更简洁

FoldMapB移出接口是正确的做法,因为Go的泛型接口不能有额外的类型参数。

改进的实现

btree.go

package btree

type BTree[T any] interface {
    IsEmpty() bool
    Display(func(...any) (int, error))
    Add(T, func(T, T) int) BTree[T]
    Find(T, func(T, T) int) BTree[T]
    Filter(func(T) bool) BTree[T]
    // 添加辅助方法避免类型断言
    GetData() (T, bool)
    GetLeft() BTree[T]
    GetRight() BTree[T]
}

// 安全的Fold实现
func Fold[T any, U any](bt BTree[T], f func(U, T) U, acc U) U {
    if bt.IsEmpty() {
        return acc
    }
    if data, ok := bt.GetData(); ok {
        left := bt.GetLeft()
        right := bt.GetRight()
        acc = Fold(left, f, acc)
        acc = f(acc, data)
        acc = Fold(right, f, acc)
    }
    return acc
}

// 改进的MapB实现
func MapB[T any, U any](bt BTree[T], f func(T) U, cmp func(U, U) int) BTree[U] {
    if bt.IsEmpty() {
        return &Empty[U]{}
    }
    
    if data, ok := bt.GetData(); ok {
        left := MapB(bt.GetLeft(), f, cmp)
        right := MapB(bt.GetRight(), f, cmp)
        mappedData := f(data)
        
        return &Node[U]{
            data:  mappedData,
            left:  left,
            right: right,
        }
    }
    return &Empty[U]{}
}

func CreateEmptyBTree[T any]() BTree[T] {
    return &Empty[T]{}
}

func CreateNode[T any](data T, left, right BTree[T]) BTree[T] {
    return &Node[T]{
        data:  data,
        left:  left,
        right: right,
    }
}

empty.go

package btree

type Empty[T any] struct{}

func (e *Empty[T]) IsEmpty() bool {
    return true
}

func (e *Empty[T]) Display(f func(...any) (int, error)) {
    f("Empty")
}

func (e *Empty[T]) Add(data T, cmp func(T, T) int) BTree[T] {
    return &Node[T]{
        data:  data,
        left:  e,
        right: e,
    }
}

func (e *Empty[T]) Find(_ T, _ func(T, T) int) BTree[T] {
    return e
}

func (e *Empty[T]) Filter(_ func(T) bool) BTree[T] {
    return e
}

func (e *Empty[T]) GetData() (T, bool) {
    var zero T
    return zero, false
}

func (e *Empty[T]) GetLeft() BTree[T] {
    return e
}

func (e *Empty[T]) GetRight() BTree[T] {
    return e
}

node.go

package btree

type Node[T any] struct {
    data  T
    left  BTree[T]
    right BTree[T]
}

func (n *Node[T]) IsEmpty() bool {
    return false
}

func (n *Node[T]) Display(f func(...any) (int, error)) {
    n.left.Display(f)
    f(n.data)
    n.right.Display(f)
}

func (n *Node[T]) Add(data T, cmp func(T, T) int) BTree[T] {
    result := cmp(n.data, data)
    switch {
    case result < 0:
        return &Node[T]{
            data:  n.data,
            left:  n.left.Add(data, cmp),
            right: n.right,
        }
    case result > 0:
        return &Node[T]{
            data:  n.data,
            left:  n.left,
            right: n.right.Add(data, cmp),
        }
    default:
        return n
    }
}

func (n *Node[T]) Find(data T, cmp func(T, T) int) BTree[T] {
    result := cmp(n.data, data)
    switch {
    case result < 0:
        return n.left.Find(data, cmp)
    case result > 0:
        return n.right.Find(data, cmp)
    default:
        return n
    }
}

func (n *Node[T]) Filter(f func(T) bool) BTree[T] {
    left := n.left.Filter(f)
    right := n.right.Filter(f)
    
    if f(n.data) {
        return joinBranches(left, right)
    }
    
    return &Node[T]{
        data:  n.data,
        left:  left,
        right: right,
    }
}

func joinBranches[T any](left, right BTree[T]) BTree[T] {
    if right.IsEmpty() {
        return left
    }
    
    // 使用接口方法避免类型断言
    if data, ok := right.GetData(); ok {
        return &Node[T]{
            data:  data,
            left:  joinBranches(left, right.GetLeft()),
            right: right.GetRight(),
        }
    }
    return left
}

func (n *Node[T]) GetData() (T, bool) {
    return n.data, true
}

func (n *Node[T]) GetLeft() BTree[T] {
    return n.left
}

func (n *Node[T]) GetRight() BTree[T] {
    return n.right
}

使用示例

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 创建整数二叉树
    bt := btree.CreateEmptyBTree[int]()
    
    // 添加元素
    bt = bt.Add(5, func(a, b int) int {
        if a < b {
            return -1
        } else if a > b {
            return 1
        }
        return 0
    })
    
    bt = bt.Add(3, compareInts)
    bt = bt.Add(7, compareInts)
    bt = bt.Add(2, compareInts)
    bt = bt.Add(4, compareInts)
    
    // 中序遍历
    fmt.Print("In-order traversal: ")
    bt.Display(func(args ...any) (int, error) {
        fmt.Print(args[0], " ")
        return 0, nil
    })
    fmt.Println()
    
    // 使用Fold计算总和
    sum := btree.Fold(bt, func(acc, val int) int {
        return acc + val
    }, 0)
    fmt.Printf("Sum of all nodes: %d\n", sum)
    
    // 映射到字符串
    stringTree := btree.MapB(bt, 
        func(i int) string { return fmt.Sprintf("Node%d", i) },
        strings.Compare)
    
    fmt.Print("Mapped tree: ")
    stringTree.Display(func(args ...any) (int, error) {
        fmt.Print(args[0], " ")
        return 0, nil
    })
}

func compareInts(a, b int) int {
    if a < b {
        return -1
    } else if a > b {
        return 1
    }
    return 0
}

关键改进点

  1. 使用指针接收器:避免值拷贝,提高性能
  2. 安全的类型访问:通过GetData()等方法避免类型断言
  3. 更清晰的接口设计:将需要额外泛型参数的操作作为包级函数
  4. 更好的错误处理:避免潜在的panic

这种实现方式在Go 1.18+中是正确且高效的泛型二叉树实现。

回到顶部