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
你好 @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 和 rightBtree字段,而应包含 left 和 rightnode字段;node类型不应知道Btree类型的存在node和Btree的所有方法都应使用指针接收器- 方法
node.display执行深度优先搜索,并在遍历的每个节点上调用f(n.data);一个更好的名字是DFS或DepthFirstSearch - 与其拥有一个比较两个类型 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 的
filter和join_branches需要更好的文档(注释),如果不阅读它们的代码,你无法理解它们的作用;根据经验,如果你仅通过阅读方法签名就能理解一个方法的作用,那么你可以不加任何注释 filter和join_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接口- 将函数
DisplayBtree、AddDataBtree、FindDataBtree、FilterBtree重命名为:Display、Add、Find和Filter。使它们成为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. 接口设计可以更简洁
将Fold和MapB移出接口是正确的做法,因为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
}
关键改进点
- 使用指针接收器:避免值拷贝,提高性能
- 安全的类型访问:通过
GetData()等方法避免类型断言 - 更清晰的接口设计:将需要额外泛型参数的操作作为包级函数
- 更好的错误处理:避免潜在的panic
这种实现方式在Go 1.18+中是正确且高效的泛型二叉树实现。

