一般二叉树有3中遍历方式
struct BinaryTreeNode{
int value;
BinaryTreeNode *pLeft;
BinaryTreeNode *pRight;
};
namespace SON_NODE{
typedef int dir_type;
constexpr dir_type LEFT = 1;
constexpr dir_type RIGHT = 2;
}
BinaryTreeNode* AddNode(BinaryTreeNode *rootNode, int value, SON_NODE::dir_type dir){
if (rootNode == nullptr){
return nullptr;
}
BinaryTreeNode *pNew = new BinaryTreeNode();
pNew->value = value;
pNew->pLeft = nullptr;
pNew->pRight = nullptr;
if (dir == SON_NODE::LEFT){
rootNode->pLeft = pNew;
}
else{
rootNode->pRight = pNew;
}
return pNew;
}
// 生成上图中的树结构
BinaryTreeNode *GetBinaryTree(){
BinaryTreeNode *rootNode = new BinaryTreeNode();
rootNode->value = 10;
rootNode->pLeft = nullptr;
rootNode->pRight = nullptr;
BinaryTreeNode *pNew = AddNode(rootNode, 6, SON_NODE::LEFT);
AddNode(pNew, 4, SON_NODE::LEFT);
AddNode(pNew, 8, SON_NODE::RIGHT);
pNew = AddNode(rootNode, 14, SON_NODE::RIGHT);
AddNode(pNew, 12, SON_NODE::LEFT);
AddNode(pNew, 16, SON_NODE::RIGHT);
return rootNode;
}
BinaryTreeNode *GetBinaryTree1(){
return nullptr;
}
BinaryTreeNode *GetBinaryTree2(){
BinaryTreeNode *rootNode = new BinaryTreeNode();
rootNode->value = 10;
rootNode->pLeft = nullptr;
rootNode->pRight = nullptr;
BinaryTreeNode *pNew = AddNode(rootNode, 6, SON_NODE::LEFT);
AddNode(pNew, 8, SON_NODE::RIGHT);
return rootNode;
}
BinaryTreeNode *GetBinaryTree3(){
BinaryTreeNode *rootNode = new BinaryTreeNode();
rootNode->value = 10;
rootNode->pLeft = nullptr;
rootNode->pRight = nullptr;
BinaryTreeNode *pNew = AddNode(rootNode, 14, SON_NODE::RIGHT);
AddNode(pNew, 12, SON_NODE::RIGHT);
return rootNode;
}
// 前序遍历-递归
void PreOrderByRecursive(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
cout << rootNode->value << " ";
PreOrderByRecursive(rootNode->pLeft);
PreOrderByRecursive(rootNode->pRight);
}
前序遍历的递归代码非常简洁,不必多说
但我们可以用递归的实现来推导非递归的实现
以上图为例,递归的思想既是先访问根节点10,然后将10的左子节点当作根节点递归调用
那么编译器具体是如何实现这种功能的呢?答案是函数栈,我们可以模拟一下大概的过程
当第一次调用函数时,便会开辟一个栈内存,这个栈中只有3个步骤
(方括号表示一个栈内存,删除线表示已经执行)
(栈顶)
A [访问node、左子节点递归调用、右子节点递归调用]
当函数执行到左子节点递归调用时,会有一个问题,递归调用的话肯定要执行一个新的函数,但是我(A)执行我自己(A’),我(A)却还没有执行完,如何才能保证执行完我自己(A’)之后,我(A)再继续执行呢?答案是利用函数栈
在递归调用时,函数栈会开辟一个新的栈内存
(栈顶)
A’ [访问node、左子节点递归调用、右子节点递归调用]
A [访问node、左子节点递归调用、右子节点递归调用]
假如这时候(A’)又要递归,那么
(栈顶)
A’’ [访问node、左子节点递归调用、右子节点递归调用]
A’ [访问node、左子节点递归调用、右子节点递归调用]
A [访问node、左子节点递归调用、右子节点递归调用]
而当(A’’)执行完后,又到了(A’)的右子节点递归调用,那么
(栈顶)
A’’ [访问node、左子节点递归调用、右子节点递归调用]
A’ [访问node、左子节点递归调用、右子节点递归调用]
A [访问node、左子节点递归调用、右子节点递归调用]
之后,直到(A’’)执行完,才发现(A’)也执行完,之后才会轮到(A)的右子节点递归调用
假如我们模拟所有的操作都在一个栈中执行过程,那么最开始的时候是
(栈顶)
A [访问node]
A [左子节点递归调用]
A [右子节点递归调用]
当执行完访问node之后,该node就会出栈
(栈顶)
A [左子节点递归调用]
A [右子节点递归调用]
接下来执行左子节点递归调用
(栈顶)
A’ [访问node]
A’ [左子节点递归调用]
A’ [右子节点递归调用]
A [右子节点递归调用]
从上面的过程中,我们可以发现一个事情,对于每一个节点,都可以用3个栈内存来进行前序遍历,那就是右子节点在栈底,左子节点在上一层栈,本节点在栈顶,接着取出栈顶元素(访问本节点),然后继续取出栈顶(左子节点)构建3个栈…如此循环直到栈为空表示所有节点都访问过
// 前序遍历-循环
/* 思想:
先将根节点放进栈
1. 取出栈顶节点node
2. 若node有右子节点,放进栈
3. 若node有左子节点,放进栈
重复1~3直到栈为空
*/
void PreOrderByLoop(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector treeNodeVec{rootNode};
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back();
treeNodeVec.pop_back();
cout << pNode->value << " ";
// 必须右子节点先进栈
if (pNode->pRight != nullptr){
treeNodeVec.push_back(pNode->pRight);
}
if (pNode->pLeft != nullptr){
treeNodeVec.push_back(pNode->pLeft);
}
}
cout << endl;
}
void TestPreOrder(){
PreOrderByLoop(GetBinaryTree());
PreOrderByLoop(GetBinaryTree1());
PreOrderByLoop(GetBinaryTree2());
PreOrderByLoop(GetBinaryTree3());
}
// 中序遍历-递归
void InOrderByRecursive(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
InOrderByRecursive(rootNode->pLeft);
cout << rootNode->value << " ";
InOrderByRecursive(rootNode->pRight);
}
根据递归代码推非递归实现,递归过程可以发现,对于每一个节点,右子节点会在栈底,该节点在中间,左子节点在栈顶构建3个栈空间,难点在于,对于栈顶节点,如何确保其左子节点已经被访问了,可以使用可访问标记来解决
/* 中序遍历-循环1
思想:
使用pair让每个节点都自带标记
1. 将根节点进栈
2. 取出栈顶元素
a. 若左子节点没有被访问过,则依次将右子节点,本节点,左子节点入栈,并标记左子节点已访问
b. 若左子节点已经被访问,输出本节点
3. 重复2至栈空
*/
void InOrderByLoop(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector< pair > treeNodeVec;
treeNodeVec.emplace_back(make_pair(rootNode, false));
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back().first;
auto &visited = treeNodeVec.back().second;
treeNodeVec.pop_back();
if (visited){
cout << pNode->value << " ";
treeNodeVec.pop_back();
continue;
}
if (pNode->pRight != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pRight, false));
}
treeNodeVec.emplace_back(make_pair(pNode, true));
if (pNode->pLeft != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pLeft, false));
}
}
cout << endl;
}
void TestInOrder(){
InOrderByLoop(GetBinaryTree());
InOrderByLoop(GetBinaryTree1());
InOrderByLoop(GetBinaryTree2());
InOrderByLoop(GetBinaryTree3());
}
若不根据递归而根据中序遍历的规则(即先处理左子节点,只有左子节点输出后才处理右子节点)来实现非递归
我们根据上图来模拟一下后序遍历
首先从10开始遍历,根据规则,一定最先找到最左下角的叶子节点4,之后访问6,再到8,以此类推,访问结果为 4、6、8、10、12、14、16
遍历到一个节点时,先不输出,而是先将往左子节点遍历,待输出左子节点后,才输出本节点,这种先进后出的现象可以使用栈来解决
而对于一个栈顶节点,首先会将所有左子节点入栈(先访问左子节点),那么此时栈顶节点有3种情况
对第3中情况,继续入栈…
而前两种情况,直接出栈输出,输出该节点之后有2种情况
第1种情况,显而易见需要处理右子节点(入栈),然后继续处理栈顶节点
第2种情况,继续处理栈顶节点…
我们用上图来模拟一下,首先从根节点开始往左子节点遍历依次入栈,直到停止时栈情况为
(栈顶)
[4]
[6]
[10]
此时由于节点4没有左子节点,直接输出,然后也没有右子节点,所以栈变成
(栈顶)
[6]
[10]
请注意,这个时候我们并不用判断节点6的左子节点是否已经处理,因为一开始我们就把6的左子节点放到了栈的更上层。也就是说,当我们一开始便把所有左子节点依次入栈之后,对于所有栈顶元素,其要么没有左子节点要么左子节点已经被访问,此时我们只需要判断有没有右子节点并访问即可…
至此,我们便可以开始写代码
// 中序遍历-循环2
/* 思想:
1. 将指针指向根节点
2. 先遍历所有左子节点并入栈
3. 当指针没有左子节点时,出栈这个节点(相当于访问该节点)
4. 接着讲指针指向该节点的右子节点
5. 重复2~4操作直至栈空
*/
void InOrderByLoop1(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector treeNodeVec;
auto *pNode = rootNode;
while (!treeNodeVec.empty() || pNode != nullptr){
while (pNode != nullptr){
treeNodeVec.push_back(pNode);
pNode = pNode->pLeft;
}
pNode = treeNodeVec.back();
treeNodeVec.pop_back();
cout << pNode->value << " ";
pNode = pNode->pRight;
}
cout << endl;
}
// 不同实现
void InOrderByLoop2(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector treeNodeVec({rootNode});
bool isNewNode = true;
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back();
// isNewNode表示当前栈顶是否是新加入的节点
while (isNewNode && pNode->pLeft != nullptr){
treeNodeVec.push_back(pNode->pLeft);
pNode = pNode->pLeft;
}
treeNodeVec.pop_back();
cout << pNode->value << " ";
isNewNode = false;
if (pNode->pRight != nullptr){
treeNodeVec.push_back(pNode->pRight);
isNewNode = true;
}
}
cout << endl;
}
// 后序遍历-递归
void PostOrderByRecursive(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
PostOrderByRecursive(rootNode->pLeft);
PostOrderByRecursive(rootNode->pRight);
cout << rootNode->value << " ";
}
后序遍历的非递归方法中,难点在于,对于每一个节点,在访问前,如何确保这个节点的左右子节点都被访问过
通过递归调用栈可以得知,对于每一个节点,先将自己入栈,再将右子节点入栈,最后是左子节点入栈。那么只要本节点的左右节点入过栈,下一次本节点在栈顶时,就表示左右子节点已经访问过,此时就可访问本节点了
我们可以使用几种方式来标记本节点的左右节点入过栈
// 后序遍历-循环1
/* 思想:
在本节点入栈后,记录一个可访问标记,再将左右子节点入栈
这样每次发现栈顶的可访问标记为true时,就表示空标记下面的节点
的左右节点已经访问过,此时直接访问本节点
*/
void PostOrderByLoop(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector treeNodeVec({rootNode});
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back();
if (pNode == nullptr){
treeNodeVec.pop_back();
cout << treeNodeVec.back()->value << " ";
treeNodeVec.pop_back();
continue;
}
// 添加可访问标记
treeNodeVec.push_back(nullptr);
if (pNode->pRight != nullptr){
treeNodeVec.push_back(pNode->pRight);
}
if (pNode->pLeft != nullptr){
treeNodeVec.push_back(pNode->pLeft);
}
}
cout << endl;
}
// 与上述思想一致
void PostOrderByLoop1(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector< pair > treeNodeVec;
treeNodeVec.emplace_back(make_pair(rootNode, false));
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back().first;
auto &visited = treeNodeVec.back().second;
if (visited){
cout << pNode->value << " ";
treeNodeVec.pop_back();
continue;
}
visited = true;
if (pNode->pRight != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pRight, false));
}
if (pNode->pLeft != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pLeft, false));
}
}
cout << endl;
}
void TestOrder(){
PostOrderByLoop(GetBinaryTree());
PostOrderByLoop(GetBinaryTree1());
PostOrderByLoop(GetBinaryTree2());
PostOrderByLoop(GetBinaryTree3());
}
代码中对于每一个节点,我们都额外创建了一个bool或者空指针来判断该节点是否左右子节点已访问,那么有没有不用为每个节点都创建标记的办法呢?
上述实现都是基于递归函数调用栈的想法来改进的,接下来我们直接通过后序遍历的规则(先处理左子节点,左子节点输出后,再处理右子节点)来实现一下
我们根据上图来模拟一下后序遍历
首先从10开始遍历,根据规则,一定最先找到最左下角的叶子节点4,之后回到6,发现6还有右子节点,那么会找到8,这时候6的左右子节点都被访问了,那么访问6,以此类推,访问结果为 4、8、6、12、16、14、10…
让我们找一下其中的规律,首先访问4之前,实际上已经经过6了,而输出4之后还要回到6再去访问8,接着才能访问6,对于这种先经过却后访问的方式,可以很容易联想到用栈来解决
我们首先创建一个栈,对于一个节点,我们先将所有左子节点依次入栈(先访问左子节点)。这时候我们可以确立一种规则,对于栈顶元素,我们可以保证该节点没有左子节点or左子节点已经被访问。接下来我们只需要判断该节点的右子节点是否被访问就能确定该节点能否访问了
此时栈顶的节点有3种情况:
我们用上图来模拟一下,假设已经访问4(节点4已经出栈),栈顶就是节点6,这时候我们要怎么确保节点8已经被访问过了呢?细心的同学一定会发现,假如节点6可以被访问,那么上一个出栈的一定是节点8(这不是废话嘛),将这个顺序转换成二叉树的结构来说就是,一个节点可以被访问,那么上一个被访问的一定是该节点的右子节点!!!
至此,我们便可以开始写代码
/* 后序遍历-循环2
思想:
1. 根节点入栈
2. 循环将栈顶元素的左子节点入栈
3. 取栈顶元素node
a. 若node没有右子节点,则输出node,出栈,重新进入步骤3
b. 若node有右子节点,但右子节点为上一个输出的节点,则输出node,出栈,重新进入步骤3
c. 若node有右子节点,且不是上一个输出的节点,右子节点进栈,返回步骤2
4. 循环2~4至栈空
*/
void PostOrderByLoop2(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector treeNodeVec({rootNode});
BinaryTreeNode *lastOutputNode = nullptr;
BinaryTreeNode *pNode = nullptr;
while (!treeNodeVec.empty()){
pNode = treeNodeVec.back();
while (pNode->pLeft != nullptr){
treeNodeVec.emplace_back(pNode->pLeft);
pNode = pNode->pLeft;
}
// 所有节点已经入过栈了,此时只需要输出所有节点即可
while (!treeNodeVec.empty()){
pNode = treeNodeVec.back();
if (pNode->pRight == lastOutputNode || pNode->pRight == nullptr){
cout << pNode->value << " ";
treeNodeVec.pop_back();
lastOutputNode = pNode;
}
else{
treeNodeVec.emplace_back(pNode->pRight);
break;
}
}
}
cout << endl;
}
至此,豁然开朗
而后在网上搜了一下,貌似对于三种非递归用统一方式实现很受欢迎,就贴一下代码吧
void OrderByLoop(BinaryTreeNode *rootNode){
if (rootNode == nullptr){
return;
}
vector> treeNodeVec;
treeNodeVec.emplace_back(make_pair(rootNode, false));
while (!treeNodeVec.empty()){
auto pNode = treeNodeVec.back().first;
auto &visited = treeNodeVec.back().second;
treeNodeVec.pop_back();
if (visited){
cout << pNode->value << " ";
treeNodeVec.pop_back();
continue;
}
// 对于不同遍历方式,只需要改变一下三种进栈顺序即可,需要先访问的后进栈
// 右子节点
if (pNode->pRight != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pRight, false));
}
// 本节点
treeNodeVec.emplace_back(make_pair(pNode, true));
// 左子节点
if (pNode->pLeft != nullptr){
treeNodeVec.emplace_back(make_pair(pNode->pLeft, false));
}
}
cout << endl;
}
参考文献:
[1] 李春葆.数据结构教程(第4版)[M].172-181.
[2] 何海涛.剑指Offer(第2版)[M].60-61.
[3] https://blog.csdn.net/czy47/article/details/81254984