LeetCode_75_DeleteNodeInABST


1. question: 删除二叉搜索树中的节点(中等)

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

首先找到需要删除的节点;
如果找到了,删除它。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/delete-node-in-a-bst

示例 1:

示例1

1
2
3
4
5
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。

另一个正确答案

示例 2:

1
2
3
输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

1
2
输入: root = [], key = 0
输出: []

提示:

1
2
3
4
5
节点数的范围 [0, 104].
-105 <= Node.val <= 105
节点值唯一
root 是合法的二叉搜索树
-105 <= key <= 105

2. answers

这道题,重点似乎是:删除二叉搜索树的根节点,并重构它。总体思路是:

  1. 找到要删除的节点
  2. 该节点及其子节点就是一棵二叉搜索树,然后将其重构。
  3. 将重构后的二叉搜索树插入到原来的位置上,因此在找节点的时候,要保留其父节点。

其实重点是如何重构一棵二叉搜索树。这棵二叉搜索树有多种情况:

  1. 只有根节点

    删除该节点,返回空即可

  2. 只有左子树

    返回其左子树即可。

  3. 只有右子树

    返回其右子树即可

  4. 普通二叉搜索树

    这里的重构二叉搜索树,其实指的就是找到一个根节点。而我们知道,二叉搜索树是中序递增的,因此,只需要找到左子树的最大值,或者右子树的最小值即可。将两个任意一个作为根节点就行。

    以找右子树的最小值为例。最小值一定是左节点。只需要遍历即可。在遍历的时候,一定要保留其父节点,因为最小值是作为最终的根节点,那么该父节点的左节点(也就是最小值)就要设为null。

    下面就是如何将最小值作为根节点。

    1. 最小值的左节点一定是空,因为如果不是空,那么它就不是最小值。所以作为最终的根节点,新的左子树就是原来的左子树
    2. 那么新的右子树怎么确定呢?因为最小值可能存在右节点,也可能不存在右节点。其实最小值的右节点一定是小于原来右子树的根节点的。只需要遍历右节点,将原来右子树作为右节点的右子树即可。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
*
* 这道题,其实就是将以删除节点为根节点的子树进行重构,或者说,重新构建二叉搜索树。而其余部分保持不变。
*
* 本质上还是删除根节点并重构二叉搜索树。
*
* 注意,二叉搜索树中序遍历是递增的。所以如果删除了根节点,其左右相邻节点,均可作为根节点。
* 而左右相邻节点,就是左右子树的最大值最小值。也就是左子树的右下角值、右子树的左下角值。
* 此时,以右子树为例,右子树左下角的值作为根节点,然后左节点就是左子树,右节点就是右子树。
*
* TODO,其实并不是左下角或者右下角,以右子树为例,因为可能最后一层只有右节点,没有左节点,此时最小值就是倒数第二层最左边的值。
* TODO,此时,如果右子树最后一层只有右节点,那么,将该节点作为新的根,其左节点就是左子树,但是右节点怎么办呢?因为右节点不为空,
* TODO,其实右节点始终是大于根节点的(原始)父节点的,即将右节点遍历到底,并将右子树变为其右子树。
*
*/
public class Solution_0074 {

public int tag;

// 重构二叉搜索树
public TreeNode constructTree(TreeNode tn) {

// 注意,有几种特殊情况,即删除的节点是叶子节点
// 删除的节点子树,只有右子树以及只有左子树
if(tn.left == null && tn.right == null) return null;

if(tn.left == null && tn.right != null) return tn.right;

if(tn.left != null && tn.right == null) return tn.left;

// 重构二叉搜索树,将根节点删除,然后从左子树中选一个最大的(也就是左子树中的最右下角节点)
// 或者从右子树中选一个最小的(也就是右子树中的最左下角节点)
// 以右子树为例
TreeNode left = tn.left;
TreeNode right = tn.right;

// 找到右子树的最小值,并保留其父节点
// 有一种特殊情况,就是右子树只有右子树,没有左子树。
if(right.left == null) {
right.left = left;
return right;
}

TreeNode temp = right;
TreeNode tempPar = right;
while(temp.left != null) {

tempPar = temp;
temp = temp.left;
}

// 此时temp就是右子树的最小值,tempPar就是其父节点。
// 父节点更新左节点为空,与temp切断
tempPar.left = null;

// 构造新的根节点,左节点为原始左子树。
temp.left = left;

// 右节点,先向右遍历到null,在将其右节点设为原始右子树
TreeNode tempRight = temp;
while(tempRight.right != null)
tempRight = tempRight.right;

// 此时,tempRight为temp右子树的最大值,拼接为原始右子树
tempRight.right = right;

return temp;
}

public TreeNode deleteNode(TreeNode root, int key) {

if(root == null) return null;

TreeNode result = root;

// 保留要删除节点的父节点
TreeNode pre = root;

// 找到要删除的节点
while(root != null) {

if(root.val > key) {
pre = root; // 注意,只有root不是key时,才会赋值为pre,因为要保证pre是key的父节点
root = root.left;
}
else if(root.val < key) {
pre = root;
root = root.right;
}
else {

// 判断pre的左节点还是右节点
if(pre.left == root) tag = -1;
if(pre.right == root) tag = 1;

break;
}
}

// 此时,要么找到了root,要么二叉搜索树中没有该key。
if(root == null) return result;

// 找到了root,获取重构后的二叉搜索树
TreeNode sub = constructTree(root);

// 判断删除的节点是否是根节点
if(pre == root) return sub;

// 不是根节点,将二者连接
if(tag == -1) pre.left = sub;
if(tag == 1) pre.right = sub;

return result;
}

public static void main(String[] args) {
System.out.println();

// TreeNode tn6 = new TreeNode(7);
// TreeNode tn5 = new TreeNode(4);
// TreeNode tn4 = new TreeNode(2);
// TreeNode tn3 = new TreeNode(6, null, tn6);
// TreeNode tn2 = new TreeNode(3, tn4, tn5);
// TreeNode tn1 = new TreeNode(5, tn2, tn3);

TreeNode tn1 = new TreeNode(0);

Solution_0074 s = new Solution_0074();

TreeNode result = s.deleteNode(tn1, 0);

// 层序遍历
Queue<TreeNode> queue = new LinkedList<>();

if(result == null) return;

queue.offer(result);

while(queue.size() > 0 ) {

int length = queue.size();

while(length-- > 0) {

result = queue.poll();

System.out.print(result.val + "\t");

if(result.left != null) queue.offer(result.left);
if(result.right != null) queue.offer(result.right);

}

System.out.println();
}

}
}

改进一下思路:其实在找新的根节点的时候,没必要是左子树的最大值或者右子树的最小值。只需要将原始左子树作为原始右子树最小值的左节点即可,原始右子树的根节点为新的根节点。或者原始右子树组委原始左子树最大值的右节点。

因为要删除的根节点,其左右子树都是二叉搜索树,只需要以最简单的形式将二者融合即可。右子树一定是大于根节点的,而根节点一定是大于左子树的最大值节点的。所以只需要找到左子树的最大值节点,将右子树拼接为最大值节点的右节点即可(相当于插入操作)。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 重构二叉搜索树
public TreeNode constructTree(TreeNode tn) {

// 注意,有几种特殊情况,即删除的节点是叶子节点
// 删除的节点子树,只有右子树以及只有左子树
if(tn.left == null && tn.right == null) return null;
if(tn.left == null && tn.right != null) return tn.right;
if(tn.left != null && tn.right == null) return tn.left;

// 重构二叉搜索树,将根节点删除,将左右子树融合
// 以右子树为例
TreeNode left = tn.left;
TreeNode right = tn.right;

// 找到右子树的最小值,
// 有一种特殊情况,就是右子树只有右子树,没有左子树。

TreeNode temp = right;
while(temp.left != null) {
temp = temp.left;
}

// 此时temp就是最小值
temp.left = left;

return right;
}

3. 备注

参考力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 (leetcode-cn.com)代码随想录 (programmercarl.com)


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录