手写二叉树

1,141 阅读16分钟

前言

最近学习了二叉树,为了巩固一下知识,这里记录一下在手写过程中需要注意的点,本文基于 Kotlin 手写一个二叉树。

预备知识

关于二叉树的一些相关定义,需要了解一下:

  1. 一棵二叉树由根节点左子树右子树组成,子树之下还可以有子树,没有节点的树称为空树

  2. 节点的度(node - degree):子树的个数

  3. 树的度(tree - degree):所有节点度中的最大值

  4. 叶子节点(leaf):度为 0 的节点

  5. 非叶子节点(non leaf):度不为 0 的节点

  6. 层数(level):根节点在第 0 层,往下以此类推(也可从 1 开始计数)

  7. 节点的深度(depth):从根节点到当前节点的唯一路径上的节点数

  8. 节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数

  9. 树的深度:所有节点深度中的最大值

  10. 树的高度:所有节点高度中的最大值

二叉树的延伸:

  1. 满二叉树(国内叫真二叉树):所有节点的度要么为 0 ,要么为 2

  2. 完美二叉树(国内叫满二叉树):所有节点的度要么为 0 ,要么为 2 ,且所有叶子节点都在最后一层

  3. 完全二叉树:叶子节点只会出现在最后 2 层,且最后 1 层的叶子节点都靠左对齐

二叉树的性质:

  1. 非空二叉树的第 i 层:最多有 2 ^ (i - 1) 个节点(i >= 1)

  2. 高度为 h 的二叉树:最多有 2 ^ h - 1 个节点(h >= 1)

  3. 任意一棵非空二叉树,设叶子节点为 n0 ,度为 2 的节点为 n2 ,则有:n0 = n2 + 1

  4. 高度为 h 的完全二叉树:最少有 2 ^ (h - 1) 个节点(h >= 1),最多则和完美二叉树一样

  5. 给定一棵总节点数为 n 的完全二叉树,则有:

    叶子节点个数:n0 = (n + 1) / 2

    非叶子节点个数:n1 + n2 = n / 2

下文对于满二叉树(国内称真二叉树)统称满二叉树,完美二叉树(国内称满二叉树)统称完美二叉树。

IBinaryTree 接口设计

首先确定一棵简单二叉树应该有哪些成员,将之抽出为接口,如下:

interface IBinaryTree<E> : IBinaryTreeTraversal<E> {

    /**
     * 总节点数量
     */
    val size: Int

    /**
     * 是否空树
     */
    fun isEmpty(): Boolean
    
    /**
     * 是否包含元素
     *
     * @return true 如果包含,false 不包含
     */
    fun contains(item: E): Boolean

    /**
     * 清空整棵树
     */
    fun clear()

    /**
     * 获取树的高度
     */
    fun height(): Int

    /**
     * 是否满二叉树(国内叫真二叉树)
     */
    fun isFull(): Boolean

    /**
     * 是否完美二叉树(国内叫满二叉树)
     */
    fun isPerfect(): Boolean

    /**
     * 是否完全二叉树
     */
    fun isComplete(): Boolean
}

IBinaryTreeTraversal 使二叉树具备遍历功能,接口如下:

interface IBinaryTreeTraversal<E> {

    /**
     * 前序遍历
     */
    fun preorderTraversal(visitor: Visitor<E>)

    /**
     * 中序遍历
     */
    fun inorderTraversal(visitor: Visitor<E>)

    /**
     * 后序遍历
     */
    fun postorderTraversal(visitor: Visitor<E>)

    /**
     * 层序遍历
     */
    fun levelOrderTraversal(visitor: Visitor<E>)
}

abstract class Visitor<E> {

    /**
     * 停止遍历标记位,由二叉树内部控制,应对外部隐藏
     */
    internal var stop = false

    /**
     * 遍历回调
     *
     * @param item 元素
     * @return true 表示中止遍历,false 则继续
     */
    abstract fun visit(item: E): Boolean
}

IBinaryTree 接口实现

abstract class BinaryTree<E> : IBinaryTree<E> {
    
    /**
     * 根节点
     */
    protected open var root: ITreeNode<E>? = null
    
    /**
     * 总节点数量,仅内部可修改
     */
    override var size: Int = 0
        protected set

    protected interface ITreeNode<E> {

        var item: E

        var parent: ITreeNode<E>?

        var left: ITreeNode<E>?

        var right: ITreeNode<E>?

        val isLeaf: Boolean
            get() = left == null && right == null

        val hasTwoChildren: Boolean
            get() = left != null && right != null
    }
}

这里先简单实现了一些必要的东西,首先二叉树内部肯定需要维护一个根节点 root ;然后 size 这里把它改为仅子类与内部可修改值,这个是语法层面的问题;接着可以看到 ITreeNode 节点接口和实现类,成员分别是:元素、父节点、左节点和右节点,并且设置了两个变量来判断该节点是叶子节点还是度为 2 的节点。

那么接下来就要真正开始实现二叉树的功能了,往下看。

1. isEmpty

判断是否为空树,显然总节点数量为 0 就是空树。

override fun isEmpty(): Boolean {
    return size == 0
}

2. clear

清空整棵树,需要将 root 置空,这样 root 及 root 之下的子树就是不可达的,JVM 适时就会进行回收,然后 size 需要置 0 。

override fun clear() {
    root = null
    size = 0
}

before 3

由于接下来需要频繁用到层序遍历,因此这里需要先讲下层序遍历。顾名思义,层序遍历就是从指定节点开始从上到下一层一层的遍历,例如有这样一棵二叉树:

        7
      /   \
     4     9
    / \   / \
   2   5 8   11

使用层序遍历则它的输出结果是这样的:[7, 4, 9, 2, 5, 8, 11] 。

那么既然明白了什么是层序遍历,那代码应该怎么写呢?答案就是使用队列来辅助遍历,一个层序遍历的代码这样写:

private fun levelOrderTraversal(node: ITreeNode<E>?) {
    node ?: return // 传入节点为空,直接返回
    val queue = LinkedList<ITreeNode<E>>().also {
        it.offer(node)
    }
    while (queue.isNotEmpty()) {
        val poll = queue.poll()
        // 这里可以选择打印或者其他操作,因为已经拿到遍历节点了
        
        // 接下来需要判断左右节点,如果不为空则入队
        if (poll.left != null) {
            queue.offer(poll.left)
        }
        if (poll.right != null) {
            queue.offer(poll.right)
        }
    }
}

来对这段代码解释下,首先 ITreeNode 是我们上面定义的接口,它代表一个节点;如果传入的 node 为空那就没办法遍历了,这里直接返回;然后创建一个队列并将 node 入队,众所周知 LinkedList 实现了 Queue 接口,所以它支持队列操作;往下走就是一段 while 循环了,队列不为空就循环,由于一开始 node 就入队了,所以看看里面逻辑;首先将队头出队,以上面的二叉树例子,这个节点的元素应该是 7 ,接着就是判断左右子节点,很明显 49 是其左右节点,依次入队,下一次循环时可以发现先入队的也先出队了,所以取出顺序就是:4, 9, ... 。

层序遍历讲完了,那么可以回去功能实现阶段了。

3. height

首先说下获取树高度的思路,如果只有一个节点那么树高度就是 1 ;如果节点还有子节点的话,那么树高度就是 1 + 子节点的高度,当左右子树都存在的情况下其实就是拿高度最大的那个子树的高度加当前节点的高度,例如这样一棵二叉树:

        7
      /   \
     4     9
    / \
   2   5

7 这个节点的高度就是 1 ,然后可以看到左右子树中明显 4 这个节点的高度比右子树高,为 2 ,因此这棵树的高度就是 1 + 2 = 3

到这里可以看出来其实是下属节点高度一直累加上来,那么可以采取递归的方式来实现:

protected fun getHeightByRecursive(node: ITreeNode<E>?): Int {
    node ?: return 0 // 节点为空则没有高度
    return 1 + max(getHeightByRecursive(node.left), getHeightByRecursive(node.right))
}

那如果不喜欢递归呢?可以用上面刚学的层序遍历来完成,会稍微麻烦一些,如下:

protected fun getHeightByTraversal(node: ITreeNode<E>?): Int {
    node ?: return 0
    var height = 0
    var levelSize = 1 // 每一层节点数量
    val queue = LinkedList<ITreeNode<E>>().also {
        it.offer(node)
    }
    while (queue.isNotEmpty()) {
        // 每一轮遍历层节点数量都需要减一
        levelSize--
        val poll = queue.poll()
        if (poll.left != null) {
            queue.offer(poll.left)
        }
        if (poll.right != null) {
            queue.offer(poll.right)
        }
        // 当层节点数量为零时代表遍历完一层,于是高度加一
        if (levelSize == 0) {
            levelSize = queue.size
            height++
        }
    }
    return height
}

在层序遍历的基础上增加了两个变量,height 用来累加高度,levelSize 用来判断是否遍历完一层。

那么在实现 height() 的时候就有了两种选择:

override fun height(): Int {
    //return getHeightByRecursive(root)
    return getHeightByTraversal(root)
}

4. isFull

判断是否满二叉树,根据满二叉树定义与 ITreeNode 接口,可以很轻松写出如下代码,同样是层序遍历:

override fun isFull(): Boolean {
    val node = root ?: return false
    val queue = LinkedList<TreeNode<E>>().also {
        it.offer(node)
    }
    while (queue.isNotEmpty()) {
        val poll = queue.poll()
        // 非叶子节点或度不为 2 则树不为满二叉树
        if (!(poll.isLeaf || poll.hasTwoChildren)) {
            return false
        }
        if (poll.left != null) {
            queue.offer(poll.left)
        }
        if (poll.right != null) {
            queue.offer(poll.right)
        }
    }
    return true
}

5. isPerfect

判断是否完美二叉树,套用公式,高度为 h 的二叉树:最多有 2 ^ h - 1 个节点(h >= 1),用代码实现:

override fun isPerfect(): Boolean {
    val height = height()
    if (height == 0) {
        return false
    }
    return (2 shl (height - 1)) - 1 == size
}

这里用到了位运算,Java 中的 << , >> , >>> 分别对应 Kotlin 的 shl , shr , ushr ,左移一位为 2 次方,左移两位为 3 次方,以此类推。

6. isComplete

override fun isComplete(): Boolean {
    val node = root ?: return false
    var isLeafFound = false
    val queue = LinkedList<ITreeNode<E>>().also {
        it.offer(node)
    }
    while (queue.isNotEmpty()) {
        val poll = queue.poll()
        // 如果前面已经找到叶子节点,那么接下来的所有节点必须是叶子节点
        if (isLeafFound && !poll.isLeaf) {
            return false
        }
        if (poll.left != null) {
            queue.offer(poll.left)
        } else if (poll.right != null) {
            return false
        }
        if (poll.right != null) {
            queue.offer(poll.right)
        } else {
            // 一出现右子节点为空,那么接下来的所有节点都必须是叶子节点
            isLeafFound = true
        }
    }
    return true
}

层序遍历中,当发现以下情况就跳出循环,直接断定树非完全二叉树:

  1. 左子节点为空且右子节点不为空,如下

            7
          /   \
         4     9
        / \   / \
       2     8
    
  2. 右子节点为空,则接下来遍历到的节点必须都是叶子节点

7. 前序遍历

override fun preorderTraversal(visitor: Visitor<E>) {
    preorderTraversal(root, visitor)
}

/**
 * 递归方式的前序遍历
 */
protected fun preorderRecursive(node: ITreeNode<E>?, visitor: Visitor<E>) {
    node ?: return
    if (visitor.stop) return
    visitor.stop = visitor.visit(node.item)
    if (visitor.stop) return
    preorderRecursive(node.left, visitor)
    if (visitor.stop) return
    preorderRecursive(node.right, visitor)
}

/**
 * 非递归方式的前序遍历
 */
protected fun preorderTraversal(node: ITreeNode<E>?, visitor: Visitor<E>) {
    node ?: return
    val stack = LinkedList<ITreeNode<E>>().also {
        it.push(node)
    }
    while (stack.isNotEmpty()) {
        val pop = stack.pop()
        if (visitor.visit(pop.item)) {
            break
        }
        if (pop.right != null) {
            stack.push(pop.right)
        }
        if (pop.left != null) {
            stack.push(pop.left)
        }
    }
}
        7
      /   \
     4     9
    / \   / \
   2   5 8   11
   
前序遍历输出:[7, 4, 2, 5, 9, 8, 11]

顺序为中间节点,左子树,右子树

8. 中序遍历

override fun inorderTraversal(visitor: Visitor<E>) {
    inorderTraversal(root, visitor)
}

/**
 * 递归方式的中序遍历
 */
protected fun inorderRecursive(node: ITreeNode<E>?, visitor: Visitor<E>) {
    node ?: return
    if (visitor.stop) return
    inorderRecursive(node.left, visitor)
    if (visitor.stop) return
    visitor.stop = visitor.visit(node.item)
    if (visitor.stop) return
    inorderRecursive(node.right, visitor)
}

/**
 * 非递归方式的中序遍历
 */
protected fun inorderTraversal(node: ITreeNode<E>?, visitor: Visitor<E>) {
    var _node: ITreeNode<E>? = node ?: return
    val stack = LinkedList<ITreeNode<E>>()
    while (_node != null || stack.isNotEmpty()) {
        if (_node != null) {
            stack.push(_node)
            _node = _node.left
        } else {
            _node = stack.pop()
            if (visitor.visit(_node.item)) {
                break
            }
            _node = _node.right
        }
    }
}
        7
      /   \
     4     9
    / \   / \
   2   5 8   11
   
中序遍历输出:[2, 4, 5, 7, 8, 9, 11]

顺序为左子树,中间节点,右子树

9. 后序遍历

override fun postorderTraversal(visitor: Visitor<E>) {
    postorderTraversal(root, visitor)
}

/**
 * 递归方式的后序遍历
 */
protected fun postorderRecursive(node: ITreeNode<E>?, visitor: Visitor<E>) {
    node ?: return
    if (visitor.stop) return
    postorderRecursive(node.left, visitor)
    if (visitor.stop) return
    postorderRecursive(node.right, visitor)
    if (visitor.stop) return
    visitor.stop = visitor.visit(node.item)
}

/**
 * 非递归方式的后序遍历
 */
protected fun postorderTraversal(node: ITreeNode<E>?, visitor: Visitor<E>) {
    node ?: return
    val inStack = LinkedList<ITreeNode<E>>().also {
        it.push(node)
    }
    val outStack = LinkedList<ITreeNode<E>>()
    while (inStack.isNotEmpty()) {
        val pop = inStack.pop()
        outStack.push(pop)
        if (pop.left != null) {
            inStack.push(pop.left)
        }
        if (pop.right != null) {
            inStack.push(pop.right)
        }
    }
    while (outStack.isNotEmpty()) {
        val pop = outStack.pop()
        if (visitor.visit(pop.item)) {
            break
        }
    }
}
        7
      /   \
     4     9
    / \   / \
   2   5 8   11
   
中序遍历输出:[2, 5, 4, 8, 11, 9, 7]

顺序为左子树,右子树,中间节点

10. 层序遍历

层序遍历在上面就讲过了,拿来就用

override fun levelOrderTraversal(visitor: Visitor<E>) {
    val node = root ?: return
    val queue = LinkedList<ITreeNode<E>>().also {
        it.offer(node)
    }
    while (queue.isNotEmpty()) {
        val poll = queue.poll()
        if (visitor.visit(poll.item)) {
            return
        }
        if (poll.left != null) {
            queue.offer(poll.left)
        }
        if (poll.right != null) {
            queue.offer(poll.right)
        }
    }
}

关于前中后序遍历,如果使用递归方式,其实就是移动访问元素的那一行代码,倒也直观;而非递归方式则用到了栈,由于栈是先进后出,而前中后序遍历的区别其实就是调整中间节点的访问时机,因此只要调整好中间节点入栈出栈的时机就能够实现。

11. contains

override fun contains(item: E): Boolean {
    return toListByLevelOrder().contains(item)
}

fun <E> IBinaryTreeTraversal<E>.toListByLevelOrder(): List<E> {
    val list = when (this) {
        is IBinaryTree<E> -> ArrayList<E>(size)
        else -> ArrayList<E>()
    }
    levelOrderTraversal(object : Visitor<E>() {
        override fun visit(item: E): Boolean {
            list.add(item)
            return false
        }
    })
    return list
}

这里默认先实现,采用层序遍历转 List 后再查交集,也可以用其他方式遍历。

到目前为止,这个二叉树类已经具备部分功能了,但可以发现它没有添加元素的地方,而这就是接下来的重点——实现二叉搜索树。

IBinarySearchTree 接口设计

interface IBinarySearchTree<E> : IBinaryTree<E> {
    
    /**
     * 往树添加元素
     *
     * @return true 如果添加成功,false 则失败
     */
    fun add(item: E): Boolean
    
    /**
     * 从树中移除元素
     *
     * @return true 如果移除成功,false 则失败
     */
    fun remove(item: E): Boolean
    
    /**
     * 根据指定元素寻找前驱
     */
    fun predecessorOf(item: E): E?
    
    /**
     * 根据指定元素寻找后继
     */
    fun successorOf(item: E): E?
}

由于二叉搜索树也是二叉树,因此需要继承 IBinaryTree 接口,并定义了操作方法。

IBinarySearchTree 接口实现

首先先实现下二叉搜索树的基础结构,如下:

open class BST<E> : BinaryTree<E>, IBinarySearchTree<E> {

    private var comparator: Comparator<E>? = null

    constructor() : this(null)

    constructor(comparator: Comparator<E>?) {
        this.comparator = comparator
    }

    private fun compare(e1: E, e2: E): Int {
        val cmp = comparator
        if (cmp != null) {
            return cmp.compare(e1, e2)
        }
        return (e1 as Comparable<E>).compareTo(e2)
    }

    protected open fun createNode(item: E, parent: ITreeNode<E>?): ITreeNode<E> {
        return TreeNode(item, parent)
    }

    private class TreeNode<E>(
        override var item: E,
        override var parent: ITreeNode<E>?
    ) : ITreeNode<E> {
        override var left: ITreeNode<E>? = null
        override var right: ITreeNode<E>? = null
    }
}

首先定义了一个具体的节点类和创建方法,接着就是对于元素比较的逻辑了,一般将小的放左子树,大的放右子树。默认需要传入元素具备可比较性,如果有传入自定义的比较器则不强求。

接下来开始实现二叉搜索树逻辑,由于大量的二叉树逻辑我们在上面已经实现了,因此其实这里并不会有太多代码,但还是得注意一些情况。

1. add

在开始写代码之前,可以先思考一下添加过程中需要注意哪些情况,这里列一下:

  1. 传入元素不能为空

    很简单,因为需要比较后判断该元素的插入位置。

  2. 根节点为空

    需要先创建根节点。

  3. 元素应该放在左子树还是右子树?还是左子树的左子树的左子树.....

    如果元素比当前节点小,那么就需要一直往左子树循环查找。

  4. 待插入元素已存在树中

    这种情况一般替换一下即可。

基于上面的几种情况,可以开始写代码了,如下:

override fun add(item: E): Boolean {
    // 不允许元素为空
    require(item != null) {
        "The item must not be null."
    }
    var node: ITreeNode<E>? = root
    if (node == null) {
        // 根节点为空需要先创建根节点
        node = createNode(item, null)
        root = node
        size++
        return true
    }
    var cmp = 0
    var parent = node
    while (node != null) {
        // 每次循环都提前将 parent 赋值,如果拿 node 当 parent 是不行的
        parent = node
        cmp = compare(item, node.item)
        if (cmp < 0) {
            // 往左子树找
            node = node.left
        } else if (cmp > 0) {
            // 往右子树找
            node = node.right
        } else {
            // 存在相同元素,替换
            node.item = item
            return true
        }
    }
    val newNode = createNode(item, parent)
    // 根据比较结果插入元素
    if (cmp < 0) {
        parent?.left = newNode
    } else {
        parent?.right = newNode
    }
    size++
    return true
}

这样就实现了上面概括的几种情况了,添加逻辑也就完成。

2. contains

在父类 BinaryTree 只是简单将树转为 List 然后判断是否存在元素,但对于二叉搜索树来说这样效率太低了,因为我们是有排序的并且天生支持二分查找,那么就需要重写这个方法以更合理的方式来编写交集逻辑。

首先思考一下 List 的 contains 做法,其实就是遍历数组,那我们的做法也一样,遍历二叉树,但这棵树又是排序树,意味着需要写一个可以查找节点的方法,这个方法支持对传入元素进行节点查找。

private fun node(item: E): ITreeNode<E>? {
    var node = root
    while (node != null) {
        val cmp = compare(item, node.item)
        if (cmp < 0) {
            node = node.left
        } else if (cmp > 0) {
            node = node.right
        } else {
            return node
        }
    }
    return null
}

查找节点逻辑写好了,那么 contains 就简单处理了,如果找的到节点则表示树中有这个元素。

override fun contains(item: E): Boolean {
    return node(item) != null
}

3. predecessorOf

根据指定元素查找它的前驱,基于节点的查找,同样的需要用到上面写好的 node() 方法。

那么什么是树节点的前驱呢?其实就是中序遍历时元素的前一个就是它的前驱,例如:

        7
      /   \
     4     9
    / \   / \
   2   5 8   11
   
中序遍历输出:[2, 4, 5, 7, 8, 9, 11]   

        7
      /   \
    4       9
   / \     / \
  2   5   8   11
 / \   \     /  \
1   3   6   10  12
 
中序遍历输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]   

7 的前驱分别是 5 和 6 ,通过观察可以发现,前驱都是在左子树的最右子节点中,但仍然存在一些特殊情况。

例如找一下 8 的前驱,你会发现上面发现的规律行不通了,因为 8 没有左子树,基于这种情况则需要从当前节点开始一直向上寻找,直到父节点或祖父节点等于父节点(祖父节点)的父节点的右子树;当然在向上找的过程中可能出现父节点(祖父节点)的父节点为空的情况,这时候父节点(祖父节点)就是前驱了。

override fun predecessorOf(item: E): E? {
    return predecessor(node(item))?.item
}

private fun predecessor(node: ITreeNode<E>?): ITreeNode<E>? {
    var _node: ITreeNode<E>? = node ?: return null
    if (_node?.left != null) {
        // 有左子树,前驱要么是当前左节点要么是左节点中最右的那个节点
        _node = _node.left
        while (_node?.right != null) {
            // 右子树不为空的情况下一直向右找
            _node = _node.right
        }
        return _node
    }
    // 来到这里就对应着上面说到的 8 的前驱这种情况,只需要一直循环向上找
    while (_node?.parent != null && _node != _node.parent?.right) {
        _node = _node.parent
    }
    // 当循环退出时,_node 可能是前驱的子节点,也可能 _node?.parent 等于空,例如寻找 1 的前驱
    return _node?.parent
}

4. successorOf

寻找后继,逻辑与前驱刚好相反,例如:

        7
      /   \
     4     9
    / \   / \
   2   5 8   11
   
中序遍历输出:[2, 4, 5, 7, 8, 9, 11]   

        7
      /   \
    4       9
   / \     / \
  2   5   8   11
 / \   \     /  \
1   3   6   10  12
 
中序遍历输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]   

7 的后继就是 8 ,直接看代码,其实就是将找前驱的逻辑反过来即可。

override fun successorOf(item: E): E? {
    return successor(node(item))?.item
}

/**
 * 寻找后继,逻辑和寻找前驱相反。
 */
private fun successor(node: ITreeNode<E>?): ITreeNode<E>? {
    var _node: ITreeNode<E>? = node ?: return null
    if (_node?.right != null) {
        _node = _node.right
        while (_node?.left != null) {
            _node = _node.left
        }
        return _node
    }
    while (_node?.parent != null && _node != _node.parent?.left) {
        _node = _node.parent
    }
    return _node?.parent
}

5. remove

删除逻辑要写的代码比较多,但可以总结出来以下几种情况:

  1. 节点度为 2 的情况
  2. 节点度为 1 的情况
  3. 节点是根节点
  4. 节点是叶子节点

直接上代码

override fun remove(item: E): Boolean {
    return remove(node(item))
}

private fun remove(node: ITreeNode<E>?): Boolean {
    var _node = node ?: return false
    // 处理度为 2 的情况
    if (_node.hasTwoChildren) {
        // 找到后继节点,前驱当然也可以,选其一
        val succ = successor(_node)!!
        // 将后继的元素赋值给要删除的节点
        _node.item = succ.item
        // 使要删除的节点指向后继,这时候 _node 其实可以算删除了
        // 因为我们做的就是找到后继替代它,那么到这里就变成了需要删除
        // 后继节点
        _node = succ
    }
    // 来到这里就只有两种情况,度为 1 或度为 0 ,
    // 因此直接拿待删除节点的左节点(如果为空就拿右节点)
    val replacement = _node.left ?: _node.right
    if (replacement != null) {
        // 能来到这里表示度为 1
        val _nodeParent = _node.parent
        // 第一步先拿待删除节点的父节点赋值给替代节点
        replacement.parent = _nodeParent
        if (_nodeParent == null) {
            // 待删除节点的父节点是可能为空的,如下:
            // _node(root)
            //     \
            //    replacement
            // 这种情况直接使根节点指向替代节点即可
            root = replacement
        } else if (_node == _nodeParent.left) {
            // 待删除节点在父节点的左子树
            _node.parent?.left = replacement
        } else {
            // 待删除节点在父节点的右子树
            _node.parent?.right = replacement
        }
    } else if (_node.parent == null) {
        // 来到这里其实就是这棵树只有一个节点,也就是根节点
        // 因此删除节点其实就是删除根节点
        root = null
    } else {
        // 来到这里表示待删除节点是叶子节点
        // 直接判断待删除节点是它爸爸的左子树还是右子树然后置空
        if (_node == _node.parent?.left) {
            _node.parent?.left = null
        } else {
            _node.parent?.right = null
        }
    }
    size--
    return true
}

到这里,二叉搜索树就真正编写完成了,那既然写完了总该测试吧?总不能每次都中序遍历出来看吧?这样就太不直观了。

这里在李明杰老师那里拿了个二叉树打印器,实现完接口就能够在控制台输出二叉树了,附上 Github 地址:二叉树打印器

打印二叉树

实现 BinaryTreeInfo 接口

open class BST<E> : BinaryTree<E>, IBinarySearchTree<E>, BinaryTreeInfo {

    // 省略其他逻辑...

    override fun root(): Any? {
        return root
    }

    override fun left(node: Any?): Any? {
        return (node as? ITreeNode<E>)?.left
    }

    override fun right(node: Any?): Any? {
        return (node as? ITreeNode<E>)?.right
    }

    override fun string(node: Any?): Any? {
        return (node as? ITreeNode<E>)?.item
    }
}

接着编写代码测试:

fun main() {
    val array = intArrayOf(7, 4, 2, 1, 3, 5, 9, 8, 11, 10, 12, 6)
    val tree = BST<Int>()
    array.forEach {
        tree.add(it)
    }
    tree.println()
    val predOrSucc = 7
    println(
        """
        height: ${tree.height()}
        isFull: ${tree.isFull()}
        isPerfect: ${tree.isPerfect()}
        isComplete: ${tree.isComplete()}
        preorder: ${tree.toListByPreorder()}
        inorder: ${tree.toListByInorder()}
        postorder: ${tree.toListByPostorder()}
        levelOrder: ${tree.toListByLevelOrder()}
        predecessorOf($predOrSucc): ${tree.predecessorOf(predOrSucc)}
        successorOf($predOrSucc): ${tree.successorOf(predOrSucc)}
    """.trimIndent())
}

print.png

结尾

到这里,整篇文章就结束了,完整的代码都已经上传 Github ,接下来要学习 AVL树 了,等学完后再写一篇手写 AVL 树,see you next time ~