二叉搜索树(三,层序遍历 深度优先 和 广度优先)

2,093 阅读9分钟

本篇文章主要介绍的是二叉搜索树的层序遍历。

首先介绍层序遍历背后的重要概念:

  • 深度优先
  • 广度优先

什么是深度优先遍历?

深度优先遍历:简单的说就是深度优先,我们的二叉搜索树不管是前序,还是中序,还是后序,他们的遍历都是以深度优先进行的。看下面这个二叉搜索树: image.png

我们之前是先查找28,然后是16、13,退回16到22,然后一直退回到28,再去搜索30、29、42,这样子进行的。

我们会先从一个节点找到底,在找另一个节点,这就是深度优先遍历。

什么是广度优先遍历?

广度优先遍历:简单的说就是广度优先,广度优先遍历又叫层序遍历。

广度优先遍历是以广度优先进行的。看下面这个二叉搜索树:

image.png

我们的广度优先查找的顺序是:

  • 先查找根节点 28,然后查找28左子树16。
  • 查找结束回退到28查找右子树30。
  • 然后查找16的左子树13,再找右子树22。
  • 然后找30左子树29,再找右子树42。

我们会发现广度优先是按照层级一层一层查找,而不是像深度优先直接找到一个节点的最深层。

如何实现广度优先遍历?

通常我们实现一个广度优先遍历需要一个队列的数据结构,队列数据结构有一个特点是先进先出,后进后出。

还是以上面的二叉搜索树举例:

  • 我们的队列数据结构是数组,我们先将数组中存储根节点28,进行搜索。
  • 搜索过后发现28不是想要数据,就将这个28弹出队列,也就是从数组中删除。
  • 然后是将左子树和右子树存储进数组中,进行搜索。
  • 搜索左子树16不对应,弹出左子树16,将16的左右子树存储进数组。
  • 这时候我们的数组中是有右子树30,左子树13和右子树22,我们继续搜索,30不是对应的数据将30弹出队列,将30左右子树分别存储进数组。
  • 此时数组中有13、22、29、42,依次搜索,直到搜索完毕。

下图是16弹出队列时,front队列中存在的元素: image.png 下图是到30弹出队列时,队列front中还剩下的元素: image.png

代码:

// 以node为根节点的二叉搜索树,插入节点(key,value)
// 返回插入新节点后的二叉搜索树
class Node {
    key
    value
    left
    right
    root = null
    count = 0
    constructor(key, value) {
        this.key = key
        this.value = value
        this.left = this.right = null
    }
    size() {
        return this.count;
    }
    isEmpty() {
        return this.count === 0;
    }
    // 层序遍历
    // 二叉搜索树的广度优先遍历
    /*
        node  :二叉搜索树
        value :要搜索的值
        front :队列
    */
    levelOrder(node, value) {
        let front = []
        front.push(node)
        while (front.length > 0) {
            // 每次取出第一个
            const currentNode = front.shift()
            if (currentNode.key === value) {
                console.log(currentNode)
                return true
            } else {
                // 操作当前 node
                if (currentNode.left !== null) {
                    front.push(currentNode.left)
                }
                if (currentNode.right !== null) { 
                    front.push(currentNode.right)
                }
            }
            return false
        }
    }
}
// 数据结构是这样的
/*
    可能需要 插入/查找 到某个key对应的位置
*/
let node = {
    key: 28,
    left: {
        key: 16,
        left: { key: 13, left: null, right: null },
        right: { key: 22, left: null, right: null }
    },
    right: {
        key: 30,
        left: { key: 29, left: null, right: null },
        right: { key: 42, left: null, right: null }
    }
}

二叉搜索树 删除节点

我们搜索一个节点很容易,删除一个节点也很容易,难得是删除之后,如何操作一个节点下面的左子树和右子树?

我们先来思考如果我要删除的节点是二分搜索树的最小值和最大值是怎么样的?

  • 从根节点28开始,最下面的左子树就是最小值
  • 从根节点28开始,最下面的右子树就是最大值

搜索最小值的节点:

// 返回最小值的节点
// 在以node为根的二叉搜索树中,返回最小键值的节点。
function _minimum(node) {
    if (node.left === null) {
        return node
    }
    return this._minimum(node.left)
}
let node = {
    key: 28,
    left: {
        key: 16,
        left: { key: 13, left: null, right: null },
        right: { key: 22, left: null, right: null }
    },
    right: {
        key: 30,
        left: { key: 29, left: null, right: null },
        right: { key: 42, left: null, right: null }
    }
}
_minimum(node)
// {key: 13, left:null, right:null}

搜索最大值的节点:

function _maximum(node) {
    if (node.right === null) {
        return node
    }
    return this._maximum(node.right)
}
let node = {
    key: 28,
    left: {
        key: 16,
        left: { key: 13, left: null, right: null },
        right: { key: 22, left: null, right: null }
    },
    right: {
        key: 30,
        left: { key: 29, left: null, right: null },
        right: { key: 42, left: null, right: null }
    }
}
_maximum(node)
// {key: 42, left: null, right: null}

搜索二叉搜索树的最大值和最小值我们已经明白了,删除二叉搜索树的最小值如何实现?

看下面这个图: 思考如果删除了22这个最小值,如何处理33和之后的节点?

image.png 如果删除22这个节点,就需要将33这个接待你转移到22这个位置,最后就是下图:

image.png

利用类似的思路来想一下如何删除下图中的二分搜索树的最大值: image.png 想必你也能知道,只要删除了58,将50放到58的位置就可以了。 image.png 代码实现:

// 删除以node为根的二叉搜索树中最小节点。
// 返回删除节点后新的二叉搜索树
function _removeMin(node) {
    if (node.left === null) {
        // 检测是否存在右节点
        const nodeRight = node.right;
        delete node;
        return nodeRight;
    }
    // 删除最后的left节点
    // 删除之后给到之前的left节点上
    node.left = this._removeMin(node.left)
    return node;
}
let node = {
    key: 28,
    left: {
        key: 16,
        left: { key: 13, left: null, right: null },
        right: { key: 22, left: null, right: null }
    },
    right: {
        key: 30,
        left: { key: 29, left: null, right: null },
        right: { key: 42, left: null, right: null }
    }
}

接下来就是删除二分搜索树的最大节点,直接上代码:

function _removeMax(node) {
    if (node.right === null) {
        const nodeRight = node.right
        delete node
        return nodeRight
    }
    node.right = this._removeMax(node.right)
    return node;
}
let node = {
    key: 28,
    left: {
        key: 16,
        left: { key: 13, left: null, right: null },
        right: { key: 22, left: null, right: null }
    },
    right: {
        key: 30,
        left: { key: 29, left: null, right: null },
        right: { key: 42, left: null, right: null }
    }
}

删除最小值和最大值,就不需要考虑他们下面是否还会存在,或者存在两个节点的情况了。

那么,如果我们要删除任意一个节点,这个节点会有左右两个子树,子树下还有子树怎么办?

就像是下面这张图: 我么要删除58这个节点,该如何操作? image.png

这就需要用到另一个算法:Hubbard Deletion

如果我们删除58,那谁是来代替58位置的?

  • 其实应该是58右子树中的最小值,这样才能满足,大于删除节点的左子树,小于删除节点的右子树。
  • 58右子树的所有节点都会大于50,而小于60的,只有60的左子树。

如果s是节点59,d是节点58:

  • 首先要找到节点60的后继节点59,删除这个节点并保存。
  • 将59的right设置为:60删除59之后的节点(58当前的right)。
  • 59的left设置为58的left。
  • 最后将59的整体节点把58的替换掉。 image.png

代码实现:

// 删除以node为根的二叉搜索树中值为value的节点。
// 返回删除即诶单后新的二分搜索树的根。
function _remove(node, value) {
    if (node === null) {
        return null
    }
    if (value < node.key) {
        node.left = this._remove(node.left, value)
        return node
    }
    if (value > node.key) {
        node.right = this._remove(node.right, value)
        return node
    }
    // 开始我们的删除逻辑
    if (value === node.key) {
        if (node.left === null) {
            const nodeRight = this._remove(node.right)
            delete node
            return nodeRight
        }
        if (node.right === null) {
            const nodeLeft = this._remove(node.left)
            delete node
            return nodeLeft
        }
        // node的查找后继
        const successor = this._minimum(node.right)
        // 给后继的这个节点,赋值左右子树
        successor.right = this._removeMin(node.right)
        successor.left = node.left
        delete node
        return successor
    }
}
// 返回最小值的节点
// 在以node为根的二叉搜索树中,返回最小键值的节点。
function _minimum(node) {
    if (node.left === null) {
        return node
    }
    return this._minimum(node.left)
}
// 删除以node为根的二叉搜索树中最小节点。
// 返回删除节点后新的二叉搜索树
function _removeMin(node) {
        if (node.left === null) {
            // 检测是否存在右节点
            const nodeRight = node.right;
            delete node;
            return nodeRight;
        }
        // 删除最后的left节点
        // 删除之后给到之前的left节点上
        node.left = this._removeMin(node.left)
        return node;
}
let node = {
    key: 41,
    left: {
        key: 22,
        left: {
            key: 15,
            left: { key: 13, left: null, right: null },
            right: null
        },
        right: {
            key: 33,
            left: null,
            right: { key: 37, left: null, right: null }
        }
    },
    right: {
        key: 58,
        left: {
            key: 50,
            left: { key: 42, left: null, right: null },
            right: { key: 53, left: null, right: null }
        },
        right: {
            key: 60,
            left: { key: 59, left: null, right: null },
            right: {key: 63, left: null, right: null}
        }
    }
}

比较完全的代码:

// 以node为根节点的二叉搜索树,插入节点(key,value)
// 返回插入新节点后的二叉搜索树
class Node {
    key
    value
    left
    right
    root = null
    count = 0
    constructor(key, value) {
        this.key = key
        this.value = value
        this.left = this.right = null
    }
    size() {
        return this.count;
    }
    isEmpty() {
        return this.count === 0;
    }
    // 寻找最小键值
    minimum() {
        if (this.count === 0) { throw new Error("node is null") }
        const minNode = this._minimum(this.root)
        return minNode.key
    }
    // 寻找最大键值
    maximum() {
        if (this.count === 0) { throw new Error("node is null") }
        const minNode = this._maximum(this.root)
        return minNode.key
    }
    // 删除最小值所在节点
    removeMin() {
        if (this.root !== null) {
            this.root = this._removeMin(this.root)
        }
    }

    remove(key) {
        this.root = this._remove(this.root, key)
    }
    // 删除最大值所在节点
    removeMax() {
        if (this.root !== null) {
            this.root = this._removeMax(this.root)
        }
    }
    // 返回最小值的节点
    // 在以node为根的二叉搜索树中,返回最小键值的节点。
    _minimum(node) {
        if (node.left === null) {
            return node
        }
        return this._minimum(node.left)
    }
    // 返回最大值节点
    // 在以node为根的二叉搜索树中,返回最大键值的节点。
    _maximum(node) {
        if (node.right === null) {
            return node
        }
        return this._maximum(node.right)
    }
    // 删除以node为根的二叉搜索树中最小节点。
    // 返回删除节点后新的二叉搜索树
    _removeMin(node) {
        if (node.left === null) {
            // 检测是否存在右节点
            const nodeRight = node.right;
            delete node;
            return nodeRight;
        }
        // 删除最后的left节点
        // 删除之后给到之前的left节点上
        node.left = this._removeMin(node.left)
        return node;
    }
    // 删除以node为根的二叉搜索树中最大节点。
    // 返回删除节点后新的二叉搜索树
    _removeMax(node) {
        if (node.right === null) {
            const nodeRight = node.right
            delete node
            return nodeRight
        }
        node.right = this._removeMax(node.right)
        return node;
    }
    // 删除以node为根的二叉搜索树中值为value的节点。
    // 返回删除即诶单后新的二分搜索树的根。
    _remove(node, value) {
        if (node === null) {
            return null
        }
        if (value < node.key) {
            node.left = this._remove(node.left, value)
            return node
        }
        if (value > node.key) {
            node.right = this._remove(node.right, value)
            return node
        }
        // 开始我们的删除逻辑
        if (value === node.key) {
            if (node.left === null) {
                const nodeRight = this._remove(node.right)
                delete node
                return nodeRight
            }
            if (node.right === null) {
                const nodeLeft = this._remove(node.left)
                delete node
                return nodeLeft
            }
            // node的查找后继
            const successor = this._minimum(node.right)
            // 给后继的这个节点,赋值左右子树
            successor.right = this._removeMin(node.right)
            successor.left = node.left
            delete node
            return successor
        }
    }
}
// 数据结构是这样的
/*
    可能需要 插入/查找 到某个key对应的位置
*/
let node = {
    key: 41,
    left: {
        key: 22,
        left: {
            key: 15,
            left: { key: 13, left: null, right: null },
            right: null
        },
        right: {
            key: 33,
            left: null,
            right: { key: 37, left: null, right: null }
        }
    },
    right: {
        key: 58,
        left: {
            key: 50,
            left: { key: 42, left: null, right: null },
            right: { key: 53, left: null, right: null }
        },
        right: {
            key: 60,
            left: { key: 59, left: null, right: null },
            right: {key: 63, left: null, right: null}
        }
    }
}

思考下面的如何实现?下面的就是前驱。 image.png

总结

主要是解决二叉搜索树删除的操作,给定任意值,如何删除一个节点,后面的节点该如何处理?

建议是打debugger看看,就好理解了。