题目链接:
二叉树的递归遍历
思路:
递归遍历二叉树的写法比较简单,都是用DFS(深度优先搜索)的方法遍历到树的子节点,然后再逐步返回到根节点。写递归首先要确认参数,在遍历二叉树中我们需要把当前子树的根节点放入DFS中,并且在每次递归中将当前节点放入一个数组中表示该节点已经被访问了,所以递归的参数为代表当前子树的根节点,以及一个数组。传引用的目的是为了更改数组。
void dfs(TreeNode *root, vector<int> &ans)
其次需要确认递归的终止条件,很显然在这题当中终止条件为root指向空节点,这说明我们已经走到了树的底层,也就是叶子节点了,叶子节点是没有子节点的,所以递归结束,直接返回。
最后需要处理单层递归的逻辑,对于不同的遍历方式,逻辑都是大致相同的,只需要稍微改一下代码即可,以下是完整代码。
中序遍历:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
dfs(root, ans);
return ans;
}
void dfs(TreeNode* root, vector<int> &ans)
{
if (root == nullptr)
return;
dfs(root->left, ans);
ans.push_back(root->val);
dfs(root->right, ans);
}
};
前序遍历:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
dfs(root, ans);
return ans;
}
void dfs(TreeNode* root, vector<int> &ans)
{
if (root == nullptr)
return;
ans.push_back(root->val);
dfs(root->left, ans);
dfs(root->right, ans);
}
};
后序遍历:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
dfs(root, ans);
return ans;
}
void dfs(TreeNode* root, vector<int> &ans)
{
if (root == nullptr)
return;
dfs(root->left, ans);
dfs(root->right, ans);
ans.push_back(root->val);
}
};
我们可以发现代码几乎都是相同的,唯一不同的是元素压入数组的顺序不一样,这都是由遍历方式的不同而导致的。
- 前序遍历:先访问中间节点,再访问左节点,最后访问右节点。
- 中序遍历:先访问左节点,再访问中间节点,最后访问右节点。
- 后序遍历:先访问左节点,再访问右节点,最后访问中间节点。
仔细观察代码后可以发现,元素压入数组的顺序和遍历的顺序是一致的。比如前序遍历中,先压入当前元素到数组,再以左边的节点为入参递归调用dfs,最后以右边的节点为入参递归调用dfs。总体来说这三种写法都十分简单,只要理清递归的思路和什么时候压入元素到数组里就可以了。
二叉树的迭代遍历
递归的本质实际上是利用了程序在调用函数和从函数中返回时的入栈和出栈操作,通过递归调用自己,参数也储存在了栈里面,但是函数调用除了需要把参数压入栈,还需要压入函数的返回地址等等,所以虽然递归遍历的逻辑简单一些,但是浪费了很多不必要的空间。所以我们可以不借助系统给程序提供的栈,而是在编程语言的层面上使用栈这一数据结构,就可以在不递归的情况下遍历整个二叉树,这种方法又叫迭代遍历。
迭代遍历的实现原理也是利用入栈和出栈的操作把访问过的元素压入数组,只不过这个栈需要自己维护,而不是依赖于程序,所以实现上会稍微复杂一些。首先看前序遍历:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
stack<TreeNode*> myStack;
myStack.push(root);
while (myStack.size() != 0)
{
TreeNode* node = myStack.top();
ans.push_back(node->val);
myStack.pop();
if (node->right)
myStack.push(node->right);
if (node->left)
myStack.push(node->left);
}
return ans;
}
};
前序遍历需要先把中间节点压入数组,再访问左节点和右节点。当栈不为空的时候,说明还有节点没有访问,所以循环终止条件为myStack.size() == 0,也可以是myStack.empty() == true。弹出中间节点后,按顺序先把右节点压入栈,再把左节点压入栈,为什么是先右再左呢?因为出栈的时候是相反顺序的,也就是先左再右,这一点要尤其注意。当栈为空的时候就表示所有的节点都访问过了。
再看后序遍历:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
stack<TreeNode*> myStack;
myStack.push(root);
while (!myStack.empty())
{
TreeNode* node = myStack.top();
ans.push_back(node->val);
myStack.pop();
if (node->left)
myStack.push(node->left);
if (node->right)
myStack.push(node->right);
}
reverse(ans.begin(), ans.end());
return ans;
}
};
后序遍历和前序遍历的代码还是十分相似的,因为这里用了一个小技巧:前序遍历的顺序是中->左->右,后序遍历的顺序是左->右->中,在前序遍历的基础之上,如果我们把左节点和右节点的入栈顺序调换一下,顺序变成了中->右->左,然后再把整个数组反转,正好变成了左->右->中,所以实际上是采用了逆向思维的办法。到这里位置处理逻辑还是很相似的,但是到了中序遍历,就没办法用相同的逻辑了。
原因就是我们访问节点的顺序和处理接节点的顺序不一致。在前序遍历里,我们是先访问中间节点,然后再分别把左节点和右节点压入栈的,在访问中间节点的同时,把元素压入到数组里,但是在中序遍历,我们还是先访问中间节点,但实际上我们首先要处理的节点并不是中间节点,而是左边的节点,这个时候就需要另一种处理方法了,直接上代码:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == nullptr)
return ans;
stack<TreeNode*> myStack;
TreeNode *cur = root;
while (cur != nullptr || !myStack.empty())
{
if (cur)
{
myStack.push(cur);
cur = cur->left;
}
else
{
cur = myStack.top();
myStack.pop();
ans.push_back(cur->val);
cur = cur->right;
}
}
return ans;
}
};
代码里用了一个指针cur,代表当前指向的节点,然后将cur初始化为了指向root节点,然后进入循环。循环的终止条件为cur指向空节点,并且栈为空。cur节点会不断往左走,并把节点压入栈,直到走到叶子节点为止。所以可以判断出,当cur指向的节点不为空或者栈不为空的时候一定还有节点没有被访问。当cur节点为空的时候,就表示了现在所在的节点为叶子节点,把当前节点压入数组后从栈中弹出,在把cur节点指向右边的节点。
为什么这种方式可以保证压入数组的顺序是中序遍历的呢?首先,在if(cur)逻辑里面可以保证,二叉树的所有左节点都可以压入栈内,然后再看else逻辑,它处理的是当cur为空的时候的情况,有两种情况会使cur为空
- cur = cur->left
- cur = cur->right
在第一种情况下,这说明cur是从if下面的逻辑过来的,这个时候栈内储存的元素由顶而下分别是左节点,中间节点,那么在else逻辑下,在弹出的时候,也是优先弹出左节点,这就保证了第一个顺序,即先访问左节点。
在第二种情况下,左节点弹出之后,cur指向了cur->right还是空节点,所以这个时候弹出的元素为中间节点,也和中序遍历的顺序是一致的。如果它需要弹出右节点,cur必须先是中间的节点cur->right才能指向右边的节点。