在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。
那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是p 和 q的公共祖先。 那问题来了,一定是最近公共祖先吗?
如图,我们从根节点搜索,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。
此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。
所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。
详细写法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
int val = root.val;
if(val > q.val && val > p.val){
TreeNode left = lowestCommonAncestor(root.left,p,q);
if(left!=null){
return left;
}
}else if(val < q.val && val < p.val){
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(right!=null){
return right;
}
}
return root;
}
}
简化:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
int val=root.val;
if(p.val<val&&q.val<val)
return lowestCommonAncestor(root.left,p,q);
else if(p.val>val&&q.val>val)
return lowestCommonAncestor(root.right,p,q);
return root;
}
迭代写法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root!=null){
if(p.val < root.val && q.val < root.val) root = root.left;
else if(p.val > root.val && q.val > root.val) root = root.right;
else return root;
}
return root;
}
}
对于二叉搜索树的最近祖先问题,其实要比普通二叉树公共祖先问题 (opens new window)简单的多。
不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。
最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。
迭代法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null) return new TreeNode(val);
insert(root,val);
return root;
}
public void insert(TreeNode root, int value){
TreeNode prev = root;
while(root!=null){
prev = root;
if(value < root.val) root = root.left;
else if(value > root.val) root = root.right;
}
if(value < prev.val){
prev.left = new TreeNode(value);
}else{
prev.right = new TreeNode(value);
}
}
}
递归法:
class Solution {
private:
TreeNode* parent;
void traversal(TreeNode* cur, int val) {
if (cur == NULL) {
TreeNode* node = new TreeNode(val);
if (val > parent->val) parent->right = node;
else parent->left = node;
return;
}
parent = cur;
if (cur->val > val) traversal(cur->left, val);
if (cur->val < val) traversal(cur->right, val);
return;
}
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
parent = new TreeNode(0);
if (root == NULL) {
root = new TreeNode(val);
}
traversal(root, val);
return root;
}
};
上面递归法和迭代法的实现都用到了记录父结点的技巧。但是如果使用递归法设计的递归函数返回值是TreeNode的话,可以进行优化:
if (root->val > val) root->left = insertIntoBST(root->left, val);
if (root->val < val) root->right = insertIntoBST(root->right, val);
return root;
完整代码:
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) // 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。
return new TreeNode(val);
if (root.val < val){
root.right = insertIntoBST(root.right, val); // 递归创建右子树
}else if (root.val > val){
root.left = insertIntoBST(root.left, val); // 递归创建左子树
}
return root;
}
}
二叉搜索树删除节点涉及到结构调整。
递归
递归三部曲:
- 确定递归函数参数以及返回值
说到递归函数的返回值,在二叉树:搜索树中的插入操作 (opens new window)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。
代码如下:
TreeNode* deleteNode(TreeNode* root, int key)
- 确定终止条件
遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
if (root == nullptr) return root;
- 确定单层递归的逻辑
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return root;
//单层递归逻辑
//1.叶子结点
//2.左孩子空,右孩子不空
//3.左孩子不空,右孩子空
//4.左右都不空
if(root.val == key){
if(root.left == null) return root.right;
else if(root.right == null) return root.left;
else{
TreeNode cur = root.right;
while(cur.left!=null) cur = cur.left;
cur.left = root.left;
root = root.right;
return root;
}
}
//单层逻辑的另一部分,完成找到deleteNode
if(key < root.val ) root.left = deleteNode(root.left,key);
if(key > root.val) root.right = deleteNode(root.right,key);
return root;
}
}
搜索过程迭代:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root == null) return root;
TreeNode prev = null;
TreeNode cur = root;
while(cur!=null){
if(cur.val == key) break;
prev = cur;
if(key < cur.val) cur = cur.left;
else if(key > cur.val) cur = cur.right;
}
if(prev == null) return deleteOneNode(cur);
if(prev.left != null && prev.left.val == key){
prev.left = deleteOneNode(cur);
}else if(prev.right !=null && prev.right.val == key){
prev.right = deleteOneNode(cur);
}
return root;
}
public TreeNode deleteOneNode(TreeNode root){
if(root == null) return root;
if(root.left == null) return root.right;
if(root.right == null) return root.left;
TreeNode cur = root.right;
while(cur.left !=null){
cur = cur.left;
}
cur.left = root.left;
return root.right;
}
}
在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图:
理解了最关键部分了我们再递归三部曲:
- 确定递归函数的参数以及返回值
这里我们为什么需要返回值呢?
因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。
但是有返回值,更方便,可以通过递归函数的返回值来移除节点。
这样的做法在二叉树:搜索树中的插入操作 (opens new window)和二叉树:搜索树中的删除操作 (opens new window)中已经了解过了。
代码如下:
TreeNode* trimBST(TreeNode* root, int low, int high)
- 确定返回条件
修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。
if (root == nullptr ) return nullptr;
如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
代码如下:
if (root->val < low) {
TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点
return right;
}
如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
代码如下:
if (root->val > high) {
TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点
return left;
}
- 确定单层递归的逻辑
将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。
最后返回root节点,代码如下:
root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子
root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子
return root;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
//返回条件
if(root == null) return root;
if(root.val < low) return trimBST(root.right,low,high);
if(root.val > high) return trimBST(root.left,low,high);
//单层处理逻辑
root.left = trimBST(root.left,low,high);
root.right = trimBST(root.right,low,high);
return root;
}
}
迭代法
因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。
在剪枝的时候,可以分为三步:
- 将root移动到[L, R] 范围内,注意是左闭右闭区间
- 剪枝左子树
- 剪枝右子树
代码如下:
class Solution {
//iteration
public TreeNode trimBST(TreeNode root, int low, int high) {
if(root == null)
return null;
while(root != null && (root.val < low || root.val > high)){
if(root.val < low)
root = root.right;
else
root = root.left;
}
TreeNode curr = root;
//deal with root's left sub-tree, and deal with the value smaller than low.
while(curr != null){
while(curr.left != null && curr.left.val < low){
curr.left = curr.left.right;
}
curr = curr.left;
}
//go back to root;
curr = root;
//deal with root's righg sub-tree, and deal with the value bigger than high.
while(curr != null){
while(curr.right != null && curr.right.val > high){
curr.right = curr.right.left;
}
curr = curr.right;
}
return root;
}
}
本题关键要理解在递归法中是如何删除节点的,如何通过三段式设计出递归法的。
在二叉树:构造二叉树登场! (opens new window)和二叉树:构造一棵最大的二叉树 (opens new window)中其实已经讲过了,如果根据数组构造一棵二叉树。
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
本题分割点就是数组中间位置的节点。
那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?
取哪一个都可以,只不过构成了不同的平衡二叉搜索树。
例如:输入:[-10,-3,0,5,9]
如下两棵树,都是这个数组的平衡二叉搜索树:
如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。
这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了。
递归
递归三部曲:
- 确定递归函数返回值及其参数
删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。
相信大家如果仔细看了二叉树:搜索树中的插入操作 (opens new window)和二叉树:搜索树中的删除操作 (opens new window),一定会对递归函数返回值的作用深有感触。
那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。
再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在二叉树:构造二叉树登场! (opens new window)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
所以代码如下:
TreeNode* traversal(vector<int>& nums, int left, int right)
这里使用左闭右开区间。
- 确定递归终止条件
这里定义的是左闭右开的区间,所以当区间 left > =right的时候,就是空节点了。
代码如下:
if (left >= right) return nullptr;
- 确定单层递归的逻辑
int mid = left + (right - left)/2;
int left_begin = left;
int left_end = mid;
int right_begin = mid+1;
int right_end = right;
TreeNode root= new TreeNode(nums[mid]);
root.left = build(nums,left_begin,left_end);
root.right = build(nums,right_begin,right_end);
return root;
整体代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
//设计递归函数参数和返回值
//左闭右开
return build(nums,0,nums.length);
}
public TreeNode build(int[] nums, int left, int right){
//处理返回逻辑
if(left >= right) return null;
//处理单层逻辑
int mid = left + (right - left)/2;
int left_begin = left;
int left_end = mid;
int right_begin = mid+1;
int right_end = right;
TreeNode root= new TreeNode(nums[mid]);
root.left = build(nums,left_begin,left_end);
root.right = build(nums,right_begin,right_end);
return root;
}
}
详细但繁琐的实现:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<Integer> list = new ArrayList<>();
int cnt = 0;
public TreeNode convertBST(TreeNode root) {
inOrderToBuildList(root);
int sum = 0;
for(int i=0 ; i<list.size() ;i++){
sum += list.get(i);
}
for(int i=0 ; i<list.size() ;i++){
int before = list.get(i);
list.set(i,sum);
sum -= before;
}
rebuild(root);
return root;
}
public void inOrderToBuildList(TreeNode root){
if(root == null) return;
if(root.left != null) inOrderToBuildList(root.left);
list.add(root.val);
if(root.right != null) inOrderToBuildList(root.right);
}
public void rebuild(TreeNode root){
if(root == null) return;
if(root.left != null) rebuild(root.left);
root.val = list.get(cnt++);
if(root.right != null) rebuild(root.right);
}
}
实际上没有必要先中序遍历一遍再记录,也没有必要两次对list遍历,通过一次逆中序遍历就可以得到结果,需要记录 prev 和 curr 的技巧。
有点动态规划选择合适的计算方向避免重复计算的思想。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
TreeNode prev = null;
public TreeNode convertBST(TreeNode root) {
reverseInOrder(root);
return root;
}
public void reverseInOrder(TreeNode root){
if(root == null) return;
if(root.right != null) reverseInOrder(root.right);
if(prev == null){
prev = root;
}else{
root.val = prev.val + root.val;
prev = root;
}
if(root.left != null) reverseInOrder(root.left);
}
}
二叉树章节总结参考:二叉树:总结篇!(需要掌握的二叉树技能都在这里了)
最后总结
在二叉树题目选择什么遍历顺序:
-
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
-
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
-
求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。
注意在普通二叉树的属性中,我用的是一般为后序,例如单纯求深度就用前序,二叉树:找所有路径 (opens new window)也用了前序,这是为了方便让父节点指向子节点。
所以求普通二叉树的属性还是要具体问题具体分析。