Day20-21-二叉树-leetcode235,701,450 - 669,108,538

  1. 二叉搜索树的最近公共祖先
  • 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
  • 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
  • 说明:
  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。
  • 思路
var lowestCommonAncestor = function(root, p, q) {
    // 使用递归的方法
    // 1. 使用给定的递归函数lowestCommonAncestor
    // 2. 确定递归终止条件
    if(root === null) {
        return root;
    }
    if(root.val > p.val && root.val > q.val) {
        // 向左子树查询
         return root.left = lowestCommonAncestor(root.left,p,q);
    }
    if(root.val < p.val && root.val < q.val) {
        // 向右子树查询
        return root.right = lowestCommonAncestor(root.right,p,q);
    }
    return root;
};
var lowestCommonAncestor = function(root, p, 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. 二叉搜索树中的插入操作
  • 给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

  • 注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。

  • 提示:

    • 树中的节点数将在 [0, 104]的范围内。
    • -108 <= Node.val <= 108
    • 所有值 Node.val 是 独一无二 的。
    • -108 <= val <= 108
    • 保证 val 在原始BST中不存在。
  • 思路

    • 遍历二叉搜索树,找到空节点 插入元素
    • 递归:
    • (1)确定递归函数参数及返回值,参数=》根节点指针、要插入的元素,返回=》节点node
    • (2)确定终止条件,找到遍历的节点为null时,即要插入节点的位置,并把插入的节点返回
    • (3)确定单层递归的逻辑,搜索树是有方向的,可以根据插入元素的值,决定递归方向
// 有返回值的递归写法
var insertIntoBST = function (root, val) {
    const setInOrder = (root, val) => {
        // 如果当前节点 root 为 null,说明找到了插入位置,创建新节点并返回。
        if (root === null) {
            let node = new TreeNode(val);
            return node;
        }
        /**
        递归逻辑
        如果 val 小于当前节点值,递归插入到左子树。
        如果 val 大于当前节点值,递归插入到右子树。
        每次递归返回的节点都赋值给对应的 left 或 right,保证插入后树结构正确。
         */
        if (root.val > val)
            root.left = setInOrder(root.left, val);
        else if (root.val < val)
            root.right = setInOrder(root.right, val);
        return root;
    }
    return setInOrder(root, val);
};
// 无返回值的递归
/**
1. parent 变量
  用于记录当前节点的父节点,方便在找到插入位置时,把新节点挂到父节点的 left 或 right。
2. preOrder 递归函数
  参数:当前节点 cur,要插入的值 val。
  如果 cur 为 null,说明找到了插入位置,创建新节点,并根据 parent 的值大小挂到左或右。
  如果没到插入位置,递归遍历左或右子树,并更新 parent。
3. 特殊情况处理
  如果 root 为空,直接新建根节点。
4. 递归查找插入位置
  从根节点开始递归查找,直到找到空位插入。
5. 返回根节点
  最后返回更新后的根节点。
 */
var insertIntoBST = function (root, val) {
    // 用于记录当前节点的父节点
    let parent = new TreeNode(0);
    const preOrder = (cur, val) => {
        if (cur === null) {
            // 找到插入位置,创建新节点
            let node = new TreeNode(val);
            // 挂到父节点的左
            if (parent.val > val)
                parent.left = node;
            // 挂到父节点的右
            else
                parent.right = node;
            return;
        }
         // 更新父节点
        parent = cur;
        // 递归左子树
        if (cur.val > val)
            preOrder(cur.left, val);
        // 递归右子树
        if (cur.val < val)
            preOrder(cur.right, val);
    }
    // 特殊情况:树为空,直接新建根节点
    if (root === null)
        root = new TreeNode(val);
     // 从根节点开始递归查找插入位置
    preOrder(root, val);
    // 返回根节点
    return root;
};
// 迭代
/**
步骤说明
1. 特殊情况处理
  如果 root 为空,直接新建一个节点作为根节点返回。
2. 查找插入位置
  用 cur 指针从根节点开始遍历,parent 记录当前节点的父节点。
  如果 val 小于当前节点值,往左子树走;否则往右子树走。
  直到 cur 变为 null,说明找到了插入位置。
3. 插入新节点
  新建节点 node。
  判断插入到 parent 的左还是右,根据 val 和 parent.val 的大小关系决定。
4. 返回根节点
  最后返回更新后的根节点。
 */
var insertIntoBST = function (root, val) {
    // 如果树为空,直接新建根节点
    if (root === null) {
        root = new TreeNode(val);
    } else {
        // 用于记录当前节点的父节点
        let parent = new TreeNode(0);
        let cur = root;
         // 循环查找插入位置
        while (cur) {
            // 记录父节点
            parent = cur;
            // 往左找
            if (cur.val > val)
                cur = cur.left;
            // 往右找
            else
                cur = cur.right;
        }
        // 创建新节点
        let node = new TreeNode(val);
        // 挂到父节点的左或右
        if (parent.val > val)
            parent.left = node;
        else
            parent.right = node;
    }
    // 返回根节点
    return root;
};

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

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

  • 首先找到需要删除的节点;

  • 如果找到了,删除它。

  • 思路

  • (1)确定递归函数参数以及返回值,参数=》root,返回值=》node

  • (2)确定终止条件,遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了

  • (3)确定单层递归的逻辑

    • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
      • 找到删除的节点
      • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
      • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
      • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
      • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
// 递归
/**
递归查找要删除的节点。
删除时分三种情况:叶子节点、只有一个孩子、两个孩子。
两个孩子时,用右子树最小节点替换当前节点,再删除右子树中的最小节点,保证 BST 性质不变。
 */
var deleteNode = function(root, key) {
    /**
    如果当前节点为空,说明没找到要删除的节点,直接返回 null。
    如果 key 比当前节点值大,递归去右子树找。
    如果 key 比当前节点值小,递归去左子树找。
    如果相等,说明找到了要删除的节点,进入下一步处理。
     */
    if (!root) return null;
    if (key > root.val) {
        root.right = deleteNode(root.right, key);
        return root;
    } else if (key < root.val) {
        root.left = deleteNode(root.left, key);
        return root;
    } else {
        /**
         * 删除节点的三种情况
         * 场景1:叶子节点(无左右孩子),直接删除,返回 null。
         * 场景2:只有一个孩子,只有左孩子,返回左孩子,右孩子同理。这样父节点会直接指向被删除节点的唯一孩子。
         * 场景3:左右孩子都存在,找到右子树的最小节点(即后继节点)。用这个最小节点的值替换当前节点的值。然后递归删除右子树中这个最小节点(因为它已经被“提上来”了)。
         */
        // 场景1: 该节点是叶节点
        if (!root.left && !root.right) {
            return null
        }
        // 场景2: 有一个孩子节点不存在
        if (root.left && !root.right) {
            return root.left;
        } else if (root.right && !root.left) {
            return root.right;
        }
        // 场景3: 左右节点都存在
        const rightNode = root.right;
        // 获取最小值节点
        const minNode = getMinNode(rightNode);
        // 将待删除节点的值替换为最小值节点值
        root.val = minNode.val;
        // 删除最小值节点
        root.right = deleteNode(root.right, minNode.val);
        return root;
    }
};
// 辅助函数:获取最小节点
// 一直往左走,直到没有左孩子,返回该节点
function getMinNode(root) {
    while (root.left) {
        root = root.left;
    }
    return root;
}

// 迭代
/**
先用迭代方式找到要删除的节点及其父节点。
用辅助函数处理三种情况(无右子树、有右子树、叶子节点)。
通过父节点指针完成删除操作,保证 BST 结构正确。
 */
var deleteNode = function (root, key) {
    /**
     *辅助函数 deleteOneNode
     用于实际删除节点并返回新的子树根节点。
     如果 target 没有右孩子,直接返回左孩子(左孩子可能为 null)。
     如果有右孩子,找到右子树最左边的节点(即右子树的最小节点),把 target 的左子树接到这个最小节点的左边,然后返回 target.right 作为新的根节点。
     */
    const deleteOneNode = target => {
        if (!target) return target
        if (!target.right) return target.left
        /**
         用 cur 指针遍历树,pre 记录父节点。
         找到值等于 key 的节点,跳出循环。
         */
        let cur = target.right
        while (cur.left) {
            cur = cur.left
        }
        cur.left = target.left
        return target.right
    }

    if (!root) return root
    let cur = root
    let pre = null
    while (cur) {
        if (cur.val === key) break
        pre = cur
        cur.val > key ? cur = cur.left : cur = cur.right
    }
    /**
     * 删除节点
    如果 pre 为 null,说明要删除的是根节点,直接返回 deleteOneNode(cur) 的结果。
    如果要删除的是父节点的左孩子,则 pre.left = deleteOneNode(cur)。
    如果要删除的是父节点的右孩子,则 pre.right = deleteOneNode(cur)。
    最后返回根节点。
     */
    if (!pre) {
        return deleteOneNode(cur)
    }
    if (pre.left && pre.left.val === key) {
        pre.left = deleteOneNode(cur)
    }
    if (pre.right && pre.right.val === key) {
        pre.right = deleteOneNode(cur)
    }
    return root
}

  1. 修剪二叉搜索树
  • 给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。

  • 所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

  • 思路

    1. 确定递归函数的参数以及返回值,参数=》root、low、hight,返回=》root
    1. 确定终止条件,遇到空节点返回
    1. 确定单层递归的逻辑,如果当前节点的元素小于low的值,应该递归右子树,并返回右子树符合条件的头节点,如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
// 递归
var trimBST = function (root, low, high){
    // 1. 终止条件: 如果当前节点为 null,直接返回 null,递归结束。
    if (root === null) {
        return null
    }
    // 2. 当前节点小于 low
    // 如果 root.val < low,说明当前节点和它的左子树所有节点都不在区间内(因为 BST 左子树都更小)。
    // 递归修剪右子树,并返回修剪后的右子树。
    if (root.val < low) {
        let right = trimBST(root.right, low, high)
        // 把节点的右孩子返回给上一层,相当于删除节点
        return right
    }
    // 3. 当前节点大于 high
    // 如果 root.val > high,说明当前节点和它的右子树所有节点都不在区间内(因为 BST 右子树都更大)。
    // 递归修剪左子树,并返回修剪后的左子树。
    if (root.val > high) {
        let left = trimBST(root.left, low, high)
        return left
    }
    // 4. 当前节点在区间内
    // 如果 root.val 在 [low, high] 区间内,递归修剪左右子树,并把结果分别赋值给 root.left 和 root.right。
    // 返回当前节点 root。
    // 用节点的左孩子 把上一层返回的节点的右孩子接住
    root.left = trimBST(root.left, low, high)
    root.right = trimBST(root.right, low, high)
    return root
 }
---
108. 将有序数组转换为二叉搜索树
- 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

- 思路
- 如何构造二叉树?
    - 整体思路:选取一个中间的节点,将这个数组分为左区间和右区间,递归遍历左区间去构成左子树,递归遍历右区间去构成右子树,
    - 本题:选取中间节点时,选取数组中间位置的节点,只有选取中间位置的节点作为我们的中节点,可以把第一个节点作为根节点,只有这样选,它的左区间节点和数量和右区间节点的数量才能保证是相同的,这样构造的二叉树才能保证是平衡的;将其分割之后,左区间再去左右分隔,右区间也去选中间节点,再去进行左右分割,这样就保证了每一层它下面的左右子树的数量是相同的,然后根据二叉搜索树节点的性质(右子树大于中节点,左子树小于中节点)去构造二叉树。本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。分割点就是数组中间位置的节点。不断中间分割,然后递归处理左区间,右区间。通过递归函数的返回值来增删二叉树


- 递归
    - (1)确定递归函数参数及返回值,用递归函数的返回值来构造中节点的左右孩子,参数=》nums、left、right,返回=》root
    - (2)确定递归终止条件,[left, right],left 》right,空节点
    - (3)确定单层递归的逻辑,首先取中间元素的位置,以中间位置的元素构造节点,并进行左右区间的划分,节点的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点,最后返回root节点

- 迭代法
    - 迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。模拟的就是不断分割的过程。

```js
// 递归
var sortedArrayToBST = function(nums) {
    const buildTree = (arr, left, right) =>{
        // 区间无元素时,返回空节点
        if (left > right) return null
        // 防止整数溢出
        let mid = Math.floor(left + (right - left) / 2)
        // 构造根节点
        let root = new TreeNode(arr[mid])
        // 递归左区间,构建左子树
        root.left = buildTree(arr, left, mid - 1)
        // 递归右区间,构建右子树
        root.right = buildTree(arr, mid + 1, right)
        // 每次递归返回当前子树的根节点
        return root
    }
    return buildTree(nums, 0, nums.length - 1)
}

  1. 把二叉搜索树转换为累加树
  • 给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

  • 提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。

  • 节点的右子树仅包含键 大于 节点键的节点。

  • 左右子树也必须是二叉搜索树。

  • 思路

  • 需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。

  • 递归

  • (1)递归函数参数及返回值,参数=》cur

  • (2)确定终止条件,遇到空就终止 cur===null

  • (3)确定单层递归逻辑,右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。

  • 迭代法其实就是中序模板题了

// 递归 利用二叉搜索树的有序性,反中序遍历+累加变量 pre,实现累加树的构建。
/**
 * 右中左遍历(反中序遍历)
  二叉搜索树中序遍历是递增序列,反中序遍历(右中左)就是递减序列。
  这样可以保证每次访问到的节点,都是比当前节点大的节点已经处理过了。
 * pre 变量
  用于记录当前节点右侧所有节点值的累加和。
  每访问一个节点,就把 pre 加到当前节点上,然后更新 pre。
 * 递归过程
  递归到最右边(最大值节点),开始累加。
  每访问一个节点,累加 pre,并更新 pre。
  递归左子树,继续累加。
 */
var convertBST = function(root) {
    let pre = 0
    const ReverseInOrder = (cur) => {
         if (cur) {
            ReverseInOrder(cur.right)      // 1. 先递归右子树(从大到小遍历)
            cur.val += pre                 // 2. 当前节点累加上 pre
            pre = cur.val                  // 3. 更新 pre 为当前节点的新值
            ReverseInOrder(cur.left)       // 4. 再递归左子树
        }
    }
    ReverseInOrder(root)
    return root
}



参考&感谢各路大神

posted @ 2025-06-17 09:29  安静的嘶吼  阅读(7)  评论(0)    收藏  举报