文中的二叉树结构如下:
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};
二叉树的前序遍历顺序为先访问根节点,然后对左子树进行前序遍历,再对右子树前序遍历,这是一个很明显的递归思想。访问当前节点的值,函数调用自身去访问左节点,然后访问右节点,那么递归函数的主体内容已经确定了
void helper(TreeNode* root)
{
//访问当前节点的值
int res = root->val;
//前序遍历左子树
helper(root->left);
//前序遍历右子树
helper(root->right);
}
函数的主体已经确定,后面就是对细节问题的一些优化了,可以看到我们并没有考虑节点为空的情况,这一点也很简单,节点为空的时候直接返回就行,然后我们要把读取到的节点值存储起来,需要一个数组来存放数值,这样一来前序遍历的递归函数就完成了,下面是完整代码
vector<int> preorderTraversal(TreeNode* root)
{
//二叉树前序遍历
vector<int> res; //定义数组
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int>& res)
{
//如果节点为空,则直接返回
if (root == nullptr)
return;
//将节点的值存到数组中
res.push_back(root->val);
//前序遍历左子树
helper(root->left, res);
//前序遍历右子树
helper(root->right, res);
}
中序遍历的顺序是先访问中序遍历左子树,然后访问根节点,最后则是中序遍历右子树,可以看到和前序遍历相比,只是左节点和根节点互换了顺序,仍然是递归的思想,前序遍历的代码中是把访问根节点的值放在了第一条语句,那么在中序遍历中只需要把访问根节点的值这一语句和遍历左子树这一语句互换即可,完整代码如下:
vector<int> inorderTraversal(TreeNode* root)
{
//二叉树中序遍历
vector<int> res; //定义数组
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int>& res)
{
//如果节点为空,则直接返回
if (root == nullptr)
return;
//中序遍历左子树
helper(root->left, res);
//将节点的值存到数组中
res.push_back(root->val);
//中序遍历右子树
helper(root->right, res);
}
后序遍历的顺序是先后序遍历左子树,然后后序遍历右子树,最后访问根节点,根据中序遍历对前序遍历的改动的经验,后序遍历算法应该也很容易想到,就是把访问根节点的值这一语句放到遍历左子树和遍历右子树后面即可
vector<int> postorderTraversal(TreeNode* root)
{
//二叉树后序遍历
vector<int> res; //定义数组
if (root == nullptr)
return res;
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int>& res)
{
//如果节点为空,则直接返回
if (root == nullptr)
return;
//后序遍历左子树
helper(root->left, res);
//后序遍历右子树
helper(root->right, res);
//将节点的值存到数组中
res.push_back(root->val);
}
介绍完了递归后,下面是非递归的版本
在递归的版本中,我们的思路是,访问根节点的值,然后再去遍历左右子树,来循环调用自身,这本身是维护了一个隐形的栈,现在我们自己维护一个栈,这样就是一个迭代的版本了。前序遍历的节点顺序为根左右,从树的根节点开始,我们一直向左前进,达到最左侧,遇到的节点统一放入栈中,同时也可以把该节点的值进行存储,因为在这一过程中我们没有考虑右孩子,如果一个节点有孩子,那么他作为父节点必然是在孩子访问之前被访问,如果该节点没有孩子,那么他是他的父节点的左孩子,此时父节点已经访问过,这时应该访问左孩子的值。
while(root!= nullptr)
{
//访问当前节点的值
res.push_back(root->val);
//节点入栈
stk.push(root);
//向左走
root = root->left;
}
当跳出上述循环时,说明此时我们已经走到了最左侧,以下图为例,走到最左侧D节点时,此时栈的结构如下:
下一个要访问的节点是E节点,上述已经处理过左孩子的问题,接下来就是访问右孩子,我们只需要把栈顶元素出栈,B则成为新的栈顶元素,取得E节点即可
vector<int> preorderTraversal(TreeNode* root)
{
//二叉树非递归前序遍历
vector<int> res; //定义数组
if(root == nullptr)
return res;
stack<TreeNode*> stk;//栈
while(!stk.empty() || root != nullptr)
{
while(root != nullptr)
{
res.push_back(root->val);//存储节点的值
stk.push(root);//将当前节点入栈
root = root->left;//寻找左节点
}
//上述while循环结束,表示已经走到二叉树的最左侧
root = stk.top();//取栈顶元素,便于后续取该节点的右孩子
stk.pop();//出栈
root = root->right;//遍历右节点
}
return res;
}
在递归版本中,中序遍历相对于前序遍历仅仅调换了一句语句的位置,那么在非递归中序遍历中,依然可以按照前序遍历的思想,但是有一点细节要调整一下,前序遍历的节点顺序为根左右,中序遍历的节点顺序为左根右,在前序遍历向左走的过程中,遇到 一个节点都可以直接访问存储,但是在这里就不能了,因为你无法确定当前节点是否还有左孩子,如果没有左孩子,该节点可以直接访问,但是若有左孩子,则左孩子应该先于该节点被访问,所以我们的任务是首先找到树的最左侧,将中途遇到的节点全部入栈,栈顶元素必然是最左侧的节点,此时可以正常访问。所以中序遍历的非递归版本中,相比于前序遍历,也仅仅是改动语句的位置即可。
vector<int> preorderTraversal(TreeNode* root)
{
//二叉树非递归中序遍历
vector<int> res; //定义数组
if(root == nullptr)
return res;
stack<TreeNode*> stk;//栈
while(!stk.empty() || root != nullptr)
{
while(root != nullptr)
{
stk.push(root);//将当前节点入栈
root = root->left;//寻找左节点
}
//上述while循环结束,表示已经走到二叉树的最左侧
//中序遍历,先访问左节点,此时就可以取出栈顶元素进行访问了
root = stk.top();//取栈顶元素,该元素必然是最左侧的节点
stk.pop();//出栈
res.push_back(root->val);
root = root->right;//转向访问右节点
}
return res;
}
后序遍历的节点顺序为左右根,和前面两种遍历方式相比,这三种方式都有一个共同点,那就是左节点都是在右节点之前被访问,而且左节点在根节点之前,这和中序遍历很相似,只是根节点和右节点的顺序不同,那么按照前两种方式,所以后序遍历的非递归算法的大致轮廓相信你已经知道怎么写了,仍然是向左走到尽头,然后访问节点的值
while(!stk.empty() || root != nullptr)
{
while(root != nullptr)
{
stk.push(root);//将当前节点入栈
root = root->left;//寻找左节点
}
root = stk.top();
stk.pop();
res.push_back(root->val);
}
想要实现后序遍历还需要对上述代码进行改动,因为后序遍历要求右节点先于根节点被访问,所以我们从栈中取出元素后还需要判断该节点是否拥有右节点,若没有则直接访问,如果拥有则遍历右节点,同时还要检查他的右节点是否被访问过,被访问过的话也可以直接访问,我们就需要一个额外的变量来记录右节点的访问情况。
vector<int> preorderTraversal(TreeNode* root)
{
//二叉树非递归中序遍历
vector<int> res; //定义数组
if(root == nullptr)
return res;
stack<TreeNode*> stk;//栈
TreeNode* flag = nullptr;//访问标识
while(!stk.empty() || root != nullptr)
{
while(root != nullptr)
{
stk.push(root);//将当前节点入栈
root = root->left;//寻找左节点
}
root = stk.top();//取栈顶元素,该元素必然是最左侧的节点
stk.pop();//出栈
//如果右孩子为空,或者右孩子已被访问过,则访问当前节点
if(root->right == nullptr || root->right == flag)
{
res.push_back(root->val);
flag = root;//当前节点被访问后,将flag指向该节点,
root = nullptr;
}
else
{
//右孩子不为空,或者右孩子未被访问过,则该节点不能访问
//需要将其入栈,待右孩子访问过后才能重新出栈访问
stk.push(root);
root = root->right;//访问右节点
}
}
return res;
}
以上就是二叉树的三种遍历方式的实现,写这篇文章的目的主要是在力扣做题后,跟随别人的思想,自己写一下,捋顺思路,加深自己的理解,写的不好请各位见谅。