一、什么是树?
1.1 定义
树 是一种数据结构,是一种非顺序数据结构。
树(tree)是包含 n(n≥0)个节点,当 n=0 时,称为空树,非空树中(n-1)条边的有穷集
非空树中
- 每个元素称为节点(node)
- 有一个特定的节点被称为根节点或树根(root)
- 除根节点之外的其余数据元素被分为m(m>=0)个互不相交的集合T1、T2、…、Tn,其中每一个集合 本身也是一棵树,被称作原树的子树。
1.2 树的相关术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;例如图中节点A的度为2,节点H的度为1
- 树的度:一棵树中,最大的节点度称为树的度;例如图中最大的节点B的度为3,树的度为3
- 叶节点或终端节点:度为零的节点(也称为叶子节点);例如图中的K,J,F,L,O,P
- 非终端节点或分支节点:度不为零的节点;
- 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;图中A节点就是B 和 C的父节点
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;图中节点G和节点H为节点C的子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;节点B和节点C就是兄弟节点
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
- 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
- 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
二、二叉树和二叉树搜索
2.1 二叉树定义
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
2.2 二叉树五种基本形态
- 空二叉树——如下图1
- 只有一个根节点的二叉树——如下图2
- 只有左子树——如下图(c)
- 只有右子树——如下图(d)
- 完全二叉树——如下图(e)
2.3 二叉搜索树
二叉搜索树是二叉树的一种,允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值。
下图就是二叉搜索树
2.4 创建二叉搜索
组织结构:
2.4.1 创建二叉树类
由定义我们可以知晓,二叉树有左侧子节点和右侧子节点
定义节点
// 定义节点
export class Node{
constructor(key){
this.key = key // 节点值
this.left = null;//左侧子节点引用
this.right = null;//右侧子节点引用
}
}
创建二叉搜索类 在这我们需要引入Compare和defaultCompare方法
export const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0
};
export function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
声明BinarySearchTree类的基本结构
import {defaultCompare} from './utils'
export default class BinarySearchTree{
constructor(compareFn = defaultCompare){
this.compareFn = compareFn;//用来比较节点的值
this.root = null;//Node类型的根节点
}() 返回树中最小的值/键
- max() 返回树中最大的值/键
- remove()
}
tree中常用的方法:
- insert(key) 向书中插入一个元素
- search(key) 在树中查找一个键。如果节点存在,则返回true;如果不存在,则返回false
- inOrderTraverse() 通过中序遍历方式遍历所有节点
- preOrderTraverse() 通过先序遍历方式遍历所有节点
- postOrderTraverse() 通过后序遍历方式遍历所有节点
- min() 返回树中最小的值/键
- max() 返回树中最大的值/键
- remove(key) 从树中移除某个键
我们先了解这几个方法的含义,稍后我们将实现这些方法
2.4.2 insert 插入
向树中插入一个新键
- 验证插入操作是否是特殊情况
insert(key) {
// 判断插入的节点是否是第一节点
if (this.root == null) {
// 是,创建一个Node类的实例并将他赋值给root属性来将root指向这个新节点
this.root = new Node(key)
}else{
// 将节点添加到根节点以外的位置,需要借助辅助方法
this.insertNode(this.root,key)
}
}
-
将节点添加到根节点以外的位置,即insertNode方法
-
如果树为空,需要找到插入新节点的位置。因此,参数为树的根节点和要插入的节点
-
如果新节点的键小于当前节点的键,那么需要检验当前节点的左侧子节点。注意:由于键可能是复杂的对象而不是数,我们使用传入二叉搜索构造函数的compareFn函数比较值。
ⅰ. 若没有左侧子节点,就在那里插入新的节点
ⅱ. 有左侧子节点,则需要通过递归调用insertNode方法,继续找到书的下一层,则下次比较的节点将会是当前节点的左侧子节点
-
如果新节点的键大于当前节点的键
ⅰ. 若没有右侧子节点,则直接插入新节点
ⅱ. 如果右侧有子节点,则需要递归insertNode方法,继续找到书的下一层,则下次比较的节点将会是当前节点的右侧子节点
-
insertNode代码
insertNode(node, key) {
//新节点的键小于当前节点的键,则检查左侧子节点
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
if (node.left == null) {
//如果没有左侧子节点,就插入新节点
node.left = new Node(key)
} else {
// 如果有左侧子节点,就递归调用insertNode方法,继续找到树的下一层
this.insertNode(node.left, key)
}
} else { //新节点的键大于当前节点的键
if (node.right == null) {
node.right = new Node(key)
} else {
this.insertNode(node.right, key)
}
}
}
测试代码
const 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);
则此时的执行过程为
上述我们就能够在树种插入元素。大家可以自己体验一下。
2.4.3 实现树的遍历
遍历一颗树是指访问树的每个节点对它们进行某种操作的过程。 通过方法介绍我们也可得知,遍历树有三种方法。我们一起来看下这三种方法的区别吧
- 中序遍历:左子树---> 根节点 ---> 右子树 左根右
- 前序遍历:根节点---> 左子树 ---> 右子树 根左右
- 后序遍历:左子树---> 右子树 ---> 根节点 左右根
2.4.3.1 中序遍历
中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。
规则:左子树---> 根节点 ---> 右子树 左根右
先了解上面的规则,我们就能快速的写出中序遍历的实现
inOrderTraverse(callback){
//接收一个节点和对应的回调函数作为参数
this.inOrderTraverseNode(this.root,callback)
}
inOrderTraverseNode(node,callback){
// 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
if(node != null){
//调用相同的函数来访问左侧子节点
this.inOrderTraverseNode(node.left,callback)
callback(node.key) //对根节点进行操作
this.inOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
}
}
测试代码
const printNode = (value) => console.log(value);
tree.inOrderTraverse(printNode);
输出:每个数会输出在不同行上 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
下图绘制了inOrderTraverse方法的访问路径
2.4.3.2 先序遍历
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。
规则:根节点---> 左子树 ---> 右子树 根左右
preOrderTraverse(callback){
this.preOrderTraverseNode(this.root,callback)
}
preOrderTraverseNode(node,callback){
// 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
if(node != null){
callback(node.key) //对根节点进行操作
//调用相同的函数来访问左侧子节点
this.preOrderTraverseNode(node.left,callback)
this.preOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
}
}
测试代码
const printNode = (value) => console.log(value);
tree.preOrderTraverse(printNode);
输出:每个数会输出在不同行上 11 7 5 3 6 9 8 10 15 13 12 14 20 18 2
下图绘制了preOrderTraverse方法的访问路径
2.4.3.3 后序遍历
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。
规则:左子树 ---> 右子树 ---> 根节点 左右根
postOrderTraverse(callback){
this.postOrderTraverseNode(this.root,callback)
}
postOrderTraverseNode(node,callback){
// 检查以参数形式传入的节点是否为null,这是停止递归继续执行的判断条件
if(node != null){
//调用相同的函数来访问左侧子节点
this.postOrderTraverseNode(node.left,callback)
this.postOrderTraverseNode(node.right,callback) //// 再访问右侧子节点
callback(node.key) //对根节点进行操作
}
}
测试代码
const printNode = (value) => console.log(value);
tree.postOrderTraverse(printNode);
输出:每个数会输出在不同行上 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
下图绘制了postOrderTraverse方法的访问路径
2.4.4 搜索树中的位置
在树中,我们常需要进行的搜索类型
- 搜索最大值
- 搜索最小值
- 搜索任意值
2.4.4.1 搜索最小值
从图中可以看出,最小值为:树最后一层最左侧的节点,即为这颗树的最小值
min(){
return this.minNode(this.root)
}
minNode(node){
let currentNode = node
//遍历树的左边,直到找到树的最下层(最左端)
while(currentNode!= null && currentNode.left!=null){
currentNode = currentNode.left
}
return currentNode
}
2.4.4.2 搜索最大值
从图中可以看出,最大值为:树最后一层最右侧的节点,即为这颗树的最大值
max(){
return this.maxNode(this.root)
}
maxNode(node){
let currentNode = node
//遍历树的右边,直到找到树的最下层(最左端)
while(currentNode!= null && currentNode.right!=null){
currentNode = currentNode.right
}
return currentNode
}
2.4.4.3 搜索任何值
实现search方法,在树中查找一个键。如果节点存在,则返回true;如果不存在,则返回false
- 判断传入的node是否合法(null或者undefined),如果不合法,则说明没有找到,直接返回false
- 如果找到的键比当前节点小,则从左侧子节点继续搜索
- 如果找到的键比当前节点大,则从右侧子节点继续搜索
- 否则找到的键与当前节点相等,返回true来表示找到这个键
我们根据上述的规则来写代码就方便多了
search(key){
return this.searchNode(this.root,key)
}
searchNode(node,key){
//检验传入的node是否合法
if(node == null){
return false
}
//要找的键比当前的节点小,则继续在左侧的子树上搜索
if(this.compareFn(key, node.key) === Compare.LESS_THAN){
return this.searchNode(node.left,key)
}
//要找的键比当前的节点大,从右侧子节点开始继续搜索
else if(this.compareFn(key, node.key) === Compare.BIGGER_THAN){
return this.searchNode(node.right,key)
}
//要找的键和当前节点相等
else{
return true
}
}
2.4.5 移除一个节点
remove方法的实现。从树中移除一个节点,我们会想到三种情况
- 移除一个叶节点
解释:
- 移除节点15,找到它右侧子树中最小的节点18
- 用右侧子树中最小节点的键去更新这个节点
- 上述操作,会是树中有两个拥有相同键的节点,要继续把右侧子树中的最小节点移除
- 向父节点返回更新后的节点的引用
综上所述,我们慢慢写出移除的代码
remove(key) {
return this.removeNode(this.root, key);
}
removeNode(node, key) {
//如果正在检测的节点为null,说明键不存在于树中
if (node == null) {
return false
}
//如果要找的键比当前节点的值小,就沿树的左边找下一个节点
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
node.left = this.removeNode(node.left, key)
return node
}
// 如果要找的键比当前节点的值大,就沿树的右边找下一个节点
else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
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
}
//第三种情况:移除有两个字节点的节点
//1 找到要移除的节点后,需要找到它右边子树中最小的节点
const aux = this.minNode(node.right)
//2. 用它右侧子树中最小节点的键去更新这个节点的值
node.key = aux.key
//3. 继续把右侧子树中的最小节点移除
node.right = this.removeNode(node.right,aux.key)
// 向它的父节点返回更新后节点的引用
return node
}
}
三、二叉树相关算法题
可查看链接: www.yuque.com/u12177228/g…
欢迎点赞收藏