开启掘金成长之旅!这是我参与「掘金日新计划 12 月更文挑战」的第 7 天,点击查看活动详情
最近在学习 Javascript 数据结构与算法相关知识,数据结构与算法对程序员来说就像是内功心法,只有不断修炼自己的内功,才能从容不迫的应对市场上各种让人应接不暇的框架,达到以不变应万变。学习数据结构与算法的过程中不仅要能看懂更要多写多练,今天就来手写下二叉搜索树数据结构。
二叉树中的节点最多只能有两个子节点: 一个是左侧子节点,另一个是右侧子节点。 二叉搜索树是二叉树的一种,但是只允许你在左侧节点存储比父节点小的值,在右侧节点存储比父节点大的值
二叉搜索树基本操作方法:
- insert(key):向树中插入一个新的键。
- search(key):在树中查找一个键。如果节点存在,则返回 true;如果不存在,则返回 false。
- inOrderTraverse():通过中序遍历方式遍历所有节点。
- preOrderTraverse():通过先序遍历方式遍历所有节点。
- postOrderTraverse():通过后序遍历方式遍历所有节点。
- min():返回树中最小的值/键。
- max():返回树中最大的值/键。
- remove(key):从树中移除某个键。
创建 BinarySearchTree 类
class Node<K> {
left: Node<K>; // 左侧子节点引用
right: Node<K>; // 右侧子节点引用
constructor(public key: K) {}
toString() {
return `${this.key}`;
}
}
class BinarySearchTree {
protected root: Node<T>;
constructor() {}
}
向二叉搜索树中插入一个键
class BinarySearchTree {
// ...
insert(key: T) {
if (!this.root) {
this.root = new Node(key);
} else {
this.insertNode(this.root, key);
}
}
protected insertNode(node: Node<T>, key: T) {
// 这里我们默认key是数值
if (key < node.key) {
// 新节点小于当前节点 插入左子树
if (!node.left) {
node.left = new Node(key);
} else {
this.insertNode(node.left, key);
}
} else {
// 新节点大于当前节点 插入右子树
if (!node.right) {
node.right = new Node(key);
} else {
this.insertNode(node.right, key);
}
}
}
}
中序遍历
中序遍历是一种以上行顺序访问 BST 所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。先访问左节点 在访问根节点 最后访问右节点
class BinarySearchTree {
// ...
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);
}
}
}
先序遍历
先序遍历是以优先于后代节点的顺序访问每个节点的。先访问根节点 在访问左节点 最后访问右节点
class BinarySearchTree {
// ...
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);
}
}
}
后续遍历
后序遍历则是先访问左节点,再访问右点,最后访问根节点
class BinarySearchTree {
// ...
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);
}
}
}
搜索最大值和最小值
对于寻找最小值,总是沿着树的左边;而对于寻找最大值,总是沿着树的右边。
class BinarySearchTree {
// ...
min() {
return this.minNode(this.root);
}
protected minNode(node: Node<T>) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
protected maxNode(node: Node<T>) {
let current = node;
while (current != null && current.right != null) {
current = current.right;
}
return current;
}
}
搜索一个特定的值
如果要找的键比当前的节点小,那么继续在左侧的子树上搜索。 如果要找的键比当前的节点大,那么就从右侧子节点开始继续搜索, 否则就说明要找的键和当前节点的键相等,返回 true 来表示找到了这个键
class BinarySearchTree {
// ...
search(key: T) {
return this.searchNode(this.root, key);
}
private searchNode(node: Node<T>, key: T): boolean {
if (node == null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
}
return true;
}
}
移除一个节点
第一种情况:该节点是一个没有左侧或右侧子节点的叶节点 第二种情况: 移除有一个左侧或右侧子节点的节点 这种情况下,需要跳过这个节点,直接将父节点指向它的指针指向子节点。 第三种情况:
要移除的节点有两个子节点——左侧子节点和右侧子节点 当找到了要移除的节点后,需要找到它右边子树中最小的节点(它的继承者——行。 然后,用它右侧子树中最小节点的键去更新这个节点的值。通过这一步,我们改变了这个节点的键,也就是说它被移除了。 但是,这样在树中就有两个拥有相同键的节点了,这是不行的。要继续把右侧子树中的最小节点移除,毕竟它已经被移至要移除的节点的位置了。 最后,向它的父节点返回更新后节点的引用
class BinarySearchTree {
// ...
remove(key: T) {
this.root = this.removeNode(this.root, key);
}
protected removeNode(node: Node<T> | null, key: T) {
if (node == null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// 第一种情况
if (node.left == null && node.right == null) {
node = null;
// 现在节点的值已经是null 了,父节点指向它的指针也会接收到这个值,这也是我们为什么要在函数中返回节点的值
return node;
}
// 第二种情况
if (node.left == null) {
node = node.right;
return node;
} else if (node.right == null) {
node = node.left;
return node;
}
// 第三种情况
const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
}
使用场景
leetcode 练习题:104
二叉树的最大深度 给定一个二叉树,找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
var maxDepth = function (root) {
if (root === null) {
return 0;
} else {
const leftHeight = maxDepth(root.left);
const rightHeight = maxDepth(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}
};
解题思路
如果我们知道了左子树和右子树的最大深度 l 和 r,那么该二叉树的最大深度即为max(l,r) + 1
而左子树和右子树的最大深度又可以以同样的方式进行计算。
因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1)时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。