二叉树的前中后序列遍历,递归调用自身的方式写出来的代码非常简洁,一目了然。
二叉树这种结构,个人认为天然的就是递归思想的载体。数学中的分形理论,有着类似的魅力。
递归思想很多大牛都有自己的解释,不再去做过多的解读,简单说起来,就是对于一个复杂的问题,将其分解成若干个简单的问题,对这若干个简单问题,规定相同的方法加以解决,并对问题的最终解决条件加以确立,避免无限的递归调用。
递归和循环的不同之处在于,递归是有去也有回,有向前的递归调用,也有向问题发起地点的回溯。
1、二叉树前中后序遍历的递归写法(C++)。
先定义数据结构。
// 节点example,你也可以定义自己的,这里方便展示用这样的结构
struct TreeNode
{
char data;
TreeNode *left, *right;
bool flag; // 用于非递归后序遍历编码
TreeNode(char ch): data(ch), left(nullptr), right(nullptr), flag(0) { }
};
typedef TreeNode *BTree;
前序:先访问根节点,再访问左儿子,再访问右儿子。
中序:先访问左儿子,再回到访问根节点,再访问右儿子。
后序:先访问左儿子,再访问右儿子,再访问根节点。
void PreOrder(BTree root)
{
if (!root) return; //若根节点空,不做任何事情
printf("%c ", root->data); // 打印访问到的每个节点
PreOrder(root->left); // 访问其左儿子
PreOrder(root->right); // 访问其右儿子
}
void InOrder(BTree root) {
if (!root) return;
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
void PostOrder(BTree root) {
if (!root) return;
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
我们发现,无论是前序,中序,还是后序,从节点的经历顺序上,都是先从根开始,然后到左子树,最后到右子树。对于每个节点,都是这样,可以运用一样的视角加以解释。前、中、后序遍历,不同点仅仅在于,打印节点的时机不同。这一点在上面的代码中可以十分明显的看出来。
2、二叉树前中后序遍历的非递归写法(C++)
我们知道,递归方法,存在调用栈溢出的风险。有没有办法去对此进行控制?当然是有的,那就是运用非递归的方法。
其实说是非递归,用的也是递归思想,不过不是调用自身,所以不会产生调用栈的问题。
怎么运用递归的思想,但是又避免调用栈呢,我们就得使用一个栈容器来模拟递归过程。
栈天然的可以用来模拟递归过程,还记得我们说递归是有去,还得回溯吗?也就是说,起点也是出口,栈也是如此,入栈的元素,还得原路返回才能出来。
我们再以此分析二叉树的三种遍历。
前面说到二叉树的访问顺序,其实本质来看,是一贯的,都是根->左->右,只不过访问节点内容的时机不同,前序就是到了根立即访问,根是什么呢,每个节点都是他的左右儿子的根,所以按照根->左->右的顺序,到一个节点,打印一个节点的内容。这里面有一个隐含的问题得注意到,这里所谓的根->左->右,其实是“根->左->根->右->根”。这里面的左和右泛指左子树、右子树,显然,左子树和右子树可能有一个节点、多个节点、没有节点(空)。
中序又是什么呢,还是按照根->左->根->右->根的顺序,这个时候访问节点的时机要做个调整,我们到了一个节点先不着急访问其内容,先一直往左,往左走不了了,肯定要回来的(递归回溯,由左->根),回到根,再从根往右。我们打印节点内容的时机,就是这个左->根的时候,把根打印出来,这个时候再往右走,如果右边为空,那么往上回溯,刚才经历的过程,其实是把更上一层的根的“左”遍历了一遍,这个时候又要回到更上一层的根,把其打印出来,再往它的右边走,如此类推。
后序遍历的大致过程,可以参照上文。
这里啰里啰嗦说一大堆,主要是想说,对于每个二叉树的节点,都存在这么个过程,也就是根->左->根->右->根。每个节点都是根,同时又可能是左儿子或者右儿子。
具体过程:
// 前序遍历非递归方法
void preOrderTraverse(BTree t) {
if (!t) return;
stack<TreeNode *> vstack;
TreeNode *curr, *top;
curr = t;
while (curr || !vstack.empty()) {
while (curr) {
if (curr == t)
printf("%c", curr->data);
else
printf(" %c", curr->data);
vstack.push(curr);
curr = curr->left;
}
top = vstack.top();
vstack.pop();
curr = top->right;
}
}
// 中序遍历非递归
void inOrderTraverse(BTree root) {
if (!root) return;
stack<TreeNode *> vstack;
TreeNode *curr, *top;
curr = root;
bool flag = 0;
while (curr || !vstack.empty()) {
while (curr) {
vstack.push(curr);
curr = curr->left;
}
top = vstack.top();
vstack.pop();
if (!flag) {
flag = 1;
printf("%c", top->data);
}
else
printf(" %c", top->data);
curr = top->right;
}
}
// 后序遍历非递归方法
void postOrderTraverse(BTree root) {
if (!root) return;
stack<TreeNode *> vstack;
TreeNode *curr, *top;
curr = root;
bool temp_flag = false;
while (curr || !vstack.empty()) {
while (curr) {
vstack.push(curr);
curr = curr->left;
}
top = vstack.top();
if (!top->flag) {
top->flag = 1;
curr = top->right;
}
else {
if (!temp_flag) {
temp_flag = 1;
printf("%c", top->data);
}
else
printf(" %c", top->data);
vstack.pop();
if (!vstack.empty())
top = vstack.top();
if (!top->flag) {
curr = top->right;
top->flag = 1;
}
}
}
}
个人认为的几个原则:
1、对于节点的入栈顺序来说,严格遵循根-左-右的原则,所以出栈的时候就能保证按照反方向回溯。
2、对于出栈的时机和打印的时机,根据遍历方式的不同有所不同
3、对于前序而言,每个节点都作为根节点,入栈即打印其内容,再到左儿子,对左儿子应用同样的访问和打印原则,再对右儿子运用同样的访问和打印原则。
4、对于中序而言,每个节点作为根节点,先要访问其左儿子,再打印其内容,具体表现在,往左访问,一直压栈,当左为空,此时栈顶元素就是往左访问后回溯的根,对于中序而言,就在此时打印栈顶元素,打印过了就出栈,接着访问其右儿子,重复前面的过程。
5、对于后序,由于必须先访问完左边和右边,才能打印根的内容,所以得设置一个flag记录该节点的状态,如果flag状态是0,代表这个节点还没访问过它的右边,flag状态是1,代表这个节点的左右儿子都已经访问过了,下次循环再碰到这个节点,就可以打印出栈了。