首先说一下二叉树这样的数据结构在很多场景下都应用广泛,因为它的结构特点,注定了它与递归有着不解之缘。所以通常我们写和二叉树相关的递归代码都很舒服。但是面试官可不会让你舒服,递归的程序都被写烂了,当然要写写非递归的才显得你对二叉树这样的数据结构的理解。所以下面进入正题。
二叉树的先序遍历递归+非递归 的 理论讲解+实现
先序遍历的思想就是对每一颗子树都是这样的遍历顺序:
根节点-左孩子-右孩子
如下图
你按先序遍历的方式去访问就有:ABCDEFGH
定义节点如下:
typedef struct Node
{
int val;
Node* left;
Node* right;
}BNode;
递归版本的前序遍历**(VLR)**
void PreOrder(BNode *root)
{
if(root == nullptr) return ;
cout<<root->val<<" ";
PreOrder(root->left);
PreOrder(root->right);
}
代码简洁,清晰。每次访问一个节点都以先序遍历的方式去访问。先访问当前节点,在代码上的体现就是输出到ostream流对象中。
再访问当前节点的左孩子,最后访问右孩子。
非递归版本的前序遍历
void NoRPreOrder(BNode *root)
{
if(root == nullptr) return ;
stack<BNode*> sta;
sta.push(root);
while(!sta.empty())
{
BNode *ptr = sta.top();
sta.pop();
cout<<ptr->val<<" ";
if(ptr->right != nullptr) sta.push(ptr->right);
if(ptr->left != nullptr) sta.push(ptr->left );
}
cout<<endl;
}
二叉树的中序遍历递归+非递归 的 理论讲解+实现
(LVR)
void InOrder(BNode *node)//以node节点为根节点的子树
{
if (node != nullptr)
{
InOrder(node->left);
cout << node->val<<" ";
InOrder(node->right);
}
}
//整体思路就是,从当前节点出发,一直往左孩子走,直到遇到nullptr,然后打印该节点,打印完之后,你并不知道人家有没有右孩子呀,就走到它的右孩子。然后它的右孩子就是下一轮循环的根,继续进行。以此利用栈的特性,来保证最先访问的一定是左孩子其次是根,再右孩子
void NoRInOrder(BNode *root)
{
if(root == nullptr) return ;
stack<BNode*> sta;
sta.push(root);
while(!sta.empty() || root != nullptr)//这个循环条件很关键
{
//一般我们按思路写的时候,一下子就能把循环体写出来,但是循环体只够执行一次,,不能把所有的节点跑完。所以一定有一个外层循环来进行遍历
while(root)
{
root = root ->left;
sta.push(root);
}
BNode *ptr = sta.top();
sta.pop();
cout<<ptr->val<<" ";
root =root->right;
}
cout<<endl;
}
二叉树的后序遍历递归+非递归 的 理论讲解+实现
(LRV)
void PostOrder(BNode *node)//以node节点为根节点的子树
{
if (node != nullptr)
{
PostOrder(node->left);
PostOrder(node->right);
cout << node->val<<" ";
}
}
非递归版本后序遍历的实现有点意思。
因为对于前中后序遍历,归根结底都是DFS也就是深度优先的访问方式。这种访问方式就注定了要和栈进行配合才能模拟来。
上图后序遍历的结果:CEFDBHGA
根据前面的前序遍历我们知道,访问这棵树的方式是VLR,那么对于后序遍历来说就是LRV,把前序遍历倒个个儿,就是VRL这很容易做到无非就是先访问右子树,而之前是先访问左子树。如下图
代码实现如下:
void NoRPostOrder(BNode *root)
{
if(root == nullptr) return ;
stack<BNode*> sta;
stack<BNode*> sta2;
sta.push(root);
while(!sta.empty())
{
BNode *ptr = sta.top();
sta.pop();
sta2.push(ptr);
if(ptr->left != nullptr) sta.push(ptr->left );
if(ptr->right != nullptr) sta.push(ptr->right);
}
while (!sta2.empty())
{
BNode* ptr2 = sta2.top();
sta2.pop();
cout << ptr2->val << " ";
}
}
还有不依赖与前序遍历的另一种非递归后序遍历的方式。
核心思想是这样的:和中序遍历思想一样,首先一直往左走,直到为空,然后这时候就不能直接打印当前节点的val值了,因为是后序遍历,所以要先看该节点是否还有右孩子,如果有,要往右走。如果没有,就可以打印了。
重点关注这个入栈的顺序:A入栈,B入栈,C入栈,C没有右孩子,打印C那么当前栈顶是B,B有右孩子,右孩子D入栈,D的左孩子E入栈,E左右孩子都为空,E出栈打印E,然后当前栈顶为D,D的右孩子不为空且并未被访问过,F入栈,F左右孩子都为空,F出栈打印,现在栈顶为D,D的右孩子已经被访问过了,所以此时D出栈。接下来的遍历过程类似,就不赘述了。
所以最最核心的一点就是,你得判断某一节点的右孩子是否被访问过。这点很关键,这关系到该节点位于当前栈顶时,是打印它本身还是将它的右孩子入栈
后序遍历非递归实现的另一种方式:
void NoRInOrder(BNode *root)
{
if(root == nullptr) return ;
stack<BNode*> sta;
sta.push(root);
BNode* tag =nullptr;
while(!sta.empty() || root != nullptr)//这个循环条件很关键
{
//一般我们按思路写的时候,一下子就能把循环体写出来,但是循环体只够执行一次,,不能把所有的节点跑完。所以一定有一个外层循环来进行遍历
while(root)
{
root = root ->left;
sta.push(root);
}
BNode *ptr = sta.top();
sta.pop();
if(ptr->right == nullptr || ptr->right == tag)//只有当当前栈顶元素没有右孩子或者右孩子已经被访问过了才能打印当前元素
{
cout<<ptr->val<<" ";
tag = ptr;
ptr = nullptr;
}
//如果说当前节点的右子树不为空并且右子树没有被访问过那么把当前pop出来的节点再入到栈中,然后去访问它的右子树。
else
{
sta.push(ptr);
root =ptr->right;
}
}
cout<<endl;
}