AIGC标识 二叉搜索树 — 插入、查找、删除与平衡概念

二叉搜索树 — 插入、查找、删除与平衡概念


一、什么是二叉搜索树

二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,它在二叉树的基础上加了一条"排序规则":

对于任意节点,左子树中所有节点的值 < 该节点的值 < 右子树中所有节点的值

这个规则看似简单,但它让查找变得极其高效——每一步你都可以排除掉一半的候选节点,就像翻字典一样:要找"苹果",你知道它在前面,绝对不会翻到"西瓜"那部分去。

BST 的核心性质

  1. 有序性:中序遍历 BST 会得到一个严格升序的序列。
  2. 查找效率:在平衡状态下,查找、插入、删除都是 O(log n)。
  3. 无重复值:标准 BST 不允许重复值(遇到重复值时要么忽略,要么用计数或链表处理)。

二、节点结构与基本操作

type BSTNode struct {
    Val   int
    Left  *BSTNode
    Right *BSTNode
}

2.1 查找(Search)

查找是 BST 最核心的操作。从根节点出发,比较目标值与当前节点的大小关系,决定向左还是向右走。

查找 7:
     10
    /  \
   5   15       10 > 7 → 往左
  / \  / \
 3  7 12 18     5 < 7 → 往右 → 找到了!
func (node *BSTNode) Search(val int) *BSTNode {
    curr := node
    for curr != nil {
        if val == curr.Val {
            return curr
        } else if val < curr.Val {
            curr = curr.Left
        } else {
            curr = curr.Right
        }
    }
    return nil
}

迭代版本比递归版本好在不需要额外的调用栈开销。如果树是平衡的,每次比较排除一半节点,查找效率就是 O(log n)。

2.2 插入(Insert)

插入的逻辑和查找很相似:先"假装"要查找这个值,走到一个空位(nil)时,那里就是新节点该待的地方。

func (node *BSTNode) Insert(val int) *BSTNode {
    if node == nil {
        return &BSTNode{Val: val}
    }
    if val < node.Val {
        node.Left = node.Left.Insert(val)
    } else if val > node.Val {
        node.Right = node.Right.Insert(val)
    }
    // val == node.Val 时什么都不做,BST 不允许重复
    return node
}

这里返回 *BSTNode 是因为插入可能改变树的根(空树插入第一个节点时)。递归写法简洁优雅,但要注意栈深度问题。也可以写成迭代版本:

func InsertIterative(root *BSTNode, val int) *BSTNode {
    newNode := &BSTNode{Val: val}
    if root == nil {
        return newNode
    }
    curr := root
    for {
        if val < curr.Val {
            if curr.Left == nil {
                curr.Left = newNode
                break
            }
            curr = curr.Left
        } else if val > curr.Val {
            if curr.Right == nil {
                curr.Right = newNode
                break
            }
            curr = curr.Right
        } else {
            // 重复值,不插入
            break
        }
    }
    return root
}

2.3 删除(Delete)

删除是 BST 操作中最复杂的一个,因为删除一个节点后还要维持 BST 的有序性质。分三种情况:

情况一:删除叶子节点

最简单,直接删掉即可。让父节点指向该节点的指针变为 nil。

删除 7:          删除后:
     10              10
    /  \            /  \
   5   15    →     5   15
    \
     7

情况二:删除只有一个子节点的节点

用它的唯一子节点"顶替"它的位置,就像公司里某个员工离职了,他带的唯一一个下属直接向他的上级汇报。

删除 5(只有右孩子):    删除后:
     10                      10
    /  \                    /  \
   5   15        →         7   15
    \
     7

情况三:删除有两个子节点的节点

这是最棘手的。不能直接删,因为两个孩子没法同时顶替一个位置。策略是:找一个"替身"来占据这个位置,然后删除替身原来的位置。

替身有两个候选:

  • 后继节点(successor):右子树中的最小值(一路向左走到底)
  • 前驱节点(predecessor):左子树中的最大值(一路向右走到底)

通常用后继节点。后继节点要么是叶子,要么只有右孩子(因为它是"最左"的),所以删它很简单。

删除 10(有两个孩子):
     10                  12 ← 后继顶上来
    /  \                /  \
   5   15      →       5   15
      /  \                 /  \
    12   18               nil 18
func (node *BSTNode) Delete(val int) *BSTNode {
    if node == nil {
        return nil
    }

    if val < node.Val {
        node.Left = node.Left.Delete(val)
    } else if val > node.Val {
        node.Right = node.Right.Delete(val)
    } else {
        // 找到了要删除的节点

        // 情况一 & 二:0或1个子节点
        if node.Left == nil {
            return node.Right
        }
        if node.Right == nil {
            return node.Left
        }

        // 情况三:2个子节点,找后继
        successor := node.Right.minNode()
        node.Val = successor.Val
        node.Right = node.Right.Delete(successor.Val)
    }
    return node
}

// minNode 找子树中的最小节点
func (node *BSTNode) minNode() *BSTNode {
    curr := node
    for curr.Left != nil {
        curr = curr.Left
    }
    return curr
}

三、BST 的退化问题

BST 的性能高度依赖树的形状。在理想情况下(平衡),高度是 log₂n,所有操作都是 O(log n)。

但如果按照有序序列(比如 1, 2, 3, 4, 5)依次插入,树会退化成一棵"右斜树"——每个节点都只有右孩子,本质上就是一条链表。

插入顺序: 1, 2, 3, 4, 5
结果:
1
 \
  2
   \
    3
     \
      4
       \
        5

高度 = 5,查找复杂度 = O(n),退化为链表!

这就是为什么我们需要平衡二叉搜索树——AVL 树和红黑树通过旋转操作保证树的高度始终是 O(log n)。它们的核心思想是:当插入或删除导致某棵子树"偏重"时,通过旋转来重新平衡。


四、复杂度总结

操作 平均情况 最坏情况(退化为链表)
查找 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)
最小值/最大值 O(log n) O(n)
中序遍历 O(n) O(n)

五、BST 的典型应用

  1. 数据库索引:B+ 树是 BST 的推广,几乎所有关系数据库的索引底层都是 B+ 树。
  2. 符号表:编译器用 BST 存储变量名到类型的映射。
  3. 自动补全:结合前缀搜索,快速找到以某字符串开头的所有词条。
  4. 区间查询:查找落在 [low, high] 范围内的所有值。

六、关键要点

  1. BST 的核心是"左小右大",这个简单规则带来了高效的查找能力。
  2. 中序遍历 = 升序输出,这是 BST 区别于普通二叉树的关键特性。
  3. 删除有两个子节点的节点时,找后继(或前驱)来顶替,保持结构的有序性。
  4. 插入顺序决定树的形状,有序插入会导致退化。实际工程中直接用平衡树(如 Go 没有内置平衡树,但可以用第三方库或自己实现红黑树)。
  5. Go 标准库没有 BST,这一点和 Java(TreeMap)、C++(std::map)不同。Go 的 sort 包提供了排序切片,但没有内置有序映射,需要时自己实现或用第三方。
posted @ 2026-08-03 09:20  FfHUCisI  阅读(7)  评论(0)    收藏  举报