简单学习二叉树

1 概念

如上图所示,这是一棵普通的

而树中的每一个数据,都代表着一个结点,…

结点是什么?

——结点是数据结构中的基础,是构成复杂数据结构的基本组成单位。

那么,什么是

定义如下:

树( Tree ),是 n 个结点的有限集( n >= 0 ),当 n = 0 时( 没有结点 )即为一棵空树。

而在任意一颗非空树中,有以下定义:

  • 有且仅有一个特定的称为根( Root )的结点
  • 当 n > 1 时,其余结点可分为 m ( m > 0 ) 个互不相交的有限集 T1、T2、… 、Tn。其中,每一个集合,本身又是一棵树,并且称为根的子树。

特别注意:

  • 当 n > 0 时,根结点是唯一的,即一棵树仅存在一个根节点。
  • 当 m > 0 时,子树的个数没有限制,且每棵子树互不相交。

至此,再了解一下什么是结点的度结点关系以及结点层次

观察下图,…

由图中可知:

  • 结点的度,即其所拥有的的子树数目。
  • 结点关系,如图中的 A 结点( 根结点 ),A 结点为 B、C 结点的双亲结点,而 B 、C 结点都为 A 结点的孩子结点 ; 而 B 、C 结点拥有同一个双亲结点,所以它们又互为兄弟结点 。另外,没有孩子结点的结点称为叶子结点,如图中的 G 、H 、I 、J 结点 。
  • 结点层次,从根结点开始定义,A 结点( 根结点 )为第一层,以此类推,…

另外,树结点最大的层次数,称作树的高度( 或树的深度 )。

图中,树的高度为 4 。

……

2 二叉树

2.1 定义

二叉树是由 n 个结点的有限集合( n >= 0 ) 。

  • 当 n = 0 时,该集合为空集,即空二叉树 。
  • 当 n > 0 时,该集合由 1 个根结点、两棵互不相交的子树(或称为根结点的左子树、右子树)组成 。

如下图所示,这是一棵普通的二叉树。

那么,二叉树有什么特点呢?

  • 每个结点最多存在两棵子树( 即左子树、右子树 )。
  • 左子树和右子树是有序的,次序不能颠倒( 即就算只存在一棵子树,也要区分它是左子树或者是右子树 )。

二叉树的性质如下:

  • 二叉树的第 i 层上,最多存在「 2i-1」个节点 。( i >= 1 )
  • 当深度为 k ,二叉树最多存在「 2k-1 」个节点。( k >= 1 )
  • 公式:n0 = n2 + 1n0 表示度数为 0 的结点数目,n2 表示度数为 2 的结点数目。
  • 在完全二叉树中,具有 n 个节点的完全二叉树的深度为 [log2n] + 1 ,其中 [log2n] 是向下取整。
  • 若对含 n 个结点的完全二叉树,从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
    • 若 i = 1,该结点为完全二叉树的根,无双亲 ; 否则,编号为 [ i / 2 ] 的结点为其双亲结点
    • 若 2i > n,该结点无左结点 ;否则,编号为 2i 的结点为其左结点;
    • 若 2i + 1 > n,该结点无右结点 ;否则,编号为 2i + 1 的结点为其右结点。

……

2.2 类型

在此,简单介绍一下二叉树的几种类型( 斜树满二叉树完全二叉树):

斜树:

  • 树中所有结点,都只存在左子树( 右子树 )的二叉树,称作左( 右 )斜树,两者统称为斜树,…

如下图所示:

……

满二叉树:

  • 树中所有的分支结点都存在左、右子树,且叶子结点处于同一层上。

特点如下:

  • 叶子结点只能处于最下一层。
  • 非叶子结点的度一定为 2 ( 即除了叶子结点,其他结点必存在左右子结点 )。
  • 在同样高度的二叉树中,满二叉树的结点个数最多,叶子结点数最多。

如下图所示,这是一棵满二叉树:

……

完全二叉树:

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

特点:

  • 叶子结点处于最后一层,或倒数第二层。
  • 最下层的叶子结点,集中在树的左部。
  • 任何一个结点不能只有右子树,而没有左子树。

如下图所示,这是一棵完全二叉树:

……

2.3 存储结构

二叉树的数据是通过什么结构存储起来的?

  • 顺序存储( 基于数组的形式 )
  • 链式存储

如下图,有一棵完全二叉树。

此时可以使用一个数组去存储树的结点,而结点的位置,就是数组中的下标索引。

由图可知,当二叉树为完全二叉树时,结点数目刚好填充满整个数组。

此时引申出一个问题,若二叉树不为完全二叉树时,那么采用的顺序存储形式是怎样的呢?

观察下图:

其中,∧表示数组中的此位置并没有存储结点。

此时观察得知,顺序存储结构中出现空间浪费的情况。

那么,该如何改善这种情况呢?

此时可以采用 链式存储

由于二叉树的每个结点最多存在两个子结点,因此,可将结点数据结构定义为:一个数据和两个指针域( 分别指向左、右子结点)。

……

采用链表结构存储二叉树( 二叉链表 ),如下图所示:

2.4 遍历

遍历,即从根结点出发,按照某种次序依次访问树中所有结点,使得每个结点被访问 1 次,且仅被访问 1 次。

二叉树的访问次序,有以下几种:

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

如下图一棵二叉树:

前序遍历 ( 根-左- 右 )

  • 即从根结点出发,当第 1 次到达结点时便输出结点的数据,按照先左后右的方向访问。
  • 输出:A B D H I E J C F G

中序遍历 ( 左-根-右 )

  • 即从根结点出发,当第 2 次到达结点时便输出结点的数据,按照先左后右的方向访问。
  • 输出:H D I B J E A F C G

后序遍历 ( 左-右-根 )

  • 即从根结点出发,当第 3 次到达结点时便输出结点的数据,按照先左后右的方向访问。
  • 输出:H I D J E B F G C A

层次遍历

  • 即按照树的层次,自上而下地遍历二叉树。
  • 输出:A B C D E F G H I J

2.5 代码实现

使用 TypeScript 实现一个简单的二叉树:

// 节点结构
class TreeNode {
  data: number;
  leftChild: TreeNode;
  rightChild: TreeNode;
  constructor(data: number) {
    this.data = data;
    this.leftChild = null;
    this.rightChild = null;
  }
}

// 树
class Tree {
  root: TreeNode;
  constructor() {
    this.root = null;
  }

  /**
   * 向树中插入一个数据(判断是否首次插入,不是则调用 insertNode 方法)
   * @param data
   */
  add(data: number): void {
    let newNode = new TreeNode(data);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }
  private insertNode(currentNode: TreeNode, newNode: TreeNode): void {
    if (newNode.data < currentNode.data) {
      if (currentNode.leftChild === null) {
        currentNode.leftChild = newNode;
      } else {
        this.insertNode(currentNode.leftChild, newNode);
      }
    } else if (newNode.data > currentNode.data) {
      if (currentNode.rightChild === null) {
        currentNode.rightChild = newNode;
      } else {
        this.insertNode(currentNode.rightChild, newNode);
      }
    } else {
      console.log("该值已存在,不允许重复");
    }
  }
  /**
   * 查找元素
   * @param data
   * @returns
   */
  hasValue(data: number): boolean {
    console.log(this.searchNode(this.root, data));
    return this.searchNode(this.root, data);
  }
  private searchNode(currentNode: TreeNode, data: number): boolean {
    if (currentNode === null) return false;
    else if (data < currentNode.data)
      return this.searchNode(currentNode.leftChild, data);
    else if (data > currentNode.data)
      return this.searchNode(currentNode.rightChild, data);
    else return true;
  }

  /**
   * 二叉树的中序遍历(即左根右),通过一个数组存储起来并打印出来
   */
  print(): void {
    let printData: number[] = [];
    this.printNode(this.root, printData);
    console.log(printData);
  }
  private printNode(currentNode: TreeNode, arr: number[]): void {
    if (currentNode != null) {
      this.printNode(currentNode.leftChild, arr);
      arr.push(currentNode.data);
      this.printNode(currentNode.rightChild, arr);
    }
  }
  /**
   * 删除节点,考虑以下几种情况
   * 1.当前节点无子节点(叶子节点),则直接将当前节点设置为 null
   * 2.当前节点仅存在左子节点,则将当前节点设置为左子节点
   * 3.当前节点仅存在右子节点,则将当前节点设置为右子节点
   * 4.当前节点存在左右子节点,则找到当前节点的右子节点中存在的最小左子节点替换当前节点,并修改相应的节点指向
   */
  remove(data: number): void {
    if (this.hasValue(data)) {
      this.root = this.removeNode(this.root, data);
    } else {
      console.log("节点不存在");
    }
  }
  private removeNode(currentNode: TreeNode, data: number): TreeNode {
    if (currentNode === null) {
      return null;
    }
    if (data < currentNode.data) {
      currentNode.leftChild = this.removeNode(currentNode.leftChild, data);
      return currentNode;
    } else if (data > currentNode.data) {
      currentNode.rightChild = this.removeNode(currentNode.rightChild, data);
      return currentNode;
    } else {
      // 当前节点无子节点(叶子节点),则直接将当前节点设置为 null
      if (currentNode.leftChild === null && currentNode.rightChild === null) {
        currentNode = null;
        return currentNode;
      }
      // 当前节点仅存在左子节点,则将当前节点设置为左子节点
      if (currentNode.rightChild === null) {
        currentNode = currentNode.leftChild;
        return currentNode;
      }
      // 当前节点仅存在右子节点,则将当前节点设置为右子节点
      if (currentNode.leftChild === null) {
        currentNode = currentNode.rightChild;
        return currentNode;
      }
      // 当前节点存在左右子节点,则找到当前节点的右子节点中存在的最小左子节点替换当前节点,并修改相应的节点指向
      let minNode = this.getMinNode(currentNode.rightChild);
      currentNode.data = minNode.data;
      currentNode.rightChild = this.removeNode(
        currentNode.rightChild,
        minNode.data
      );
      return currentNode;
    }
  }
  // 此方法用于找到当前节点的右子树中的最小左子树节点
  private getMinNode(targetNode: TreeNode): TreeNode {
    while (targetNode && targetNode.leftChild != null) {
      targetNode = targetNode.leftChild;
    }
    return targetNode;
  }
}

参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值