二叉树的前序|中序|后序遍历以及层序遍历
二叉树
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有前序、中序以及后序三种遍历方法。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。而对于树的遍历若采用非递归的方法,就要采用栈去模拟实现。在三种遍历中,前序和中序遍历的非递归算法都很容易实现,非递归后序遍历实现起来相对来说要难一点
层序遍历
思路:使用队列FIFO的特点,注意要将节点指针存入队列
我们可以用很多方式去遍历一颗二叉树,比如先序遍历,中序遍历,后序遍历,其实都是通过递归的来实现。今天我们来对二叉树进行层序遍历,层序遍历的时候需要借助另一种数据结构——队列。本篇的示例代码上传至 https://github.com/chenyufeng1991/LevelOrderBinaryTree 。
层序遍历的基本思路是,当访问到一个节点的时候,把它放入队列,然后访问该值,同时把该节点的左右孩子节点放入队列,出队列该节点。其中需要注意的是,如果某个节点已经是叶子节点了,则不需要把它的左右孩子节点(虽然它没有左右孩子)放入队列。因为把一个空的节点push进队列会造成bug。同样的,如果它只有左孩子或者只有右孩子,也是同样的处理。递归结束的条件是当队列为空时结束。
层序遍历
思路:使用队列FIFO的特点,注意要将节点指针存入队列
我们可以用很多方式去遍历一颗二叉树,比如先序遍历,中序遍历,后序遍历,其实都是通过递归的来实现。今天我们来对二叉树进行层序遍历,层序遍历的时候需要借助另一种数据结构——队列。本篇的示例代码上传至 https://github.com/chenyufeng1991/LevelOrderBinaryTree 。
层序遍历的基本思路是,当访问到一个节点的时候,把它放入队列,然后访问该值,同时把该节点的左右孩子节点放入队列,出队列该节点。其中需要注意的是,如果某个节点已经是叶子节点了,则不需要把它的左右孩子节点(虽然它没有左右孩子)放入队列。因为把一个空的节点push进队列会造成bug。同样的,如果它只有左孩子或者只有右孩子,也是同样的处理。递归结束的条件是当队列为空时结束。
核心代码如下:
[cpp] view plain copy
//层序遍历
void LevelOrder(queue<Node *> &nodeQueue)
{
if (nodeQueue.empty())
{
return;
}
Node *frontNode = nodeQueue.front();
cout << frontNode->element << " ";
nodeQueue.pop();
if (frontNode->lChild != NULL)
{
nodeQueue.push(frontNode->lChild);
}
if (frontNode->rChild != NULL)
{
nodeQueue.push(frontNode->rChild);
}
LevelOrder(nodeQueue);
}
前序遍历
前序遍历按照“根结点-左孩子-右孩子”的顺序进行访问。
递归实现
void PreOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
cout <<root->val;
PreOrder(root->left);
PreOrder(root->right);
}
1
2
3
4
5
6
7
8
9
10
11
12
非递归实现
根据前序遍历访问的顺序,优先访问根结点,然后再分别访问左孩子和右孩子。
即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问其左子树时,再访问它的右子树。
从根节点开始,开始遍历,并输出(先序遍历首先输出根)
递归输出直至最左(先序根后面输出的是左孩子)
当到达最左节点的时候,访问右节点
因此其处理过程如下:
对于任一结点P:
访问结点P,并将结点P入栈;
判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
直到P为NULL并且栈为空,则遍历结束。
void PreOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
中序遍历
中序遍历按照“左孩子-根结点-右孩子”的顺序进行访问。
递归实现
void InOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
InOrder(root->left);
cout <<root->val;
InOrder(root->right);
}
非递归实现
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才进行访问,然后按相同的规则访问其右子树。
从根节点开始,开始遍历
递归输出直至最左,然后输出(中序先输出左孩子,而中序遍历第一个输出的是其最左叶子节点)
当到达最左节点的时候,访问右节点
因此其处理过程如下:
对于任一结点P,
若其左孩子不为空,则将P入栈并将P的左孩子置为当前的P,然后对当前结点P再进行相同的处理;
若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的P置为栈顶结点的右孩子;
直到P为NULL并且栈为空则遍历结束
void InOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
后序遍历
后序遍历按照“左孩子-右孩子-根结点”的顺序进行访问。
递归实现
void PostOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
cout <<root->val;
}
非递归实现
后序遍历的非递归实现是三种遍历方式中最难的一种。因为在后序遍历中,要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难题。下面介绍两种思路。
第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
1
2
void PostOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
include
include
include
include
include
using namespace std;
// 调试开关
ifdef DEBUG
define debug cout
else
define debug 0 && cout
endif // __tmain
//#define __ISFIRST_FOR_POSTPRDER
class TreeNode
{
public :
int val;
TreeNode *left;
TreeNode *right;
ifdef __ISFIRST_FOR_POSTPRDER
int isFirst; // 非递归中序遍历中保存其是否首次被访问
endif // __ISFIRST_FOR_POSTPRDER
TreeNode(int x)
: val(x), left(NULL), right(NULL)
{
}
static void PreOrder(TreeNode *root);
static void PreOrderDev(TreeNode *root);
static void InOrder(TreeNode *root);
static void InOrderDev(TreeNode *root);
static void PostOrder(TreeNode *root);
static void PostOrderDev(TreeNode *root);
static int PrintLevel(TreeNode *root, int level = 0);
static void LevelOrder(TreeNode *root);
static void LevelOrderDev(TreeNode *root);
static void LevelOrderUsePoint(TreeNode *root);
static void LevelOrderUseSize(TreeNode *root);
static void LevelOrderUseEnd(TreeNode *root);
};
//
void TreeNode::PreOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
cout <val;
PreOrder(root->left);
PreOrder(root->right);
}
void TreeNode::InOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
InOrder(root->left);
cout <val;
InOrder(root->right);
}
void TreeNode::PostOrder(TreeNode *root)
{
if(root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
cout <val;
}
// 非递归实现
//
// 根据前序遍历访问的顺序,优先访问根结点,然后再分别访问左孩子和右孩子。
//
// 即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;
// 当访问其左子树时,再访问它的右子树。因此其处理过程如下:
//
// 对于任一结点P:
//
// 1)访问结点P,并将结点P入栈;
//
// 2)判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
//
// 3)直到P为NULL并且栈为空,则遍历结束。
void TreeNode::PreOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
ifdef __ISFIRST_FOR_POSTPRDER
// 对于任一结点P,将其入栈,
// 然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,
// 此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。
//
// 所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,
// 此时可以将其出栈并访问。这样就保证了正确的访问顺序。
// 可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
void TreeNode::PostOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
else //
void TreeNode::PostOrderDev(TreeNode *root)
{
if(root == NULL)
{
debug <<”The tree is NULL…” <
endif // __ISFIRST_FOR_POSTPRDER
int TreeNode::PrintLevel(TreeNode *root, int level)
{
if(root == NULL || level < 0)
{
return 0;
}
else if(level == 0)
{
cout <val;
return 1;
}
else
{
return PrintLevel(root->left, level - 1) + PrintLevel(root->right, level - 1);
}
}
void TreeNode::LevelOrder(TreeNode *root)
{
for(int level = 0; ; level++)
{
if(PrintLevel(root, level) == 0)
{
break;
}
cout <