树(Tree)
本文作为树数据结构的第四篇,介绍二叉搜索树BST上的删除某个节点的方法。删除节点可能会引起二叉树的重新排布,所以具有一定的复杂性,这篇文章中主要使用了分类讨论的办法抽丝剥茧,逐步解决复杂问题。
1. 预备知识
在实现结点删除操作之前,需要明确一个基本问题:如何将一个结点从树中删除掉,这个问题的本质是什么:
- 将一个结点从树上删掉的本质是其父节点不再保持对此子节点的引用
- 假如待删除的节点是其父节点的左子节点,那么只需要将其父结点的left属性置空即可;同理右子节点
- 不难看出来,要想删除一个节点需要三个要素:待删除的结点、待删除的结点的父节点、待删除的结点是左/右子节点
2. 删除一个结点的步骤
-
- 找到目标节点;这个过程不能复用之前的search方法,原因是还要得到目标结点的父结点和它是父结点的左还是右子结点
-
- 删除目标结点
-
- 处理删除之后的变动
searchPlus(key: number): [null] | [null |_Node, null | _Node, boolean] {
return this.searchNodePlus(this.root, null, key);
}
searchNodePlus(node: null | _Node, parentNode: null | _Node, key: number): [null] | [null |_Node, null | _Node, boolean] {
if(!node) return [null];
if (node.key > key) {
// 此时应该搜索其左边
return this.searchNodePlus(node.left, node, key);
} else if (node.key < key) {
// 此时应该搜索其右边
return this.searchNodePlus(node.right, node, key);
} else {
// 找到的话就直接返回
const isLeft = node === parentNode?.left;
return [node, parentNode, isLeft];
}
}
3. 逐步分析
显而易见,步骤1和步骤2是比较简单的,步骤1中只需要增加保存数据的变量个数就可以记录目标结点及其是左/右结点的信息;而步骤3是比较复杂的,因为目标节点被删除之后其子结点如何处理是一个比较棘手的问题。
4. 分类讨论
针对结点删除之后该如何处理,可以根据目标节点所处的位置分别处理:
- 如果是叶子结点,也就是没有子结点,此时只需要将其删除掉,根本不用处理其子结点,因为它没有子节点。那么如何判断目标结点是不是子结点呢,可以增加一个方法
isLeaf
, 判断传入的结点的left和right属性是否存在。 - 如果目标结点只有左结点或者右结点,那也比较简单,只需要将其原来的位置让给它的左/右子结点就可以了。
- 如果目标结点的左右结点都存在,那确实就比较困难了,因为要考虑的问题比较多。
// 判断是否为叶子结点
isLeaf (targetNode: null | _Node) {
if(!targetNode) return false;
if(!targetNode.left && !targetNode.right) return false;
return true;
}
5. 删除度为2的结点
复杂性体现在,删除之后是左子结点还是右子结点或者说是其他的什么结点来占据被删除结点的位置。
- 先给结论:替换原来被删除结点的其它结点应该是:被删除结点的前驱或者后继;
- 结点的前趋或者后继指的都是距离此结点的key差值的绝对值最小的结点,只不过前驱的key小于目标结点的key,后继的key大于目标节点的key;
- 显然,结点的前驱和后继可能是不存在的,但是对于度为2的结点,前驱和后继确实是存在的;
- 这里需要注意的是,使用前驱或者后继替换被删除的结点的位置都是可以的;
- 根据定义,前驱是目标节点左子树的右下角结点;而后继则是右子树的左下角结点;所以前驱只有可能有左子结点而后继只可能有右子结点,这很重要!
- 假如使用前驱替代了目标结点,由于前驱没有右子结点,并且和目标结点的key值最接近,这意味着目标结点的左侧子树上没有其它结点的key会大于此前驱,只需要
1. 前驱的左子结点替换前驱原来的位置;2. 前驱的左子结点和右子结点变成目标子结点原来的左子结点和右子结点; 3. 目标结点的父结点直接指向前驱
;如果用到的是后继,情况也是一样的! - 所以需要一个寻找结点前驱
getPredecessor
的方法,以及一个寻找结点后继getSuccessor
的方法:
getSuccessor (node: null | _Node) {
if(!node) return null;
// 保存后继的变量
let successor = node;
// 保存后继的父结点的变量
let successorParent = node;
// 寻找后继的node指针
let nodePointer = node.right;
// 循环查找
while (nodePointer) {
successorParent = successor; // 往下走一层
successor = nodePointer; // 暂存node指针的值
nodePointer = nodePointer.left; // 后继是右子树上的左下角
}
// 处理后继的右子树
successorParent.left = successor.right;
// 由于后继需要继承目标结点的左子结点和右子结点,所以这里必须排除后继本身是目标结点的右子结点,否则就成了自己是自己的右子结点了
// 这个简称为:排除后继是右子结点的可能
// 处理之后需要为后继找新的右子结点
if (successor !== node.right) {
successor.right = node.right;
}
return successor;
}
getPredecessor (node: null | _Node) {
if(!node) return null;
// 保存前驱的变量
let predecessor = node;
// 保存前驱的父结点的变量
let predecessorParent = node;
// 寻找前驱的node指针
let nodePointer = node.left;
// 循环查找
while (nodePointer) {
predecessorParent = predecessor; // 往下走一层
predecessor = nodePointer; // 暂存node指针的值
nodePointer = nodePointer.right; // 前驱是左子树上右下角
}
// 处理前驱的左子树
predecessorParent.right = predecessor.left;
// 由于前驱需要继承目标结点的左子结点和右子结点,所以这里必须排除前驱本身是目标结点的左子结点,否则就成了自己是自己的左子结点了
// 这个简称为:排除前驱是左子结点的可能
// 处理之后要为前驱找新的左子结点
if (predecessor !== node.left) {
predecessor.left = node.left;
}
}
6. 删除某个结点的remove方法的实现
remove(key: number): boolean {
// 首先找到key对应的结点
const [node, parentNode, isLeft] = this.searchPlus(key);
// 如果找不到就谈不上删除
if(!node) return false;
// 然后判断是不是叶子结点
if(this.isLeaf(node)){
// 如果父结点不存在,表示找到的结点是根结点,而且根节点还是叶子结点,也就是说整个树就只有一个根节点
if (!parentNode) {
this.root = null;
} else if (isLeft) {
// 如果要删除的叶子节点是左子结点,就将其父结点的left值置为null即可
parentNode.left = null;
} else {
parentNode.right = null;
}
} else {
// 如果不是叶子结点,则先判断是否只有一个子结点
// 只有一个左子结点的情况
if (!node.right) {
// 此时只需要将其左子结点替换到node的位置上就可以了
// 但是同样需要先判断node是不是根节点
if (node == this.root) {
this.root = node.left;
} else if (isLeft) {
parentNode!.left = node.left;
} else {
parentNode!.right = node.left;
}
} else if (!node.left) {
// 只有一个右子结点的情况
// 此时只需要将其右子结点替换到node的位置上就可以了
// 但是同样需要先判断node是不是根节点
if (node == this.root) {
this.root = node.right;
} else if (isLeft) {
parentNode!.left = node.right;
} else {
parentNode!.right = node.right;
}
} else {
// 下面使用后继处理度为2的待删除的结点
const successor = this.getSuccessor(node)!;// 从getSuccessor出来的时候已经处理了后继的子结点的结构关系和后继的右子结点了
// 如果node是根结点,并且有两个子结点,那么让后继做根节点
if (node == this.root) {
this.root = successor;
} else if (isLeft) {
parentNode!.left = successor;
} else {
parentNode!.right = successor;
}
parentNode!.right = successor;
successor.left = node.left;
}
}
return true;
}
7. BST整体代码
不难看出,删除一个结点的remove方法的复杂性远远大于BST中其它方法的复杂性!
class _Node {
left: _Node | null = null;
right: _Node | null = null;
constructor (public key: number) {}
}
class _BST {
root: _Node | null = null;
insert(key: number): void{
// 根据key产生一个新节点
const newNode = new _Node(key);
// 判断根节点是不是存在
if (!this.root) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(targetNode: _Node, newNode: _Node): void {
if (newNode.key < targetNode.key) {
if (!targetNode.left) {
targetNode.left = newNode;
} else {
this.insertNode(targetNode.left, newNode);
}
} else {
if (!targetNode.right) {
targetNode.right = newNode;
} else {
this.insertNode(targetNode.right, newNode);
}
}
}
preOrderTraverse(handler: Function): void {
this.preOrderTraverseNode(this.root, handler);
}
preOrderTraverseNode(node: null | _Node, handler: Function): void {
if(node){
// 先访问根节点
handler(node);
// 然后是左子树
this.preOrderTraverseNode(node.left, handler);
// 最后是右子树
this.preOrderTraverseNode(node.right, handler);
}
}
midOrderTraverse(handler: Function): void {
this.midOrderTraverseNode(this.root, handler);
}
midOrderTraverseNode(node: null | _Node, handler: Function): void {
if(node){
// 先访问左子树
this.midOrderTraverseNode(node.left, handler);
// 然后是根节点
handler(node);
// 最后是右子树
this.midOrderTraverseNode(node.right, handler);
}
}
postOrderTraverse(handler: Function): void {
this.postOrderTraverseNode(this.root, handler);
}
postOrderTraverseNode(node: null | _Node, handler: Function): void {
if(node){
// 先访问左子树
this.postOrderTraverseNode(node.left, handler);
// 然后是右子树
this.postOrderTraverseNode(node.right, handler);
// 最后是根节点
handler(node);
}
}
min(): number {
if(!this.root) return NaN;
let pointerNode = this.root;
while(pointerNode.left){
pointerNode = pointerNode.left;
}
return pointerNode.key;
}
max(): number {
if(!this.root) return NaN;
let pointerNode = this.root;
while(pointerNode.right){
pointerNode = pointerNode.right;
}
return pointerNode.key;
}
search(key: number): null | _Node {
return this.searchNode(this.root, key);
}
searchNode(node: null | _Node, key: number): null | _Node {
if(!node) return null;
if (node.key > key) {
// 此时应该搜索其左边
return this.searchNode(node.left, key);
} else if (node.key < key) {
// 此时应该搜索其右边
return this.searchNode(node.right, key);
} else {
// 找到的话就直接返回
return node;
}
}
// 非常难搞
remove(key: number): boolean {
// 首先找到key对应的结点
const [node, parentNode, isLeft] = this.searchPlus(key);
// 如果找不到就谈不上删除
if(!node) return false;
// 然后判断是不是叶子结点
if(this.isLeaf(node)){
// 如果父结点不存在,表示找到的结点是根结点,而且根节点还是叶子结点,也就是说整个树就只有一个根节点
if (!parentNode) {
this.root = null;
} else if (isLeft) {
// 如果要删除的叶子节点是左子结点,就将其父结点的left值置为null即可
parentNode.left = null;
} else {
parentNode.right = null;
}
} else {
// 如果不是叶子结点,则先判断是否只有一个子结点
// 只有一个左子结点的情况
if (!node.right) {
// 此时只需要将其左子结点替换到node的位置上就可以了
// 但是同样需要先判断node是不是根节点
if (node == this.root) {
this.root = node.left;
} else if (isLeft) {
parentNode!.left = node.left;
} else {
parentNode!.right = node.left;
}
} else if (!node.left) {
// 只有一个右子结点的情况
// 此时只需要将其右子结点替换到node的位置上就可以了
// 但是同样需要先判断node是不是根节点
if (node == this.root) {
this.root = node.right;
} else if (isLeft) {
parentNode!.left = node.right;
} else {
parentNode!.right = node.right;
}
} else {
// 下面使用后继处理度为2的待删除的结点
const successor = this.getSuccessor(node)!;// 从getSuccessor出来的时候已经处理了后继的子结点的结构关系和后继的右子结点了
// 如果node是根结点,并且有两个子结点,那么让后继做根节点
if (node == this.root) {
this.root = successor;
} else if (isLeft) {
parentNode!.left = successor;
} else {
parentNode!.right = successor;
}
parentNode!.right = successor;
successor.left = node.left;
}
}
return true;
}
// 判断是否为叶子结点
isLeaf (targetNode: null | _Node) {
if(!targetNode) return false;
if(!targetNode.left && !targetNode.right) return false;
return true;
}
searchPlus(key: number): [null] | [null |_Node, null | _Node, boolean] {
return this.searchNodePlus(this.root, null, key);
}
searchNodePlus(node: null | _Node, parentNode: null | _Node, key: number): [null] | [null |_Node, null | _Node, boolean] {
if(!node) return [null];
if (node.key > key) {
// 此时应该搜索其左边
return this.searchNodePlus(node.left, node, key);
} else if (node.key < key) {
// 此时应该搜索其右边
return this.searchNodePlus(node.right, node, key);
} else {
// 找到的话就直接返回
const isLeft = node === parentNode?.left;
return [node, parentNode, isLeft];
}
}
getSuccessor (node: null | _Node) {
if(!node) return null;
// 保存后继的变量
let successor = node;
// 保存后继的父结点的变量
let successorParent = node;
// 寻找后继的node指针
let nodePointer = node.right;
// 循环查找
while (nodePointer) {
successorParent = successor; // 往下走一层
successor = nodePointer; // 暂存node指针的值
nodePointer = nodePointer.left; // 后继是右子树上的左下角
}
// 处理后继的右子树
successorParent.left = successor.right;
// 由于后继需要继承目标结点的左子结点和右子结点,所以这里必须排除后继本身是目标结点的右子结点,否则就成了自己是自己的右子结点了
// 这个简称为:排除后继是右子结点的可能
// 处理之后需要为后继找新的右子结点
if (successor !== node.right) {
successor.right = node.right;
}
return successor;
}
getPredecessor (node: null | _Node) {
if(!node) return null;
// 保存前驱的变量
let predecessor = node;
// 保存前驱的父结点的变量
let predecessorParent = node;
// 寻找前驱的node指针
let nodePointer = node.left;
// 循环查找
while (nodePointer) {
predecessorParent = predecessor; // 往下走一层
predecessor = nodePointer; // 暂存node指针的值
nodePointer = nodePointer.right; // 前驱是左子树上右下角
}
// 处理前驱的左子树
predecessorParent.right = predecessor.left;
// 由于前驱需要继承目标结点的左子结点和右子结点,所以这里必须排除前驱本身是目标结点的左子结点,否则就成了自己是自己的左子结点了
// 这个简称为:排除前驱是左子结点的可能
// 处理之后要为前驱找新的左子结点
if (predecessor !== node.left) {
predecessor.left = node.left;
}
}
}