前面实现了二叉搜索树的添加以及二叉树的遍历,也知道了前驱节点和后继节点的概念,现在就可以实现二叉搜索数的删除功能,实现一个完整的二叉搜索树了。
继续为二叉搜索树实现以下两个方法:
// 删除一个元素,即删除这个元素对应的节点
- (void)removeObject:(id)object {
[self removeWithNode:[self nodeWithObject:object]];
}
// 删除一个节点
- (void)removeWithNode:(JKRBinaryTreeNode *)node {
}
复制代码
删除叶子节点
如上图,删除二叉树中的叶子节点,只需要移除该节点就可以,二叉树依然满足二叉搜索树的性质。
// 叶子节点既没有左子树也没有右子树
if (!node.left && !node.right) {
// 如果是根节点就是直接让根节点为空
if (!node.parent) {
_root = nil;
} else {
// 如果是父节点的左子节点,就让父节点的左字节点为空
if (node == node.parent.left) {
node.parent.left = nil;
} else {
node.parent.right = nil;
}
}
}
复制代码
删除叶子节点的逻辑:
- 判断被删除节点如否是根节点,是进入第2步,否进入第3步。
- 直接将二叉树的根节点指向空
- 判断被删除节点是父节点的左子节点还是右子节点,左子节点进入第4步,右子节点进入第5步
- 将断被删除节点的父节点的左子节点置空
- 将断被删除节点的父节点的右子节点置空
根据如上逻辑,删除二叉树中的 5 和 8:
[tree removeObject:@5];
┌-7 (p: (null))-┐
│ │
┌-4 (p: 7) ┌-9 (p: 7)-┐
│ │ │
┌-2 (p: 4)-┐ 8 (p: 9) ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
[tree removeObject:@8];
┌-7 (p: (null))-┐
│ │
┌-4 (p: 7) 9 (p: 7)-┐
│ │
┌-2 (p: 4)-┐ ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
复制代码
删除度为1的节点
如上图,删除度为1节点后,非常类似于删除链表的节点的操作,只需要将节点的父节点指向它的指针指向它自己的字节点就可以:
if ((node.left && !node.right) || (node.right && !node.left)) {
JKRBinaryTreeNode *replacement = node.left ? node.left : node.right;
// 一定不要忘记度parent的维护
replacement.parent = node.parent;
if (!node.parent) {
_root = replacement;
} else if(node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
}
复制代码
删除度为1的节点的逻辑:
- 判断被删除节点如否是根节点,是进入第2步,否进入第3步
- 被删除节点的度为1,只有一个子节点,获取被删除节点的子节点
- 将被删除节点的子节点的parent指向被删除节点的父节点
- 判断被删除节点是父节点的左子节点还是右子节点,左子节点进入第5步,右子节点进入第6步
- 将断被删除节点的父节点的左子节点指向被删除节点的子节点
- 将断被删除节点的父节点的右子节点指向被删除节点的子节点
根据如上逻辑,删除二叉树中的 4 和 9:
[tree removeObject:@4];
┌-7 (p: (null))-┐
│ │
┌-2 (p: 7)-┐ 9 (p: 7)-┐
│ │ │
1 (p: 2) 3 (p: 2) ┌-11 (p: 9)-┐
│ │
10 (p: 11) 12 (p: 11)
[tree removeObject:@9];
┌----7 (p: (null))----┐
│ │
┌-2 (p: 7)-┐ ┌-11 (p: 7)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
复制代码
一定不要忘记对parent的维护。
删除度为2的节点
二叉搜索树的中度为2的节点如下图:
可以看到这些节点都有两个子节点,即同时存在左子树和右子树,这些节点不能直接移除,也不能直接让它的一个子节点替代它的位置,但是可以根据如下方法删除:
- 找到被删除节点的后继节点(或前驱节点)做为替代节点
- 将替代节点的保存的元素移动到被删除节点中
- 删除替代节点
比如删除上述二叉搜索树中的根节点 7 :
先找到7的后继节点做为替代的节点
将后继节点的值移动到根节点上
删除后继节点:
可以看到,这样操作之后,二叉树还是一棵二叉搜索树。
这里以后继节点来分析,前驱节点同理。后继节点是中序遍历时,一个节点的后一个节点,二叉搜索树的中序遍历又好是升序排列。所以在二叉搜索树中的,一个节点的后继节点的元素刚按照升序排列的后一个元素。
二叉搜索树中一个度为2的节点,它的后继节点一定在它的右子树中,且一定不是度为2的节点,它的度只能是1或者0。
原因:
- 二叉搜索树中序遍历刚好是升序,所以它的后继节点一定是按照升序排列后一个节点
- 二叉搜索树任意节点的左子树所有元素都比它小,右子树所有元素都比它大
- 所以一个度2的节点的后继节点,一定在它的右子树中,而且是它的右子树中所有元素最小的
- 所以一个度为2的节点的后继节点,一定不会存在左子树。因为假如它有左子树,它就不是最小的。
综上逻辑找到一个二叉搜索树节点的前驱节点或后继节点如下:
#pragma mark - 节点的前驱节点
- (JKRBinaryTreeNode *)predecessorWithNode:(JKRBinaryTreeNode *)node {
if (!node) {
return nil;
}
// 节点有左子树的情况下,前驱节点在它的左子树中
if (node.left) {
// 前驱节点是 node.left.right.right...
JKRBinaryTreeNode *p = node.left;
while (p.right) {
p = p.right;
}
return p;
}
// 节点没有左子树的情况下,如果有父节点,在父节点往上找
while (node.parent && node == node.parent.left) {
node = node.parent;
}
// 没有左子树,也没有父节点,就没有前驱节点
// !node.left && (!node.parent || node == node.parent.right)
return node.parent;
}
#pragma mark - 节点的后继节点
- (JKRBinaryTreeNode *)successorWithNode:(JKRBinaryTreeNode *)node {
if (!node) {
return nil;
}
if (node.right) {
JKRBinaryTreeNode *p = node.right;
while (p.left) {
p = p.left;
}
return p;
}
while (node.parent && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
复制代码
而删除度为2节点,只需要先找到这个节点后继节点,然后将后继节点的值移动后,直接利用上面删除度为0或者1的节点逻辑就可以:
if (node.left && node.right) {
JKRBinaryTreeNode *s = [self successorWithNode:node];
node.object = s.object;
node = s;
}
if ((node.left && !node.right) || (node.right && !node.left)) {
JKRBinaryTreeNode *replacement = node.left ? node.left : node.right;
// 一定不要忘记度parent的维护
replacement.parent = node.parent;
if (!node.parent) {
_root = replacement;
} else if(node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
}
if (!node.left && !node.right) {
// 如果是根节点就是直接让根节点为空
if (!node.parent) {
_root = nil;
} else {
// 如果是父节点的左子节点,就让父节点的左字节点为空
if (node == node.parent.left) {
node.parent.left = nil;
} else {
node.parent.right = nil;
}
}
}
复制代码
将如下二叉搜索树依次进行删除操作:
┌---7 (p: (null))---┐
│ │
┌-4 (p: 7)-┐ ┌-9 (p: 7)-┐
│ │ │ │
┌-2 (p: 4)-┐ 5 (p: 4) 8 (p: 9) ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
[tree removeObject:@7];
┌-8 (p: (null))-┐
│ │
┌-4 (p: 8)-┐ 9 (p: 8)-┐
│ │ │
┌-2 (p: 4)-┐ 5 (p: 4) ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
[tree removeObject:@4];
┌-8 (p: (null))-┐
│ │
┌-5 (p: 8) 9 (p: 8)-┐
│ │
┌-2 (p: 5)-┐ ┌-11 (p: 9)-┐
│ │ │ │
1 (p: 2) 3 (p: 2) 10 (p: 11) 12 (p: 11)
[tree removeObject:@8];
┌-9 (p: (null))-┐
│ │
┌-5 (p: 9) ┌-11 (p: 9)-┐
│ │ │
┌-2 (p: 5)-┐ 10 (p: 11) 12 (p: 11)
│ │
1 (p: 2) 3 (p: 2)
复制代码
删除代码汇总
将上面的三种情况代码进行合并:
- (void)removeWithNode:(JKRBinaryTreeNode *)node {
if (!node) {
return;
}
_size--;
if (node.hasTwoChildren) {
JKRBinaryTreeNode *s = [self successorWithNode:node];
node.object = s.object;
node = s;
}
// 实际被删除节点的子节点
JKRBinaryTreeNode *replacement = node.left ? node.left : node.right;
if (replacement) { // 被删除的节点度为1
replacement.parent = node.parent;
if (!node.parent) {
_root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
} else {
if(!node.parent) { // 被删除的节点度为0且没有父节点,被删除的节点是根节点且二叉树只有一个节点
_root = nil;
} else { // 被删除的节点是叶子节点且不是根节点
if (node.isLeftChild) {
node.parent.left = nil;
} else {
node.parent.right = nil;
}
}
}
}
复制代码