二叉树的遍历主要有3种模式,前序,中序和后序。其递归版本,非常简单,大致如下:
void Traversal(TreeNode* root){
if(!root)
return;
// visit root (preorder)
Traversal(root->left);
// visit root (inorder)
Traversal(root->right);
// visit root (postorder)
}
根据要求不同,分别在对应地方访问节点就可以了。
但是一方面,这种递归版本由于函数调用会影响效率,另一方面面试时很多面试官会让你转化成非递归版本。本来说不难,但是为了给面试官留下好印象,还是要非常熟悉这一套的,要求0失误快速写出来。
非递归版本:
由于是递归转非递归,因此肯定会用到栈,即用栈去模拟计算机函数调用的过程。
前序遍历非常容易模拟,因为root先于其左右子树被访问,因此每次从栈顶取到root后直接访问,然后将其右儿子和左儿子依次压入栈中就可以了(注意先压右儿子,因为按照栈是FILO,所以右儿子会被后访问到)。
vector<int> preorderTraversal(TreeNode *root) {
vector<int> ans;
if(root == NULL)
return ans;
stack<TreeNode*> st;
st.emplace(root);
while(!st.empty()){
//访问root
TreeNode * tmp = st.top();
st.pop();
ans.emplace_back(tmp->val);
//先压右儿子
if(tmp->right != NULL) st.emplace(tmp->right);
//再压左儿子
if(tmp->left != NULL) st.emplace(tmp->left);
}
return ans;
}
但是到了中序和后序遍历就稍微麻烦一点了,因为root并不是第一个被访问到,因此当我们从栈顶弹出root时,会遇到一个麻烦,即它是第一次被访问到,还是已经访问完它的左右子树了呢?
如果是第一次被访问到,显然现在我们还不能正式遍历它,而是要先遍历它的子树,那么这个时候我们就需要把它暂时放回到栈中,等遍历完它的子树后,再从栈中把它取出来遍历。 那么如何判断它的左右子树被遍历到,当然我们可以给每个节点加一个变量来标记它是否已经被遍历过;或者利用C++中的pair作为栈的基本元素, 除了在栈中记录节点,同时在栈中记录每个节点进出栈的次数,通过进出栈的次数决定它的左右子树是否被遍历完了。
但是不论哪种方法,都要利用额外的空间,显然这样做虽然能行,但是不够好。当然有更好的解决办法。
下面我们先考虑中序遍历,当我们拿到一个节点root时:
- 显然最先遍历的是它的左子树(记为 T1 , T1 的根记为 l1 ),然后才回来访问root和root的右子树;
- 而当我们第一次访问子树 T1 ,应该先遍历它的左子树(记为 T2 , T2 的根记为 l2 ),然后才是 l1 和它的右子树
以此类推,每次都是先遍历节点的左子树,然后才是节点本身
那么既然这样,我们拿到一个节点root后先按顺序将它以及 l1,l2,…,lh 压入栈中。然后再从栈中取出元素,这时栈顶元素一定是树中最左下角的元素 lh ,此时我们遍历它,然后开始遍历 lh 的右子树就可以了。当访问完 lh 的右子树时,我们再从栈顶弹出元素,这个元素按照我们的压栈顺序一定是 lh 的父节点 lh−1 ,而此时由于以 lh 为根的子树恰好刚遍历完,即 lh−1 的左子树遍历完了,自然我们就需要遍历 lh−1 ,然后再遍历 lh−1 的右子树就可以了。
以此类推,这样我们就可以完成中序遍历。
vector<int> inorderTraversal(TreeNode *root) {
vector<int> ans;
TreeNode* t = root;
stack<TreeNode* > st;
while(t != NULL || !st.empty()){
//如果t不空,将t左边的后代(即l1,l2...)依次压入栈中
while( t != NULL){
st.emplace(t);
t = t->left;
}
if(!st.empty()){
//此时栈顶元素的左子树一定遍历完了,那么我们开始遍历栈顶元素
t = st.top();
st.pop();
ans.emplace_back(t->val);
//然后开始遍历栈顶元素的右子树
t = t->right;
}
}
return ans;
}
最后是后序遍历,还是用压栈的方法,但是比上面简单但又巧妙很多。由于后序遍历的特点是节点本身最后被访问到,这样就给了我们一个简单的方法判断某个节点root的左右子树是否被遍历到:
因为我们想遍历root时,一定是其右子树被遍历完的时候。而遍历右子树时,最后遍历到的一定是右子树的根,即root的右儿子。此时我们只需要一个变量pre, 用来记录之前遍历的那个节点,。然后和root的右儿子做一下比较,如果两者相同,说明右子树已经遍历完,这时我们直接遍历root就可以了。当然遇到root没有右儿子时,说明右子树为空,这样我们就要拿pre和root的左儿子比较。如果root连左儿子都没有,那么说明它是叶子节点,直接访问就可以了。
vector<int> postorderTraversal(TreeNode *root) {
vector<int> ans;
if(root == NULL)
return ans;
//pre表示之前遍历到的那个节点
TreeNode *pre = NULL, *tmp;
stack<TreeNode* >st;
st.emplace(root);
while(!st.empty()){
tmp = st.top();
//如果pre等于tmp的右儿子;或者没有右儿子的情况下,pre等于其左儿子,或者甚至连左儿子都没有。说明tmp的左右子树已经访问完了,那么我们开始访问tmp,并更新pre
if( (tmp->right != NULL && tmp->right == pre) ||
(tmp->right == NULL && (tmp->left == pre ||
tmp->left == NULL)) ){
pre = tmp;
st.pop();
ans.emplace_back(tmp->val);
}
//否则将tmp的右儿子和左儿子加入栈中
else{
if(tmp->right != NULL) st.emplace(tmp->right);
if(tmp->left != NULL) st.emplace(tmp->left);
}
}
return ans;
}
除了上面这些方法,还有其他用栈模拟的,这里不再冗述。不过需要指的提一下的是,如果允许遍历时修改节点指针,只要保证遍历完之后能恢复原状,那么可以不用栈就完成遍历,即O(1)的空间,具体的方法就是Morris Traversal,有兴趣的同学自己去看看吧,这种方法能够在空间要求极为严格的情况下,完成二叉树的搜索。