二叉树遍历是二叉树的最基本的操作,其实现方式主要有三种:
递归遍历非递归遍历Morris遍历递归遍历的实现非常容易,非递归实现需要用到栈。而Morris算法可能很多人都不太熟悉,其强大之处在于只需要使用O(1)的空间就能实现对二叉树O(n)时间的遍历。
每个二叉树结点包括一个值以及左孩子和右孩子结点,其定义如下:
class TreeNode { public: int val; TreeNode *left, *right; TreeNode(int val) { this->val = val; this->left = this->right = NULL; } };二叉树的遍历,就是按照某条搜索路径访问树中的每一个结点,使得每个结点均被访问一次,而且仅被访问一次。常见的遍历次序有:
先序遍历:先访问根结点,再访问左子树,最后访问右子树中序遍历:先访问左子树,再访问根结点,最后访问右子树后序遍历:先访问左子树,再访问右子树,最后访问根结点下面介绍,二叉树3种遍历方式的实现。
递归实现非常简单,按照遍历的次序,对当前结点分别调用左子树和右子树即可。
二叉树遍历的递归实现,每个结点只需遍历一次,故时间复杂度为O(n)。而使用了递归,最差情况下递归调用的深度为O(n),所以空间复杂度为O(n)。
二叉树遍历的非递归实现,可以借助栈。
按照以上顺序入栈,这样出栈顺序就与先序遍历一样:先根结点,再左子树,最后右子树。
void preOrder2(TreeNode *root) { if(root == NULL) return; stack<TreeNode *> stk; stk.push(root); while(!stk.empty()) { TreeNode *pNode = stk.top(); stk.pop(); cout << pNode->val << endl; if(pNode->right != NULL) stk.push(pNode->right); if(pNode->left != NULL) stk.push(pNode->left); } }第一个栈的入栈顺序是:根结点,左孩子和右孩子;于是,压入第二个栈的顺序是:根结点,右孩子和左孩子。因此,弹出的顺序就是:左孩子,右孩子和根结点。
void postOrder2(TreeNode *root) { if(root == NULL) return; stack<TreeNode *> stk, stk2; stk.push(root); while(!stk.empty()) { TreeNode *pNode = stk.top(); stk.pop(); stk2.push(pNode); if(pNode->left != NULL) stk.push(pNode->left); if(pNode->right != NULL) stk.push(pNode->right); } while(!stk2.empty()) { cout << stk2.top()->val << endl; stk2.pop(); } }另外,二叉树的后序遍历的非递归实现,也可以只使用一个栈来实现。
void postOrder2(TreeNode *root) { if(root == NULL) return; stack<TreeNode *> stk; stk.push(root); TreeNode *prev = NULL; while(!stk.empty()) { TreeNode *pNode = stk.top(); if(!prev || prev->left == pNode || prev->right == pNode) { // traverse down if(pNode->left) stk.push(pNode->left); else if(pNode->right) stk.push(pNode->right); /* else { cout << pNode->val << endl; stk.pop(); } */ } else if(pNode->left == prev) { // traverse up from left if(pNode->right) stk.push(pNode->right); } /* else if(pNode->right == prev) { // traverse up from right cout << pNode->val << endl; stk.pop(); } */ else { cout << pNode->val << endl; stk.pop(); } prev = pNode; } }二叉树遍历的非递归实现,每个结点只需遍历一次,故时间复杂度为O(n)。而使用了栈,空间复杂度为二叉树的高度,故空间复杂度为O(n)。
Morris遍历算法最神奇的地方就是,只需要常数的空间即可在O(n)时间内完成二叉树的遍历。O(1)空间进行遍历困难之处在于在遍历的子结点的时候如何重新返回其父节点?在Morris遍历算法中,通过修改叶子结点的左右空指针来指向其前驱或者后继结点来实现的。
因为只使用了两个辅助指针,所以空间复杂度为O(1)。对于时间复杂度,每次遍历都需要找到其前驱的结点,而寻找前驱结点与树的高度相关,那么直觉上总的时间复杂度为O(nlogn)。其实,并不是每个结点都需要寻找其前驱结点,只有左子树非空的结点才需要寻找其前驱,所有结点寻找前驱走过的路的总和至多为一棵树的结点个数。因此,整个过程每条边最多走两次,一次使定位到该结点,另一次是寻找某个结点的前驱,所以时间复杂度为O(n)。
如以下一棵二叉树。首先,访问的是根结点F,其左孩子非空,所以需要先找到它的前驱结点(寻找路径为B->D->E),将E的右指针指向F,然后当前结点为B。依然需要找到B的前驱结点A,将A的右指针指向B,并将当前结点设置为A。下一步,输出A,并把当前结点设置为A的右孩子B。之后,会访问到B的前驱结点A指向B,那么令A的右指针为空,继续遍历B的右孩子。依次类推。
与中序遍历类似,区别仅仅是输出的顺序不同。
void preOrder3(TreeNode *root) { if(root == NULL) return; TreeNode *pNode = root; while(pNode) { if(pNode->left == NULL) { cout << pNode->val << endl; pNode = pNode->right; } else { TreeNode *pPre = pNode->left; while(pPre->right != NULL && pPre->right != pNode) pPre = pPre->right; if(pPre->right == NULL) { pPre->right = pNode; cout << pNode->val << endl; pNode = pNode->left; } else { pPre->right = NULL; pNode = pNode->right; } } } }