首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go 数据结构和算法篇(十七):二叉排序树

Go 数据结构和算法篇(十七):二叉排序树

作者头像
学院君
发布于 2023-03-03 06:09:54
发布于 2023-03-03 06:09:54
43900
代码可运行
举报
文章被收录于专栏:学院君的专栏学院君的专栏
运行总次数:0
代码可运行

前面已经介绍了二叉树的存储和遍历,今天这篇教程我们以二叉排序树为例,来演示如何对二叉树的节点进行「增删改查」。开始之前,我们先来介绍什么是二叉排序树,以及为什么要引入这种二叉树。

什么是二叉排序树

我们前面已经介绍了很多数据结构,比如数组链表哈希表等,数组查找性能高,但是插入、删除性能差,链表插入、删除性能高,但查找性能差,哈希表的插入、删除、查找性能都很高,但前提是没有哈希冲突,此外,哈希表存储的数据是无序的,哈希表的扩容非常麻烦,涉及到哈希冲突时,性能不稳定,另外,哈希表用起来爽,构造起来可不简单,要考虑哈希函数的设计、哈希冲突的解决、扩容缩容等一系列问题。

有没有一种插入、删除、查找性能都不错,构建起来也不是很复杂,性能还很稳定的数据结构呢?这就是我们今天要介绍的数据结构 —— 二叉排序树。

二叉排序树也叫二叉搜索树、二叉查找树,它是一种特殊的二叉树,我们重点关注「排序」二字,二叉排序树要求在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值,所以这么看来,二叉排序树是天然有序的。如果按照上篇教程讲的中序遍历,得到的结果将是一个从小到大的有序数据集:

但是构造二叉排序树的目的,并不是为了排序,而是为了提高查找、插入和删除的速度。不管怎么说,在一个有序数据集上查找数据肯定比无序数据集要快,同时二叉排序树这种非线性结构,也非常有利于插入和删除的实现。

下面我们就来看看如何实现二叉排序树的插入、查找和删除以及它们对应的时间复杂度。

定义二叉排序树

首先,我们需要定义好表示二叉树节点的数据结构,还是通过二叉链表来存储二叉排序树,为了提高代码的复用性,我们将上篇教程定义的 Node 结构体抽取出来放到独立的 node.go 文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
  "fmt"
)

// Node 通过链表存储二叉树节点信息
type Node struct {
  Data  interface{}
  Left  *Node
  Right *Node
}

func NewNode(data interface{}) *Node {
  return &Node{
    Data:  data,
    Left:  nil,
    Right: nil,
  }
}

func (node *Node) GetData() string {
  return fmt.Sprintf("%v", node.Data)
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
然后,我们新建一个 search.go 文件定义下二叉排序树对应的结构体,只需要根节点作为入口即可获取整棵树的结构:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type BinarySearchTree struct {
  root *Node
}

// NewBinarySearchTree 初始化二叉排序树
func NewBinarySearchTree(node *Node) *BinarySearchTree {
  return &BinarySearchTree{
    root: node,
  }
}

这里我们通过结构体组合的方式声明二叉排序树的根节点是一个 Node 类型的指针,并为其定义了一个构造函数。

二叉排序树的插入

接下来,我们按照二叉排序树的定义,实现二叉排序树节点的插入方法:

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Insert 插入数据到二叉排序树
func (tree *BinarySearchTree) Insert(data interface{}) error {
  var value int
  var ok bool
  if value, ok = data.(int); !ok {
    return errors.New("暂时只支持int类型数据")
  }
  if node := tree.root; node == nil {
    // 二叉排序树为空,则初始化
    tree.root = NewNode(value)
    return nil
  } else {
    // 否则找到要插入的位置插入新的节点
    for node != nil {
      if value < node.Data.(int) {
        if node.Left == nil {
          node.Left = NewNode(value)
          break
        }
        node = node.Left
      } else if value > node.Data.(int) {
        if node.Right == nil {
          node.Right = NewNode(value)
          break
        }
        node = node.Right
      } else {
        return errors.New("对应数据已存在,请勿重复插入")
      }
    }
    return nil
  }
}

如果是空树,则将其作为根节点,否则判断插入节点数据与当前节点数据值的大小,如果小于当前节点数据值,则递归遍历左子树,找到对应的位置插入,如果大于当前节点数据值,则递归遍历右子树找到对应的位置插入。

我们可以写一段简单的测试代码测试节点插入方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
  tree := NewBinarySearchTree(nil)
  tree.Insert(3)
  tree.Insert(2)
  tree.Insert(5)
  tree.Insert(1)
  tree.Insert(4)

  fmt.Println("中序遍历二叉排序树:")
  midOrderTraverse(tree.root)
  fmt.Println()
}

这里我们使用了上一篇编写的中序遍历方法 midOrderTraverse,对应的打印结果如下:

二叉排序树的查找

二叉排序树的删除比较复杂,我们先来看查找逻辑的实现,查找实现非常简单,和插入逻辑类似,依次递归比较就好了,直到目标节点数据值和待查找的数据值一致,则返回该节点的指针,或者返回空,表示没有找到:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Find 查找值为data的节点
func (tree *BinarySearchTree) Find(data int) *Node {
  if node := tree.root; node == nil {
    // 二叉排序树为空返回空
    return nil
  } else {
    // 否则返回值等于data的节点指针
    for node != nil {
      if data < node.Data.(int) {
        node = node.Left
      } else if data > node.Data.(int) {
        node = node.Right
      } else {
        return node
      }
    }
    return nil
  }
}

在上一步编写的测试代码基础上,我们可以通过编写如下代码打印找到节点的对象信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fmt.Println("查找值为 3 的节点:")
node := tree.Find(3)
fmt.Printf("%v\n", node)

fmt.Println("查找值为 10 的节点:")
node = tree.Find(10)
fmt.Printf("%v\n", node)

执行代码,打印结果如下:

二叉排序树的删除

二叉排序树的删除相对而言要复杂一些,需要分三种情况来处理:

  • 第一种情况:如果要删除的节点没有子节点,我们只需要直接将父节点中指向要删除节点的指针置为 nil。比如下图中的删除节点 55。
  • 第二种情况:如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如下图中的删除节点 13。
  • 第三种情况:如果要删除的节点有两个子节点,这就比较复杂了。我们需要先找到这个节点的右子树中节点值最小的节点,把它替换到要删除的节点上,然后再删除掉这个最小节点。因为最小节点肯定没有左子节点(如果有左子节点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如下图中的删除节点 18。(同理,也可以通过待删除节点的左子树中的最大节点思路来实现)

二叉排序树的删除

根据上面的思路,我们给出二叉排序树节点删除逻辑的实现代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Delete 删除值为 data 的节点
func (tree *BinarySearchTree) Delete(data int) error {
  if tree.root == nil {
    return errors.New("二叉树为空")
  }

  p := tree.root
  var pp *Node = nil // p 的父节点

  // 找到待删除节点
  for p != nil && p.Data.(int) != data {
    pp = p
    if p.Data.(int) < data {
      // 当前节点值小于待删除值,去右子树查找
      p = p.Right
    } else {
      // 否则去左子树查找
      p = p.Left
    }
  }
  if p == nil {
    return errors.New("待删除节点不存在")
  }

  // 待删除节点有两个子节点,需要将待删除节点值设置为该节点右子树最小节点值,再删除右子树最小节点
  if p.Left != nil && p.Right != nil {
    minP := p.Right // 右子树中的最小节点
    minPP := p      // minP 的父节点
    // 查找右子树中的最小节点
    for minP.Left != nil {
      minPP = minP
      minP = minP.Left
    }
    p.Data = minP.Data // 将 minP 的数据设置到 p 中
    p = minP           // 下面就变成删除 minP 了
    pp = minPP
  }

  // 应用待删除节点只有左/右子节点的逻辑
  var child *Node
  if p.Left != nil {
    child = p.Left
  } else if p.Right != nil {
    child = p.Right
  } else {
    child = nil
  }

  if pp == nil {
    // 删除跟节点
    tree.root = nil
  } else if pp.Left == p {
    // 仅有左子节点
    pp.Left = child
  } else if pp.Right == p {
    // 仅有右子节点
    pp.Right = child
  }

  return nil
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
在之前测试代码的基础上,编写节点删除方法的测试代码:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fmt.Println("删除值为 3 的节点:")
err := tree.Delete(3)
if err != nil {
  fmt.Printf("%v\n", err)
} else {
  fmt.Println("删除成功")
}

fmt.Println("中序遍历删除节点后的二叉排序树:")
midOrderTraverse(tree.root)
fmt.Println()

执行 search.go,打印结果如下:

二叉排序树的时间复杂度

不论是插入、删除、还是查找,二叉排序树的时间复杂度都等于二叉树的高度,最好的情况当然是满二叉树或完全二叉树,此时根据完全二叉树的特性,时间复杂度是 O(logn),性能相当好,最差的情况是二叉排序树退化为线性表(斜树),此时的时间复杂度是 O(n),所以二叉排序树的形状也很重要,不同的形状会影响最终的操作性能,这也就引入了学院君接下来要继续深入介绍的二叉树内容 —— 平衡二叉树和红黑树。

(本文完)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极客书房 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数据结构与算法-二叉树(二)
二叉查找树是一种特殊的二叉树,它支持动态的数据集合的快速插入、删除和查找操作。二叉查找树的一般结构如下图所示:
用户3470542
2019/06/26
4400
数据结构与算法-二叉树(二)
Go 数据结构和算法篇(十八):平衡二叉树
上篇教程学院君给大家介绍了二叉排序树,并且提到理想情况下,二叉排序树的插入、删除、查找时间复杂度都是 O(logn),非常高效,而且它是一种动态的数据结构,插入删除性能和查找一样好,不像之前提到的二分查找,虽然查找性能也是 O(logn),但是需要先对线性表进行排序,而排序的最好时间复杂度也是 O(nlogn),所以二分查找不适合动态结构的排序。
学院君
2023/03/03
8050
Go 数据结构和算法篇(十八):平衡二叉树
重温数据结构:二叉排序树的查找、插入、删除
根据给定的文章内容,撰写摘要总结。
张拭心 shixinzhang
2018/01/05
1.3K0
重温数据结构:二叉排序树的查找、插入、删除
二叉查找树-增删查和针对重复数据处理的 Java 实现
大家好,我是多选参数的程序锅,一个正在”研究“操作系统、学数据结构和算法以及 Java 的疯狂猛补生。本篇将带来的是二叉查找树的相关知识,知识提纲如图所示。
syy
2020/08/03
1.4K0
二叉排序树
数组和链表在增删改查数据时,都有各自的缺点,比如数组要在中间插入数据,就要让后面的数据整体都移动,而链表检索数据很慢。之前说二叉树时,说到树这种结构就是就是为了弥补数组和链表的缺点而诞生的,二叉排序树(Binary search tree,简称BST),更是体现了这一点。二叉排序树有以下特点:
贪挽懒月
2022/05/13
3050
二叉排序树
PHP数据结构(十三) ——动态查找表(二叉排序树)
PHP数据结构(十三) ——动态查找表(二叉排序树) (原创内容,转载请注明来源,谢谢) 一、概念 1、动态查找表特点 当对动态查找表进行查找时,如果查找成功,会返回查找结果;如果查找失败,会对动态查找表插入查找结果,并且根据各类动态查找表的性质,对表进行动态调整。 2、二叉排序树(又称二叉查找树) 二叉排序树或者是一棵空树,或者满足以下特性: 1)若左子树非空,则左子树的所有节点小于根节点; 2)若右子树非空,则右子树的所有节点大
用户1327360
2018/03/07
1.8K0
PHP数据结构(十三) ——动态查找表(二叉排序树)
二叉搜索树基本操作
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值 它的左右子树也分别为二叉搜索树
小雨的分享社区
2022/10/26
2090
二叉搜索树基本操作
算法与数据结构(十) 二叉排序树的查找、插入与删除(Swift版)
在上一篇博客中,我们主要介绍了四种查找的方法,包括顺序查找、折半查找、插入查找以及Fibonacci查找。上面这几种查找方式都是基于线性表的查找方式,今天博客中我们来介绍一下基于二叉树结构的查找,也就是我们今天要聊的二叉排序树。今天主要聊的是二叉排序树的查找、插入与删除的内容,二叉排序的创建过程其实就是不断查找与插入的过程,也就是说当我们在创建二叉排序树时,我们会先搜索该节点在二叉排序树中的位置,若没有找到该节点则返回该节点将要插入的父节点,然后将该结点插入。而二叉排序树结点的删除则有些复杂,分为几种情况讨
lizelu
2018/01/11
1.2K0
算法与数据结构(十) 二叉排序树的查找、插入与删除(Swift版)
数据结构和算法——二叉排序树
一、二叉排序树 对于无序的序列“62,58,88,47,73,99,35,51,93,29,37,49,56,36,48,50”,是否存在一种高效的查找方案,使得能够快速判断在序列中是否存在指定的数值?二叉排序树是一种简单,高效的数据结构。 二叉排序树,又称为二叉查找树。二叉排序树或者是一棵空树,或者是具有以下性质的二叉树:若其左子树不为空,则左子树上的所有节点的值均小于它的根结点的值;若其右子树不为空,则右子树上的所有节点的值均大于它的根结点的值;左右子树又分别是二叉排序树。 对于以上的序列,我们构
felixzhao
2018/03/14
1.5K0
数据结构和算法——二叉排序树
【数据结构】【算法】二叉树、二叉排序树、树的相关操作
每个圆圈表示树的一个节点,其中节点A被称为树的根节点。 每一棵子树本身也是树。
WuShF
2023/07/08
6120
【数据结构】【算法】二叉树、二叉排序树、树的相关操作
【化解数据结构】详解树结构,并实现二叉搜索树
根据上面的图,我们大致知道了树是一个怎样的数据结构,虽然对于实现它还一头雾水,现在我们先来了解一下关于树的相关术语
小丞同学
2021/11/29
3450
【化解数据结构】详解树结构,并实现二叉搜索树
经典算法之二叉搜索树
二叉树(Binary Tree)是一种特殊的树类型,其每个节点最多只能有两个子节点。这两个子节点分别称为当前节点的左孩子(left child)和右孩子(right child)。
用户3467126
2019/11/26
8020
《Java 数据结构与算法》第8章:树(BST)
二叉搜索树算法是由包括 PF Windley、Andrew Donald Booth、Andrew Colin、Thomas N. Hibbard 在内的几位研究人员独立发现的。该算法归功于 Conway Berners-Lee 和 David Wheeler ,他们在 1960 年使用它在磁带中存储标记数据。最早和流行的二叉搜索树算法之一是 Hibbard 算法。
小傅哥
2022/12/13
6040
《Java 数据结构与算法》第8章:树(BST)
程序员,你心里就没点‘树’吗?
看官,不要生气,我没有骂你也没有鄙视你的意思,今天就是想单纯的给大伙分享一下树的相关知识,但是我还是想说作为一名程序员,自己心里有没有点树?你会没点数吗?言归正传,树是我们常用的数据结构之一,树的种类很多有二叉树、二叉查找树、平衡二叉树、红黑树、B树、B+树等等,我们今天就来聊聊二叉树相关的树。
田维常
2019/09/02
4360
程序员,你心里就没点‘树’吗?
Python二叉树详解笔记
退化(或病态)树(Degenerate (or pathological) tree)
小锋学长生活大爆炸
2020/08/13
1.2K0
Python二叉树详解笔记
[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
时间静止不是简史
2020/07/27
7990
[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树
算法:二叉排序树的删除节点策略及其图形化(二叉树查找)
该文章介绍了如何基于Linux操作系统,使用C语言编写代码,实现一个轻量级的TCP/UDP协议栈,包括服务器端和客户端的程序。该代码具有高性能、易于扩展、易于维护的特点。同时,该代码还支持多种数据包过滤规则,可以灵活地根据不同的规则进行转发。该文章还介绍了如何基于该代码实现一个简单的网关程序,以支持多种客户端接入。
s1mba
2018/01/03
1.3K0
算法:二叉排序树的删除节点策略及其图形化(二叉树查找)
重学数据结构和算法(二)之二叉树、红黑树、递归树、堆排序
最近学习了极客时间的《数据结构与算法之美]》很有收获,记录总结一下。 欢迎学习老师的专栏:数据结构与算法之美 代码地址:https://github.com/peiniwan/Arithmetic
六月的雨
2021/03/04
4880
数据结构与算法—小白也能搞懂二叉排序(查找)树
暑期将结束,好好沉淀数据结构增加竞争力吧!二叉排序树是每个程序员必须攻克的问题,我们一起学习吧!
bigsai
2019/09/24
5850
数据结构与算法—小白也能搞懂二叉排序(查找)树
数据结构与算法(十五):二叉排序树[通俗易懂]
而二叉排序树的查找类似二分查找,而插入类似链表,相较以上三种结构可以较好的平衡查找和插入效率
全栈程序员站长
2022/09/23
1.9K0
数据结构与算法(十五):二叉排序树[通俗易懂]
推荐阅读
相关推荐
数据结构与算法-二叉树(二)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档