1. 概述

本文将使用 Kotlin 编程语言实现二叉树的基本操作。如果你对 Java 实现的二叉树感兴趣,可以参考我们之前写的 Java 二叉树教程

2. 定义

在编程中,二叉树是一种每个节点最多有两个子节点的树结构。我们通常称这两个子节点为“左子节点”和“右子节点”。

我们以整数作为节点的键值,定义一个递归的数据结构:

class Node(
    var key: Int,
    var left: Node? = null,
    var right: Node? = null
)

每个节点包含一个整型值 key,以及可选的左子节点和右子节点。由于节点之间的链接关系,整棵树只需一个根节点就可以完整描述。

本文我们讨论的是有序二叉树(Binary Search Tree),它满足以下不变式(invariant):

  1. 树中不包含重复的键值
  2. 对于任意节点,其左子树中的所有节点键值都小于它
  3. 其右子树中的所有节点键值都大于它

3. 基本操作

常见的操作包括:

  • 查找某个键值对应的节点
  • 插入新节点
  • 删除节点
  • 遍历树中所有节点

3.1 查找(Lookup)

在有序二叉树中查找效率很高。如果当前节点的键值等于目标值,查找结束;如果目标值更大,则只需查找右子树;反之查找左子树:

fun find(value: Int): Node? = when {
    this.key > value -> left?.find(value)
    this.key < value -> right?.find(value)
    else -> this
}

注意返回值可能是 null,表示未找到目标节点。

这里我们使用了 Kotlin 的 when 表达式,它比 Java 的 switch-case 更加灵活和强大。

3.2 插入(Insertion)

插入新节点时,我们需确保树中没有重复的键值。若当前节点为空,就在此插入新节点;否则根据大小关系递归地插入到左子树或右子树中:

fun insert(value: Int) {
    if (value > this.key) {
        if (this.right == null) {
            this.right = Node(value)
        } else {
            this.right!!.insert(value)
        }
    } else if (value < this.key) {
        if (this.left == null) {
            this.left = Node(value)
        } else {
            this.left!!.insert(value)
        }
    }
}

3.3 删除(Removal)

删除操作相对复杂,需要根据被删除节点的子节点数量分情况处理:

✅ 情况一:无子节点

如果目标节点是叶子节点,直接将其父节点的对应引用设为 null 即可。但注意不能删除根节点。

private fun removeNoChildNode(node: Node, parent: Node?) {
    if (parent == null) {
        throw IllegalStateException("Can not remove the root node without child nodes")
    }
    if (node == parent.left) {
        parent.left = null
    } else if (node == parent.right) {
        parent.right = null
    }
}

aa

✅ 情况二:仅有一个子节点

此时只需将子节点“上提”即可,替换掉当前节点:

private fun removeSingleChildNode(parent: Node, child: Node) {
    parent.key = child.key
    parent.left = child.left
    parent.right = child.right
}

bb

✅ 情况三:两个子节点都存在

这是最复杂的情况。我们需要找到一个合适的替代节点,通常选择左子树中最大的节点或右子树中最小的节点。

private fun removeTwoChildNode(node: Node) {
    val leftChild = node.left!!
    leftChild.right?.let {
        val maxParent = findParentOfMaxChild(leftChild)
        maxParent.right?.let {
            node.key = it.key
            maxParent.right = null
        } ?: throw IllegalStateException("Node with max child must have the right child!")
    } ?: run {
        node.key = leftChild.key
        node.left = leftChild.left
    }
}

cc

3.4 遍历(Traversal)

常见的遍历方式有:

  1. 前序遍历(Pre-order):根节点 -> 左子树 -> 右子树
  2. 中序遍历(In-order):左子树 -> 根节点 -> 右子树
  3. 后序遍历(Post-order):左子树 -> 右子树 -> 根节点

Kotlin 中实现非常简洁:

fun visit(): Array<Int> {
    val a = left?.visit() ?: emptyArray()
    val b = right?.visit() ?: emptyArray()
    return a + arrayOf(key) + b
}

⚠️ 注意:这种写法虽然简洁,但不是尾递归,对于深度较大的树可能会导致栈溢出。

4. 小结

本文我们用 Kotlin 实现了二叉搜索树的基本结构和操作,并演示了 Kotlin 中一些优于 Java 的语法特性,如 when、空安全操作符等。

完整代码可在 GitHub 仓库 获取。


原始标题:Implementing a Binary Tree in Kotlin