144. 二叉树的前序遍历
145. 二叉树的后序遍历
94. 二叉树的中序遍历
102. 二叉树的层序遍历
103. 二叉树的锯齿形层序遍历
589. N 叉树的前序遍历
590. N 叉树的后序遍历
429. N 叉树的层序遍历

这两天做了好多二叉树N叉树的各种遍历题,稍微总结一下。

概念

对于二叉树来说,前中后序遍历(pre, in, postorder, 记一下英文)分别代表根节点在遍历时的位置。前序就是根-左-右,中序左-右-根,后序左-根-右。

对于实际问题来说,需要记住考虑根节点为空的情况,可以先写一个判断,后面根据需要删除或者留着。

递归

这三种遍历方式都可以很快用递归来实现。直接写一个dfs,其中在结果中添加的顺序:前序最先添加,然后遍历左右节点。中序先遍历左节点,然后添加,然后遍历右节点。后序先遍历左右节点最后添加。实现都比较简单。代码逻辑同样可以推广到N叉树,具体实现除了左右节点变成N个子节点其他都一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
void preorder(TreeNode *root, vector<int> &res) {
if (root == nullptr) {
return;
}
res.push_back(root->val);
preorder(root->left, res);
preorder(root->right, res);
}

vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
preorder(root, res);
return res;
}
};

// 作者:LeetCode-Solution
// 链接:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/er-cha-shu-de-qian-xu-bian-li-by-leetcode-solution/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

迭代

如果使用迭代的话,需要使用栈模拟一个DFS的过程,其中前序最简单,首先在栈中压入根节点,然后弹出根节点并按顺序(因为弹出的顺序与加入的顺序相反,需要先加入右节点再加入左节点)加入根节点。后序的话可以使用前序相反的顺序,其中仅需要将加入节点的顺序反过来,即先加入左节点然后右节点,这样从栈中弹出节点的顺序就是中-右-左,然后将输出的序列整个反向,就变成了左-右-中。

使用visited set记录访问状态:

这种方法是在做N叉树的后序遍历时候看到的,对于二叉树来说同样比较适用,同时也可以应用到二叉树的中序遍历中。

对于后序遍历和中序遍历来说,重要的是在遍历时何时弹出一个节点,如果一个节点是叶子结点,可以直接弹出。不是的话,如果是后序遍历,需要在弹出左右子节点后再弹出根节点,如果是中序遍历,需要在弹出左子节点后弹出根节点。于是可以使用一个额外的visited set记录节点访问的状态。

后序遍历

对于后序遍历,在每次访问栈顶的结点时,首先判断是否是叶子结点/是否在visited set中出现过,然后将其左右子节点加入并将根节点加入visited set。这样在访问完左右子节点后,下一个访问的根节点就一定已经被标记visited了,所以可以直接弹出并记录结果。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vector<int> postorderTraversal(TreeNode* root) {
vector<int>result;
if (!root) return result;
stack<TreeNode*>s;
set<TreeNode*>visited;
s.push(root);
while (!s.empty()) {
TreeNode* temp = s.top();
if (!temp -> left && !temp -> right || visited.count(temp)) {
result.push_back(temp -> val);
s.pop();
continue;
}
if (temp -> right) s.push(temp -> right);
if (temp -> left) s.push(temp -> left);
visited.insert(temp);
}
return result;
}

这种思路同样运用于N叉树。

中序遍历

对于中序遍历来说和后序遍历不一样,仅在根节点的左子节点被访问过后就可以弹出根节点了,所以首先将根节点从栈里取出来,把右子节点压入栈底,然后再把根节点压入,最后将子节点压入并将根节点标记为visited。左子节点被弹出后,下一个访问的是根节点(已经被标记为visited),而最后才是右子节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vector<int> inorderTraversal(TreeNode* root) {
vector<int>result;
if (!root) return result;
stack<TreeNode*>s;
set<TreeNode*>visited;
s.push(root);
while (!s.empty()) {
TreeNode* temp = s.top();
if (!temp -> left && !temp -> right || visited.count(temp)) {
result.push_back(temp -> val);
s.pop();
continue;
}
s.pop();
if (temp -> right) s.push(temp -> right);
s.push(temp);
if (temp -> left) s.push(temp -> left);
visited.insert(temp);
}
return result;
}

层序遍历

层序遍历的话类似于一个BFS的过程,在代码中使用队列而不是栈。因为在每次遍历时,都会将本层的所有节点遍历一遍,然后弹出,压入下一层的所有节点,因此每次只需要访问队列中的所有结点就可以完成一层的遍历了。同样的思路可以运用于N叉树。

对于锯齿形遍历来说,只需要记录该层是奇数层还是偶数层,然后在对应的层上将结果反向就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<vector<int>> levelOrder(Node* root) {
vector<vector<int>>result;
if (!root) return result;
queue<Node*>q;
q.push(root);
while (!q.empty()) {
vector<int>level;
int size = q.size();
for (int i = 0; i < size; i++) {
Node* temp = q.front();
if (temp -> children.size() > 0) {
for (auto child: temp -> children) {
q.push(child);
}
}
level.push_back(temp -> val);
q.pop();
}
result.push_back(level);
}
return result;
}

评论