概述
在了解二叉树之前我们先了解一下 树 在计算机科学中的概念和作用,首先树是一种非线性的数据结构,以分层的方式存储数据,被用来存储具有层级关系的数据,比如文件系统中的文件,有序列表等。
关于树的一些基本概念
以下是一个树的局部图:
- 根节点:一棵树最上面的节点称为根节点,对应上图中23那个点。
- 父节点:如果一个节点下面链接多个节点,那么可以称该节点是其下面节点的父节点,其下的节点称之为子节点,如上图中的13是7、9、15的父节点,而7、9、15是13的子节点。
- 叶子节点:如果一个节点没有任何子节点,那么它就是叶子节点,例如上图中的42、7、9、15。
- 树的遍历:以某种特定顺序访问树中所有的节点称为树的遍历。
- 层次:见上图中第几层的标注,我们通常以此来定义树的深度
二叉树
当我们对树的概念有了一些了解以后,可以基于此来认识一下二叉树,二叉树和正常的树一样,只不过我们对它做了一些限制,比如二叉树的每个节点的子节点不允许超过两个,并且通过这个限制,我们可以写出高效的程序在树中插入、查找和删除数据。
二叉树的一些特点
- 每个节点最多拥有两个子节点
- 二叉树中节点处于左侧位置的子节点叫左节点,右侧的叫右节点
- 同一组子节点中,左节点数据一定要小于右节点数据
实现二叉树
基于以上认识,我们尝试通过javascript实现一个二叉树,由于树是由节点组成,所以我们要先定义一个对象TreeNode。 要实现TreeNode,我们先了解一个他的特点,首先节点中会保存一个数据,其次它可能存在两个属性指向它的左右子节点,最后可以有一个方法可以获取到当前节点的值,基于此我们开始实现。
class TreeNode {
constructor(data, left, right) {
// 存放节点数据
this.data = data;
// 存放左节点
this.left = left;
// 存放右节点
this.right = right;
}
// 获取节点数据
show() {
return this.data;
}
}
进入正题,我们基于节点类开始实现一个二叉树
class BST {
constructor() {
this.root = null;
}
// 插入节点
insert(data) {
const n = new Node(data, null, null);
if(this.root === null) {
this.root = n;
} else {
let current = this.root;
let parent;
while(true) {
parent = current;
if(data < current.data) {
current = current.left;
if(current === null) {
parent.left = n;
break;
}
} else {
current = current.right;
if(current === null) {
parent.right = n;
break;
}
}
}
}
}
}
BST先要有一个 insert() 方法,用来向树中加入新节点。首先我们要创建一个TreeNode对象,将数据传入该对象保存。
其次检查 BST 是否有根节点,如果没有,那么这是棵新树,该节点就是根节点,这个方法 到此也就完成了,否则进入下一步。
如果待插入节点不是根节点,那么就需要准备遍历 BST,找到插入的适当位置。该过程类似于遍历链表。用一个变量存储当前节点,一层层地遍历 BST。
进入 BST 以后,下一步就要决定将节点放在哪个地方。找到正确的插入点时,会跳出循环。查找正确插入点的算法如下。
- 设根节点为当前节点。
- 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第 4 步。
- 如果当前节点的左节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
- 设新的当前节点为原节点的右节点。
- 如果当前节点的右节点为 null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
以上一个简单的二叉树就实现了,但是如果我们想要通过遍历去查找或者展示二叉树中的节点数据,我们应该怎么做呢。为此我们要继续给BST添加遍历功能,本文将介绍二叉树常用的三种遍历方式。
先序遍历
先访问根节点,然后以同样方式访问左子树和右子树,下图虚线即为先序访问顺序
中序遍历
先访问左子树,再访问根节点,最后以同样方式访问右子树,下图虚线即为中序访问顺序
后序遍历
先访问左子树,然后以同样方式访问右子树,最后访问根节点
基于此,我们继续完善BST
class BST {
...
// 中序遍历, 按照节点上的键值,以升序访问 BST 上的所有节点
inOrder(node) {
if(node) {
this.inOrder(node.left);
console.log(node.show() + ' ');
this.inOrder(node.right)
}
}
// 先序遍历, 先访问根节点,然后以同样方式访问左子树和右子树
preOrder(node) {
if(node) {
console.log(node.show() + ' ');
this.preOrder(node.left);
this.preOrder(node.right)
}
}
// 后序遍历, 先访问叶子节点,从左子树到右子树,再到根节点
lastOrder(node) {
if(node) {
this.lastOrder(node.left);
this.lastOrder(node.right);
console.log(node.show() + ' ');
}
}
}
我们对于二叉树的功能需求可能还远不止于此,所以,我们继续完善BST
class BST {
...
// 获取最大节点
getMax(node) {
let current = node || this.root;
while(current.right) {
current = current.right;
}
return current;
}
// 获取最小节点
getMin(node) {
let current = node || this.root;
while(current.left) {
current = current.left;
}
return current;
}
// 查找给定值
find(data) {
let current = this.root;
while(current) {
if(current.data > data) {
current = current.left;
} else if(current.data < data) {
current = current.right;
} else {
return current
}
}
return null
}
// remove 删除
remove(data) {
const root = this.removeNode(this.root, data);
console.log(root)
}
// removeNode 删除节点
removeNode(node, data) {
if(node == null) return null;
if(data == node.data) {
if(!node.left && !node.right) {
return null;
}
if(!node.left) {
return node.right;
}
if(!node.right) {
return node.left;
}
let tempNode = this.getMin(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;
}
}
}
ok到此,一个基本的二叉树以及常用方法都提供了,需要注意的是在删除方法中,逻辑会比较复杂,我们在这里特别解释一下。
为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法:remove() 和 removeNode()。
从 BST 中删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该 节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果待删除数据小于当前 节点上的数据,则移至当前节点的左子节点继续比较;如果删除数据大于当前节点上的数 据,则移至当前节点的右子节点继续比较。
如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接 指向 null。
如果待删除节点只包含一个子节点,那么原本指向它的节点久得做些调整,使其指向它的 子节点。
最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树 上的最大值,要么查找其右子树上的最小值。这里我们选择后一种方式(为了保证左节点一定小于右节点,我们需要寻找一个数作为中间点,而左子树中的最大值和右子树中的最小值恰恰就满足一要求)。
总结
本文是在作者阅读相关书籍后做的一个小总结,目的是为了帮助自己更好的理解二叉树,同时也希望能帮助其他有需要的小伙伴。