235. 二叉搜索树的最近公共祖先

  • 有序树,如果中间节点是 q 和 p 的公共祖先,那么中节点的数组一定是在 [p, q]区间的,搜索树的最底层两边的叶子节点肯定是把根节点包在中间的。
  • 上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先,为什么是最近的呢,因为再往下遍历就会错过p和q,不存在更近的公共祖先了。
  • 如果遍历到的点大于p和q(不确定p和q谁大),那么向左搜索,小于p和q,向右搜索,留下来的就是[p,q]和[q,p]注意是左闭右闭包含p和q本身就是公共祖先的情况。

迭代法,注意return root的时候else一定不能少,上面的判断里都没有return,这里不加else会中断搜索,如果前面的情况对后面情况的变量做了改变,除非改完直接return了,否则一定要加else。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        while(root) {
        if (root->val > p->val && root->val > q->val) root = root->left;
        else if (root->val < p->val && root->val < q->val) root = root->right;
        else return root;//else一定要写
        }
        return nullptr;
    }
};

递归法,思想和迭代法差不多,但是递归的结果是要回溯返回才能得到的,而迭代是一直在更新指针,找到就返回。


public class Solution {
    public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return root;
        if (root.val > p.val && root.val > q.val) return LowestCommonAncestor(root.left, p, q);
        else if (root.val < p.val && root.val <q.val) return LowestCommonAncestor(root.right, p, q);
        else return root;
        return null;
    }
}

701.二叉搜索树中的插入操作

两种大逻辑,如果提前操作,可以避免使用前驱,否则必须使用parent记录插入位置的上一层,要不没法操作,跟链表插入是一样的思想。这里有一个技巧,用有返回值的递归是可以在上一层接住下一层的返回值的,相当于重建了一遍树,就不用parent记录了,无返回值的就不行。

提前在插入节点的上一层逻辑进行插入

迭代法,分左右找插入位置。

//C++
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr) return new TreeNode(val);
        TreeNode* cur = root;
        while (cur != nullptr) {
            if (cur->val > val) {
                if (cur->left == nullptr) {
                    cur->left = new TreeNode(val);
                    return root;
                } else cur = cur->left;
            }
            else if (cur->val < val) {
                if (cur->right == nullptr) {
                    cur->right = new TreeNode(val);
                    return root;
                } else cur = cur->right;
            }
        }
        return root;
    }
};

尾递归法,无返回值,此时和迭代法也差不多,无返回值也不需要回溯,用递归代替指针移动。

public class Solution {
    public TreeNode InsertIntoBST(TreeNode root, int val) {
        if (root == null) return new TreeNode(val);
        doit(root, val);
        return root;
        
    }
    public void doit(TreeNode cur, int val) {
        //if (cur == null) return;
        if (cur.val > val) {
            if (cur.left == null) {
                cur.left = new TreeNode(val);
                return;
            } else doit(cur.left, val);
        }
        if (cur.val < val) {
            if (cur.right == null) {
                cur.right = new TreeNode(val);
                return;
            } else doit(cur.right, val);
        }
    }
}

递归法有返回值,头递归,使用回溯的返回值判断是否为空来找插入点,实际上也就是最后一层递归返回的时候操作了下,后续返回都是当前节点cur,直到最后返回root,尾递归是到最后一层前判断为空操作并返回。

public class Solution {
    public TreeNode InsertIntoBST(TreeNode root, int val) {
        if (root == null) return new TreeNode(val);
        return doit(root, val);
    }
    public TreeNode doit(TreeNode cur, int val) {
        if (cur == null) return cur;
        if (cur.val > val) {
            TreeNode tmp = doit(cur.left, val);
            if (tmp == null) cur.left = new TreeNode(val);
        }
        if (cur.val < val) {
            TreeNode tmp = doit(cur.right, val);
            if (tmp == null) cur.right = new TreeNode(val);
        }
        return cur;
    }
}

有返回值递归其实节点为空的特殊条件是可合并的,只不过这种相当于把整个树重新连接了一遍,因为没有单独最最后一层节点做插入操作。

//C++ 有返回值
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr) return new TreeNode(val);
        if (root->val > val) root->left = insertIntoBST(root->left, val);
        if (root->val < val) root->right = insertIntoBST(root->right,val);
        return root;
    }
};

在找到插入点的那一层逻辑里处理

无返回值递归,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。

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;
    }
};

迭代法,注意parent使用。

//C++
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr) return new TreeNode(val);
        TreeNode* parent = root;
        TreeNode* cur = root;
        while (cur != nullptr) {
            parent = cur;//注意parent要在cur更新之前
            if (cur->val > val) {
                cur = cur->left;
            } else cur = cur->right; //不可能存在cur->val = val
        }
        TreeNode* tmp = new TreeNode(val);
        if (parent->val > val) parent->left = tmp;
        else parent->right = tmp;
        return root;
    }
};

通过递归函数的返回值完成父子节点的赋值是可以带来便利的。

450.删除二叉搜索树中的节点

二刷去看普通二叉树的删除。考虑这些搜索过程不区分左右的内在原理(没有中,先搜哪个都可以),做一下无返回值递归用parent来链接节点的方法,也就是双指针法 核心思想:类似插入节点,使用有返回值递归不断接入下一层来重建整个二叉树。

先根据二叉搜索树的性质找到删除的位置,再进行删除(注意,不用记录前驱节点,通过返回新的根节点在上一层完成连接操作),具体有以下五种情况:

第一种情况:没找到删除的节点,遍历到空节点直接返回了 找到删除的节点 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点,右子树放到左子树最右边节点的右孩子上也可以。

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;//第一种情况,没找到删除的节点,遍历到空节点直接返回
        if (root->val > key) root->left = deleteNode(root->left, key);
        if (root->val < key) root->right = deleteNode(root->right, key);
        if (root->val == key) {
            //第二种情况:左右孩子都为空(叶子节点),直接删除节点,返回NULL为根节点
            if (root->left == nullptr && root->right == nullptr) {
                delete root;
                return nullptr;
            }
            //第三种情况;其左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
            else if (root->left == nullptr) {
                TreeNode* rightNode = root->right;
                delete root;
                return rightNode;
            }
            //第四种情况: 其左孩子不为空,右孩子为空,删除节点,左孩子补位,返回左孩子作为根节点
            else if (root->right == nullptr) {
                TreeNode* leftNode = root->left;
                delete root;
                return leftNode;
            }
            //第五种情况:左右孩子都不为空,即将删除节点的左子树放到删除节点的右子树的最左边节点的左孩子位置
            //返回删除节点的右孩子作为新的根节点
            else {
                TreeNode* cur = root->right;
                while (cur->left != nullptr) {
                    cur = cur->left;
                }
                cur->left = root->left;
                TreeNode* tmp = root;
                root = root->right;
                delete tmp;
                return root;
            }
        }

        return root;

    }
};

C#重新练一下手。


public class Solution {
    public TreeNode DeleteNode(TreeNode root, int key) {
        if (root == null) return root;
        if (root.val > key) root.left = DeleteNode(root.left, key);
        if (root.val < key) root.right = DeleteNode(root.right, key);
        if (root.val == key) {
            if (root.left == null && root.right == null) {
                return null;
            }
            else 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;
                return root.right;
            }
        }
        return root;
    }
}