当前位置: 首页 > news >正文

【LeetCode Hot100----08-二叉树篇中(06-10),包含多种方法,详细思路与代码,让你一篇文章看懂所有!】

6.543. 二叉树的直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

     1/ \2   3/ \     4   5    

其最长路径是 5→2→1→3或 4→2→1→3,边数为 3,因此直径为 3。

核心思路分析

二叉树直径问题的关键 insight 是:任意节点的直径等于其左子树的最大深度加上右子树的最大深度。因此,我们需要:

  1. 计算每个节点的左、右子树深度
  2. 跟踪所有节点中 “左深度 + 右深度” 的最大值
  3. 这个最大值就是二叉树的直径

解法:深度优先搜索(DFS)

思路概述

通过后序遍历计算每个节点的深度,同时在遍历过程中更新最大直径:

  1. 递归计算左子树深度
  2. 递归计算右子树深度
  3. 当前节点的直径为左深度 + 右深度
  4. 更新全局最大直径
  5. 返回当前节点的深度(1 + 左右深度的最大值)

具体步骤

  1. 初始化一个变量记录最大直径(可以使用数组或包装类实现引用传递)
  2. 定义递归函数计算节点深度:
    • 若节点为null,返回 0
    • 递归计算左子树深度
    • 递归计算右子树深度
    • 计算当前节点的直径(左深度 + 右深度)
    • 更新最大直径
    • 返回当前节点的深度(1+max (左深度,右深度))
  3. 从根节点开始调用递归函数
  4. 返回最大直径

代码实现

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;}
}public class BinaryTreeDiameter {private int maxDiameter = 0;public int diameterOfBinaryTree(TreeNode root) {// 计算深度的同时更新最大直径calculateDepth(root);return maxDiameter;}private int calculateDepth(TreeNode node) {if (node == null) {return 0;}// 计算左子树深度int leftDepth = calculateDepth(node.left);// 计算右子树深度int rightDepth = calculateDepth(node.right);// 更新最大直径:当前节点的左深度+右深度maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);// 返回当前节点的深度return 1 + Math.max(leftDepth, rightDepth);}
}

执行流程示例

以上面的示例树为例:

  1. 调用diameterOfBinaryTree(root),root 为节点 1

  2. 调用calculateDepth(1):

    • 调用calculateDepth(2)

      (左子树):

      • 调用calculateDepth(4):
        • 4 的左右子树都为 null,返回 0
        • 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter=0
        • 返回 1(1+0)
      • 调用calculateDepth(5):
        • 5 的左右子树都为 null,返回 0
        • 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter 保持 0
        • 返回 1(1+0)
      • 节点 2 的左深度 = 1,右深度 = 1,当前直径 = 2,maxDiameter=2
      • 返回 2(1+1)
    • 调用calculateDepth(3)

      (右子树):

      • 3 的左右子树都为 null,返回 0
      • 左深度 = 0,右深度 = 0,当前直径 = 0,maxDiameter 保持 2
      • 返回 1(1+0)
    • 节点 1 的左深度 = 2,右深度 = 1,当前直径 = 3,maxDiameter=3

    • 返回 3(1+2)

  3. 最终返回 maxDiameter=3

复杂度分析

  • 时间复杂度:O (n),其中 n 是二叉树的节点数。每个节点只需访问一次。
  • 空间复杂度:O (h),其中 h 是二叉树的高度。递归调用栈的深度取决于树的高度,在最坏情况下(二叉树为链状),h=n,空间复杂度为 O (n)。

关键细节说明

  1. 直径定义:直径是路径的边数,而不是节点数。例如,两个相邻节点之间的直径是 1(1 条边)。
  2. 路径特性:最长路径可能不经过根节点,因此必须检查每个节点的左右子树深度之和。
  3. 递归设计
    • 递归函数返回的是当前节点的深度,用于计算父节点的直径
    • 最大直径通过一个成员变量在递归过程中不断更新
  4. 空树处理:如果树为空(root=null),则直径为 0。

这种解法巧妙地将直径计算与深度计算结合在一起,通过一次遍历完成所有计算,效率很高,是解决二叉树直径问题的最优方法。

7.102. 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

二叉树的层序遍历(也称为广度优先遍历)是按照树的层次顺序(从根节点开始,逐层从左到右)访问所有节点的过程。例如,对于二叉树:

    3/ \9  20/  \15   7

其层序遍历结果为 [[3], [9, 20], [15, 7]]

一、解法一:迭代法(队列实现)

(一)思路概述

利用队列先进先出(FIFO)的特性,逐层处理节点:

  1. 根节点入队
  2. 记录当前层的节点数量,依次出队并收集节点值
  3. 将当前层节点的左右子节点入队(下一层节点)
  4. 重复步骤 2-3,直到队列为空

核心是通过记录每一层的节点数量,实现 “一层处理完再处理下一层” 的逻辑。

(二)具体步骤

  1. 边界处理:若根节点为null,返回空列表。

  2. 初始化:创建队列并将根节点入队,创建结果列表result

  3. 循环处理

    :当队列不为空时:

    • 记录当前层节点数量size = queue.size()
    • 创建当前层的列表level
    • 遍历当前层的size个节点:
      • 出队节点,将其值加入level
      • 若节点有左子树,左子节点入队。
      • 若节点有右子树,右子节点入队。
    • level加入result
  4. 返回结果result

(三)代码实现

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;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 List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> result = new ArrayList<>();if (root == null) {return result; // 空树返回空列表}Queue<TreeNode> queue = new LinkedList<>();queue.offer(root); // 根节点入队while (!queue.isEmpty()) {int size = queue.size(); // 当前层节点数量List<Integer> level = new ArrayList<>();for (int i = 0; i < size; i++) {TreeNode node = queue.poll(); // 出队当前节点level.add(node.val); // 收集当前节点值// 左右子节点入队(下一层节点)if (node.left != null) {queue.offer(node.left);}if (node.right != null) {queue.offer(node.right);}}result.add(level); // 当前层加入结果}return result;}
}

(四)执行流程示例(以上述示例树为例)

  1. 初始队列:[3]result = []
  2. 第 1 次循环:size = 1
    • 出队 3,level = [3]
    • 入队 3 的左右子节点 9、20,队列变为[9, 20]
    • result加入[3][[3]]
  3. 第 2 次循环:size = 2
    • 出队 9,level加入 9→[9];9 无子女,队列变为[20]
    • 出队 20,level加入 20→[9, 20];入队 20 的子女 15、7,队列变为[15, 7]
    • result加入[9, 20][[3], [9, 20]]
  4. 第 3 次循环:size = 2
    • 出队 15,level加入 15→[15];15 无子女,队列变为[7]
    • 出队 7,level加入 7→[15, 7];7 无子女,队列空。
    • result加入[15, 7][[3], [9, 20], [15, 7]]
  5. 队列空,循环结束,返回结果。

二、解法二:递归法(DFS 实现)

(一)思路概述

利用递归的深度优先搜索,通过记录当前节点的层数,将节点值放入对应层的列表中:

  1. 递归遍历节点,同时传递当前层数。
  2. 若当前层的列表未创建,则新建并加入结果。
  3. 将节点值加入当前层的列表。
  4. 递归遍历左子树(层数 + 1)和右子树(层数 + 1)。

核心是通过层数参数,在 DFS 过程中 “记住” 节点所属的层,间接实现层序遍历。

(二)具体步骤

  1. 边界处理:若根节点为null,返回空列表。

  2. 初始化:创建结果列表result

  3. 递归遍历:调用辅助函数dfs(root, 0, result),其中0

    表示初始层数(根节点为第 0 层)。

    • 辅助函数逻辑:
      • 若当前层数等于result大小,说明该层列表未创建,新建列表并加入result
      • 将当前节点值加入当前层的列表。
      • 若左子树不为空,递归左子树(层数 + 1)。
      • 若右子树不为空,递归右子树(层数 + 1)。
  4. 返回结果result

(三)代码实现

import java.util.ArrayList;
import java.util.List;class TreeNode {// 节点定义同前
}class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> result = new ArrayList<>();if (root == null) {return result;}dfs(root, 0, result);return result;}// 辅助递归函数:node为当前节点,level为当前层数private void dfs(TreeNode node, int level, List<List<Integer>> result) {// 若当前层的列表未创建,新建并加入结果if (level == result.size()) {result.add(new ArrayList<>());}// 当前节点加入对应层的列表result.get(level).add(node.val);// 递归遍历左子树(层数+1)if (node.left != null) {dfs(node.left, level + 1, result);}// 递归遍历右子树(层数+1)if (node.right != null) {dfs(node.right, level + 1, result);}}
}

(四)执行流程示例(以上述示例树为例)

  1. 初始调用dfs(3, 0, result)result = []

  2. level=0等于result.size()

    (0),新建列表加入result→[[]]。

    • 3 加入result.get(0)[[3]]
    • 递归左子树dfs(9, 1, result)
  3. level=1等于result.size()(1),新建列表加入result→[[3], []]。
    
    • 9 加入result.get(1)[[3], [9]]
    • 9 无左子树,递归右子树(null,不执行)。
  4. 返回上层,递归 3 的右子树dfs(20, 1, result)。

    • level=1 < result.size()(2),直接操作result.get(1)
    • 20 加入result.get(1)[[3], [9, 20]]
    • 递归左子树dfs(15, 2, result)
  5. level=2等于result.size()(2),新建列表加入result→[[3], [9, 20], []]。
    
    • 15 加入result.get(2)[[3], [9, 20], [15]]
    • 15 无子女,返回。
  6. 递归 20 的右子树dfs(7, 2, result)。

    • 7 加入result.get(2)[[3], [9, 20], [15, 7]]
    • 7 无子女,返回。
  7. 所有递归结束,返回结果。

三、两种解法对比

解法核心思想时间复杂度空间复杂度优点缺点
迭代法(队列)利用队列逐层处理节点O(n)O (w)(w 为最大宽度)直观体现层序遍历过程需要维护队列,代码稍长
递归法(DFS)记录层数,DFS 中分层收集O(n)O (h)(h 为树高)代码简洁,无需手动维护队列非直观,依赖层数参数
  • 时间复杂度:两种方法均需访问所有节点一次,故为 O (n),n 为节点总数。
  • 空间复杂度:
    • 迭代法:队列最多存储一层的节点,最坏情况(满二叉树最后一层)为 O (n/2)≈O (n)。
    • 递归法:递归栈深度为树高 h,最坏情况(链状树)为 O (n)。
  • 适用性:
    • 迭代法适合需要按层处理的场景(如分层打印、计算每层平均值)。
    • 递归法适合代码简洁性优先的场景,或已熟悉 DFS 的实现思路。

总结

层序遍历的核心是 “按层次划分节点”,两种方法从不同角度实现这一目标:

  • 迭代法通过队列显式分隔每层节点,逻辑直接,符合 “广度优先” 的直觉。
  • 递归法通过深度优先遍历 + 层数标记,间接实现分层,代码更简洁。

实际应用中,迭代法是层序遍历的标准实现,因其更直观地反映了层序遍历的过程,且便于扩展到类似问题(如锯齿形层序遍历)。递归法则适合作为辅助思路,帮助理解不同遍历方式的联系。

8.108. 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

将有序数组转换为平衡二叉搜索树(BST)是一个经典问题。平衡 BST 的定义是:左右两个子树的高度差的绝对值不超过 1,且左右两个子树都是平衡二叉树。由于输入数组是升序的,这正好符合 BST 的中序遍历特性(左→根→右为升序),为构建提供了便利。

一、核心思路分析

平衡 BST 的构建关键在于选择合适的根节点,确保左右子树的节点数量尽可能均衡。对于升序数组:

  • 数组的中间元素作为根节点,可使左右子树的节点数量差不超过 1,天然满足平衡性。
  • 左半部分数组构建左子树,右半部分数组构建右子树(递归思想)。

二、解法一:递归法(选择中间元素为根)

(一)思路概述

利用分治思想,每次选择数组的中间元素作为根节点,递归构建左右子树:

  1. 找到当前数组的中间索引mid,以nums[mid]为根节点。
  2. 左子树由nums[0..mid-1]递归构建。
  3. 右子树由nums[mid+1..end]递归构建。
  4. 递归终止条件:数组为空(返回null)。

(二)具体步骤

  1. 边界处理:若数组为空,返回null
  2. 定义递归函数:buildBST(nums, left, right),表示用nums[left…right]构建 BST。
    • left > right,返回null(子数组为空)。
    • 计算中间索引mid = left + (right - left) / 2(避免left + right溢出)。
    • 创建根节点root,值为nums[mid]
    • 递归构建左子树:root.left = buildBST(nums, left, mid-1)
    • 递归构建右子树:root.right = buildBST(nums, mid+1, right)
    • 返回root
  3. 初始调用buildBST(nums, 0, nums.length-1)

(三)代码实现

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) {if (nums == null || nums.length == 0) {return null;}return buildBST(nums, 0, nums.length - 1);}private TreeNode buildBST(int[] nums, int left, int right) {if (left > right) {return null;  // 子数组为空,返回空节点}// 计算中间索引(避免溢出)int mid = left + (right - left) / 2;// 中间元素作为根节点TreeNode root = new TreeNode(nums[mid]);// 递归构建左子树(左半部分数组)root.left = buildBST(nums, left, mid - 1);// 递归构建右子树(右半部分数组)root.right = buildBST(nums, mid + 1, right);return root;}
}

(四)执行流程示例(以nums = [-10, -3, 0, 5, 9]为例)

    1. 初始调用buildBST(nums, 0, 4):

      • mid = 0 + (4-0)/2 = 2,根节点为0nums[2])。

      • 左子树:buildBST(nums, 0, 1)。

        • mid = 0 + (1-0)/2 = 0,根节点为-10
        • 左子树:buildBST(nums, 0, -1)null
        • 右子树:buildBST(nums, 1, 1)→根节点-3(左右子树均为null)。
        • 左子树结果:-10的右子树为-3,即-10→-3
      • 右子树:buildBST(nums, 3, 4)。

        • mid = 3 + (4-3)/2 = 3,根节点为5
        • 左子树:buildBST(nums, 3, 2)null
        • 右子树:buildBST(nums, 4, 4)→根节点9(左右子树均为null)。
        • 右子树结果:5的右子树为9,即5→9
      • 最终树结构:

            0/ \-10  5\   \-3   9 
        

三、解法二:递归法(选择中间偏左 / 偏右元素为根)

(一)思路概述

平衡 BST 的构建不唯一,当数组长度为偶数时,可选择中间偏左或偏右的元素作为根节点,仍能保证平衡性:

  • 中间偏左:mid = left + (right - left) / 2(与解法一相同)。
  • 中间偏右:mid = left + (right - left + 1) / 2(适用于偶数长度数组)。

两种选择均能满足平衡条件,仅树的结构略有差异。

(二)代码实现(中间偏右示例)

class Solution {public TreeNode sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) {return null;}return buildBST(nums, 0, nums.length - 1);}private TreeNode buildBST(int[] nums, int left, int right) {if (left > right) {return null;}// 中间偏右的索引(偶数长度时选择右侧中间元素)int mid = left + (right - left + 1) / 2;TreeNode root = new TreeNode(nums[mid]);root.left = buildBST(nums, left, mid - 1);root.right = buildBST(nums, mid + 1, right);return root;}
}

(三)执行流程示例(同数组[-10, -3, 0, 5, 9]

  • 初始调用mid = 0 + (4-0+1)/2 = 2(与解法一相同,因长度为 5 是奇数)。

  • 若数组为[-10, -3, 0, 5]

    (长度 4,偶数):

    • 中间偏右mid = 0 + (3-0+1)/2 = 2,根节点为0

    • 左子树:[-10, -3]→根节点-3mid = 0 + (1-0+1)/2 = 1)。

    • 右子树:[5]→根节点5

    • 树结构:

          0/ \-3   5/
      -10
      

四、解法三:迭代法(模拟递归过程)

(一)思路概述

用栈模拟递归的分治过程,栈中存储 “子数组范围 + 父节点 + 左右标记”,手动控制构建步骤:

  1. 栈初始化:压入整个数组范围(left=0, right=n-1),父节点为null,标记为 “根节点”。
  2. 循环处理栈顶元素:
    • 弹出范围(left, right),计算mid,创建当前节点。
    • 根据标记将当前节点连接到父节点的左 / 右子树。
    • 压入右子数组范围(mid+1, right),父节点为当前节点,标记为 “右子树”。
    • 压入左子数组范围(left, mid-1),父节点为当前节点,标记为 “左子树”(栈先进后出,确保左子树先处理)。
  3. 栈为空时,构建完成。

(二)代码实现

import java.util.Stack;class Solution {// 辅助类:存储子数组范围、父节点、是否为右子树static class StackNode {int left;int right;TreeNode parent;boolean isRight;StackNode(int left, int right, TreeNode parent, boolean isRight) {this.left = left;this.right = right;this.parent = parent;this.isRight = isRight;}}public TreeNode sortedArrayToBST(int[] nums) {if (nums == null || nums.length == 0) {return null;}Stack<StackNode> stack = new Stack<>();TreeNode root = null;// 初始压入整个数组范围,父节点为null,标记为非右子树(根节点)stack.push(new StackNode(0, nums.length - 1, null, false));while (!stack.isEmpty()) {StackNode node = stack.pop();int left = node.left;int right = node.right;TreeNode parent = node.parent;boolean isRight = node.isRight;if (left > right) {continue;}// 计算中间索引int mid = left + (right - left) / 2;TreeNode current = new TreeNode(nums[mid]);// 连接到父节点if (parent == null) {root = current;  // 根节点} else {if (isRight) {parent.right = current;} else {parent.left = current;}}// 压入右子树范围(先压入,后处理)stack.push(new StackNode(mid + 1, right, current, true));// 压入左子树范围(后压入,先处理)stack.push(new StackNode(left, mid - 1, current, false));}return root;}
}

(三)执行流程示例(核心步骤)

nums = [-10, -3, 0, 5, 9]为例:

  1. 栈初始:[(0,4,null,false)]
  2. 弹出(0,4,null,false),mid=2,创建节点0,根节点root=0。
    • 压入右子树(3,4,0,true)和左子树(0,1,0,false),栈:[(3,4,0,true), (0,1,0,false)]
  3. 弹出(0,1,0,false),mid=0,创建节点-10,连接到0.left。
    • 压入右子树(1,1,-10,true)和左子树(0,-1,-10,false),栈:[(3,4,0,true), (1,1,-10,true), (0,-1,-10,false)]
  4. 弹出(0,-1,-10,false)left>right,跳过)。
  5. 弹出(1,1,-10,true),mid=1,创建节点-3,连接到-10.right。
    • 压入右子树(2,1,-3,true)和左子树(1,0,-3,false)(均跳过),栈:[(3,4,0,true)]
  6. 后续处理右子树(3,4,0,true),最终构建出与解法一相同的树。

五、三种解法对比

解法核心思想时间复杂度空间复杂度优点缺点
递归法(中间根)分治 + 递归构建O(n)O (log n)(递归栈)代码简洁,直观易懂递归深度大时可能栈溢出
递归法(偏左 / 右根)分治 + 灵活选择根节点O(n)O(log n)构建多种平衡 BST与标准解法类似,差异较小
迭代法(栈模拟)栈模拟递归分治过程O(n)O (log n)(栈空间)无栈溢出风险代码复杂,需辅助类
  • 时间复杂度:三种方法均需访问数组所有元素一次,故为 O (n)。
  • 空间复杂度:递归法的递归栈深度和迭代法的栈空间均为 O (log n)(平衡树的高度)。
  • 唯一性:平衡 BST 的构建不唯一,选择不同的中间节点(如中间偏左 / 右)会得到不同但均平衡的树。

总结

将有序数组转换为平衡 BST 的核心是利用中间元素作为根节点,通过分治思想递归构建左右子树。递归法是最简洁的实现,迭代法通过栈模拟递归过程,适合对递归深度敏感的场景。由于平衡 BST 不唯一,可根据需求选择中间偏左或偏右的元素作为根节点,均能满足平衡性要求。

9.98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 严格小于 当前节点的数。
  • 节点的右子树只包含 严格大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树

注意:仅检查节点与其直接左右子节点的关系是不够的(如右子树的左节点可能小于根节点),必须保证整个子树的约束。

一、解法一:递归法(带上下界约束)

(一)思路概述

通过递归为每个节点设置合法值范围(下界和上界),确保节点值严格在范围内:

  • 根节点的合法范围:(-∞, +∞)
  • 左子节点的合法范围:(父节点下界, 父节点值)(必须小于父节点)
  • 右子节点的合法范围:(父节点值, 父节点上界)(必须大于父节点)

若所有节点都在各自的合法范围内,则为有效的 BST。

(二)具体步骤

  1. 递归函数定义isValid(node, min, max),表示检查node为根的子树是否在(min, max)范围内。
  2. 终止条件:
    • node == null:空树是 BST,返回true
    • node.val <= minnode.val >= max:超出范围,返回false
  3. 递归检查:
    • 左子树:isValid(node.left, min, node.val)(左子树上限为当前节点值)。
    • 右子树:isValid(node.right, node.val, max)(右子树下限为当前节点值)。
    • 只有左右子树都有效时,返回true
  4. 初始调用isValid(root, Long.MIN_VALUE, Long.MAX_VALUE)(用long避免整数边界溢出)。

(三)代码实现

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 boolean isValidBST(TreeNode root) {// 初始范围:(-∞, +∞),用Long避免int边界问题return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE);}private boolean isValid(TreeNode node, long min, long max) {if (node == null) {return true; // 空树是有效的BST}// 节点值超出范围,无效if (node.val <= min || node.val >= max) {return false;}// 左子树范围:(min, node.val),右子树范围:(node.val, max)return isValid(node.left, min, node.val) && isValid(node.right, node.val, max);}
}

(四)执行流程示例

示例 1:有效 BST
    2/ \1   3
  • 调用isValid(2, -∞, +∞):2 在范围内。
    • 左子树isValid(1, -∞, 2):1 在(-∞, 2)内,且 1 无子女→返回true
    • 右子树isValid(3, 2, +∞):3 在(2, +∞)内,且 3 无子女→返回true
  • 最终返回true
示例 2:无效 BST(右子树左节点小于根)
    5/ \1   4/ \3   6
  • 调用isValid(5, -∞, +∞):5 在范围内。
    • 左子树isValid(1, -∞, 5)→返回true
    • 右子树isValid(4, 5, +∞):4 <= 5(下界)→返回false
  • 最终返回false

二、解法二:中序遍历法(递归)

(一)思路概述

利用 BST 的核心特性:中序遍历结果为严格递增序列。通过递归中序遍历,记录前一个节点的值,若当前节点值 <= 前一个节点值,则不是有效的 BST。

(二)具体步骤

  1. 初始化:定义成员变量prev记录前一个节点值,初始为Long.MIN_VALUE
  2. 中序遍历递归函数:
    • node == null,返回true
    • 递归遍历左子树,若左子树无效,返回false
    • 检查当前节点:若node.val <= prev,返回false;否则更新prev = node.val
    • 递归遍历右子树,返回右子树的检查结果。
  3. 初始调用:从根节点开始中序遍历。

(三)代码实现

class TreeNode {// 节点定义同前
}class Solution {private long prev = Long.MIN_VALUE; // 记录前一个节点值public boolean isValidBST(TreeNode root) {return inorder(root);}private boolean inorder(TreeNode node) {if (node == null) {return true;}// 先检查左子树if (!inorder(node.left)) {return false;}// 检查当前节点是否大于前一个节点if (node.val <= prev) {return false;}prev = node.val; // 更新前一个节点值// 检查右子树return inorder(node.right);}
}

(四)执行流程示例(以示例 1 的有效 BST 为例)

  • prev = -∞,调用inorder(2)。
    
    • 递归inorder(1):
      • 递归inorder(null)→返回true
      • 检查 1:1 > -∞→更新prev=1
      • 递归inorder(null)→返回true
      • 左子树返回true
    • 检查 2:2 > 1→更新prev=2
    • 递归inorder(3):
      • 递归inorder(null)→返回true
      • 检查 3:3 > 2→更新prev=3
      • 递归inorder(null)→返回true
      • 右子树返回true
  • 最终返回true

三、解法三:中序遍历法(迭代)

(一)思路概述

用栈模拟中序遍历过程,手动记录前一个节点值,实时检查序列的严格递增性。

(二)具体步骤

  1. 初始化:创建栈,prev = Long.MIN_VALUEcurrent = root
  2. 迭代中序遍历:
    • 当current != null或栈不为空时:
      • current的所有左节点入栈(深入左子树)。
      • 弹出栈顶节点node,作为当前访问节点。
      • 检查:若node.val <= prev,返回false;否则更新prev = node.val
      • 移动currentnode.right(处理右子树)。
  3. 遍历完成未发现异常,返回true

(三)代码实现

while(current!=null||!stack.isEmpty())条件拆解

  1. current != null:表示当前还有未访问的左子树节点需要入栈。
    中序遍历的顺序是 “左→根→右”,因此需要先将所有左子节点依次入栈,直到最左节点(左子树为空)。
  2. !stack.isEmpty():表示栈中还有已入栈但未访问的节点(根节点或右子树的前驱节点)。
    当左子树遍历完成后,需要从栈中弹出节点(访问根节点),然后处理其右子树。
import java.util.Deque;
import java.util.LinkedList;class TreeNode {// 节点定义同前
}class Solution {public boolean isValidBST(TreeNode root) {Deque<TreeNode> stack = new LinkedList<>();long prev = Long.MIN_VALUE;TreeNode current = root;while (current != null || !stack.isEmpty()) {// 左子树全部入栈while (current != null) {stack.push(current);current = current.left;}// 访问栈顶节点(当前最左节点)TreeNode node = stack.pop();if (node.val <= prev) {return false; // 不满足严格递增}prev = node.val; // 更新前一个节点值// 处理右子树current = node.right;}return true;}
}

(四)执行流程示例(以示例 2 的无效 BST 为例)

    5/ \1   4/ \3   6
  • 初始:stack = []prev = -∞current = 5
  1. 左子树入栈:current=5→1→null,栈变为[5,1]
  2. 弹出 1:1 > -∞→prev=1current=1.right=null
  3. 弹出 5:5 > 1→prev=5current=5.right=4
  4. 4 的左子树入栈:current=4→3→null,栈变为[4,3]
  5. 弹出 3:3 > 5?3 <= 5→返回false(验证失败)。

四、三种解法对比

解法核心思想时间复杂度空间复杂度优点缺点
递归法(上下界)为每个节点设置合法范围O(n)O (h)(递归栈)直观体现 BST 约束,无冗余递归深度大时可能栈溢出
中序递归法利用中序遍历严格递增特性O(n)O(h)代码简洁,依赖 BST 特性需维护全局变量prev
中序迭代法栈模拟中序遍历,检查递增O(n)O (h)(栈空间)无栈溢出风险,可控性强代码稍复杂,需手动维护栈
  • 时间复杂度:均为 O (n),需访问所有节点一次。
  • 空间复杂度:均为 O (h),h 为树高(平衡树 h=log n,链状树 h=n)。
  • 边界处理:均需考虑int极值(如[Integer.MAX_VALUE]是有效 BST),故用long存储边界或prev

总结

验证 BST 的核心是确保所有左子树节点 < 根节点 < 所有右子树节点

  • 递归法通过上下界约束直接体现这一规则,逻辑清晰。
  • 中序遍历法利用 BST 的特性(中序递增),间接验证合法性,实现简单。

实际应用中,递归法(上下界)是最直接的选择,因其无需依赖 BST 的遍历特性,逻辑更通用;中序遍历法则更适合已熟悉 BST 特性的场景。两种方法均需注意整数边界的处理,避免误判。

10.230. 二叉搜索树中第 K 小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

二叉搜索树中第 K 小的元素:多种解法与实现思路

二叉搜索树(BST)的特性是:中序遍历结果为严格递增的有序序列。利用这一特性,我们可以高效地找到第 K 小的元素。

一、解法一:中序遍历(递归法)

(一)思路概述

根据 BST 中序遍历的有序性,第 K 次访问的节点即为第 K 小的元素:

  1. 递归进行中序遍历(左→根→右)。
  2. 记录访问节点的次数,当计数达到 K 时,当前节点即为结果。

(二)具体步骤

  1. 初始化:定义成员变量 count 记录访问次数,result 存储第 K 小的元素。
  2. 中序遍历递归函数:
    • 若当前节点为 null 或已找到结果,直接返回。
    • 递归遍历左子树。
    • 访问当前节点:count++,若 count == k,记录 result = node.val 并返回。
    • 递归遍历右子树。
  3. 初始调用:从根节点开始遍历,最终返回 result

(三)代码实现

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 {private int count = 0;private int result = 0;public int kthSmallest(TreeNode root, int k) {inorder(root, k);return result;}private void inorder(TreeNode node, int k) {if (node == null || count >= k) {return; // 终止条件:节点为空或已找到结果}// 遍历左子树inorder(node.left, k);// 访问当前节点count++;if (count == k) {result = node.val;return;}// 遍历右子树inorder(node.right, k);}
}

(四)执行流程示例(BST 如下,k=3)

plaintext

      5/ \3   6/ \2   4/
1

中序遍历顺序为:1 → 2 → 3 → 4 → 5 → 6,第 3 小元素为 3。

  1. 初始 count=0,调用 inorder(5, 3)

  2. 递归左子树

    inorder(3, 3)
    

    • 递归左子树

      inorder(2, 3)
      

      • 递归左子树

        inorder(1, 3)
        

        • 1 的左子树为空,返回。
        • 访问 1:count=1(≠3)。
        • 1 的右子树为空,返回。
      • 访问 2:count=2(≠3)。

      • 2 的右子树为空,返回。

    • 访问 3:count=3(=3),记录 result=3,返回。

  3. 后续遍历终止,最终返回 3。

二、解法二:中序遍历(迭代法,栈实现)

(一)思路概述

用栈模拟中序遍历过程,每弹出一个节点(访问)就计数,当计数达到 K 时,当前节点即为结果。相比递归法,迭代法可以在找到结果后立即终止,无需遍历完整棵树。

(二)具体步骤

  1. 初始化:创建栈,current 指向根节点,count=0
  2. 迭代中序遍历:
    • 当current != null或栈非空时:
      • current 的所有左节点入栈(深入左子树)。
      • 弹出栈顶节点 nodecount++
      • count == k,返回 node.val
      • 移动 currentnode.right(处理右子树)。

(三)代码实现


class Solution {public int kthSmallest(TreeNode root, int k) {Deque<TreeNode> stack = new LinkedList<>();TreeNode current = root;int count = 0;while (current != null || !stack.isEmpty()) {// 左子树全部入栈while (current != null) {stack.push(current);current = current.left;}// 访问栈顶节点TreeNode node = stack.pop();count++;if (count == k) {return node.val; // 找到第k小元素,直接返回}// 处理右子树current = node.right;}return -1; // 理论上不会执行(k合法时)}
}

(四)执行流程示例(同前例,k=3)

  1. 初始 current=5stack=[5]current=3stack=[5,3]current=2stack=[5,3,2]current=1stack=[5,3,2,1]current=null
  2. 弹出 1:count=1(≠3),current=1.right=null
  3. 弹出 2:count=2(≠3),current=2.right=null
  4. 弹出 3:count=3(=3),返回 3。

三、解法三:优化解法(记录子树节点数)

(一)思路概述

对于频繁查询第 K 小元素的场景,可在每个节点中记录左子树的节点数量,实现 O (h) 时间复杂度的查询(h 为树高):

  1. 若当前节点左子树的节点数 leftCount >= k:第 K 小元素在左子树中。
  2. leftCount + 1 == k:当前节点即为第 K 小元素。
  3. leftCount + 1 < k:第 K 小元素在右子树中,需查找右子树的第 k - (leftCount + 1) 小元素。

(二)具体步骤

  1. 预处理:为每个节点计算左子树的节点数(可在插入 / 删除时维护)。
  2. 查询逻辑:根据当前节点左子树节点数与 K 的关系,递归缩小查找范围。

(三)代码实现

class TreeNode {int val;int leftCount; // 左子树节点数量TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val = val; }TreeNode(int val, int leftCount, TreeNode left, TreeNode right) {this.val = val;this.leftCount = leftCount;this.left = left;this.right = right;}
}class Solution {public int kthSmallest(TreeNode root, int k) {if (root == null) {return -1;}// 左子树节点数 >= k,第k小在左子树if (root.leftCount >= k) {return kthSmallest(root.left, k);}// 左子树节点数 + 1 == k,当前节点是第k小else if (root.leftCount + 1 == k) {return root.val;}// 否则在右子树,查找第 k - (leftCount + 1) 小else {return kthSmallest(root.right, k - (root.leftCount + 1));}}// 辅助方法:计算节点左子树的节点数(实际应用中需在插入时维护)private int calculateLeftCount(TreeNode node) {if (node == null || node.left == null) {return 0;}return countNodes(node.left);}// 计算子树总节点数private int countNodes(TreeNode node) {if (node == null) {return 0;}return 1 + countNodes(node.left) + countNodes(node.right);}
}

(四)执行流程示例(同前例,k=3)

树中各节点的 leftCount 如下:

  • 1:0(无左子树)
  • 2:1(左子树含 1)
  • 3:2(左子树含 1、2)
  • 4:0
  • 5:3(左子树含 1、2、3、4 → 左子树节点数为 4?修正:3 的左子树节点数为 2,因此 5 的 leftCount 为 3(3 节点 + 其左子树 2 节点))
  • 6:0
  1. 初始节点为 5,leftCount=3
  2. 3 >= 3 → 查找左子树(节点 3)。
  3. 节点 3 的 leftCount=2
  4. 2 < 32 + 1 = 3 == k → 当前节点 3 即为结果,返回 3。

四、三种解法对比

解法核心思想时间复杂度空间复杂度适用场景
递归中序遍历利用中序遍历有序性,计数到 KO(h + k)O (h)(递归栈)一次性查询,实现简单
迭代中序遍历栈模拟遍历,找到结果立即终止O(h + k)O (h)(栈空间)一次性查询,效率略高
记录子树节点数预处理左子树节点数,快速定位O (h)(查询)O (n)(存储节点数)频繁查询,需预处理维护
  • h 为树高:平衡 BST 中 h=log n,最坏情况(链状)h=n。
  • k 的影响:前两种方法的时间与 k 相关(找到即终止),适合 k 较小时;优化解法与 k 无关,适合 k 较大时。

总结

BST 第 K 小元素的求解核心是利用其中序遍历有序性

  • 递归 / 迭代中序遍历是最直接的解法,实现简单,无需额外预处理。
  • 记录子树节点数的方法适合频繁查询的场景,通过空间换时间提升效率。

实际应用中,若查询不频繁,迭代中序遍历是最优选择(无递归栈溢出风险,且可立即终止);若查询频繁,可采用优化解法并在树结构变更时维护节点计数。

http://www.dtcms.com/a/389335.html

相关文章:

  • ARM(12) - ADC 检测光照强度
  • 网格生成引擎:设计原则、关键组件
  • 【开发AI】Spring AI Alibaba:集成AI应用的Java项目实战
  • Spark专题-第二部分:Spark SQL 入门(2)-算子介绍-Scan/Filter/Project
  • Selenium 自动化爬虫:处理动态电商页面
  • 无需Selenium:巧用Python捕获携程机票Ajax请求并解析JSON数据
  • Python版Kafka基础班 - 学习笔记
  • IDEA 查看 Maven 依赖树与解决 Jar 包冲突
  • 【LVS入门宝典】LVS与Nginx、HAProxy的对比:四层(LVS) vs 七层(Nginx)的适用场景
  • 系统安全配置与加固
  • 【AI-Agent】AI游戏库
  • 病毒库更新原理
  • 服务器内存爆炸,日志无报错,通过分析 Dump 文件查找问题原因
  • 【Redis学习】服务端高并发分布式结构演变之路
  • 【JavaScript 性能优化实战】第三篇:内存泄漏排查与根治方案
  • 关于JavaScript性能优化实战的技术
  • 分布式流处理与消息传递——Paxos Stream 算法详解
  • ​​瑞芯微RK3576多路AHD摄像头实测演示,触觉智能配套AHD硬件方案
  • mysql删除数据库命令,如何安全彻底地删除MySQL数据库?
  • vscode中创建项目、虚拟环境,安装项目并添加到工作空间完整步骤来了
  • 如何快速传输TB级数据?公司大数据传输的终极解决方案
  • Linux的进程调度及内核实现
  • 使用BeanUtils返回前端为空值?
  • Windows Server数据库服务器安全加固
  • Linux TCP/IP调优实战,性能提升200%
  • Amazon ElastiCache:提升应用性能的云端缓存解决方案
  • 查找并替换 Excel 中的数据:Java 指南
  • 多线服务器具体是指什么?
  • Golang语言基础篇001_常量变量与数据类型
  • pytest文档1-环境准备与入门