最近在学习数据结构与算法,记录下二叉树的删除操作是如何实现的,相对于其他的插入,排序等操作。删除操作逻辑上稍微比较复杂,今天通过图形化的方式帮你们彻底搞懂到底是怎么实现删除操作的。话不多说,直接开始。
注:本文采用js语言作为演示
我们如何用数据结构来表示出二叉树呢?
毫无疑问是对象的结构更符合我们的结果,我们直接定义一个类来帮我们生成节点。
这里我们就不探讨如何插入数据了,相对来说是比较简单。
// 二叉树的实现
// 创建节点
class CreateNode {
right = null
left = null
constructor(key) {
this.key = key
}
}
class BinarySerachTree {
// 根节点
root = null
// 查看遍历后的顺序
traversal = []
// 向树中插入数据 insert
insert(key) {
const newNode = new CreateNode(key)
if (this.root === null) { // 没有根节点
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
// 判断插入的key大于还是小于节点
// 再根据对应方向的子树是否为空 为空则直接赋值 否则继续递归查找
insertNode(node, insertNode) {
if (insertNode.key === node.key) return false
if (insertNode.key < node.key) { //左子树
if (node.left === null) { // 是否为空
node.left = insertNode
return true
} else {
this.insertNode(node.left, insertNode)
}
} else {//右子树
if (node.right === null) { // 是否为空
node.right = insertNode
return true
} else {
this.insertNode(node.right, insertNode)
}
}
}
}
进入今天的正题,下面我们用这样的二叉树结构来模拟我们想要进行的删除操作。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEoTj5Yr-1662787014279)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/86fb3c2126e34d7e9c5ec63206659503~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722492.png)
首先我们要做的是当条件给定我们一个节点让我们去执行删除操作,我们第一步应该是寻找到该条件对应的节点。
拿我们上面创建的结构来讲就是,通过 key 寻找到 newNode节点。
我们可以通过while循环依次比较大小寻找左右节点的方式来进行查找,例如我们要删除key为10的节点。
那么它的查找顺序应该是这样的
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNWMbI9g-1662787014282)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3e5217f015d547579af6c79e6095c970~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722492.png)
这时候我们来想想删除操作应该是怎样的呢?
就拿现在这个 10 节点来举例
9节点的右节点指向null --> 9.right = null
所以说我们要拿到删除节点的父节点
我们再来想一想 如果此时删除的是 7节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPwNo0ZA-1662787014283)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8233f145e6cc43a59333d28c992a96bf~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722469.png)
那么操作应该是 --> 8.left = null
细心的同学就会发现,有时候是改左节点有时候是改右节点。
所以说我们还得记录下最后寻找节点的方向是左还是右,判断找到前是大于还是小于即可得到。
下面我们通过代码来实现:
class BinarySerachTree {
// 根节点
root = null
// 查看遍历后的顺序
traversal = []
// 删除节点 remove
remove(key) {
// 1.寻找要删除的节点 父节点 节点方向
let current = this.root //当前节点 根节点开始往下找
let partent = null //当前节点的父节点
let delNode = null // 找到要删除的节点
let isLeft = true // 记录最后是父节点的左还是右节点
while (current) {// 直到找到最后为null
if (key === current.key) {//找到了
delNode = current
current = false
} else if (key > current.key) {//去当前节点的右节点找
isLeft = false
partent = current
current = current.right
} else {//去当前节点的左节点找
isLeft = true
partent = current
current = current.left
}
}
// 没有找到该节点
if (!delNode) return false
}
现在我们找到了删除节点以及其它的对应条件,下面我们要对此节点进行分析:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSUozEXS-1662787014292)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef4c632d9796435f8797306e68dc2587~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722471.png)
如果是这种情况我们还需要考虑
根节点删除图例:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1QONkj0-1662787014294)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c65724121af44312a72eeb7acd639b2f~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722398.png)
只需要进行 this.root = null 初始化值就行了 11节点未被引用后面会被垃圾回收掉的
普通节点删除图例:
删除 7 节点 --> 8.left = null
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MuuXfr9p-1662787014296)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73271b43637d44128f2375103ca4294a~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722491.png)
删除 19节点 --> 18.right = null
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdvJyQPa-1662787014297)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27a46137a01348e8afa227c5562ba85b~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722489.png)
我们只需要根据之前的isLeft参数就能知道删除节点是父节点的左还是右节点了!
class BinarySerachTree {
// 根节点
root = null
// 查看遍历后的顺序
traversal = []
// 删除节点 remove
remove(key) {
// 1.寻找要删除的节点 父节点 节点方向
let current = this.root //当前节点 根节点开始往下找
let partent = null //当前节点的父节点
let delNode = null // 找到要删除的节点
let isLeft = true // 记录最后是父节点的左还是右节点
while (current) {// 直到找到最后为null
if (key === current.key) {//找到了
delNode = current
current = false
} else if (key > current.key) {//去当前节点的右节点找
isLeft = false
partent = current
current = current.right
} else {//去当前节点的左节点找
isLeft = true
partent = current
current = current.left
}
}
// 没有找到该节点
if (!delNode) return false
//2.如果删除的是叶节点 两端没有节点
if (delNode.left === null && delNode.right === null) {
if (delNode === this.root) {// 如果是根节点
this.root = null
} else {
if (isLeft) {// 左节点
partent.left = null
} else {
partent.right = null
}
}
}
}
这里也有四种情况
让我们通过图来看看吧
第一种情况:删除18节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OaqfcG8b-1662787014299)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e433348acca94f1d81ef69191d45cbd4~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722492.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZCREa73Q-1662787014302)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/578517034cf741728322b3f413b338a8~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722488.png)
我们只需要执行操作 20.left = 18.right
那么18.right 要不要重置为null呢?其实是不需要的,因为18引用了19但是它最后会被回收,因为从根节点找下来没有节点依赖于18,所以它会被回收掉。下面的也是同理哦!
第二种情况:删除19节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PdIYKDK-1662787014303)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f0795287a5848c1a04b46693f8dcedc~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722272.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUSlaf7x-1662787014304)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8138c7fc9332422995ea1ce52cde2ebf~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722302.png)
同理可得 20.left = 19.left
第三种情况:删除9节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NUQFogJu-1662787014306)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e940b7258e4349738b43882e4cfa0138~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722350.png)
同理可得 8.right = 9.right
第四种情况:删除10节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u8uFmH7A-1662787014307)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/31ce51d00bcf495ca84d4f7d75704244~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722673.png)
同理可得 8.right = 10.left
代码实现:
class BinarySerachTree {
// 根节点
root = null
// 查看遍历后的顺序
traversal = []
// 删除节点 remove
remove(key) {
// 1.寻找要删除的节点
let current = this.root //当前节点
let partent = null //当前节点的父节点
let delNode = null // 找到要删除的节点
let isLeft = true // 记录最后是父节点的左还是右节点
while (current) {
if (key === current.key) {
delNode = current
current = false
} else if (key > current.key) {
isLeft = false
partent = current
current = current.right
} else {
isLeft = true
partent = current
current = current.left
}
}
// 没有找到该节点
if (!delNode) return false
// 2.如果删除的是叶节点 两端没有节点
if (delNode.left === null && delNode.right === null) {
if (delNode === this.root) {// 如果是根节点
this.root = null
} else {
if (isLeft) {// 左节点
partent.left = null
} else {
partent.right = null
}
}
//3.如果删除的节点有一端节点
} else if (delNode.left === null) {// 左节点为空 右节点有值 右端有节点
if (delNode === this.root) {// 如果是根节点
this.root = delNode.right
} else {
if (isLeft) {// 左节点
partent.left = delNode.right
} else {
partent.right = delNode.right
}
}
} else if (delNode.right === null) {// 右节点为空 左节点有值 左端有节点
if (delNode === this.root) {// 如果是根节点
this.root = delNode.left
} else {
if (isLeft) {// 左节点
partent.left = delNode.left
} else {
partent.right = delNode.left
}
}
}
}
在写代码之前,我们先来了解一个概念,前驱 与 后继
只有找到了前驱或者后继节点我们才能真正的来删除完整节点
例如我们要删除根节点 11 那我们应该怎么做才能保持二叉树的结构呢?
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3RrFapYi-1662787014309)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6c13130801ad4bfca95ab3b29f84ef34~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722444.png)
聪明的你肯定能想到用 10 节点 或者 13节点来替代11节点,这样能保持我们的二叉树结构依然符合条件。
那么它到底有什么规律呢?
我们发现 10 节点是 11节点的左节点下的最右边的节点 前驱节点
而13 节点是 11节点的右节点的最左边的节点 后继节点
我们可以通过以下代码来实现:
// 找前驱的方法 getSuccessPre
getSuccessPre(node) {// 传入被删节点
let preNode = node.left
let parent = node
while (preNode.right) {
parent = preNode
preNode = preNode.right
}
return [preNode, parent]
}
// 找后继的方法 getSuccessor
getSuccessor(node) {// 传入被删节点
let sorNode = node.right
let parent = node
while (sorNode.left) {
parent = sorNode
sorNode = sorNode.left
}
return [sorNode, parent]
}
那这里为什么也要保存对应的父节点呢?
如果我们删除的是6节点,这里采用寻找后继节点的方式来删除会进行一下操作:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bp7JjobY-1662787014311)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b5a7c9c314f045d58ddf9e149bdd3275~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722449.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9sTSwt5H-1662787014312)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9a396b49fe34b559245d22ee6f355b0~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722492.png)
大功告成!
所以我们需要得到后继节点的父节点来断开之前的连接或者还有一种情况
如果我们删除的是17节点,这里采用寻找后继节点的方式来删除会进行一下操作:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z7nQ3hav-1662787014314)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b229c8a8cc9c4f9ea25e713c68dd76e8~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722449.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5m1U5qa-1662787014316)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0041c2d5482422da7e3fcf458788f9d~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722521.png)
上面描述的两种情况其实可以统一进行处理,我们只需要执行:
步骤三中将后继没有右节点的情况也包含了,没有的话后继的右节点就是null
下面我们来讨论另外一种情况:
这里以删除8节点为例:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56Nbp0Ke-1662787014317)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/72048451b3124f5f920dd560203c3b01~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722449.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1HcVkYMP-1662787014318)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c516a58b9f4f4ca096f961ee87fbe0c6~tplv-k3u1fbpfcp-watermark.image?)]](https://1000bd.com/contentImg/2023/11/06/033722493.png)
总结成功!开始分情况写代码:
class BinarySerachTree {
// 根节点
root = null
// 查看遍历后的顺序
traversal = []
// 删除节点 remove
remove(key) {
// 1.寻找要删除的节点
let current = this.root //当前节点
let partent = null //当前节点的父节点
let delNode = null // 找到要删除的节点
let isLeft = true // 记录最后是父节点的左还是右节点
while (current) {
if (key === current.key) {
delNode = current
current = false
} else if (key > current.key) {
isLeft = false
partent = current
current = current.right
} else {
isLeft = true
partent = current
current = current.left
}
}
// 没有找到该节点
if (!delNode) return false
// 2.如果删除的是叶节点 两端没有节点
if (delNode.left === null && delNode.right === null) {
if (delNode === this.root) {// 如果是根节点
this.root = null
} else {
if (isLeft) {// 左节点
partent.left = null
} else {
partent.right = null
}
}
//3.如果删除的节点有一端节点
} else if (delNode.left === null) {// 左节点为空 右节点有值 右端有节点
if (delNode === this.root) {// 如果是根节点
this.root = delNode.right
} else {
if (isLeft) {// 左节点
partent.left = delNode.right
} else {
partent.right = delNode.right
}
}
} else if (delNode.right === null) {// 右节点为空 左节点有值 左端有节点
if (delNode === this.root) {// 如果是根节点
this.root = delNode.left
} else {
if (isLeft) {// 左节点
partent.left = delNode.left
} else {
partent.right = delNode.left
}
}
} else {// 4.两端都有节点
let [successsor, sorParent] = this.getSuccessor(delNode)
if (delNode === sorParent) { //后继节点的父节点是被删节点
if (delNode === this.root) {// 如果是根节点
this.root = successsor
} else {
partent.right = successsor
}
successsor.left = delNode.left
} else {// 后继节点父节点不是被删节点
if (delNode === this.root) {// 如果是根节点
this.root = successsor
} else if (isLeft) { // 根据被删节点再父节点的位置选择合适的位置进行替换
partent.left = successsor
} else {
partent.right = successsor
}
sorParent.left = successsor.right
successsor.left = delNode.left
successsor.right = delNode.right
}
}
return true
}
// 找前驱的方法 getSuccessPre
getSuccessPre(node) {
let preNode = node.left
let parent = node
while (preNode.right) {
parent = preNode
preNode = preNode.right
}
return [preNode, parent]
}
// 找后继的方法 getSuccessor
getSuccessor(node) {
let sorNode = node.right
let parent = node
while (sorNode.left) {
parent = sorNode
sorNode = sorNode.left
}
return [sorNode, parent]
}
// 中序遍历 inOrderTraversal
inOrderTraversal() {
this.traversal = []
this.inOrderTraversalNode(this.root)
}
inOrderTraversalNode(node) {
if (node) {
this.inOrderTraversalNode(node.left)
this.traversal.push(node.key)
this.inOrderTraversalNode(node.right)
}
}
}
终于是写完了!我们可以通过代码里的中序遍历来验证,如果是从小到大排序说明成功实现了!
快来动手写一写吧!
如果此文对你有帮助欢迎大家点赞收藏加关注!如有不对之处,望各位大佬不吝赐教。
三连加关注,更新不迷路!
