数据结构与算法 - 树、二叉搜索树

优质
小牛编辑
135浏览
2023-12-01

树是一种非线性的数据结构,以分层的方式存储数据,它对于存储需要快速查找的数据非常有用。 树是一种一对多的数据结构。树又有很多子集,比如:二叉树、二叉搜索树、2-3树、红黑树等等。

现实例子就是公司的组织架构,总裁为树的最顶端叫根节点,各部门按照领导人区分为子树。 在计算机科学中,HTML结构就是典型的树结构 image.png

树的节点可以有0个或多个子节点。当一棵树(的所有节点)最多只能有两个子节点时,这样的树被称为二叉树。 image.png

结点(root):指树中的一个元素; 叶子:度为0的结点,也称为终端结点; 层(levle):根在第一层,以此类推; 结点的度:指结点拥有的子树的个数,二叉树的度不大于2; 数的度:指树中的最大结点度数;如上图25 高度:叶子节点的高度为1,根节点高度最高;

取决于二叉树节点的组织方式,一棵二叉树又可以分为:

  • 完满二叉树(Full binary tree):除去叶节点,每个节点都有两个子节点。
  • 完全二叉树(Complete binary tree):除了最深一层之外,其余所有层的节点都必须有两个子节点。
  • 完美二叉树(Perfect binary tree):满足完全二叉树性质,即满二叉树,树的叶子节点均在最后一,

image.png

完满二叉树、完全二叉树与完美二叉树并不总是互斥的:

  • 完美二叉树 必然是 完满二叉树和完全二叉树
    • 完美的二叉树正好有 2 的 k 次方 减 1 个节点,其中 k 是树的最深一层(从1开始)
    • 深度为k的二叉树最多有2k-1个结点,2^k-1,如上图完美二叉树,深度为3,2^3-1=7。
  • 完全二叉树并不总是完满二叉树。
    • 正如上面的完全二叉树例子,最右侧的灰色节点是它父子点仅有的一个子节点。如果移除掉它,这棵树就既是完全二叉树,也是完满二叉树。(译者注:其实有了那个灰色节点的话,这颗树不能算是完全二叉树的,因为完满二叉树需要左对齐)
  • 完满二叉树并不一定是完全二叉树与完美二叉树。

二叉树遍历原理

二叉树的遍历:是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。 这里有两个关键词:访问和次序。

二叉树的遍历次序不同于线性结构,最多也就是从头到尾、循环和双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。

二叉树遍历方法

二叉树的遍历方式可以有很多,如果我们限制从左到右的顺序,就主要分为四种:

  • 前序遍历
  • 中序遍历
  • 后序遍历
  • 层序遍历

image.png

前序遍历中序遍历后序遍历
1. 访问根节点1. 中序遍历左子树1. 后序遍历左子树
2. 前序遍历左子树2. 访问根节点1. 后序遍历右子树
3. 前序遍历右子树3. 中序遍历右子树1. 访问根节点

一句话总结:

  • 先序 (根->左->右)
  • 中序 (左->根->右)
  • 后序 (左->右->根)

前序遍历

若二叉树为空,则空操作返回,否则,然后,再前树。 如下图遍历顺序为:A B D G H C E I F。

中序遍历

若二叉树为空,则空操作返回,否则从,中序遍历根结点左子树,然后访问根结点,最后中序遍历右子树。 如下图遍历顺序为:G D H B A E I C F。

后序遍历

若二叉树为空,则空操作返回,,最后是访问根结点,如下图遍历顺序为:G H D B I E F C A。

层序遍历

若二叉树为空,则空操作返回,否则从树的第一层开始,也就是从根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。 如下图遍历顺序为:A B C D E F G H I。

定义

  • 二叉搜索树 (BST, Binary Search Tree), 也称二叉排序树或二叉查找树。只允许你在值,在。
  • 二叉搜索树是一颗二叉树,可以为空;

特点

  • 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意节点的左、右子树也分别为二叉查找树;
  • 没有键值相等的节点。

image.png

实现

<meta charset="UTF-8">
<script>
  class Node {
    constructor(key) {
      this.key = key
      this.left = null
      this.right = null
    }
  }

  class BinarySearchTree {
    constructor() {
      this.root = null
    }

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

    /**
     * 左边子树小于右边子树的值
     * @param node
     * @param 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)
        }
      }
    }

    search(key) {
      let node = this.root
      while (node !== null) {

        if (node.key > key) {          // 根节点大于key 左子树
          node = node.left
        } else if (node.key < key) {   // 根节点小于key 右子树
          node = node.right
        } else {
          // 找到(包含root)
          return true
        }
      }
      // 未找到
      return false
    }

    getRoot() {
      return this.root
    }

    /**
     * 最大的节点: 根节点  右子树上
     */
    maxNode() {
      let node = this.root
      if (node) {
        while (node && node.right !== null) {
          node = node.right
        }
        return node
      }
      return null
    }

    /**
     * 最小节点: 根节点  左子树上
     */
    minNode() {
      let node = this.root
      if (node) {
        while (node && node.left !== null) {
          node = node.left
        }

        return node
      }
      return null
    }

    /**
     移除节点的实现情况比较复杂,它会有三种不同的情况:
     1. 需要移除的节点是一个叶子节点
     2. 需要移除的节点包含一个子节点
     3. 需要移除的节点包含两个子节点
     和实现搜索指定节点一元,要移除某个节点,必须先找到它所在的位置,因此移除方法的实现中部分代码和上面相同:
     */
    remove(key) {
      this.removeNode(this.root, key)
    }

    removeNode(node, key) {
      if (node === null) {
        return null
      }
      if (key < node.key) {
        node.left = this.removeNode(node.left, key)
        return node
      } else if (key > node.key) {
        node.right = this.removeNode(node.right, key)
        return node
      } else {
        //需要移除的节点是一个叶子节点
        if (node.left === null && node.right === null) {
          node = null
          return node
        }
        //需要移除的节点包含一个子节点
        if (node.left === null) {
          node = node.right
          return node
        } else if (node.right === null) {
          node = node.left
          return node
        }
        //需要移除的节点包含两个子节点
        let aux = this.minNode(node.right)
        node.key = aux.key
        node.right = this.removeNode(node.right, aux.key)
        return node
      }
    }

    /**
     * 先序遍历  根 左 右
     */
    preOrderTraversal(cb) {
      this.preOrderTraversalNode(this.root, cb)
    }

    preOrderTraversalNode(node, cb) {
      if (node !== null) {
        cb(node.key)
        // 递归算法
        this.preOrderTraversalNode(node.left, cb)
        this.preOrderTraversalNode(node.right, cb)
      }
    }

    /**
     非递归遍历(利用栈:将遍历到的结点都依次存入栈中,拿结果时从栈中访问)

     1. 初始化一个栈,将根节点压入栈中;
     2. 当栈为非空时,循环执行步骤3到4,否则执行结束;
     3. 从队列取得一个结点(取的是栈中最后一个结点),将该值放入结果数组;
     4. 若该结点的右子树为非空,则将该结点的右子树入栈,若该结点的左子树为非空,则将该结点的左子树入栈;
     (注意:先将右结点压入栈中,后压入左结点,从栈中取得时候是取最后一个入栈的结点,而先序遍历要先遍历左子树,后遍历右子树)
     */

    ptn() {
      let result = []
      let stack = []
      stack.push(this.root)

      while (stack.length) {
        let node = stack.pop()
        result.push(node.key)
        if (node.right) stack.push(node.right)
        if (node.left) stack.push(node.left)
      }
      return result
    }

    /**
     * 中序遍历 左 根 右
     */
    inOrderTraversal(cb) {
      this.inOrderTraversalNode(this.root, cb)
    }

    inOrderTraversalNode(node, cb) {
      if (node !== null) {
        this.inOrderTraversalNode(node.left, cb)
        cb(node.key)
        this.inOrderTraversalNode(node.right, cb)
      }
    }

    // 左 根 右
    itn(node) {
      let result = []
      let stack = []

      while (stack.length || node) {
        if (node) {
          stack.push(node)
          node = node.left
        } else {
          node = stack.pop()
          result.push(node.key)
          node = node.right
        }
      }
      return result
    }

    /**
     * 后序遍历 左 右 根
     */
    postOrderTraversal(cb) {
      this.postOrderTraversalNode(this.root, cb)
    }

    postOrderTraversalNode(node, cb) {
      if (node !== null) {
        this.postOrderTraversalNode(node.left, cb)
        this.postOrderTraversalNode(node.right, cb)
        cb(node.key)
      }
    }

    // 层遍历
    bfs() {
      let result = []
      let stack = [this.root]

      let count = 0; // 用来记录执行到第一层
      let bfs2 = function () {
        let node = stack[count];
        if (node) {
          result.push(node.key);
          if (node.left) stack.push(node.left);
          if (node.right) stack.push(node.right);
          count++;
          bfs2();
        }
      }
      bfs2()
      return result
    }

    // 非递归算法
    bfs2(node) {
      let result = [];
      let queue = [];
      queue.push(node);
      let pointer = 0;
      while (pointer < queue.length) {
        let node = queue[pointer++]; // // 这里不使用 shift 方法(复杂度高),用一个指针代替
        result.push(node.ke);
        node.left && queue.push(node.left);
        node.right && queue.push(node.right);
      }
      return result;
    }

    /**
     翻转二叉树
     4
     /   \
     2     7
     / \   / \
     1   3 6   9

     4
     /   \
     7     2
     / \   / \
     9  6 3   1
     */
    invertTree(node = this.root) {
      if (node === null) {
        return
      }
      this.invertTree(node.left)
      this.invertTree(node.right)
      this.exchange(node)
    }

    exchange(node) {
      let temp = node.left
      node.left = node.right
      node.right = temp
    }
  }

  /**
   11,7,15,5,3,9,8,10,13,12,14,20,18,25,6
   先序遍历: 11,7,5,3,6,9,8,10,15,13,12,14,20,18,25
   中序遍历: 3,5,6,7,8,9,10,11,12,13,14,15,18,20,25
   后序遍历: 3,6,5,8,10,9,7,12,14,13,18,25,20,15,11
   */

  var tree = new BinarySearchTree()
  tree.insert(11)
  tree.insert(7)
  tree.insert(15)
  tree.insert(5)
  tree.insert(3)
  tree.insert(9)
  tree.insert(8)
  tree.insert(10)
  tree.insert(13)
  tree.insert(12)
  tree.insert(14)
  tree.insert(20)
  tree.insert(18)
  tree.insert(25)
  tree.insert(6)

  let res = ''

  function print(node) {
    res += node + ' '
    // console.log(node)
  }

  tree.preOrderTraversal(print) //先序遍历11,7,5,3,6,9,8,10,15,13,12,14,20,18,25
  console.log(res)
  res = ''

  tree.inOrderTraversal(print) //3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
  console.log(res)
  res = ''

  tree.postOrderTraversal(print) //3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
  console.log(res)
  res = ''

  let result = tree.ptn()
  console.log('1111111', result)

  console.log(tree.itn(tree.root))


  console.log(tree.bfs())

  tree.invertTree()
</script>

参考 初学者应该了解的数据结构: Tree 「JarunWang」javascript 二叉树(Trees)算法与说明