Implement the binary tree traversal with the following functionalities
Could traverse the tree in preorder, inorder, and postorder.
Could access all the nodes on the path from the root to the current one.
Could be suspended and resumed.
Traverse the tree in O(N) time complexity
Implementation
1 2 3 4 5
structTreeNode { int val; TreeNode *left; TreeNode *right; }
We use the definition of the binary tree in Leetcode. Notice that there is no pointer to the parent in each TreeNode. We take baby steps towards our goal and implement these functionalities one by one.
Three Orders
Assume that you already know all three traversal orders. It’s easy to implement all three traversal orders in one recursive function.
1 2 3 4 5 6 7 8 9 10 11 12
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G InorderFunc, H postorderFunc, TreeNode* node){ if(!node) { return; }
By analyzing the recursive traversal function, we know that each call stack of a TreeNode is invoked by its parent (except the root node).
1 2 3 4 5 6 7 8 9 10 11 12 13 14
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G InorderFunc, H postorderFunc, TreeNode* node){ if(!node) { return; }
preorderFunc(node); // new call stack created by executing the following line of code. Traverse(preorderFunc, InorderFunc, postorderFunc, node->left); inorderFunc(node); // new call stack created by executing the following line of code. Traverse(preorderFunc, InorderFunc, postorderFunc, node->left); postorderFunc(node); }
Consider the following snapshot of call stacks while traversing the tree. If we could access the previous call stacks, we could retrieve the values of node variables on previous stacks. These nodes build the path from the current node to the root.
However, accessing a given variable directly in the previous call stack is not possible without the help of a library like libunwind. We have to maintain the path by ourselves and pass the path’s reference during the recursion.
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G InorderFunc, H postorderFunc, TreeNode* node, // path should be initialized as empty. std::vector<TreeNode*>& path){ if(!node) { return; } // push current node into `path` so that its children could start // building their path following their parent. path.push_back(node); preorderFunc(node, path); Traverse(preorderFunc, InorderFunc, postorderFunc, node->left, path); inorderFunc(node, path); Traverse(preorderFunc, InorderFunc, postorderFunc, node->left, path); postorderFunc(node, path); // We are going back to our parent. // We have to remove ourself from the path. path.pop_back(node); }
Suspend and Resume
Suspension is kind of tricky. We pass a reference to a bool shouldStop in the parameters.
We check shouldStop before and after calling preorderFunc, inorderFunc, and postOrderFunc. We check it after calling them because shouldStop could be modified during their execution. During the check after their execution, if shouldStop is true, we set the stopToken as the current node.
stopToken serves as a handle indicating where to resume.
(Assume that shouldStop will only be modified on the current thread.)
The previous code does not really “stop” when shouldStop is set. It will continue its tree traversal without calling preorderFunc, inorderFunc, and postOrderFunc.
Compared to suspending, resuming is much easier to implement. Given the stopToken set by Traverse on suspending, we first traverse to stopToken to resume what the stack is like while suspending without calling preorderFunc, inorderFunc, and postOrderFunc. Upon we find stopToken, we could do our traversal as usual.
Look what a miserable piece of junk we have here. We get nine parameters in the parameter list of Traverse. We check two flags every here and there to make it suspend or resume. Furthermore, this function could not even really “stop” executing when shouldStop is true!
Maybe we should consider refactoring the code to pack all the parameters into a control struct as suggested in the Refactoring book. Maybe we should stop passing all the flags and pack the whole function into a function object. Maybe…
But wait! There should be an elegant way to implement the suspending and resuming.
The Elegant Way
The root of all evil is that we cannot access the call stack. If only we could access the call stack, we do not need to build the path ourselves and pass it. If only we could access the call stack, we could take a snapshot of the call stack upon suspending. Or even better, just a summary of the call stack with some variables we need could be helpful.
A summary of the call stack? That sounds interesting. How about simulating the call stack of the recursion by ourselves? In others words, we abandon the function call convention provided by the operating system. Instead, on this very problem, we consider building our own stack so that we could have all the privileges to access and even modify it!
Extract the Variables
Let’s first extract variables that are necessary to our stack.
1 2 3 4 5 6 7 8 9 10 11 12
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G InorderFunc, H postorderFunc, TreeNode* node){ if(!node) { return; }
Please take a closer look at the initial code segment we had at the very beginning. List all the variables on the stack:
preorderFunc
InorderFunc
postorderFunc
node
The first three of them remain unchanged during the recursion. However, node changes in each recursive call. Therefore, node is the only variable necessary to our stack.
Program Counter
Besides the stack variables, another essential provided by the call stack of the operating system is the program counter. Program counter stores the address of the next instruction to be executed. While returning from the callee, the caller should resume its execution using the saved program counter.
We do not need to get the program counter and save it in our case. Not to mention that it’s hard to get the current program counter when you are not debugging.
Take a closer look at the code segment again. What are the three most important checkpoints?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G InorderFunc, H postorderFunc, TreeNode* node){ if(!node) { return; } // check point 1, before the first recursion. preorderFunc(node); Traverse(preorderFunc, InorderFunc, postorderFunc, node->left); // check point 2, between the two recursions. inorderFunc(node); Traverse(preorderFunc, InorderFunc, postorderFunc, node->left); // check point 3, after the second recursion. postorderFunc(node); }
We do not need to know exactly where our program counter is. The three checkpoints are enough to instruct us:
what function to call: preorderFunc, inorderFunc, or postorderFunc
From which node should we start our recursion: node->left or node->right
Thus, the program counter for our own stack could be described with just an enum.
template<typename F, typename G, typename H> voidTraverse(F preorderFunc, G inorderFunc, H postorderFunc, TreeNode* root){ auto s = std::vector<StackInfo>{}; while(true) { while(root) { preorderFunc(root); // push the node into the stack at the first time we encoutner it s.push_back(StackInfo{root, ProgramCounter::BEFORE_FIRST_REC}); // move to left continuously until there is no left child to move to. root = root->left; } if(s.empty()) { // the stack is empty, the traversal finishes here. break; }
// move the program counter on the second or third time // popping this node from the stack s.back().pc_ = NextPCState(s.back().pc_);
if(s.back().pc_ == ProgramCounter::BETWEEN_TWO_REC) { inorderFunc(s.back().node_); // traverse the right subtree. root = s.back().node_->right; }
if(s.back().pc_ == ProgramCounter::AFTER_SECOND_REC) { postorderFunc(s.back().node_); // there is no subtree to traverse. // pop the node out of the stack. root = nullptr; s.pop_back(); } } }
Suspendable Binary Tree Traversal
Simply extract root and the stack s as data members to make it suspendable. Then we encapsulate the function into a function object.
To access the path from the root and the level information of the current node, we also have two utility functions, PathFromRoot and Level.
To make it step-wise, the outer while(true) loop in the Traverse function is removed. Besides, the inner while(root) loop is also replaced with an if(root_) statement.
voidTestTraverseSimple(){ auto* root = new TreeNode{7, new TreeNode{3}, new TreeNode{15, new TreeNode{9}, new TreeNode{20}}}; auto prefixPrint = [](const std::string& prefix){ return [prefix](TreeNode* x){ std::cout<<prefix<<": "<<x->val<<std::endl; }; };
execute the code above, the result is as expected: every time we call Next(), either one of preorderFunc, inorderFunc, or postorderFunc will be called.
After constructing the function object, we just keep calling next() until next() returns false.
1 2 3 4 5 6 7 8 9 10 11 12 13
vector<int> preorderTraversal(TreeNode* root){ auto result = std::vector<int>{}; auto placeHolder = [](TreeNode*){}; auto resultBuilder = [&result](TreeNode* node){ result.push_back(node->val); }; // build the function object // C++17 is supported in Leetcode, thus we have CTAD, yeah! auto t = Traverse{resultBuilder, placeHolder, placeHolder, root}; // keep calling next() while(t.next()); return result; }
Iterate a BST with inorde traversal. We have to trim our code a little bit to meet its requirement.
Remember that in the previous section, every time we call Next() in our code, either one of the three provided functions will be called. However, this problem requires that whenever we call next(), only the inorderFunc will be invoked.
There is also another difference between the problem API and our previous one. In the problem, the iteration is controlled by a pair of functions — next() and hasNext() instead of just one Next() function in our previous code.
We could notice that the postorder traversal is not needed anymore. Thus, after consuming the node with invoking inorderFunc, we could pop the stack frame out of the call stack. Besides, there is no need to keep the program counter anymore. This is because we only have two states now — ProgramCounter::BEFORE_FIRST_REC and ProgramCounter::BETWEEN_TWO_REC. When the stack frame is popped out, it must be in the state ProgramCounter::BETWEEN_TWO_REC. After it popped out, there is no need to keep its state.
Therefore, we could simplify our Traverse a bit. The while(node_) loop comes back because we skip preorder turns. We do not need to bring the outer while(true). The stack frame is popped out immediately after being revisited in the inorder turn. Thus, there is no postorder turn following the inorder turn. So we do not need to loop to the next inorder turn.