上一篇《TypeScript 数据结构与算法:散列表》实现了 Typescript
中散列表的数据结构与算法,本篇继续实现二叉搜索树
。
二叉搜索树(Binary Search Tree
),可以把这个词分成三部分来理解:二叉|搜索|树
。首先它是一个树,第二每个节点最多有两个子节点,第三左侧子节点 < 本节点 < 右侧子节点
有序排列,便于搜索。整体的结构如下图所示:
下面是将要在二叉搜索树中中实现的方法。
insert(key)
:向树中插入一个新的节点。search(key)
:在树中查找一个键。存在返回true
;不存在返回 false。inOrderTraverse()
:中序遍历所有节点。preOrderTraverse()
:先序遍历所有节点。postOrderTraverse()
:后序遍历所有节点。min()
:返回树中最小的键的节点。max()
:返回树中最大的键的节点。remove(key)
:从树中移除某个节点。
数据结构
Node 节点
使用数据结构来表述就相当于每个节点都有三部分: left
、key
、right
。
如上图所示,key
为本节点的键,left
指向比 key
小的左侧子节点,right
指向比 key
大的右侧子节点。所以先给每个节点写一个单独的数据结构 Node
:
export class Node<K> {
left: Node<K>;
right: Node<K>;
constructor(public key: K) {}
toString() {
return `${this.key}`;
}
}
辅助方法
因为二叉搜索树是有序排列的树,所以需要有个比较大小的方法:
// 比较结果的枚举值
export enum Compare {
LESS_THAN = -1,
BIGGER_THAN = 1,
EQUALS = 0,
}
// 规定自定义Compare的类型
export type ICompareFunction<T> = (a: T, b: T) => number;
/**
* @description: 默认的大小比较函数
* @param {T} a
* @param {T} b
* @return {Compare} 返回 -1 0 1 代表 小于 等于 大于
*/
export function defaultCompare<T>(a: T, b: T): Compare {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
BinarySearchTree 类
接下来创建 BinarySearchTree
类的骨架,使用 root
表示根节点。
import { Compare, defaultCompare, ICompareFunction } from '../util';
import { Node } from './models/node';
export default class BinarySearchTree<T> {
protected root: Node<T>;
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {}
}
具体的方法分成三类,getRoot
、min
、max
三种非递归
的方法,inOrderTraverse
、preOrderTraverse
、postOrderTraverse
三种递归有序遍历
方法,以及 search
、insert
和 remove
三种递归树操作
方法。
非递归方法
三种非递归
方法的实现都非常简单,使用一张图就可以解释清楚:
根节点直接返回 root
即可,而极值
则是分别向左
和向右
迭代查找,一直找到 undefined
的父节点
即为极值。
/**
* @description: 返回根节点
*/
getRoot(): Node<T> {
return this.root;
}
/**
* @description: 返回树中的最小元素
*/
min(): Node<T> {
// 调用迭代方法
return this.minNode(this.root);
}
/**
* @description: 返回指定子树下的最小元素
*/
protected minNode(node: Node<T>): Node<T> {
let current = node;
// 不断向左查
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
/**
* @description: 返回树中的最大元素
*/
max(): Node<T> {
// 调用迭代方法
return this.maxNode(this.root);
}
/**
* @description: 返回指定子树下的最大元素
*/
protected maxNode(node: Node<T>): Node<T> {
let current = node;
// 不断向右查
while (current != null && current.right != null) {
current = current.right;
}
return current;
}
有序遍历
三种有序遍历
的作用就是按照特定的方式挨个遍历整个树,访问每个节点并执行回调函数
。在二叉树中,递归只有两个方向(向左
和向右
),先中后
序遍历的区别就是查看 key
并执行回调函数的步骤放在两次递归步骤的前中后
的相对位置
。
先序遍历
先序遍历
的顺序就是执行回调 -> 左 -> 右
,所以访问顺序是父节点优先
,适合打印结构化的文档:
/**
* @description: 先序遍历
*/
preOrderTraverse(callback: Function) {
// 调用先序遍历迭代方法
this.preOrderTraverseNode(this.root, callback);
}
private preOrderTraverseNode(node: Node<T>, callback: Function) {
// 基线条件
if (node != null) {
// 先序遍历的执行顺序是 执行回调 -> 左 -> 右
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
中序遍历
中序遍历
的顺序就是左 -> 执行回调 -> 右
,所以符合从小到大排列的顺序,可以用来打印排序后的结果:
/**
* @description: 中序遍历
*/
inOrderTraverse(callback: Function) {
// 调用中序遍历迭代方法
this.inOrderTraverseNode(this.root, callback);
}
private inOrderTraverseNode(node: Node<T>, callback: Function) {
// 基线条件
if (node != null) {
// 中序遍历的顺序是 左 -> 执行回调 -> 右
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
后序遍历
后序遍历
的顺序就是左 -> 右 -> 执行回调
,所以访问顺序是子节点优先
,可以应用到计算一个目录及其子目录中所有文件所占空间的大小。
/**
* @description: 后序遍历
*/
postOrderTraverse(callback: Function) {
// 调用后序遍历迭代方法
this.postOrderTraverseNode(this.root, callback);
}
private postOrderTraverseNode(node: Node<T>, callback: Function) {
// 基线条件
if (node != null) {
// 后序遍历的执行顺序是 左 -> 右 -> 执行回调
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
递归树操作
search
、insert
和 remove
三种增删查
的常规方法,均需要对树进行递归。
查
查询的递归操作很简单,当 key
比 node.key
小时向左查,反之向右查,直到查到值
或者 null
为止:
/**
* @description: 搜索元素
*/
search(key: T): boolean {
// 调用递归方法
return this.searchNode(this.root, key);
}
/**
* @description: 递归搜索
*/
private searchNode(node: Node<T>, key: T): boolean {
// 基线条件:查到尽头返回false
if (node == null) {
return false;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
// key 比 node.key 小,向左查
return this.searchNode(node.left, key);
} else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
// key 比 node.key 大,向右查
return this.searchNode(node.right, key);
} else {
// 基线条件:既不大也不小,说明查到该元素,返回true
return true;
}
}
增
插入的操作时首先判断是否树是否为空,为空则插入到根节点
。当树不为空时,肯定是要插入到一个合适的叶子节点
的位置,所以就通过大小
来递归判断
合适的插入位置即可:
/**
* @description: 插入元素
*/
insert(key: T) {
if (this.root == null) {
// 边界情况:插入到根节点
this.root = new Node(key);
} else {
// 递归找到插入位置
this.insertNode(this.root, key);
}
}
/**
* @description: 递归插入方法
*/
protected insertNode(node: Node<T>, key: T) {
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
// key 比 node.key 小就向左查
if (node.left == null) {
// 基线条件:左面为空直接赋值
node.left = new Node(key);
} else {
// 否则就接着递归
this.insertNode(node.left, key);
}
} else {
// key 比 node.key 大就向右查
if (node.right == null) {
// 基线条件:右面为空直接赋值
node.right = new Node(key);
} else {
// 否则就接着递归
this.insertNode(node.right, key);
}
}
}
删
删除节点的操作之所以放到最后,是因为删除操作是最复杂的。因为二叉搜索树是有序的,所以插入的时候会被插到正确的位置,不会插在树中间。但是在删除时,可以将树中的任何一个节点(包括树中间的节点)删除,这样就必须重新构造树的结构。分成以下三种情况:
- 删除
叶子节点
移除叶子节点是最简单的操作,因为叶子节点被删除后不涉及树结构的重新构造:
- 删除
只有一个子节点
的节点
删除只有一个左侧或右侧子节点的节点后,子树就孤立了,需要将子树重新融入到树中。但是情况也不复杂,只需要将子节点夹带子树
替代掉被删除的节点即可:
- 删除
有两个子节点
的节点
如果被删除的节点拥有两个子节点,情况就变复杂了。需要先在被删除的节点的右子树中找到最小
的节点,然后替代掉被删除节点的位置,再重新指向
右侧子树建立结构:
代码如下:
/**
* @description: 移除指定元素
*/
remove(key: T) {
// 调用递归方法,这里的递归很特殊,会将删除后的树返回
this.root = this.removeNode(this.root, key);
}
/**
* @description: 递归方法,在指定子树中移除指定元素,每次处理完后都需要将处理后的节点返回给本节点
*/
protected removeNode(node: Node<T>, key: T): Node<T> {
// 基线条件
if (node == null) {
return null;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
// 当 key 小于 node.key 时,向左去找
node.left = this.removeNode(node.left, key);
return node;
} else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
// 当 key 大于 node.key 时,向右去找
node.right = this.removeNode(node.right, key);
return node;
} else {
// 此时已经查到了要删除的节点
if (node.left == null && node.right == null) {
// 当要删除的节点为叶子节点
node = null;
return node;
} else if (node.left == null) {
// 当要删除的节点只有一个子节点
node = node.right;
return node;
} else if (node.right == null) {
// 同样删除的节点只有一个子节点
node = node.left;
return node;
} else {
// 当要删除的节点有两个子节点
const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
}
下一篇来分析 AVL树
。
前端记事本,不定期更新,欢迎关注!