非递归实现二叉树的遍历方式
任何的递归函数都可以改成非递归的方式,因为递归函数在系统中是通过将一个一个函数压栈的方式实现的,所以我们也可以用栈来实现这一过程
递归遍历
递归实现前中后序的遍历比较简单,我就直接上代码了哈
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
return;
cout << root->_data << ' ';
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
return;
BinaryTreeInOrder(root->_left);
cout << root->_data << ' ';
BinaryTreeInOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
return;
BinaryTreePostOrder(root->_left);
BinaryTreePostOrder(root->_right);
cout << root->_data << ' ';
}
非递归先序遍历
原理:先将更根结点放入栈中,让后后面就是3步走,
1.弹出就打印
2.如果有右孩子,就将右孩子压入栈中
3.如果有左孩子,就将左孩子压入栈中
这样循环下去直至栈中没有元素
void BinaryTreePrev(BTNode* root)
{
stack<BTNode*> sk;
sk.push(root);
while (!sk.empty())
{
BTNode* tmp = sk.top(); // 将栈顶取出并打印
sk.pop();
cout << tmp->_data << ' ';
if (tmp->_right) // 如果右孩子不空就先入右孩子
sk.push(tmp->_right);
if (tmp->_left) // 如果左孩子不空就再入左孩子
sk.push(tmp->_left);
}
}
首先是将栈顶的元素入栈
然后就是将右孩子入栈(如果有右孩子的话)再将左孩子入栈(如果有左孩子的话),然后循环再拿出栈顶的元素打印…
可以看一下画的图
有一个问题:问啥一定要先入右孩子,再入左孩子呢?不是头结点->左子树->右子树么?
这个问题很简单,其实是因为栈的特性,因为我们使用栈来手动模拟调用关系,而栈是后进先出的,所以先入的元素后才杯打印,所以入栈的元素在出栈的时候回使元素逆序,如果是右孩子先入,那么右孩子就会比左孩子先出栈,这样就不对了。要满足头结点先打印,直接取出栈顶的元素就可以了。
非递归中序遍历
中序遍历的方式就比较暴力,就是顺着最左边的路径先找最左边的孩子,然后打印出来之后就去找这个结点的右子树的最左边的孩子重复上面的操作,如果没有右子树或者右子树已经被遍历过了,那就退回去找父亲结点(直接找sk.top()即可,因为已经用一个栈保存起来了),然后再重复上面的循环(去找这个结点的右子树的最左边的结点)。其实就是找完了最左边的一条路径之后,从下到上的再去找右子树,当然找完左子树是先打印中间结点,然后再去找右子树,因为是从左边的路径出发,所以一直往右找即可,只不过再开始一个新的子树的时候需要重复一开始的过程
就是像这种铺天盖地式的方式
// 非递归实现中序遍历
void BinaryTreeIn(BTNode* root)
{
if (root == NULL)
return;
stack<BTNode*> sk;
while (!sk.empty() || root)
{
if (root)
{
sk.push(root);
root = root->_left; // 一直往左找
}
else
{
root = sk.top(); // 先打印当前结点
sk.pop();
cout << root->_data << ' ';
root = root->_right; // 一直往右找
}
}
}
非递归后序遍历
后序遍历和先序遍历的方法差不多,后序遍历的顺序是:左孩子->右孩子->头结点。要是倒着看是不是头结点->右孩子->左孩子,这个顺序是不是很熟悉呀,先序遍历不就是头结点->左孩子->右孩子,只是左右孩子的顺序颠倒了而已,那是不是可以用先序遍历的方式,只不过这次要先压入左孩子再压入右孩子,至于怎么把整个树的顺序倒过来,就再可以用一个栈来保存数据就可以了,我们是不是说过栈的特性就是可以让入栈的元素出栈的时候顺序可以颠倒过来。
void BinaryTreePost(BTNode* root)
{
stack<BTNode*> sk;
stack<BTNode*> ans;
sk.push(root);
while (!sk.empty())
{
BTNode* tmp = sk.top();
ans.push(tmp);
sk.pop();
if (tmp->_left)
sk.push(tmp->_left);
if (tmp->_right)
sk.push(tmp->_right);
}
while (!ans.empty()) // 将答案栈中的元素逆序输出出来
{
cout << ans.top()->_data << ' ';
ans.pop();
}
}
当然还有一种更为直接的方式输出后序遍历的方式,而不是这种利用了其他的性质,但是这种方法会有点难想。
大概的思路是这样的:不是要先遍历左孩子嘛,那我就先找到整个树的最左边的整条路径,然后首先就顺着这条路径可以找到第一个最左边的叶结点或者是左右两边的孩子都已经入过栈了,然后打印出来,然后顺着这条路径往上走,找到离上次最近的而且左孩子入过栈了,而右孩子没有入过栈的元素,然后这样循环下去,这样说太抽象了,还是结合代码来看一看吧。
// 后序遍历方法2
void BinaryTreePost2(BTNode* root)
{
if (root == NULL)
return;
stack<BTNode*> sk;
BTNode* cur = NULL;
sk.push(root);
while (!sk.empty())
{
cur = sk.top();
// 如果cur是最左边的叶子结点为空或者是左右两边都没有打印过就将cur入栈
if (cur->_left && cur->_left != root && cur->_right != root)
sk.push(cur->_left);
// 如果左边孩子已经打印过了,并且右边的孩子没有打印过或者最右边的叶结点为空(即不能打印)就将cur再入栈
else if (cur->_right && cur->_right != root)
sk.push(cur->_right);
// cur左右两边的孩子都已经打印过了那就直接打印cur自己的值并且打印过后要将打印过得元素出栈
// 并用root来标记一下这次打印的元素,方便后面可以判断元素是否要入栈
else
{
cout << cur->_data << ' ';
sk.pop();
root = cur; // 标记一下cur这个结点已经被打印过了
}
}
}
其中第一个判断要满足是该节点的左树没处理和右树没处理
第二个判断要满足该节点的左树处理完了但是右树没处理
可以看出root其实一开始入栈是没有意义的,只是为了不干扰cur找到最左边的孩子而已,之后root就起到标记上一次已经打印过的元素,而cur每次都是栈顶元素也就是上一次入栈的元素,我们的核心判断就是,因为是后序遍历,所以先考虑左孩子是否打印过了,然后考虑右孩子会否打印过了,最后打印元素自己本身
层序遍历
层序遍历就比较简单了,但是这个和上面的大结构不太一样,上面的递归使用栈来实现的,但是层寻遍历是广度优先遍历,所以就要用到队列,每次将队头的元素取出,并将队头的元素的左右子树按顺序放入队列即可,然后没要将对头的元素去掉,这样保证了队列中的元素可以层层递进的往下走
这就是出队的顺序,注意A是队头G是队尾
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
if (root == NULL)
return;
queue<BTNode*> q;
q.push(root);
while (!q.empty())
{
BTNode* tmp = q.front();
q.pop();
cout << tmp->_data << ' ';
if (tmp->_left)
q.push(tmp->_left);
if (tmp->_right)
q.push(tmp->_right);
}
}