数据结构以学带练day22——二叉搜索树的最近公共祖先、二叉搜索树中的插入操作、删除二叉搜索树中的节点

114 阅读6分钟

题目

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

image.png

思路

由于是二叉搜索树(有序的),则利用层序遍历,首次出现在[p,q]区间内的那个节点即为公共祖先。即因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。也即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p

如图,我们从根节点搜索,第一次遇到 cur节点是数值在[p, q]区间中,即 节点5,此时可以说明 p 和 q 一定分别存在于 节点 5的左子树,和右子树中。所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。

image.png

⭐迭代法

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

递归法

  1. 确定递归函数返回值以及参数

    • 参数就是当前节点,以及两个结点 p、q
    • 返回值是要返回最近公共祖先,所以是TreeNode *
  2. 确定终止条件

    • 遇到空返回就可以了
  3. 确定单层递归的逻辑

    • 在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭右闭)
    • 如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。
class Solution {
private:
    TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {
        if (cur == NULL) return cur;
                                                        // 中
        if (cur->val > p->val && cur->val > q->val) {   // 左
            TreeNode* left = traversal(cur->left, p, q);
            if (left != NULL) {
                return left;
            }
        }

        if (cur->val < p->val && cur->val < q->val) {   // 右
            TreeNode* right = traversal(cur->right, p, q);
            if (right != NULL) {
                return right;
            }
        }
        return cur;
    }
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        return traversal(root, p, q);
    }
};

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

image.png

我自己的想法

首先中序遍历二叉搜索树,存入数组,再将val按从小到大顺序插入数组对应位置,再根据新数组用中序遍历构造树。

⭐递归法

  1. 确定递归函数参数以及返回值

    • 参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢?
    • 有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作
    • 递归函数的返回类型为节点类型TreeNode * 。
  2. 确定终止条件

    • 终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。
  3. 确定单层递归的逻辑

    • 搜索树是有方向的,可以根据插入元素的数值,决定递归方向。
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        //插入操作
        if (root == NULL) {
            //创建新节点
            TreeNode* node = new TreeNode(val);
            return node;
        }
        //向左遍历
        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;
        }
        TreeNode* cur = root;
        TreeNode* parent = root; // 这个很重要,需要记录上一个节点,否则无法赋值新节点
        while (cur != NULL) {
            parent = cur;
            if (cur->val > val) cur = cur->left;
            else cur = cur->right;
        }
        TreeNode* node = new TreeNode(val);
        if (val < parent->val) parent->left = node;// 此时是用parent节点的进行赋值
        else parent->right = node;
        return root;
    }
};

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

image.png

我的思路

首先中序遍历二叉搜索树,存入数组,从数组里找值为val并删除,再根据新数组用中序遍历构造树。(可行,day23讲了方法)

递归法

  1. 确定递归函数参数以及返回值
  2. 遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
  3. 二叉搜索树中删除节点遇到的情况:
    • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
    • 找到删除的节点:
      • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
      • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
      • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
      • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
        //如果找到了
        if (root->val == key) {
            // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
            if (root->left == nullptr && root->right == nullptr) {
                ///! 内存释放
                delete root;
                return nullptr;
            }
            // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
            else if (root->left == nullptr) {
                auto retNode = root->right;
                ///! 内存释放
                delete root;
                return retNode;
            }
            // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
            else if (root->right == nullptr) {
                auto retNode = root->left;
                ///! 内存释放
                delete root;
                return retNode;
            }
            // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
            // 并返回删除节点右孩子为新的根节点。
            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;
            }
        }
        if (root->val > key) root->left = deleteNode(root->left, key);
        if (root->val < key) root->right = deleteNode(root->right, key);
        return root;
    }
};

auto用法

  • auto的原理就是根据后面的值,来自己推测前面的类型是什么。
  • auto的作用就是为了简化变量初始化,如果这个变量有一个很长很长的初始化类型,就可以用auto代替。
  • 下面是一些用法:
auto x = 5;                 // OK: x被推导是int类型
auto pi = new auto(1);      // OK: pi被推导为int*
const auto *v = &x, u = 6;  // OK: v是const int*类型,u是const int类型
static auto y = 0.0;        // OK: y是double类型
auto int r;                 // error: auto不再表示存储类型指示符
auto s;                     // error: auto无法推导出s的类型

注意点:

  1. 用auto声明的变量必须初始化(auto是根据后面的值来推测这个变量的类型,如果后面没有值,自然会报错)
  2. 函数和模板参数不能被声明为auto(原因同上)
  3. 因为auto是一个占位符,并不是一个他自己的类型,因此不能用于类型转换或其他一些操作,如sizeof和typeid
  4. 定义在一个auto序列的变量必须始终推导成同一类型