二叉树遍历是笔试面试里常考的问题了。这里做一下总结,以求加深印象。以 LeetCode 相关题目为例。
三序遍历
最常规的遍历是三序遍历:先、中、后。区别在于打印节点的顺序。
先序遍历是“根-左-右”,中序遍历是“左-根-右”,后序遍历是“左-右-根”。
遍历方法有两种:递归和迭代。绝大多数二叉树题目都会涉及递归,递归也是三序遍历最直接的实现,非常简单;迭代则是依靠栈去等效递归,并不很难但需要多理解或者干脆记住。
中序遍历(#94):
// 递归方法
vector<int> inorderTraversal(TreeNode* root){
vector<int> res;
recur(root, res);
return res;
}
void recur(TreeNode* root, vector<int>& res){
if (root){
if (root->left)
recur(root->left, res); // 先左
res.push_back(root->val); // 再根
if (root->right)
recur(root->right, re); // 后右
}
}
// 迭代方法
vector<int> inorderTraversal(TreeNode* root){
stack<TreeNode*> s;
vector<int> res;
TreeNode* cur = root;
while (cur || !s.empty()){
while (cur){
s.push(cur);
cur = cur->left;
}
cur = s.top(); s.pop();
res.push_back(cur->val);
cur = cur->right;
}
return res;
}
先序遍历(#144):
// 递归
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
preOrderRecur(root, res);
return res;
}
void preOrderRecur(TreeNode* root, vector<int>& res){
if (root){
res.push_back(root->val); // 根
if (root->left)
preOrderRecur(root->left, res); // 左
if (root->right)
preOrderRecur(root->right, res); // 右
}
}
// 迭代
vector<int> preorderTraversal(TreeNode* root) {
if (root == nullptr)
return {};
stack<TreeNode*> s;
TreeNode* cur = root;
vector<int> res;
s.push(cur);
while (!s.empty()){
cur = s.top(); s.pop();
res.push_back(cur->val);
if (cur->right) s.push(cur->right);
if (cur->left) s.push(cur->left);
}
return res;
}
后序遍历(#145):
后续遍历的迭代实现比较麻烦,要保证左右子树已经依次访问过才能访问根节点。
// 递归
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
recur(root, res);
return res;
}
void recur(TreeNode* root, vector<int>& res){
if (root){
if (root->left)
recur(root->left, res);
if (root->right)
recur(root->right, res);
res.push_back(root->val);
}
}
有一种取巧的办法,即改写先序遍历的迭代版本,存储一个“根-右-左”的答案,最后将其逆序。
vector<int> postorderTraversal(TreeNode* root) {
if (root == nullptr)
return {};
vector<int> res;
TreeNode* cur = root;
stack<TreeNode*> s;
s.push(cur);
while (!s.empty()){
cur = s.top(); s.pop();
res.push_back(cur->val);
if (cur->left) s.push(cur->left);
if (cur->right) s.push(cur->right);
}
reverse(res.begin(), res.end());
return res;
}
若不能使用逆序,就要设置一个记录节点,保存上一次访问过的元素。
vector<int> postorderTraversal(TreeNode* root){
vector<int> res;
stack<TreeNode*> s;
TreeNode* cur = root;
TreeNode* last = nullptr;
while (cur || !s.empty()){
if (cur){
s.push(cur); cur = cur->left;
}
else {
TreeNode* topNode = s.top(); //
if (topNode -> right && last != topNode -> right)
cur = topNode->right;
else{
res.push_back(topNode->val);
last = topNode; s.pop();
}
}
}
return res;
}
深度遍历(#102, #107)
两个题目要求相反。
102 要求按树的样子从上(根)到下(叶)逐层打印出来。可以借助队列 queue
完成。
vector<vector<int>> levelOrder(TreeNode* root) {
if (root == nullptr)
return {};
vector<int> row;
vector<vector<int>> res;
queue<TreeNode*> q;
q.push(root);
int count = q.size();
while (!q.empty()){
if (q.front()->left) q.push(q.front()->left);
if (q.front()->right) q.push(q.front()->right);
row.push_back(q.front()->val);
q.pop();
count--;
if (count == 0){
res.push_back(row);
row.clear();
count = q.size();
}
}
return res;
}
所以 107 也就很简单了。增加一行 reverse
将结果逆序即可。
也有递归方法:
vector<vector<int>> levelOrderBottom(TreeNode* root){
vector<vector<int>> res;
levelRecur(root, res, 0);
reverse(res.begin(), res.end());
return res;
}
void levelRecur(TreeNode* root, vector<vector<int>>& res, int level){
if (root == nullptr) return;
if (res.empty() || level > res.size() - 1)
res.push_back({});
res[level].push_back(root->val);
levelRecur(root->left, res, level+1);
levelRecur(root->right, res, level+1);
}
最小(大)深度(#104, #111)
先看最小深度。注意最小深度指根节点到叶子的最短距离,所以根节点有一个叶子节点的情况下不能算 path=1
,比如输入[1, 2]
输出应为2;输入为空的时候返回 0
;输入为一个根节点的时候返回1
。
递归做法 :
int minDepth(TreeNode* root) {
if (!root)
return 0;
if (!root->left) return 1+minDepth(root->right);
if (!root->right) return 1+minDepth(root->left);
return min(minDepth(root->left), minDepth(root->right))+1;
}
迭代做法1:
int minDepth(TreeNode* root) {
if (!root) return 0;
queue<TreeNode*> q;
q.push(root);
int depth = 0;
while (!q.empty()){
depth ++;
int k = q.size();
for (int j = 0; j < k; j++){
TreeNode* rt = q.front();
if (rt->left) q.push(rt->left);
if (rt->right) q.push(rt->right);
q.pop();
if (rt->left == nullptr && rt->right == nullptr) return depth ;
}
}
return -1;
注意到这个法子跟上面逐层遍历二叉树其实思路完全一样,都是 BFS。所以两份代码稍微改改就能互换了:
// 方法2
int minDepth(TreeNode* root) {
int depth = 1;
if (!root) return 0;
queue<TreeNode*> q;
q.push(root);
int k = q.size();
while (!q.empty()){
if (q.front()->left) q.push(q.front()->left);
if (q.front()->right) q.push(q.front()->right);
if (q.front()->left == nullptr && q.front()->right == nullptr) return depth;
q.pop();
k--;
if (k == 0){
k = q.size();
depth++;
}
}
return -1;
}
但有趣的是 OJ 跑过几次,深度遍历题用方法2速度快,最小深度用方法1速度快,有点微妙。
最大深度比较简单。递归轻松解:
int maxDepth(TreeNode* root) {
if (root==nullptr)
return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
迭代思路跟上文类似:
int maxDepth(TreeNode* root) {
if (root==nullptr)
return 0;
int depth = 0;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()){
depth++;
int k = q.size();
for (int i = 0; i < k; i++){
TreeNode* tp = q.front();
q.pop();
if (tp->left) q.push(tp->left);
if (tp->right) q.push(tp->right);
}
}
return depth;
}
当然另一种写法也是完全没问题的。
两种写法有很细微的差异,注意理清楚,不要搞混。