1 概念
![](https://img-blog.csdnimg.cn/2021062116051728.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Zpbjc3Nzc3Nzc=,size_16,color_FFFFFF,t_70)
如上图所示,这是一棵普通的树。
而树中的每一个数据,都代表着一个结点,…
结点是什么?
——结点是数据结构中的基础,是构成复杂数据结构的基本组成单位。
…
那么,什么是树?
定义如下:
树( 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 + 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 的结点为其右结点。
……
2.2 类型
在此,简单介绍一下二叉树的几种类型( 斜树、满二叉树、完全二叉树):
斜树:
- 树中所有结点,都只存在左子树( 右子树 )的二叉树,称作左( 右 )斜树,两者统称为斜树,…
如下图所示:
![](https://img-blog.csdnimg.cn/20210621172050589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Zpbjc3Nzc3Nzc=,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20210621172131857.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Zpbjc3Nzc3Nzc=,size_16,color_FFFFFF,t_70)
……
满二叉树:
- 树中所有的分支结点都存在左、右子树,且叶子结点处于同一层上。
特点如下:
- 叶子结点只能处于最下一层。
- 非叶子结点的度一定为 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;
}
}