JavaScript 实现经典数据结构之树

404 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前两节介绍了栈、队列以及链表,不同于它们,树是非线性的。因为树决定了其存储的数据直接有明确的层级关系,因此对于维护具有层级特性的数据,树是一个天然良好的选择。

树具有以下特点:

  • 除了根节点以外,所有的节点都有一个父节点;
  • 每一个节点都可以有若干子节点,如果没有子节点,则称此节点为叶子节点;
  • 一个节点所拥有的叶子节点的个数,称之为该节点的度,因此叶子节点的度为 0;
  • 所有节点中,最大的度为整棵树的度;
  • 树的最大层次称为树的深度。

二叉树是最基本的树,它的结构最简单,每个节点至多包含两个子节点。根据二叉树可以延伸出二叉搜索树、平衡二叉搜索树、红黑树等。因此,以下针对二叉树搜索树来讨论。

二叉搜索树的特点有:

  • 左子树上所有结点的值均小于或等于它的根结点的值;
  • 右子树上所有结点的值均大于或等于它的根结点的值;
  • 左、右子树也分别为二叉搜索树。

构造节点类

class Node() {
    constructor(data) {
        this.left = null
        this.right = null
        this.value = data
    }
}

insertNode:根据父节点,插入一个子节点

insertNode(root, newNode) {
    // 根据待插入节点的值的大小,递归调用 insertNode
    if(newNode.value < root.value) {
        !root.left ? root.left = newNode : this.insertNode(root.left, newNode)
    } else {
        !root.right ? root.right = newNode : this.insertNode(root.right, newNode)
    }
}

insert:插入一个新节点

insert(value) {
    let newNode = new Node(value)
    if(!this.root) {
        this.root = newNode
    } else {
        this.insertNode(this.root, newNode)
    }
}

insertNode 方法先判断目标父节点和插入节点的值,如果插入节点的值更小,则放在父节点的左边,递归调用 this.insertNode(root.left, newNode) ;如果插入节点的值更大,以此类推即可。而 insert 只是多了一步构造 Node 节点实例,然后区分有无父节点的情况,调用 this.insertNode 即可。

removeNode:根据一个父节点,移除一个子节点

removeNode(root, value) {
    if(!root) {
        return null
    }
    if(value < root.value) {
        root.left = this.removeNode(root.left, value)
        return root
    } else if(value > root.value) {
        root.right = this.removeNode(root.right, value)
        return root
    } else {
        // root.value === value
        if(!root.left && !root.right) { // 当前root无左右子节点
            root = null
            return root
        } 
        if(root.left && !root.right) { // 当前root只有左节点
            root = root.left
            return root
        } else if(root.right) { // 当前root只有右节点
            root = root.right
            return root
        }
        // 左右节点都有
        let minRight = this.findMinNode(root.right) 
        root.value = minRight.value
        root.right = this.removeNode(root.right, minRight.value)
        return root
    } 
}

// 找最小的节点
findMinNode(root) {
    if(!root.left) {
        return root
    } else {
        return this.findMinNode(root.left)
    }
}

searchNode:根据一个父节点,查找子节点

searchNode(root, value) {
    if(!root) {
        return null
    } 
    if(value < root.value) {
        return this.searchNode(root.left, value)
    } else if(value > root.value) {
        return this.searchNode(root.right, value)
    }
    return root
}

preOrder:前序遍历

preOrder(root) {
    if(root) {
        console.log(root.value)
        this.preOrder(root.left)
        this.preOrder(root.right)
    }
}

inOrder:中序遍历

inOrder(root) {
    if (root) {
        this.inOrder(root.left)
        console.log(root.value)
        this.inOrder(root.right)
    }
}

postOrder:后序遍历

postOrder(root) {
    if (root) {
        this.postOrder(root.left)
        this.postOrder(root.right)
        console.log(root.value)
    }
}

上述前、中、后序遍历的区别其实就在于 console.log(root.value) 方法执行的位置

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。