Leetcode 122 二叉树的最近公共祖先 | 二叉搜索树迭代器
1 题目
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出:3 解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出:5 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2 输出:1
提示:
- 树中节点数目在范围
[2, 105]内。 -109 <= Node.val <= 109- 所有
Node.val互不相同。 p != qp和q均存在于给定的二叉树中。
2 代码实现
c++
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if (root == nullptr || root == p || root == q ){ return root ; } TreeNode* left = lowestCommonAncestor(root -> left , p , q ) ; TreeNode* right = lowestCommonAncestor(root -> right , p , q ); if (left == nullptr){ return right ; } if (right == nullptr){ return left ; } return root ; } };js
/** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.right = null; * } */ /** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */ var lowestCommonAncestor = function(root, p, q) { if (root == null || root == p || root == q ){ return root ; } let left = lowestCommonAncestor (root.left , p , q ); let right = lowestCommonAncestor (root.right , p , q ); if (left == null){ return right ; } if (right == null){ return left ; } return root ; };思考
看不太懂,也不知道怎么实现。
题解
你想实现二叉树最近公共祖先(LCA)的查找功能,并且希望用 C++ 来完成这个实现,目前你对这个问题的解法思路还不太清晰。我会先帮你理解核心逻辑,再给出完整的可运行代码。
核心思路理解
要找到二叉树中两个节点的最近公共祖先,最经典且高效的解法是递归法,核心逻辑可以总结为:
- 递归遍历二叉树的左、右子树
- 递归终止条件:遇到空节点 或 找到了 p/q 节点,直接返回当前节点
- 递归判断逻辑:
- 如果左子树返回空,说明 p/q 都在右子树,返回右子树的结果
- 如果右子树返回空,说明 p/q 都在左子树,返回左子树的结果
- 如果左右子树都有返回值,说明当前节点就是最近公共祖先
C++ 完整实现代码
#include <iostream> #include <vector> using namespace std; // 定义二叉树节点结构 struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { // 递归终止条件: // 1. 节点为空,返回null // 2. 找到p或q节点,直接返回该节点 if (root == nullptr || root == p || root == q) { return root; } // 递归遍历左子树 TreeNode* left = lowestCommonAncestor(root->left, p, q); // 递归遍历右子树 TreeNode* right = lowestCommonAncestor(root->right, p, q); // 核心判断逻辑 if (left == nullptr) { // 左子树没找到,说明p/q都在右子树,返回右子树结果 return right; } if (right == nullptr) { // 右子树没找到,说明p/q都在左子树,返回左子树结果 return left; } // 左右子树都找到,说明当前节点就是最近公共祖先 return root; } }; // 辅助函数:根据数组构建二叉树(方便测试) TreeNode* buildTree(vector<int>& nums, int index) { if (index >= nums.size() || nums[index] == -1) { return nullptr; } TreeNode* root = new TreeNode(nums[index]); root->left = buildTree(nums, 2 * index + 1); root->right = buildTree(nums, 2 * index + 2); return root; } // 辅助函数:根据值查找节点(方便测试) TreeNode* findNode(TreeNode* root, int val) { if (root == nullptr) return nullptr; if (root->val == val) return root; TreeNode* left = findNode(root->left, val); if (left != nullptr) return left; return findNode(root->right, val); } // 测试示例 int main() { // 示例1测试:root = [3,5,1,6,2,0,8,-1,-1,7,4] vector<int> nums1 = {3,5,1,6,2,0,8,-1,-1,7,4}; TreeNode* root1 = buildTree(nums1, 0); TreeNode* p1 = findNode(root1, 5); TreeNode* q1 = findNode(root1, 1); Solution s; TreeNode* res1 = s.lowestCommonAncestor(root1, p1, q1); cout << "示例1结果:" << res1->val << endl; // 输出3 // 示例2测试:root = [3,5,1,6,2,0,8,-1,-1,7,4], p=5, q=4 TreeNode* p2 = findNode(root1, 5); TreeNode* q2 = findNode(root1, 4); TreeNode* res2 = s.lowestCommonAncestor(root1, p2, q2); cout << "示例2结果:" << res2->val << endl; // 输出5 // 示例3测试:root = [1,2], p=1, q=2 vector<int> nums3 = {1,2}; TreeNode* root3 = buildTree(nums3, 0); TreeNode* p3 = findNode(root3, 1); TreeNode* q3 = findNode(root3, 2); TreeNode* res3 = s.lowestCommonAncestor(root3, p3, q3); cout << "示例3结果:" << res3->val << endl; // 输出1 return 0; } #include <iostream> using namespace std; // 二叉树节点 struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { // 终止条件: // 1. 走到头了(空节点),返回空 // 2. 找到p或q了,返回这个节点 if (root == NULL || root == p || root == q) { return root; } // 派左小弟去左子树找 TreeNode* 左小弟汇报 = lowestCommonAncestor(root->left, p, q); // 派右小弟去右子树找 TreeNode* 右小弟汇报 = lowestCommonAncestor(root->right, p, q); // 开始判断: if (左小弟汇报 == NULL) { // 左小弟没找到,说明p/q都在右边,返回右小弟的结果 return 右小弟汇报; } if (右小弟汇报 == NULL) { // 右小弟没找到,说明p/q都在左边,返回左小弟的结果 return 左小弟汇报; } // 左右小弟都找到了人 → 说明p和q分别在我左右两边,我就是最近老祖宗 return root; } }; // 测试:手动构建示例2的树 int main() { // 构建树 TreeNode* root = new TreeNode(3); root->left = new TreeNode(5); root->right = new TreeNode(1); root->left->left = new TreeNode(6); root->left->right = new TreeNode(2); root->left->right->left = new TreeNode(7); root->left->right->right = new TreeNode(4); // 要找的两个节点 TreeNode* p = root->left; // 节点5 TreeNode* q = root->left->right->right; // 节点4 Solution s; TreeNode* res = s.lowestCommonAncestor(root, p, q); cout << "最近公共祖先是:" << res->val << endl; // 输出5 return 0; }代码关键部分解释
- TreeNode 结构体:定义二叉树节点,包含值、左子节点、右子节点
- lowestCommonAncestor 核心函数:
- 终止条件:遇到空节点或目标节点直接返回
- 递归遍历左右子树,分别获取查找结果
- 根据左右子树的返回值判断公共祖先位置
- 辅助函数:
buildTree:根据数组构建二叉树(-1 表示空节点)findNode:根据值查找对应的节点指针(方便测试)
- main 函数:测试题目中的 3 个示例,验证代码正确性
运行说明
- 环境要求:支持 C++11 及以上的编译器(如 g++、clang++)
- 编译命令:
g++ -std=c++11 lca.cpp -o lca - 运行命令:
./lca - 输出结果:会依次打印 3 个示例的结果(3、5、1)
总结
- 二叉树最近公共祖先的核心解法是后序递归遍历,先找左右子树的结果,再回溯判断当前节点是否为公共祖先
- 递归终止条件是找到目标节点或空节点,核心判断逻辑是:左右都有结果则当前节点是 LCA,否则返回非空的一侧
- 该解法时间复杂度 O (n)(遍历所有节点),空间复杂度 O (n)(递归栈深度),是最优解法之一
3 题目
实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root)初始化BSTIterator类的一个对象。BST 的根节点root会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext()如果向指针右侧遍历存在数字,则返回true;否则返回false。int next()将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。
你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。
示例:

输入 ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"] [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []] 输出 [null, 3, 7, true, 9, true, 15, true, 20, false] 解释 BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]); bSTIterator.next(); // 返回 3 bSTIterator.next(); // 返回 7 bSTIterator.hasNext(); // 返回 True bSTIterator.next(); // 返回 9 bSTIterator.hasNext(); // 返回 True bSTIterator.next(); // 返回 15 bSTIterator.hasNext(); // 返回 True bSTIterator.next(); // 返回 20 bSTIterator.hasNext(); // 返回 False
提示:
- 树中节点的数目在范围
[1, 105]内 0 <= Node.val <= 106- 最多调用
105次hasNext和next操作
进阶:
- 你可以设计一个满足下述条件的解决方案吗?
next()和hasNext()操作均摊时间复杂度为O(1),并使用O(h)内存。其中h是树的高度。
4 代码实现
c++
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class BSTIterator { private: stack<TreeNode*> stk ; void pushAllLeft (TreeNode* node ){ while (node != nullptr){ stk.push(node); node = node -> left ; } } public: BSTIterator(TreeNode* root) { pushAllLeft (root ); } int next() { TreeNode* cur = stk.top(); stk.pop(); if (cur -> right != nullptr){ pushAllLeft(cur -> right ); } return cur -> val ; } bool hasNext() { return !stk.empty(); } }; /** * Your BSTIterator object will be instantiated and called as such: * BSTIterator* obj = new BSTIterator(root); * int param_1 = obj->next(); * bool param_2 = obj->hasNext(); */js
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root */ var BSTIterator = function(root) { this.stack = [] ; let node = root ; while (node !== null){ this.stack.push(node); node = node.left ; } }; /** * @return {number} */ BSTIterator.prototype.next = function() { const cur = this.stack.pop(); let node = cur.right ; while (node !== null ){ this.stack.push(node); node = node.left ; } return cur.val; }; /** * @return {boolean} */ BSTIterator.prototype.hasNext = function() { return this.stack.length > 0 ; }; /** * Your BSTIterator object will be instantiated and called as such: * var obj = new BSTIterator(root) * var param_1 = obj.next() * var param_2 = obj.hasNext() */你现在想实现二叉搜索树(BST)迭代器,要求支持 next()(返回中序遍历下一个元素)和 hasNext()(判断是否有下一个元素),并且希望用 C++ 完成,还需要理解进阶要求(均摊 O (1) 时间、O (h) 空间)。我先帮你理清核心思路,再给出完整的可运行代码,确保新手也能看懂。
核心思路理解
首先明确两个关键前提:
- BST 的特性:中序遍历 BST 的结果是升序序列(这是解题的核心);
- 进阶要求分析:
- 不能先把所有节点遍历存到数组里(空间复杂度会是 O (n),不满足 O (h));
- 需要用迭代式中序遍历的思路,用栈模拟递归,只保存从根到当前节点的路径(空间 O (h),h 是树高);
- 均摊 O (1):每个节点最多入栈、出栈各一次,总操作次数 O (n),分摊到 n 次
next()调用就是 O (1)。
核心实现逻辑(模拟 “受控的中序遍历”):
- 构造函数:把根节点的所有左子节点依次入栈(找到最小元素的路径);
- next():弹出栈顶节点(当前最小元素),如果该节点有右子节点,把右子节点的所有左子节点入栈(为下一次
next()做准备); - hasNext():只需判断栈是否为空(栈空则没有下一个元素)。
C++ 完整实现代码
#include <iostream> #include <stack> using namespace std; // 二叉树节点定义(题目给定) struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; class BSTIterator { private: // 用栈保存待遍历的节点(核心数据结构) stack<TreeNode*> stk; // 辅助函数:把某个节点的所有左子节点依次入栈 void pushAllLeft(TreeNode* node) { while (node != nullptr) { stk.push(node); node = node->left; // 一直往左走,找最小的路径 } } public: // 构造函数:初始化栈,找到最小元素的路径 BSTIterator(TreeNode* root) { pushAllLeft(root); } // 返回下一个元素(升序) int next() { // 栈顶就是当前最小的元素 TreeNode* curr = stk.top(); stk.pop(); // 如果当前节点有右子树,把右子树的所有左节点入栈 if (curr->right != nullptr) { pushAllLeft(curr->right); } return curr->val; } // 判断是否有下一个元素 bool hasNext() { // 栈不为空就有下一个元素 return !stk.empty(); } }; // 测试示例 int main() { // 构建示例中的树: // 7 // / \ // 3 15 // / \ // 9 20 TreeNode* root = new TreeNode(7); root->left = new TreeNode(3); root->right = new TreeNode(15); root->right->left = new TreeNode(9); root->right->right = new TreeNode(20); // 初始化迭代器 BSTIterator* obj = new BSTIterator(root); // 模拟题目中的调用流程 cout << obj->next() << endl; // 输出3 cout << obj->next() << endl; // 输出7 cout << boolalpha << obj->hasNext() << endl; // 输出true cout << obj->next() << endl; // 输出9 cout << boolalpha << obj->hasNext() << endl; // 输出true cout << obj->next() << endl; // 输出15 cout << boolalpha << obj->hasNext() << endl; // 输出true cout << obj->next() << endl; // 输出20 cout << boolalpha << obj->hasNext() << endl; // 输出false // 释放内存(新手可暂时忽略,重点看逻辑) delete obj; delete root->right->right; delete root->right->left; delete root->right; delete root->left; delete root; return 0; } 代码关键部分解释
- 栈的作用:
- 栈里保存的是 “待访问” 的节点,且始终保证栈顶是当前未访问的最小元素;
- 比如构造函数处理示例树时,栈会依次压入 7 → 3(因为先压 7,再压 7 的左子节点 3),栈顶是 3(最小元素)。
- pushAllLeft 辅助函数:
- 核心功能是 “一路向左走到底”,把路径上的节点都入栈;
- 比如处理节点 15 的右子节点时,会压入 15 → 9(15 的左子节点),保证栈顶是 9。
- next () 函数执行流程(示例中第一次调用):
- 弹出栈顶 3,返回 3;
- 3 没有右子节点,栈里剩下 7;
- 第二次调用 next ():弹出 7,返回 7;7 有右子节点 15,调用 pushAllLeft (15),栈压入 15 → 9,栈顶变为 9;
- 第三次调用 next ():弹出 9,返回 9;9 没有右子节点,栈里剩下 15;以此类推。
- hasNext () 函数:
- 只需判断栈是否为空,逻辑极简单,时间复杂度 O (1)。
运行说明
- 环境要求:支持 C++11 及以上的编译器(g++、clang++ 均可);
- 编译命令:
g++ -std=c++11 bst_iterator.cpp -o bst_iterator; - 运行命令:
./bst_iterator; - 输出结果:依次打印 3、7、true、9、true、15、true、20、false,和题目示例一致。
进阶要求验证
- 时间复杂度:每个节点最多入栈、出栈各一次,n 次 next () 调用总操作数 O (n),均摊到每次 next () 是 O (1);hasNext () 是纯判断,O (1);
- 空间复杂度:栈最多保存树的高度 h 个节点(比如斜树 h=n,平衡树 h=logn),满足 O (h) 的要求。
总结
- BST 迭代器的核心是利用 BST 中序遍历升序的特性,结合栈模拟迭代式中序遍历;
- 关键操作是 “构造函数一路左压栈,next () 弹出栈顶后处理右子树的左路径”;
- 进阶要求的核心是 “不预存所有节点,只存当前路径”,用栈控制遍历节奏,实现 O (h) 空间和均摊 O (1) 时间。
理解题意
先把题目翻译成 “人话”
这个题的核心需求是:做一个 “工具”,能按 “从小到大” 的顺序,逐个取出二叉搜索树(BST)里的节点值。
我们拆解题目里的关键描述:
| 题目里的抽象说法 | 大白话解释 |
|---|---|
| 二叉搜索树迭代器(BSTIterator) | 一个专门用来 “遍历 BST” 的工具 |
| 指针初始化为小于 BST 中任何元素的数字 | 这个 “指针” 一开始指向 “最小元素的左边”,第一次取就是最小的数 |
| hasNext() | 问:“工具里还有没取的数吗?”(有就返回 true,没有返回 false) |
| next() | 做两件事:1. 把 “指针” 移到下一个数;2. 返回这个数(第一次调用返回最小的数) |
用生活例子理解(超形象)
把 BST 想象成一个按从小到大排好的书架(因为 BST 的中序遍历就是升序):
- 书架上的书:3 → 7 → 9 → 15 → 20(对应示例里的 BST);
- 迭代器(BSTIterator):就是你的 “取书手”;
- 初始化(BSTIterator (root)):把手放在 “3 的左边”(还没碰到任何书);
- next ():手伸到下一本书,把书拿出来给你(第一次拿 3,第二次拿 7,依此类推);
- hasNext ():看看手的右边还有没有书(比如拿完 20 后,右边没书了,返回 false)。
题目示例的调用流程,翻译成 “取书” 就是:
初始化手 → 拿第一本书(3)→ 拿第二本书(7)→ 问:还有书吗?(有→true)→ 拿第三本书(9)→ 问:还有书吗?(有→true)→ 拿第四本书(15)→ 问:还有书吗?(有→true)→ 拿第五本书(20)→ 问:还有书吗?(没有→false) 结合代码,逐步模拟示例的执行过程
还是用示例里的树:
7 / \ 3 15 / \ 9 20 我们一步步看代码里的栈和函数调用变化:
步骤 1:初始化(BSTIterator (root))
调用pushAllLeft(root),也就是从 7 开始 “一路往左压栈”:
- 压入 7 → 压入 7 的左子节点 3(3 没有左子节点,停止);
- 此时栈里的元素(栈底→栈顶):7 → 3;
- 对应 “取书手” 的状态:停在 3 的左边,准备拿 3。
步骤 2:第一次调用 next ()
- 弹出栈顶 3,返回 3(拿到第一本书);
- 3 没有右子节点,栈里剩下 7;
- 对应 “取书手”:移到 3 的位置,拿完 3 后,手停在 7 的左边。
步骤 3:第二次调用 next ()
- 弹出栈顶 7,返回 7(拿到第二本书);
- 7 有右子节点 15,调用
pushAllLeft(15):压入 15 → 压入 15 的左子节点 9(9 没有左子节点,停止); - 此时栈里的元素:15 → 9;
- 对应 “取书手”:移到 7 的位置,拿完 7 后,手停在 9 的左边。
步骤 4:调用 hasNext ()
- 栈不为空(有 15、9),返回 true(还有书)。
步骤 5:第三次调用 next ()
- 弹出栈顶 9,返回 9(拿到第三本书);
- 9 没有右子节点,栈里剩下 15;
- 对应 “取书手”:移到 9 的位置,拿完 9 后,手停在 15 的左边。
步骤 6:调用 hasNext ()
- 栈不为空(有 15),返回 true(还有书)。
步骤 7:第四次调用 next ()
- 弹出栈顶 15,返回 15(拿到第四本书);
- 15 有右子节点 20,调用
pushAllLeft(20):压入 20(20 没有左子节点,停止); - 此时栈里的元素:20;
- 对应 “取书手”:移到 15 的位置,拿完 15 后,手停在 20 的左边。
步骤 8:调用 hasNext ()
- 栈不为空(有 20),返回 true(还有书)。
步骤 9:第五次调用 next ()
- 弹出栈顶 20,返回 20(拿到第五本书);
- 20 没有右子节点,栈为空;
- 对应 “取书手”:移到 20 的位置,拿完 20 后,手右边没书了。
步骤 10:调用 hasNext ()
- 栈为空,返回 false(没有书了)。
为什么要这么设计(补充理解)
题目里的 “进阶要求”(均摊 O (1) 时间、O (h) 空间),本质是不让你 “作弊”:
- 不能一开始就把所有数存到数组里(比如先遍历一遍存 [3,7,9,15,20],然后 next () 就取数组下标)—— 这种方法空间是 O (n),不符合 O (h) 的要求;
- 用栈的方式,只存 “当前要取的数的路径”(比如初始化时只存 7、3,而不是所有数),空间只和树的高度 h 有关(示例里树高是 3),效率更高。
总结
- 题目核心:做一个 “按从小到大取数” 的工具,支持 “取下一个数” 和 “判断有没有下一个数” 两个操作;
- 核心思路:利用 BST 中序遍历升序的特性,用栈模拟 “一路往左找最小数”,取完一个数后处理右子树的左路径;
- 关键操作:
- 初始化:把根的所有左子节点压栈(找到最小数的路径);
- next ():弹栈顶(取数),有右子树就压右子树的所有左节点;
- hasNext ():看栈空不空。
JS 完整实现代码
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root */ var BSTIterator = function(root) { // 用数组模拟栈(JS里数组的push/pop就是栈的操作) this.stack = []; // 初始化:把根节点的所有左子节点压入栈 this.pushAllLeft(root); }; /** * 辅助方法:把某个节点的所有左子节点依次压入栈 * @param {TreeNode} node */ BSTIterator.prototype.pushAllLeft = function(node) { // 一路向左走,直到节点为空 while (node !== null) { this.stack.push(node); node = node.left; } }; /** * @return {number} */ BSTIterator.prototype.next = function() { // 栈顶就是当前最小的元素,弹出 const currNode = this.stack.pop(); // 如果当前节点有右子节点,把右子节点的所有左子节点压栈 if (currNode.right !== null) { this.pushAllLeft(currNode.right); } // 返回当前节点的值 return currNode.val; }; /** * @return {boolean} */ BSTIterator.prototype.hasNext = function() { // 栈不为空就有下一个元素 return this.stack.length > 0; }; // 测试示例(可以直接运行) function test() { // 构建示例中的树: // 7 // / \ // 3 15 // / \ // 9 20 const root = new TreeNode(7); root.left = new TreeNode(3); root.right = new TreeNode(15); root.right.left = new TreeNode(9); root.right.right = new TreeNode(20); // 初始化迭代器 const iterator = new BSTIterator(root); // 模拟题目中的调用流程,打印结果 console.log(iterator.next()); // 输出3 console.log(iterator.next()); // 输出7 console.log(iterator.hasNext()); // 输出true console.log(iterator.next()); // 输出9 console.log(iterator.hasNext()); // 输出true console.log(iterator.next()); // 输出15 console.log(iterator.hasNext()); // 输出true console.log(iterator.next()); // 输出20 console.log(iterator.hasNext()); // 输出false } // 执行测试 test(); 代码关键部分解释
1. 核心数据结构:栈(用数组模拟)
JS 里没有专门的栈结构,但可以用数组的 push()(入栈)和 pop()(出栈,取最后一个元素)来模拟,和 C++ 的 stack 功能完全一致。
2. 构造函数 BSTIterator(root)
- 初始化空栈
this.stack; - 调用
pushAllLeft(root):把根节点的所有左子节点压栈(比如示例中先压 7,再压 3),保证栈顶是最小元素(3)。
3. 辅助方法 pushAllLeft(node)
- 核心逻辑:“一路向左走到底”,把路径上的节点都压入栈;
- 比如传入 15 时,会先压 15,再压 15 的左子节点 9(9 没有左节点,停止)。
4. next() 方法(核心)
- 弹出栈顶节点(当前最小元素);
- 如果该节点有右子节点,调用
pushAllLeft处理右子树的左路径(为下一次next()准备); - 返回当前节点的值。示例中第一次调用
next():弹出 3,3 没有右节点,栈剩 [7],返回 3;第二次调用next():弹出 7,7 有右节点 15,调用pushAllLeft(15)压入 15、9,栈变成 [15,9],返回 7。
5. hasNext() 方法
- 最简单的逻辑:判断栈的长度是否大于 0(栈空 = 没有下一个元素,返回 false;否则返回 true);
- 时间复杂度 O (1),没有任何额外操作。
运行说明
- 直接把代码复制到浏览器控制台(Chrome/Firefox)或 Node.js 环境中,执行
test()即可; - 输出结果和题目示例完全一致:3、7、true、9、true、15、true、20、false;
- 满足进阶要求:
- 空间复杂度 O (h):栈最多保存树的高度个节点(示例中树高 3);
- 均摊时间复杂度 O (1):每个节点只入栈、出栈一次,总操作数 O (n),分摊到 n 次
next()就是 O (1)。
总结
- JS 实现的核心逻辑和 C++ 完全一致,只是语法差异(数组模拟栈、原型链定义方法);
- 关键操作:初始化压左节点、next () 弹栈顶 + 处理右子树左路径、hasNext () 判断栈空;
- 这个实现既满足题目基础要求,也符合进阶的时间 / 空间复杂度要求,是最优解法。
5 小结
第一题看明白了,其实就是左右小弟出去找两个人(两个目标节点),如果左小弟没找到人,那返回右小弟汇报的结果;反之是汇报左小弟汇报的结果;如果左右小弟都找到了一个人,说明我(当前节点)就是最小公共祖先。递归之处在于每一个左(右)小弟都有对应的左(右)小小弟,每隔一个子树看成一颗新的树递归下去。
第二题我觉得我没懂,但是也许写多了就会了,,现在不着急马上就理解,因为我看了题解都没有恍然大悟的感觉。这个应该是底层的栈实现或者是数组模拟栈,我不知道怎么写,也不太会看。
召唤ai老师!!
你的小结写得特别棒!能把抽象的递归逻辑转化成 “小弟找人” 的通俗理解,说明第一题的核心已经真的吃透了;对于第二题的坦诚认知也特别好 —— 编程里很多知识点不是 “看一遍就悟”,而是 “写多了、用多了自然就会”,你的心态特别对。
我针对你的小结补充几点,帮你巩固第一题的理解,也给第二题的学习指个小方向:
一、关于第一题(最近公共祖先)的补充
你的总结完全精准:
每一个左(右)小弟都有对应的左(右)小小弟,每个子树看成一颗新的树递归下去
这正是递归的核心思想(分治) —— 把 “找整棵树的公共祖先” 拆解成 “找左子树的公共祖先” 和 “找右子树的公共祖先”,直到拆解到 “空节点” 或 “找到目标节点” 这个最小问题,再回溯合并结果。✅ 关键点巩固:你记住 “左右小弟都找到人 → 当前节点是 LCA” 这个核心判断,就抓住了这道题的灵魂,后续遇到变种题(比如 BST 的最近公共祖先)也能举一反三。
二、关于第二题(BST 迭代器)的学习建议
你说 “没懂但不着急”,这个心态太重要了!这道题的难点不在 “逻辑”,而在 “对栈模拟遍历的体感”—— 新手对 “用栈替代递归” 本来就不熟悉,不用逼自己立刻顿悟。
给你一个低成本的学习方法(不用死磕,练几次就有感觉):
- 先 “抄代码 + 改代码”:把我给你的 JS/C++ 代码复制下来,手动改一改树的结构(比如把示例树改成
[5,3,8,2,null,7,9]),运行看看输出结果,观察栈的变化(可以在代码里加一行console.log(this.stack)打印栈); - 只记 “固定套路”:BST 迭代器的代码是典型的 “模板题”,核心就 3 步,你不用理解 “为什么要这么设计”,先记 “必须这么写”:
- 初始化:一路往左压栈(找最小元素);
- next ():弹栈顶 + 右子节点一路往左压栈;
- hasNext ():看栈空不空。
- 写 2-3 遍就停:不用今天就吃透,过两天再回头写一遍,你会发现 “哎,好像突然就懂了”—— 编程的 “体感” 就是靠重复写出来的,不是靠 “想” 出来的。
三、核心小结
- 第一题:你的理解 100% 到位,“小弟找人” 的比喻可以一直用,后续遇到二叉树递归题都能套这个思路;
- 第二题:不用追求 “恍然大悟”,先记 “模板步骤”,通过 “抄→改→写” 积累体感,这是新手掌握栈模拟遍历的最优路径;
- 心态上:你能坦诚面对 “没懂”,还能主动总结,这已经是远超很多新手的优势 —— 编程学习没有 “立刻会”,只有 “慢慢会”,你的节奏特别好。