二叉树的遍历,是程序员面试的最基本问题,对于基础分为三种遍历顺序:前序、中序、后序,这个 “前、中、后” 都是指根,也就是对应先根序、中根序、后根序,左右子节点的顺序默认都是先左后右。
LeetCode 中有详细的解释: https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/992/
一、递归形式
二叉树的遍历,用递归的形式,也就是递归的DFS很好写,而且三种遍历方式几乎没有区别,只是遍历的位置与DFS左节点和DFS右节点的位置关系不一样,因为默认都是先左后友,所以默认的DFS中,都是先递归左节点,再递归右节点。三种遍历方式如下:
vector<int> threeOrdersTraversal(TreeNode* root) {
vector<int> v;
traversal(root, v);
return v;
}
void traversal(TreeNode* root, vector<int>& v){
if(!root) return;
// pre-order 先根序 // in-order 中根序 // post-order 后根序
v.push_back(root->val); traversal(root->left, v); traversal(root->left, v);
traversal(root->left, v); v.push_back(root->val); traversal(root->right, v);
traversal(root->right, v); traversal(root->right, v); v.push_back(root->val);
}
为了看的清楚就写在一起了,对于递归形式来说,前中后的区别就是先递归还是先对根节点进行操作(如上的 v.push_back(root->val);)。递归形式简单、好写、好理解,所以一般不会有让写递归形式的,都是在树的题目中作为基础的遍历方式进行考察。
二、非递归形式(用 stack 模拟递归)
前序遍历比较简单,直接每次 操作节点(比如push_back)、右节点入栈、左节点入栈(先右后左),就可以了。
vector<int> preorderTraversal(TreeNode* root) {
vector<int> v;
if(!root) return v;
TreeNode* temp = root;
stack<TreeNode*> s;
s.push(root);
while(!s.empty()){
tmp = s.top();
s.pop();
v.push_back(tmp->val); // 对节点进行操作
if(tmp->right) s.push(tmp->right);
if(tmp->left) s.push(tmp->left);
}
return v;
}
中序遍历会复杂一点,每个遍历到一个,都先遍历到最左边的,然后再遍历节点自己,再是右节点。
vector<int> inorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> s;
while (root || !s.empty()) {
while (root) {
s.push(root);
root = root->left;
}
root = s.top();
s.pop();
v.push_back(root->val); // 对节点进行操作
root = root->right;
}
return v;
}
后序遍历会更复杂一点,每个节点都先遍历到最左边节点后,还要遍历最右边的,需要用一个 last 来记录遍历过哪个了,否则一个节点有右节点就push,那遍历过的还会再被 push 进stack。
vector<int> postorderTraversal(TreeNode* root) {
vector<int> nodes;
stack<TreeNode*> s;
TreeNode* last = NULL;
while (root || !s.empty()) {
while (root) {
s.push(root);
root = root->left;
}
TreeNode* node = s.top();
if (node->right && last != node->right)
root = node->right;
else {
nodes.push_back(node->val);
last = node;
s.pop();
}
}
return nodes;
}
三、Morris Traversal(非递归,不用栈,O(1)空间)
Morris Traversal 的目的是,不递归,不是用辅助空间进行二叉树的遍历,这个思想来自于线索二叉树(Threaded BinaryTree)。在二叉树的结点上加上线索的二叉树称为线索二叉树,
对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化,线索化的结果如上图所示。简单理解,就是二叉树遍历顺序变成一个链表,就是二叉树的线索化。
要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针)。由于不能用栈作为辅助空间,Morris 方法使用 cur 和 prev 两个指针来实现。步骤如下(以中序遍历为例):
Initialize current as root
While current is not NULL
1. If the current does not have left child
a) Print cur’s data
b) Go to the right, i.e., cur = cur->right
2. Else
a) Make current as the right child of the rightmost node in current’s left subtree
b) Go to this left child, i.e., cur = cur->left
用图解的形式看会更容易理解,对于一个有 left 的节点 cur,把 cur 暂时复制到 cur 中序遍历上一个位置的 right 上,也就是 cur 的 left 中最右侧节点。同理于 “寻找二叉树中序遍历的下一个节点” 的思路,https://blog.csdn.net/lx8211667846947/article/details/81639281,不过这里找的是上一个节点。
中序遍历代码如下:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> v;
TreeNode *cur = root, *prev = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) { // 1.
v.push_back(cur->val); // 对节点进行操作
cur = cur->right;
}
else {
/* Find the inorder predecessor of cur */
prev = cur->left;
while (prev->right != nullptr && prev->right != cur)
prev = prev->right;
if (prev->right == nullptr) { // 2.a)
prev->right = cur;
cur = cur->left;
}
/* Revert the changes made in the 'if' part to restore
the original tree i.e., fix the right child of predecessor */
else { // 2.b)
prev->right = nullptr;
v.push_back(cur->val); // 对节点进行操作
cur = cur->right;
}
}
}
return v;
}
前序遍历个中序遍历的唯一区别就是,当 cur->left != nullptr 时,找到 prev 后,如果 prev->right == nullptr 那就 v.push_back(cur->val);,也就是这个对节点操作从中序的 else 中变到了 if 中:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> v;
TreeNode *cur = root, *prev = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) {
v.push_back(cur->val);
cur = cur->right;
}
else {
prev = cur->left;
while (prev->right != nullptr && prev->right != cur)
prev = prev->right;
if (prev->right == nullptr) {
v.push_back(cur->val);
prev->right = cur; // difference!!
cur = cur->left;
}
else {
prev->right = nullptr;
cur = cur->right;
}
}
}
return v;
}
后序遍历有点复杂,参考 https://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html。