173. Binary Search Tree Iterator
題目 / Problem
中文: 實作一個 BSTIterator 類別,它是二元搜尋樹(BST)中序走訪的迭代器。指標一開始被放在「一個比樹中任何元素都小、且實際不存在的數」的位置,所以第一次呼叫 next() 會回傳最小的元素。
BSTIterator(TreeNode root):用 BST 的根節點初始化迭代器。boolean hasNext():如果指標右邊還有數字(還沒走完),回傳true,否則false。int next():把指標往右移一格,然後回傳指標所在的數字。
English: Implement a BSTIterator class that iterates over the in-order traversal of a binary search tree (BST). The pointer starts at a conceptual, non-existent number smaller than every element, so the first next() returns the smallest value.
BSTIterator(TreeNode root): initialize the iterator with the BST root.boolean hasNext(): returntrueif a number still remains to the right of the pointer.int next(): move the pointer one step right, then return that number.
Constraints / 限制條件:
- 節點數量在 [1, 10^5]。/ Number of nodes is in [1, 10^5].
- 0 <= Node.val <= 10^6。
- 最多 10^5 次 hasNext 與 next 呼叫。/ At most 10^5 calls total.
Worked Example / 範例:
Tree [7, 3, 15, null, null, 9, 20]:
7
/ \
3 15
/ \
9 20
In-order traversal (中序序列) = 3, 7, 9, 15, 20.
Calls: next→3, next→7, hasNext→true, next→9, hasNext→true, next→15, hasNext→true, next→20, hasNext→false.
名詞解釋 / Glossary
- 二元搜尋樹 / Binary Search Tree (BST): 一種二元樹,每個節點的左子樹所有值都比它小、右子樹所有值都比它大。/ A binary tree where every node's left subtree holds smaller values and its right subtree holds larger values.
- 中序走訪 / In-order traversal (LNR): 先走左子樹 → 再訪問自己 → 再走右子樹的順序。對 BST 來說,這個順序剛好是「由小到大排序」。/ Visit Left subtree → Node → Right subtree. For a BST this yields values in sorted ascending order.
- 迭代器 / Iterator: 一個可以「一次吐一個元素」的物件,記住目前走到哪裡,不需要一次把所有東西攤開。/ An object that yields elements one at a time, remembering its current position instead of materializing everything at once.
- 堆疊 / Stack: 一種「後進先出(LIFO)」的容器,只能從頂端 push(放入)和 pop(取出)。/ A Last-In-First-Out container; you push onto the top and pop from the top.
- 樹高 / Height (h): 從根到最深葉子的邊數。用堆疊模擬走訪時,堆疊最多存
h個節點。/ The number of edges on the longest root-to-leaf path; our stack holds at mosthnodes. - 指標 / Pointer: 在 C/C++ 中儲存「某個變數在記憶體位址」的變數;
node->val表示透過指標存取節點的值。/ A variable holding a memory address;node->valreads a field through that address. - 攤平 (預先展開) / Flatten (precompute): 暴力法會先把整棵樹中序走訪一遍存進陣列。/ The brute-force approach walks the whole tree once and stores the in-order sequence in an array.
思路
中文:最直覺的暴力法是——在建構子裡就把整棵樹做一次完整的中序走訪,把結果存進一個陣列,再用一個索引 i 當指標。next() 回傳 arr[i++],hasNext() 檢查 i < 陣列長度。這樣完全正確,而且 next/hasNext 都是 O(1)。它的缺點是空間:必須一開始就存下全部 n 個節點,也就是 O(n) 記憶體。題目的 follow-up 希望我們只用 O(h)(樹高)的記憶體。
要省記憶體,關鍵觀察是:中序走訪其實可以「用一個堆疊來延遲展開」。我們不需要一開始就走完整棵樹,只要隨時保證堆疊裡放著「接下來即將被訪問的節點鏈」。做法是定義一個輔助動作 pushLeft(node):從 node 開始,沿著左子鏈一路往下,把每個節點都 push 進堆疊,直到沒有左子。這樣堆疊頂端永遠是「目前尚未訪問的最小節點」。建構子只對 root 做一次 pushLeft。next() 時,pop 出堆疊頂端 cur(它就是下一個中序值),然後對 cur->right 做一次 pushLeft——因為在中序裡,訪問完一個節點後,下一步就是進到它的右子樹再找最左。hasNext() 只要看堆疊是不是空的。堆疊最多同時裝著一條根到某節點的左鏈,長度不超過樹高 h,所以空間是 O(h)。每個節點一生只會被 push、pop 各一次,總共 2n 次操作攤分到 n 次 next,所以 next 平均是 O(1)。
English: The most obvious brute force is to do one full in-order traversal inside the constructor, store the sorted values in an array, and keep an index i. Then next() returns arr[i++] and hasNext() checks i < length. This is correct and both operations are O(1), but it costs O(n) memory because it holds every node up front. The follow-up asks for O(h) memory instead, where h is the tree height.
The trick is that in-order traversal can be lazily driven by a stack. We never expand the whole tree at once; instead we maintain the invariant that the stack always contains the chain of not-yet-visited ancestors whose top is the next node to emit. Define a helper pushLeft(node) that walks down the left spine from node, pushing every node until there is no left child — this leaves the smallest unvisited node on top. The constructor calls pushLeft(root) once. In next(), we pop the top node cur (the next in-order value) and then call pushLeft(cur->right), because in in-order order the successor of a node lives in the leftmost part of its right subtree. hasNext() just checks whether the stack is non-empty. Since the stack only ever holds one left-spine at a time, its size is bounded by the height h, giving O(h) space. Each node is pushed and popped exactly once over the iterator's whole life, so across n calls the amortized cost of next() is O(1).
逐步走查 / Walkthrough
Tree: root 7, left 3, right 15 (with 15's children 9 and 20). Expected in-order: 3, 7, 9, 15, 20.
Notation: stack shown bottom→top.
| Step / 步驟 | Action / 動作 | Stack after / 操作後堆疊 | Returned / 回傳 |
|---|---|---|---|
| Init 建構 | pushLeft(7): push 7, then left 3, then 3 has no left |
[7, 3] |
— |
next() |
pop 3; pushLeft(3->right = null) does nothing |
[7] |
3 |
next() |
pop 7; pushLeft(7->right = 15): push 15, then left 9, 9 no left |
[15, 9] |
7 |
hasNext() |
stack not empty | [15, 9] |
true |
next() |
pop 9; pushLeft(9->right = null) does nothing |
[15] |
9 |
hasNext() |
stack not empty | [15] |
true |
next() |
pop 15; pushLeft(15->right = 20): push 20, 20 no left |
[20] |
15 |
hasNext() |
stack not empty | [20] |
true |
next() |
pop 20; pushLeft(20->right = null) does nothing |
[] |
20 |
hasNext() |
stack empty | [] |
false |
Notice the stack never held more than 2 nodes — bounded by the tree height, not the node count. / 注意堆疊最多只裝 2 個節點,受限於樹高而非節點總數。
Solution — C
/*
* 演算法 / Algorithm:
* 用一個堆疊延遲進行中序走訪。pushLeft 把某節點及其整條左子鏈壓入堆疊,
* 讓堆疊頂端永遠是下一個要訪問的最小節點。next() pop 頂端並對其右子做 pushLeft。
* We drive an in-order traversal lazily with a stack: pushLeft pushes a node and
* its entire left spine so the top is always the next value; next() pops it and
* pushLeft's its right child. Space O(h), amortized O(1) per next().
*/
// LeetCode 已定義 struct TreeNode(含 val、left、right)/ TreeNode is predefined by LeetCode.
typedef struct {
struct TreeNode** stack; // 存節點指標的動態陣列,當作堆疊 / dynamic array of node pointers used as a stack
int top; // 堆疊頂端索引:-1 表示空 / index of top element; -1 means empty
} BSTIterator;
// 輔助函式:從 node 開始,沿左子鏈全部壓入堆疊 / Helper: push node and its whole left spine.
void pushLeft(BSTIterator* it, struct TreeNode* node) {
while (node != NULL) { // 只要還有節點就繼續往左 / keep going while a node exists
it->top++; // 頂端上移一格,指向新的寫入位置 / advance top to the next slot
it->stack[it->top] = node; // 把目前節點存入堆疊頂端 / store current node on top
node = node->left; // 移到左子,準備下一次壓入 / descend to the left child
}
}
BSTIterator* bSTIteratorCreate(struct TreeNode* root) {
// malloc 配置一塊 BSTIterator 大小的記憶體,回傳其指標 / allocate the iterator struct
BSTIterator* it = (BSTIterator*)malloc(sizeof(BSTIterator));
// 最多 10^5 個節點,配置足夠大的陣列當堆疊 / stack big enough for up to 1e5 nodes
it->stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 100000);
it->top = -1; // 一開始堆疊為空 / start empty
pushLeft(it, root); // 建構時先壓入根的左子鏈 / seed with root's left spine
return it;
}
int bSTIteratorNext(BSTIterator* it) {
// 取出堆疊頂端節點:它就是下一個中序值 / pop the top node = next in-order value
struct TreeNode* cur = it->stack[it->top];
it->top--; // 頂端下移,等於彈出 / move top down = pop
// 中序後繼在右子樹的最左處,故對右子做 pushLeft / successor is leftmost of right subtree
pushLeft(it, cur->right);
return cur->val; // 回傳該節點的值 / return the popped node's value
}
bool bSTIteratorHasNext(BSTIterator* it) {
return it->top >= 0; // 頂端 >= 0 代表堆疊還有元素 / non-empty iff top >= 0
}
void bSTIteratorFree(BSTIterator* it) {
free(it->stack); // 先釋放內部陣列,避免記憶體洩漏 / free inner array first
free(it); // 再釋放結構本身 / then free the struct
}
/**
* 使用方式 / Usage:
* BSTIterator* obj = bSTIteratorCreate(root);
* int param_1 = bSTIteratorNext(obj);
* bool param_2 = bSTIteratorHasNext(obj);
* bSTIteratorFree(obj);
*/
Solution — C++
/*
* 演算法 / Algorithm:
* 用 std::stack 延遲中序走訪。pushLeft 壓入一個節點及其整條左子鏈,
* 使堆疊頂端永遠是下一個中序節點。next() 取頂端並對其右子 pushLeft。
* Lazy in-order traversal with a stack: pushLeft loads a node's left spine so the
* top is the next value; next() pops it and pushLeft's its right child.
* Space O(h), amortized O(1) per next().
*/
#include <stack>
// TreeNode 由 LeetCode 預先定義 / TreeNode is predefined by LeetCode.
class BSTIterator {
private:
// std::stack 是後進先出容器;這裡存節點指標 / a LIFO container of node pointers
std::stack<TreeNode*> st;
// 把 node 及其整條左子鏈壓入堆疊 / push node and its entire left spine
void pushLeft(TreeNode* node) {
while (node != nullptr) { // while 迴圈沿左子一路下走 / walk down the left children
st.push(node); // 壓入目前節點 / push current node onto the stack
node = node->left; // 移到左子 / move to the left child
}
}
public:
// 建構子:初始化時壓入根的左子鏈 / constructor seeds root's left spine
BSTIterator(TreeNode* root) {
pushLeft(root);
}
int next() {
TreeNode* cur = st.top(); // 讀取頂端節點:下一個中序值 / peek the next in-order node
st.pop(); // 彈出它 / remove it from the stack
pushLeft(cur->right); // 後繼在右子樹最左處 / successor lives in the right subtree
return cur->val; // 回傳其值 / return its value
}
bool hasNext() {
return !st.empty(); // 堆疊非空就還有下一個 / more remain iff stack is non-empty
}
};
/**
* 使用方式 / Usage:
* BSTIterator* obj = new BSTIterator(root);
* int param_1 = obj->next();
* bool param_2 = obj->hasNext();
*/
複雜度 / Complexity
- Time / 時間:
hasNext()是 O(1)。next()單次最壞是 O(h)(要沿右子樹的左鏈往下),但每個節點一生只被 push 與 pop 各一次,總工作量 O(n) 攤分到 n 次next,因此攤還 O(1)。/hasNext()is O(1). A singlenext()is O(h) worst case, but each node is pushed/popped exactly once, so across all calls the total is O(n), giving amortized O(1) pernext(). - Space / 空間: O(h)。堆疊在任一時刻只裝著一條由根到某節點的左鏈,其長度不超過樹高
h。相較暴力法的 O(n) 大幅節省(尤其在平衡樹中 h ≈ log n)。/ O(h): the stack only ever holds one root-to-node left spine, bounded by the heighth. Much better than the brute-force O(n), especially for a balanced tree where h ≈ log n.
Pitfalls & Edge Cases
- 不要一開始就攤平整棵樹 / Don't flatten upfront: 把整個中序序列存陣列雖然正確,但用 O(n) 空間,過不了 follow-up 的 O(h) 要求。堆疊做法才符合。/ Precomputing the full array works but uses O(n) space and misses the O(h) follow-up.
pushLeft(node->right)傳入 null 是安全的 / Passing null to pushLeft is safe: 當節點沒有右子時傳入NULL/nullptr,while迴圈條件立刻為假、什麼都不做,不會崩潰。這正是葉節點能正確運作的原因。/ A null right child just makes thewhileloop skip; no crash. This is why leaves work correctly.- 只能 pop 一次 / Pop exactly once per next:
next()必須先取頂端再 pop,順序弄反或忘記 pop 會重複回傳同一值或跳過。C++ 中st.top()只是「看」,st.pop()才是「移除」,兩者要成對。/ In C++,top()peeks andpop()removes — you need both; forgettingpop()repeats a value. - 空樹 / Empty tree: 本題保證至少 1 個節點,但程式仍安全——
pushLeft(NULL)不壓任何東西,hasNext()立刻回傳false。/ Constraints guarantee ≥1 node, yet the code is still safe:pushLeft(null)pushes nothing andhasNext()returnsfalse. - C 版記憶體 / C memory management: 用完要呼叫
bSTIteratorFree釋放stack陣列與結構本身,否則記憶體洩漏。這裡用固定大小 100000 是因為節點上限已知;動態成長也可以但較複雜。/ Free both the inner array and the struct to avoid leaks; the fixed 1e5 size is safe because the node count is bounded. - 值域無溢位風險 / No overflow risk:
Node.val最大10^6,遠在int範圍內,回傳值不需特別處理。/ Values fit comfortably inint, so returns need no special handling.