上篇文章也提到了二叉树的优势以及重要性,那么就让我们来着手封装一个二叉树吧。我们先来看看它有哪些常见的操作方法。
二叉搜索树常见的操作
insert(key):向树中插入一个新的键
search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false
preOrderTraverse:通过先序遍历方式遍历所有节点
inOrderTraverse:通过中序遍历方式遍历所有节点
postorderTraverse:通过后序遍历方式遍历所有节点
min:返回树中最小的值
max:返回树中最大的值
remove(key):从树中移除某个键
insert(插入)方法
封装代码前我们先来捋一下insert的逻辑
要向树中插入一个新的节点或键要经历三个步骤。
1 第一步是验证插入操作树是否为一个空树。如果是,我们要做的就是创建一个Node 类的实例并将它赋值给 root(根节点) 属性。
2 如果树非空需要找到插入新节点的位置,寻找对应节点的方法:
如下图,如果我们需要在下图插入一个键为6的节点,那么就先拿6和11做比较,发现6<11,就向左移,再然后6和7做比较,6<7那么继续向下,和5做比较,6>5,那就让6插入到5的右节点
插入6的过程
封装代码
首先每个节点都需要left(左指针),right(右指针)和key(键)组成,所以我们需要先实现一个Node类
class CreateNode {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
insert封装
// 插入新节点
insert(key) {
let newNode = new CreateNode(key);
//判断是否为空树
if (!this.root) {
this.root = newNode
} else {
let parent = null;
let current = this.root;
// isLeftChild来判断是左侧还是右侧
let isLeftChild = false;
while (current) {
parent = current
if (key < current.key) {
current = current.left;
isLeftChild = true
} else {
current = current.right;
isLeftChild = false;
}
}
isLeftChild ? parent.left = newNode : parent.right = newNode
}
}
先序遍历(preOrderTraverse)
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。
我们来看实现.callBack是为了测试的一个函数,下面同理。
preOrderTraverse(callBack) {
this.preOrderTraverseNode(this.root, callBack)
}
preOrderTraverseNode(node, callBack) {
if (node) {
callBack(node.key);
this.preOrderTraverseNode(node.left, callBack);
this.preOrderTraverseNode(node.right, callBack)
}
}
上面代码是根据函数调用栈来实现的this.preOrderTraverseNode(node.left, callBack);
走这段代码的时候将11.7.5.3相继压入栈中,当node==null的时候结束递归,栈中的函数依次释放。由于已经执行完了node.left,每次出栈的时候会执行下面的node.right,依次类推就形成了上图的执行顺序
中序遍历
中序遍历是一种以上行顺序访问 BST(BinarySearchTree)所有节点的遍历方式也就是以从最小到最大的顺序访问所有节点
中序遍历的一种应用就是对树进行排序操作。我们来看看它的实现。
inOrderTraverse(callBack) {
this.inOrderTraverseNode(this.root, callBack)
}
inOrderTraverseNode(node, callBack) {
if (node) {
this.inOrderTraverseNode(node.left, callBack);
callBack(node.key);
this.inOrderTraverseNode(node.right, callBack)
}
}
后序遍历
后序遍历则是先访问节点的后代节点再访问节点本身。后序遍历的一种应用是计算一个目录及其子目录中所有文件所占空间的大小。
我们来看看它的实现。
// 后序遍历
postorderTraverse(callBack) {
this.postorderTraverseNode(this.root, callBack)
}
postorderTraverseNode(node, callBack) {
if (node) {
this.postorderTraverseNode(node.left, callBack);
this.postorderTraverseNode(node.right, callBack);
callBack(node.key);
}
}
想必明白了一种其他两种就会自然而然的明白了,要结合栈内存一起理解。
min(寻找最小值)
在一棵二叉树中最左侧就是最小值
min() {
let current = this.root;
while (current.left) {
current = current.left
}
return current.key
}
max(寻找最大值)
在一棵二叉树中最右侧就是最小值
max() {
let current = this.root;
while (current.right) {
current = current.right
}
return current.key
}
search(根据key值寻找)
根据key值寻找只要当前的key!=要寻找的key那么就继续向深一层的寻找,如果找不到直接返回false,找到了返回true
search(key) {
let current = this.root;
while (current && current.key != key) {
if (key < current.key) {
current = current.left
} else if (key > current.key) {
current = current.right
}
}
return current ? true : false
}
删除(remove)
删除操作可以说是二叉树中最难的一个方法了;下面会一一列举删除需要顾及的场景;
第一步
删除的第一步是先寻找要删除的key
parent:是对应节点的父节点
isLeftChild:是为了区分是左子树还是右子树
let current = this.root;
let parent = null;
let isLeftChild = false;
while (current && current.key != key) {
parent = current;
if (key < current.key) {
isLeftChild = true;
current = current.left;
} else if (key > current.key) {
isLeftChild = false;
current = current.right
}
}
if (!current) return;
第二步
判断要删除的节点是不是叶子节点,也就是没有左子树(left)和右子树(right);这种情况下直接让parent的左子树或者右子树置为null即可。例如删除的是下图中6这个节点,上面寻找到的current为6,parent为5.
if (!current.left && !current.right) {
isLeftChild ? parent.left = null : parent.right = null;
}
第三步
如果有左子树没有右子树的情况下
1 删除{5}节点,{5}节点只有左子树,删除{5}这个节点直接让{7}的左指针指向{5}的左子树 parent.left = current.left
2 删除{5}节点,如图{5}节点只有右子树,删除{5}这个节点直接让{7}的左指针指向{5}的右子树 parent.left = current.right
3 删除{20}节点,如图{20}节点只有右子树,删除{20}这个节点直接让{15}的右指针指向{20}的右子树parent.right = current.right
4 删除{20}节点,如图{20}节点只有左子树,删除{20}这个节点直接让{15}的右指针指向{20}的左子树parent.right = current.left
上面的情况都已经列举清除,由于我们上面已经定义了isLeftChild的变量,可以轻松知道parent的指针方向,至于删除当前节点的左节点还是有节点这个需要我们判断一下
if (current.left && !current.right) {
isLeftChild ? parent.left = current.left : parent.right = current.left;
} else if (current.right && !current.left) {
isLeftChild ? parent.left = current.right : parent.right = current.left;
}
第四步
删除有两个子节点的树,这一步也是删除方法中场景最多的一步。
如下图删除{15}这个节点,那么我们需要从它的子节点中的某一个提上去而又不会影响二叉树的规则,那么该提取哪一个呢。
这里有个词叫前驱后继,其中我们选择前驱也好选择后继也可以。比如下面删除{15}节点他的前驱是14,那么把14提上去又变成了一棵符合规则的二叉树。{15}的后继为18,用18替代15也是一棵符合规则的树。参照上面我们可以找到寻找前驱后继的规则:
前驱:删除某节点的左子树中最大的键
后继:删除某节点的右子树中最小的键
删除15需要做的逻辑
1.找到15的后继(前驱也可以,这里我选择后继)18
2.将18提到15的位置,让18的左指针指向13,让18的右指针指向20
3.将20的左指针指向19
删除11需要做的逻辑
直接让根 = 后继
获取前驱的代码
getSuccessor(node) {
let successParent = null;
while (node.left) {
successParent = node;
node = node.left
}
if (successParent) {
successParent.left = node.right;
}
return node
}
删除有两个子节点的代码
let successor = this.getSuccessor(current.right);
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor;
}
successor.left = current.left;
if (current.right != successor) {
successor.right = current.right
}
完整代码
class CreateNode {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
// 插入新节点
insert(key) {
let newNode = new CreateNode(key);
if (!this.root) {
this.root = newNode
} else {
let parent = null;
let current = this.root;
let isLeftChild = false;
while (current) {
parent = current
if (key < current.key) {
current = current.left;
isLeftChild = true
} else {
current = current.right;
isLeftChild = false;
}
}
isLeftChild ? parent.left = newNode : parent.right = newNode
}
}
// 先序遍历
preOrderTraverse(callBack) {
this.preOrderTraverseNode(this.root, callBack)
}
preOrderTraverseNode(node, callBack) {
if (node) {
callBack(node.key);
this.preOrderTraverseNode(node.left, callBack);
this.preOrderTraverseNode(node.right, callBack)
}
}
// 中序遍历
inOrderTraverse(callBack) {
this.inOrderTraverseNode(this.root, callBack)
}
inOrderTraverseNode(node, callBack) {
if (node) {
this.inOrderTraverseNode(node.left, callBack);
callBack(node.key);
this.inOrderTraverseNode(node.right, callBack)
}
}
// 后序遍历
postorderTraverse(callBack) {
this.postorderTraverseNode(this.root, callBack)
}
postorderTraverseNode(node, callBack) {
if (node) {
this.postorderTraverseNode(node.left, callBack);
this.postorderTraverseNode(node.right, callBack);
callBack(node.key);
}
}
// 最大值
max() {
let current = this.root;
while (current.right) {
current = current.right
}
return current.key
}
// 最小值
min() {
let current = this.root;
while (current.left) {
current = current.left
}
return current.key
}
// 查询值
search(key) {
let current = this.root;
while (current && current.key != key) {
if (key < current.key) {
current = current.left
} else if (key > current.key) {
current = current.right
}
}
return current ? true : false
}
// 删除
remove(key) {
let current = this.root;
let parent = null;
let isLeftChild = false;
while (current && current.key != key) {
parent = current;
if (key < current.key) {
isLeftChild = true;
current = current.left;
} else if (key > current.key) {
isLeftChild = false;
current = current.right
}
}
if (!current) return;
if (!current.left && !current.right) {
isLeftChild ? parent.left = null : parent.right = null;
} else {
if (current.left && !current.right) {
isLeftChild ? parent.left = current.left : parent.right = current.left;
} else if (current.right && !current.left) {
isLeftChild ? parent.left = current.right : parent.right = current.left;
} else {
let successor = this.getSuccessor(current.right);
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor;
}
successor.left = current.left;
if (current.right != successor) {
successor.right = current.right
}
}
}
}
getSuccessor(node) {
let successParent = null;
while (node.left) {
successParent = node;
node = node.left
}
if (successParent) {
successParent.left = node.right;
}
return node
}
}
let bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(10);
bst.insert(8);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(19);
// 测试先序遍历
let resString = '';
bst.preOrderTraverse(function (key) {
resString += key + ' ';
})
alert(resString)
// 测试中序遍历
let resString = '';
bst.inOrderTraverse(function (key) {
resString += key + ' ';
});
alert(resString)
// 测试后序遍历
let resString = '';
bst.postorderTraverse(function (key) {
resString += key + ' ';
});
alert(resString)
//最大值
alert(bst.max());
//最小值
alert(bst.min());
// 查询值
alert(bst.search(18))
// 删除
bst.remove(11)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。