算法学习记录~2023.5.10~二叉树Day8~701.二叉搜索树中的插入操作 & 450.删除二叉搜索树中的节点 & 669. 修剪二叉搜索树

算法学习记录| 2023.X.XX| 章节DayX| 题目号.题目标题 & 题目号.题目标题

  • 701.二叉搜索树中的插入操作
    • 题目链接
    • 思路
    • 代码1:递归且有返回值
    • 代码2:递归且无返回值
    • 代码3:迭代
    • 总结
  • 450.删除二叉搜索树中的节点
    • 题目链接
    • 思路1:利用二叉搜索树的性质,递归/迭代
    • 代码1:递归法
    • 代码2:迭代法
    • 思路2:当做普通二叉树来删除
    • 代码
    • 总结
  • 669. 修剪二叉搜索树
    • 题目链接
    • 思路
    • 代码1:递归
    • 代码2:迭代
    • 总结


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

题目链接

力扣题目链接

思路

其实不一定必须重构二叉树,可以按照二叉搜索树的规则去遍历,这样当找到空节点的时候直接将要插入的值插入即可,由于一直按照二叉搜索树的规则遍历,因此插入了新的节点也同样符合要求,不需要重构。可参考下图理解
算法学习记录~2023.5.10~二叉树Day8~701.二叉搜索树中的插入操作 & 450.删除二叉搜索树中的节点 & 669. 修剪二叉搜索树_第1张图片

代码1:递归且有返回值

有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作,在单层递归的逻辑处的下面代码处可以体现。
下一层将加入节点返回,本层用root->left或者root->right将其接住

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){      //找到空节点时就将需要插入的节点插入
            TreeNode* node = new TreeNode(val);
            return node;
        }
        if (val < root -> val){
            root-> left = insertIntoBST(root->left, val);
        }
        if (root -> val < val){
            root -> right = insertIntoBST(root -> right, val);
        }
        return root;        //题目要求最终返回根节点,因此通过上述递归后树就被重新建立好了,返回root即可
    }
};

代码2:递归且无返回值

如果不用返回值,那么在找到插入的节点位置(NULL位置)时,直接让其父节点指向插入节点,结束递归。

所以函数定义如下

TreeNode* parent; // 记录遍历节点的父节点
void traversal(TreeNode* cur, int val)

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

整体代码如下

class Solution {
public:
    TreeNode* parent;
    void traversal(TreeNode* cur, int val){
        if (cur == NULL){       //找到了目标位置则判断下val应该在parent的左节点还是右节点
            TreeNode* node = new TreeNode(val);
            if(parent -> val > val)
                parent -> left = node;
            else
                parent -> right = node;
            return ;
        }
        parent = cur;           //parent不断更新为当前节点
        if(cur -> val > val)
            traversal(cur -> left, val);
        else
            traversal(cur -> right, val);
        return;
    }

    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root == NULL){       //如果根节点为空节点则直接把根节点设为目标节点然后返回
            root = new TreeNode(val);
            return root;
        }
        traversal(root, val);
        return root;        //题目要求最终返回根节点,因此通过上述递归后树就被重新建立好了,返回root即可
    }
};

代码3:迭代

利用pre和cur两个指针,pre用于记录当前遍历节点的父节点

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == NULL){
            TreeNode* node = new TreeNode(val);
            return node;
        }
        TreeNode* cur = root;
        TreeNode* parent;    //用于记录上一个节点,否则无法赋值新节点
        while (cur != NULL){        //找到插入位置
            parent = cur;           //当前不为空时则更新parent为cur,这样当cur循环到下一层的空节点时parent正好记录的时上一层
            if (cur -> val > val)
                cur = cur -> left;
            else
                cur = cur -> right;
        }
        TreeNode* node = new TreeNode(val);
        if (parent -> val > val )
            parent -> left = node;
        else
            parent -> right = node;
        return root;
    }
};

总结

一上来很容易想到要重构二叉树,因而思考的特别复杂,但是由于二叉搜索树性质,一定能找到仍旧为空节点的属于目标节点的位置,因此其实并不需要重构,找到满足条件的空节点然后将目标节点插入为新的叶子节点即可


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

题目链接

力扣题目链接

思路1:利用二叉搜索树的性质,递归/迭代

一共有五种情况:

  1. 没有找到要被删除的节点,最终遍历到空节点。直接返回
  2. 要删除的节点为叶子结点,左右孩子都为空。直接删除节点,返回NULL为根结点
  3. 删除节点的左孩子为空,右孩子不为空。删除节点,右孩子补位,返回右孩子为根结点
  4. 删除节点的左孩子不为空,右孩子为空。删除节点,左孩子补位,返回左孩子为根结点
  5. 左右子节点都不为空。将删除节点的左子树头结点(左子节点)放到删除节点右子树的最左节点的左孩子上,返回删除节点为右孩子为新的根结点(反之应该亦然)。

对于第五种情况可以结合下面的图理解
算法学习记录~2023.5.10~二叉树Day8~701.二叉搜索树中的插入操作 & 450.删除二叉搜索树中的节点 & 669. 修剪二叉搜索树_第2张图片

代码1:递归法

递归三部曲:

  • 确定递归函数参数以及返回值
    同701.二叉搜索树中的插入操作一样,可以通过递归返回值来加入新节点,那这里也可以通过递归返回值来删除节点
TreeNode* deleteNode(TreeNode* root, int key)
  • 确定终止条件
    遇到空则返回,这也说明没找到删除的节点,遍历到空节点直接返回
if (root == NULL) return root;
  • 确定单层递归的逻辑
    也就是上面说的五种情况的具体处理(其中第一种其实和终止条件一致,因此不在这写了)
if (root->val == key) {
    // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
    if (root->left == nullptr) return root->right;
    // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    else if (root->right == nullptr) return root->left;
    // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
    // 并返回删除节点右孩子为新的根节点。
    else {
        TreeNode* cur = root->right; // 找右子树最左面的节点
        while(cur->left != nullptr) {
            cur = cur->left;
        }
        cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
        TreeNode* tmp = root;   // 把root节点保存一下,下面来删除
        root = root->right;     // 返回旧root的右孩子作为新root
        delete tmp;             // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)
        return root;
    }
}

这里相当于把新的节点返回给上一层,上一层就需要用 root->left 或者 root->right接住

if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;

整体代码:

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == NULL)   //情况1:没找到要删除的节点,遍历到空节点直接返回
            return root;
        if (root -> val == key){
            //情况2:左右子节点均为空,此时为叶子节点,直接删除该节点,返回null为根结点
            if (root -> left == NULL && root -> right == NULL){
                delete root;    //内存释放
                return NULL;
            }
            //情况3:左子节点为空,右子节点不为空,删除节点,右子节点补位,返回右子节点为根结点
            else if (root -> left == NULL && root -> right != NULL){
                TreeNode* node = root -> right;
                delete root;    //内存释放
                return node;
            }
            //情况4:左不空右空,删除节点,左子节点补位,返回左子节点
            else if (root -> left != NULL && root -> right == NULL){
                TreeNode* node = root -> left;
                delete root;    //内存释放
                return node;
            }
            //情况5:左右都不为空,将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
            //返回删除节点的右子节点为新的根结点
            else{
                TreeNode* cur = root -> right;  //找右子树最左面的节点
                while (cur->left != NULL) {
                    cur = cur -> left;
                }
                cur -> left = root -> left;     //把要删除的节点(root)左子树放在cur的左孩子的位置
                TreeNode* tmp = root;           //保存下root节点,下面来删除
                root = root -> right;           //返回旧root的右子节点作为新的root
                delete tmp;                    //内存释放
                return root;
            }
        }
        //通过递归返回值删除节点
        if (root -> val > key)
            root -> left = deleteNode(root -> left, key);
        if (root -> val < key)
            root -> right = deleteNode(root -> right, key);
        return root;
    }
};

代码2:迭代法

class Solution {
private:
    // 将目标节点(删除节点)的左子树放到目标节点的右子树的最左面节点的左孩子位置上
    // 并返回目标节点右孩子为新的根节点
    // 是动画里模拟的过程
    TreeNode* deleteOneNode(TreeNode* target) {
        if (target == nullptr) return target;
        if (target->right == nullptr) return target->left;
        TreeNode* cur = target->right;
        while (cur->left) {
            cur = cur->left;
        }
        cur->left = target->left;
        return target->right;
    }
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;
        TreeNode* cur = root;
        TreeNode* pre = NULL;   //记录cur的父节点,用来删除cur

        while (cur != NULL) {   //寻找需要被删除的节点
            if (cur->val == key) break;
            pre = cur;
            if (cur->val > key) cur = cur->left;
            else cur = cur->right;
        }

        if (pre == nullptr) { // 如果搜索树只有头结点
            return deleteOneNode(cur);
        }
        // pre 要知道是删左孩子还是右孩子
        if (pre->left && pre->left->val == key) {
            pre->left = deleteOneNode(cur);
        }
        if (pre->right && pre->right->val == key) {
            pre->right = deleteOneNode(cur);
        }
        return root;
    }
};

思路2:当做普通二叉树来删除

通用的删除方式的话,需要遍历整棵树,用交换值的操作来删除目标节点。

目标节点(要被删除的节点)被操作了两次:

  1. 第一次和目标节点的右子树最左面节点交换

  2. 第二次直接被NULL覆盖

    看代码思路比较巧妙不太好想,第一遍没有好好琢磨,第二遍再看看
    

代码

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;
        if (root->val == key) {
            if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用
                return root->left;
            }
            TreeNode *cur = root->right;
            while (cur->left) {
                cur = cur->left;
            }
            swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。
        }
        root->left = deleteNode(root->left, key);
        root->right = deleteNode(root->right, key);
        return root;
    }
};

总结


669. 修剪二叉搜索树

题目链接

力扣题目链接

思路

从根节点开始遍历的过程中,

如果当前节点小于最小值,则它本身连带左子树都要被抛弃,那么此时把当前节点的右子树全部赋给当前节点的父节点即可。

如果当前节点大于最大值,则它本身连带右子树都要被抛弃,把当前节点的左子树全部赋给当前节点的父节点即可。

代码1:递归

class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (root == NULL)
            return NULL;
        if (root -> val < low){     //当前节点小于最小值则抛弃左子树返回右子树
            TreeNode* right = trimBST(root -> right, low, high);
            return right;
        }
        if (root -> val > high){    //当前节点大于最大值则抛弃左子树返回右子树
            TreeNode* left = trimBST(root -> left, low, high);
            return left;
        }

        root -> left = trimBST(root -> left, low, high);    //root->left接入符合条件的左孩子
        root -> right = trimBST(root->right, low, high);    //root->right接入符合条件的右孩子
        return root;        //最后返回根节点
    }
};

代码2:迭代

由于二叉搜索树是有序的,因此不需要使用栈来模拟递归。

剪枝时有三步:

  1. 将root移动到[L, R] 范围内,注意是左闭右闭区间
  2. 剪枝左子树
  3. 剪枝右子树

其中需要注意剪枝完一边子树需要返回到root再剪另一边

class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (root == NULL)
            return NULL;

        //处理头结点,让root移动到[low, high] 范围内,注意是左闭右闭
        while (root != NULL && (root->val < low || root->val > high)){
            if (root->val < low)            //小于low往右走,左边的肯定全要抛弃
                root = root -> right;
            else                            //大于high往左走,右边的肯定全要抛弃
                root = root -> left;
        }
        TreeNode* cur = root;
        //此时root已经在[low, high]范围内,处理左孩子小于low的情况
        while (cur != NULL){
            while (cur->left != NULL && cur->left->val < low){
                cur->left = cur->left->right;       //抛掉左子树
            }
            cur = cur->left;            //继续遍历
        }

        cur = root;            //重要,不能忘记,因为需要回到根结点root再处理另一条子树

        //此时root已经在[low, high]范围内,处理右孩子大于high的情况
        while (cur != NULL) {
            while (cur->right && cur->right->val > high) {
                cur->right = cur->right->left;      //抛掉右子树
            }
            cur = cur->right;           //继续遍历
        }
        return root;
    }
};

总结

很容易犯只考虑当前节点的左右子节点,而忽略了他的子树不一定全都不符合要求。

你可能感兴趣的:(算法记录,算法,学习,数据结构,c++)