上一篇《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
:
1 2 3 4 5 6 7 8 9 10
| export class Node<K> { left: Node<K>; right: Node<K>;
constructor(public key: K) {}
toString() { return `${this.key}`; } }
|
辅助方法
因为二叉搜索树是有序排列的树,所以需要有个比较大小的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export enum Compare { LESS_THAN = -1, BIGGER_THAN = 1, EQUALS = 0, }
export type ICompareFunction<T> = (a: T, b: T) => number;
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
表示根节点。
1 2 3 4 5 6 7 8
| 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
的父节点
即为极值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
getRoot(): Node<T> { return this.root; }
min(): Node<T> { return this.minNode(this.root); }
protected minNode(node: Node<T>): Node<T> { let current = node; while (current != null && current.left != null) { current = current.left; } return current; }
max(): Node<T> { return this.maxNode(this.root); }
protected maxNode(node: Node<T>): Node<T> { let current = node; while (current != null && current.right != null) { current = current.right; } return current; }
|
有序遍历
三种有序遍历
的作用就是按照特定的方式挨个遍历整个树,访问每个节点并执行回调函数
。在二叉树中,递归只有两个方向(向左
和向右
),先中后
序遍历的区别就是查看 key
并执行回调函数的步骤放在两次递归步骤的前中后
的相对位置
。
先序遍历
先序遍历
的顺序就是执行回调 -> 左 -> 右
,所以访问顺序是父节点优先
,适合打印结构化的文档:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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); } }
|
中序遍历
中序遍历
的顺序就是左 -> 执行回调 -> 右
,所以符合从小到大排列的顺序,可以用来打印排序后的结果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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); } }
|
后序遍历
后序遍历
的顺序就是左 -> 右 -> 执行回调
,所以访问顺序是子节点优先
,可以应用到计算一个目录及其子目录中所有文件所占空间的大小。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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
为止:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
search(key: T): boolean { return this.searchNode(this.root, key); }
private searchNode(node: Node<T>, key: T): boolean { 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; } }
|
增
插入的操作时首先判断是否树是否为空,为空则插入到根节点
。当树不为空时,肯定是要插入到一个合适的叶子节点
的位置,所以就通过大小
来递归判断
合适的插入位置即可:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
insert(key: T) { if (this.root == null) { this.root = new Node(key); } else { this.insertNode(this.root, key); } }
protected insertNode(node: Node<T>, key: T) { if (this.compareFn(key, node.key) === Compare.LESS_THAN) { if (node.left == null) { node.left = new Node(key); } else { this.insertNode(node.left, key); } } else { if (node.right == null) { node.right = new Node(key); } else { this.insertNode(node.right, key); } } }
|
删
删除节点的操作之所以放到最后,是因为删除操作是最复杂的。因为二叉搜索树是有序的,所以插入的时候会被插到正确的位置,不会插在树中间。但是在删除时,可以将树中的任何一个节点(包括树中间的节点)删除,这样就必须重新构造树的结构。分成以下三种情况:
- 删除
叶子节点
移除叶子节点是最简单的操作,因为叶子节点被删除后不涉及树结构的重新构造:

- 删除
只有一个子节点
的节点
删除只有一个左侧或右侧子节点的节点后,子树就孤立了,需要将子树重新融入到树中。但是情况也不复杂,只需要将子节点夹带子树
替代掉被删除的节点即可:

- 删除
有两个子节点
的节点
如果被删除的节点拥有两个子节点,情况就变复杂了。需要先在被删除的节点的右子树中找到最小
的节点,然后替代掉被删除节点的位置,再重新指向
右侧子树建立结构:

代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
remove(key: T) { this.root = this.removeNode(this.root, key); }
protected removeNode(node: Node<T>, key: T): Node<T> { if (node == null) { return null; }
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; } 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树
。
前端记事本,不定期更新,欢迎关注!