数据结构-树结构(二叉树的封装)

1,325 阅读5分钟

昨天讲了二叉搜索树的概念,现在我们来实现一个自己的二叉树:
首先应该定义一个根属性root用来表示二叉搜索树的根

    constructor(){
      root = null
    }

然后就是开始插入节点,这里要判断插入节点时根属性是否为空,如果为空那么就将这个节点作为根节点,如果不为空就执行插入非空二叉树的操作(insertNode)。

   // 插入节点
   insert(key) {
     //创建一个新的节点对象
     const newNode = {
       key,
       left: null
       right: null
     }
     // 判断根节点是否为空
     if(root === null) {
       root = newNode
     } else {
       insertNode(root, newNode)  
     }
   }

插入非空二叉树操作

    // 将新节点插入到非空二叉搜索树中
      insertNode(node, newNode) {
        // 如果新节点的键值小于当前节点的键值,则向当前节点的左子树插入新节点
        if (newNode.key < node.key) {
          if (node.left === null) {
            node.left = newNode
          } else {
            insertNode(node.left, newNode)
          }
        } else {
          // 如果新节点的键值大于当前节点的键值,则向当前节点的右子树插入新节点
          if (node.right === null) {
            node.right = newNode
          } else {
            insertNode(node.right, newNode)
          }
        }
      }

接下来现实遍历操作,一般常见的遍历搜索二叉树的方式有三种:

  • 先序遍历
  • 后序遍历
  • 中序遍历
先序遍历

先序遍历指的是先遍历根节点,然后遍历左子树,再遍历右子树,如下图: image.png
遍历顺序为:11->7->5->3->6->9->8->10-15->13->12->14->20->18->25,具体代码实现:

    // 先序遍历
      preOrder(node = this.root) {
        if (node !== null) {
          console.log(node.key)
          this.preOrder(node.left)
          this.preOrder(node.right)
        }
      }

这里理解起来可能会有一点困难,就是连续使用递归来实现遍历。实际上这个过程就是不断的向函数调用栈中压入和弹出函数的过程,先压入参数为root.leftprevOrderTraversalNode的函数,直到下一个root.leftnull时,从函数调用栈中弹出当前函数,接着开始压入参数为root.rightprevOrderTraversalNode的函数,直到下一个root.leftnull时,返回上一个函数,如此循环就可以按顺序从左子树到右子树遍历得到每一个节点的值。

中序遍历

我们了解了先序遍历后,中序遍历就很好理解了,即先遍历左子树,再遍历根节点,最后遍历右子树。
image.png
遍历顺序为:3->5->6->7->8->9->10->11-15->12->13->14->18->20->25,代码实现就是把先序遍历的代码打印顺序颠倒一下:

    // 中序遍历
  inOrder(node = this.root) {
    if (node !== null) {
      this.inOrder(node.left)
      console.log(node.key)
      this.inOrder(node.right)
    }
后序遍历

这个就不用再解释了吧,先遍历右子树,再遍历根节点,最后遍历左子树。直接贴图和代码了:
image.png

    // 后序遍历
  postOrder(node = this.root) {
    if (node !== null) {
      this.postOrder(node.left)
      this.postOrder(node.right)
      console.log(node.key)
    }
  }

二叉搜索树的极值

二叉搜索树最大值最小值,其实我们在上面遍历的时候就已经找到了,关键点就是判断下一个左节点或者右节点是否为空。但是要注意先判断是否为空树,如果为空树直接返回null

    // 最大值
  max() {
    // 判断根节点是否为空
    if (this.root === null) {
      return null
    }
    const node = this.root
    while (node.right !== null) {
      node = node.right
    }
    return node.key
  }
  // 最小值
  min() {
    // 判断根节点是否为空
    if (this.root === null) {
      return null
    }
    const node = this.root
    while (node.left !== null) {
      node = node.left
    }
    return node.key
  }

搜索指定的值

还是拿一幅图来解释: image.png
我想判断3或者25或者任意一个数是否在这颗二叉搜索树里要怎么办?肉眼判断(手动狗头)。还记得你说家是唯一的城堡二分法查找吗,这里也同样适用。首先同样判断是否为空树,是的话直接返回false。再判断目标数字是否大于根节点11,大于往右找,小于往左找,等于直接返回,然后重复这一操作。代码表示为:

    // 查找
  search(key) {
    // 判断根节点是否为空
    if (this.root === null) {
      return false
    }
    // 创建一个临时变量,用于存储当前节点
    let current = this.root
    // 判断当前节点是否为空
    while (current !== null) {
      // 如果当前节点的键值小于要查找的键值,则向右子树查找
      if (current.key < key) {
        current = current.right
      } else if (current.key > key) {
        // 如果当前节点的键值大于要查找的键值,则向左子树查找
        current = current.left
      } else {
        // 如果找到了要查找的键值,则返回当前节点
        return current
      }
    }
    // 如果没有找到要查找的键值,则返回 null
    return null
  }

删除操作

删除操作相对于其他操作来说,稍微复杂了亿点点,要分四种情况。

  1. 如果要删除的节点没有左右子节点,即删除的节点是叶节点,则直接删除。
  2. 如果要删除的节点只有一个左子节点,则用左子节点替换要删除的节点。
  3. 如果要删除的节点只有一个右子节点,则用右子节点替换要删除的节点。
  4. 如果要删除的节点有两个子节点,则用右子节点的最小值替换要删除的节点。
    // 删除
  remove(key) {
    let current = this.root
    let parent = null
    let isLeftChild = false
    // 查找要删除的节点
    while (key != current) {
      parent = current
      if (key < current.key) {
        isLeftChild = true
        current = current.left
      } else {
        isLeftChild = false
        current = current.right
      }
      if (current == null) {
        return false
      }
    }
    // 如果要删除的节点没有左右子节点,则直接删除
    if (current.left == null && current.right == null) {
      if (current == this.root) {
        this.root = null
      } else if (isLeftChild) {
        parent.left = null
      } else {
        parent.right = null
      }
    }
    // 如果要删除的节点只有一个左子节点,则用左子节点替换要删除的节点
    else if (current.right == null) {
      if (current == this.root) {
        this.root = current.left
      } else if (isLeftChild) {
        parent.left = current.left
      } else {
        parent.right = current.left
      }
    }
    // 如果要删除的节点只有一个右子节点,则用右子节点替换要删除的节点
    else if (current.left == null) {
      if (current == this.root) {
        this.root = current.right
      } else if (isLeftChild) {
        parent.left = current.right
      } else {
        parent.right = current.right
      }
    }
    // 如果要删除的节点有两个子节点,则用右子节点的最小值替换要删除的节点
    else {
      let successor = this.getSuccessor(current)
      if (current == this.root) {
        this.root = successor
      } else if (isLeftChild) {
        parent.left = successor
      } else {
        parent.right = successor
      }
      successor.left = current.left
    }
  }

以上是具体代码,过程稍微有亿点点长,耐心看吧。最后贴上完整代码。

    class BinarySearchTree {
      constructor() {
        // 根属性
        this.root = null
      }
      // 插入节点
      insert(key) {
        // 创建一个新的节点
        const newNode = {
          key,
          left: null,
          right: null
        }
        // 如果根节点为空,则将新节点作为根节点
        if (this.root == null) {
          this.root = newNode
        } else {
          // 如果根节点不为空,则将新节点插入到二叉搜索树中
          this.insertNode(this.root, newNode)
        }
      }
      // 将新节点插入到非空二叉搜索树中
      insertNode(node, newNode) {
        // 如果新节点的键值小于当前节点的键值,则向当前节点的左子树插入新节点
        if (newNode.key < node.key) {
          if (node.left === null) {
            node.left = newNode
          } else {
            this.insertNode(node.left, newNode)
          }
        } else {
          // 如果新节点的键值大于当前节点的键值,则向当前节点的右子树插入新节点
          if (node.right === null) {
            node.right = newNode
          } else {
            this.insertNode(node.right, newNode)
          }
        }
      }
      // 先序遍历
      preOrder(node = this.root) {
        if (node !== null) {
          console.log(node.key)
          this.preOrder(node.left)
          this.preOrder(node.right)
        }
      }
      // 中序遍历
      inOrder(node = this.root) {
        if (node !== null) {
          this.inOrder(node.left)
          console.log(node.key)
          this.inOrder(node.right)
        }
      }
      // 后序遍历
      postOrder(node = this.root) {
        if (node !== null) {
          this.postOrder(node.left)
          this.postOrder(node.right)
          console.log(node.key)
        }
      }
      // 最大值
      max() {
        // 判断根节点是否为空
        if (this.root === null) {
          return null
        }
        const node = this.root
        while (node.right !== null) {
          node = node.right
        }
        return node.key
      }
      // 最小值
      min() {
        // 判断根节点是否为空
        if (this.root === null) {
          return null
        }
        const node = this.root
        while (node.left !== null) {
          node = node.left
        }
        return node.key
      }
      // 查找
      search(key) {
        // 判断根节点是否为空
        if (this.root === null) {
          return false
        }
        // 创建一个临时变量,用于存储当前节点
        let current = this.root
        // 判断当前节点是否为空
        while (current !== null) {
          // 如果当前节点的键值小于要查找的键值,则向右子树查找
          if (current.key < key) {
            current = current.right
          } else if (current.key > key) {
            // 如果当前节点的键值大于要查找的键值,则向左子树查找
            current = current.left
          } else {
            // 如果找到了要查找的键值,则返回当前节点
            return current
          }
        }
        // 如果没有找到要查找的键值,则返回 null
        return null
      }
      // 删除
      remove(key) {
        let current = this.root
        let parent = null
        let isLeftChild = false
        // 查找要删除的节点
        while (key != current) {
          parent = current
          if (key < current.key) {
            isLeftChild = true
            current = current.left
          } else {
            isLeftChild = false
            current = current.right
          }
          if (current == null) {
            return false
          }
        }
        // 如果要删除的节点没有左右子节点,则直接删除
        if (current.left == null && current.right == null) {
          if (current == this.root) {
            this.root = null
          } else if (isLeftChild) {
            parent.left = null
          } else {
            parent.right = null
          }
        }
        // 如果要删除的节点只有一个左子节点,则用左子节点替换要删除的节点
        else if (current.right == null) {
          if (current == this.root) {
            this.root = current.left
          } else if (isLeftChild) {
            parent.left = current.left
          } else {
            parent.right = current.left
          }
        }
        // 如果要删除的节点只有一个右子节点,则用右子节点替换要删除的节点
        else if (current.left == null) {
          if (current == this.root) {
            this.root = current.right
          } else if (isLeftChild) {
            parent.left = current.right
          } else {
            parent.right = current.right
          }
        }
        // 如果要删除的节点有两个子节点,则用右子节点的最小值替换要删除的节点
        else {
          let successor = this.getSuccessor(current)
          if (current == this.root) {
            this.root = successor
          } else if (isLeftChild) {
            parent.left = successor
          } else {
            parent.right = successor
          }
          successor.left = current.left
        }
      }
}