js 数据结构之树

80 阅读7分钟

树的基本概念,二叉树的种类及原理……


一 基本概念

树(Tree),是由 n(n>=1)个有限结点组成一个具有层次关系的集合。因为它根朝上,而叶朝下,看起来像一棵倒挂的树,所以把它叫做“树”。

树的特点:

  • 每个结点有零个或多个子结点;
  • 没有父结点的结点称为根结点;
  • 每一个非根结点有且只有一个父结点;
  • 除了根结点外,每个子结点可以分为多个不相交的子树。

树.png

树的相关概念

名词说明
兄弟节点拥有共同父节点的节点互称为兄弟节点
节点的分支数目
结点层数从根结点开始算,根结点是第一层,依次往下
树的深度树中结点的最大层数
节点深度对任意节点x,其深度为根节点到x节点的路径长度。根节点深度为0,第二层节点深度为1
森林m 颗互不相交的树构成的集合就是森林

层次.webp

二 二叉树

二叉树的特点:

  • 每个节点最多只有两个子节点,可以没有或者只有一个。
  • 左子树和右子树是有顺序的,次序不能任意颠倒

二叉树的计算特性:

  • 在二叉树的第i层上最多有2i-1 个节点(i>=1)。
  • 二叉树中如果深度为k,那么最多有2k-1个节点(k>=1)。
  • n0=n2+1 ,n0 表示度数为 0 的节点数,n2 表示度数为 2 的节点数。
  • 在完全二叉树中,具有n个节点的完全二叉树的深度为 [log2n] + 1,其中 [log2n] 是向下取整。

若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:

  • 若 i = 1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
  • 若 2i > n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
  • 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。

二叉树的结构简单,存储效率较高。

1. 斜树

左斜树.png

右斜树.png

2. 完全二叉树

除了最下面一层节点外,其他各层的节点达到最大个数,只有最下层最右侧节点可以缺少子节点,而且最下层按照从左到右的顺序连续存在。

完全二叉树.png

3. 满二叉树

除最下层节点外,其他节点都有两个子节点。

满二叉树.png

满二叉树一定是完全二叉树,但反过来不一定成立。

4. 搜索二叉树

搜索二叉树.png

其特点是:对树中的每一个节点而言,如果存在左子树,则左子树的值不能大于节点的值;如果存在右子树,右子树的值不能小于节点的值。

5. 平衡二叉树

平衡二叉树1.png

平衡二叉树2.png

平衡二叉树的特点是,根节点的左右子树深度之差的绝对值 <= 1;左右子树也是平衡二叉树;节点的左子树深度减去右子树的深度,值只能是 -1,1,0其中的一项。

三 二叉树的存储结构

1. 顺序存储

二叉树的顺序存储,是使用数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。

如下图:

顺序存储1.webp

顺序存储2.webp

当二叉树为完全二叉树时,结点数刚好填满数组。如果不是完全二叉树,顺序存储就会出现空间浪费的情况。因此,顺序存储一般适用于完全二叉树。

顺序存储3.webp

顺序存储4.webp

链表存储(二叉链表)

二叉树的每个结点最多有两个孩子,所以可以将结点数据结构定义为一个数据和两个指针的链表。

链式存储1.webp

链式存储2.webp

四 二叉树的遍历

二叉树的遍历思路是,从二叉树的根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次,且仅被访问一次。

二叉树的访问次序分为四种:前序遍历 / 中序遍历 / 后序遍历 / 层序遍历。

顺序存储1.webp

1. 前序遍历

前序遍历,从二叉树的根结点出发,当第一次到达结点时输出结点数据,按照先左再右的方向访问。也就是按根节点-左孩子-右孩子的顺序。

如上图,输出顺序为:ABDHIEJCFG。

步骤分析:

从根节点 A 触发,第一次到达 A,输出 A;
向左访问,第一次访问 B, 输出 B;
向左访问,输出 D,输出 H;
H 无子节点,输出 H 后返回到 D;第二次访问 D,故不输出 D,进而访问 I,输出 I;
返回 D,返回 B,访问 E,输出 E;
输出 J……
C F G 同理。

2. 中序遍历

中序遍历,从二叉树的根结点出发,当第二次到达结点时输出结点数据,按照先左再右的方向访问。也就是按左孩子-根节点-右孩子的顺序。

如上图,输出顺序为:HDIBJEAFCG。

步骤分析:

第一次到达 A,不输出 A; 继续向左访问,第一次访问结点 B,不输出 B;继续到达 D,H;
到达 H,H 左子树为空,则返回到 H,此时第二次访问 H,故输出 H;
H 右子树为空,则返回至 D,此时第二次到达 D,故输出 D;
由 D 返回至 B,第二次到达 B,故输出 B;
其他同理。

3. 后序遍历

后序遍历,从二叉树的根结点出发,当第三次到达结点时输出结点数据,按照先左再右的方向访问。也就是按左孩子-右孩子-根节点的顺序。

如上图,后序遍历输出为:HIDJEBFGCA。

步骤分析:

第一次到达 A,不输出 A; 继续向左访问,第一次访问结点 B,不输出 B;继续到达 D,H;
到达 H,H 左子树为空,则返回到 H,此时第二次访问 H,不输出 H;
H 右子树为空,则返回至 H,此时第三次到达 H,故输出 H;
由 H 返回至 D,第二次到达 D,不输出 D;
继续访问至 I,I 左右子树均为空,故第三次访问 I 时,输出 I;
返回至 D,此时第三次到达 D,输出 D;
其他同理。

4. 层序遍历

层次遍历,顾名思义,就是按照树的层次自上而下的遍历。 如上图,层次遍历输出为:ABCDEFGHIJ

五 搜索二叉树的代码实现

// 节点类
class Node {
  constructor(data, left, right) {
    // 节点值
    this.data = data;
    // 左孩子
    this.left = left;
    // 有孩子
    this.right = right;
  }
  // 获取节点值
  getNode() {
    return this.data;
  }
}

// 二叉树类
class BST {
  constructor() {
    this.root = null;
  }
  // insert
  insert(data) {
    // 创建节点
    let node = new Node(data, null, null);
    // 如果跟节点为空,则作为跟节点
    if (this.root === null) {
      this.root = node;
    } else {
      let current = this.root;
      let parent;
      // 循环查找子节点,查到某个子节点为空时,将 node 赋值到此节点上
      while (true) {
        parent = current;
        if (data < current.data) {
          current = current.left;
          // 没有左子节点
          if (current === null) {
            parent.left = node;
            break;
          }
        } else {
          current = current.right;
          // 没有右子节点
          if(current === null) {
            parent.right = node;
            break;
          }
        }
      }
    }
  }
  // 前序遍历: 根节点-左孩子-右孩子
  preOrder (node) {
    if (node !== null) {
      console.log(node.getNode()); // 打印
      this.preOrder(node.left);
      this.preOrder(node.right);
    }
  }
  // 中序遍历: 左孩子-根节点-右孩子
  inOrder (node) {
    if (node !== null) {
      this.inOrder(node.left);
      console.log(node.getNode());
      this.inOrder(node.right);
    }
  }
  // 后序遍历: 左孩子-右孩子-根节点
  afterOrder (node) {
    if (node !== null) {
      this.afterOrder(node.left);
      this.afterOrder(node.right);
      console.log(node.getNode());
    }
  }
  // 获取最小值 沿着根节点一直向左孩子查找,最后一个左孩子就是最小值
  getMin () {
    let current = this.root;
    while (current.left !== null) {
      current = current.left;
    }
    return current.data;
  }
  // 获取最大值 沿着根节点一直向右孩子查找,最后一个右孩子就是最大值
  getMax () {
    let current = this.root;
    while (current.right !== null) {
      current = current.right;
    }
    return current.data;
  }
  // 查找某个值
  find (data) {
    let current = this.root;
    while (current !== null) {
      // 如果当前节点的值等于要查找的元素,返回此节点
      if (data === current.data) {
        return current;
      }
      // 如果大于节点值 就顺着右孩子继续查找
      if (data < current.data) {
        current = current.left;
      } else if (data > current.data) {
        // 否则,顺着左孩子进行查找
        current = current.right;
      }
    }
    // 查找不到,返回空
    return null;
  };
	// 删除元素
  remove (data) {
    this.root = this._removeNode(this.root, data); //将根节点转换
  }
	// 找到最小值
  _getSmallest (node) {
    while(node.left!=null){
      node=node.left;
    }
    return node;
  }
	// 删除元素并且重新构建二叉树
  _removeNode (node, data) {
    if (node === null) {
      return null;
    }
    if (data === node.data) {
      // 如果没有子节点
      if (node.right === null && node.left === null) {
        return null;
      }
      // 如果没有左子节点
      if (node.left === null) {
        return node.right;
      }
      // 如果没有右子节点
      if (node.right === null) {
        return node.left;
      }
      // 如果有两个节点 找到最小的右节点
      let tempNode = this._getSmallest(node.right);
      node.data = tempNode.data;
      node.right = this._removeNode(node.right, tempNode.data);
      return node;
    } else if (data < node.data){
      node.left = this._removeNode(node.left, data);
      return node;
    } else {
      node.right = this._removeNode(node.right, data);
      return node;
    }
  }
}

六 其他树结构

霍夫曼树 / AVL树 / 红黑树 / 伸展树 / 替罪羊树 / B-tree / B+树 ……


参考:yancy__天山老霸王MrHorse1992