遍历二叉树

04月27日 收藏 0 评论 0 java开发

遍历二叉树

转载声明:文章来源https://zhuanlan.zhihu.com/p/83956119

任何算法的递归版本都可以改成非递归版本,因为函数递归调用其实质就是压栈的过程,那么我们完全可以使用堆栈来模拟这个过程。二叉树的遍历(递归法)很容易实现,本质是采用了栈帧的实现方式,函数调用就是压栈,函数求解就是出栈的过程,那么我们完全可以手动创建栈来模拟这个过程,按照指定遍历顺序迭代的访问二叉树的所有结点。

因为前序、中序、后序三种遍历方式在当前函数体内都是先调用f(root.left),再调用f(root.right),函数再往下逐层调用,直到f(root.left)返回为空时。所以三种遍历方式的切入点都是要先遍历到二叉树的最左子结点(root.left == null),在遍历的过程中将根节点入栈,出栈的时候访问当前根节点以及其右子二叉树。创建栈的作用主要就是能够让我们存储访问顺序的那些根节点,可以回溯的对二叉树进行访问。

前序遍历二叉树:

前序遍历的访问顺序是 【根 左 右】,所以我们需要从二叉树的左分支入手,依次push入栈,入栈的同时访问了该结点,根节点向左更新,直到二叉树的最左边,栈中存放的结点方便了我们进行回溯父结点。入栈的顺序就是访问的顺序,为了模拟递归,函数调用是入栈,函数执行是出栈,出栈return到上一个调用它的函数。

/*
1 如图,先访问二叉树的根节点1,同时结点1入栈,
/ \ 再访问二叉树的左子树,左子树的跟结点2被访问,同时入栈,
2 3 重复上述过程,直到到达二叉树的最左结点的left指针域,为null
/\ /\ 需要回溯到栈中第一个弹出的结点且那根节点具有右孩子,则更新root
4 5 6 7 让root访问结点4或结点2或结点1....的右子树,依次进行,直到根结点为空且栈空
*/
class Solution {
public List preorderTraversal(TreeNode root) {
List preOrderList = new ArrayList<>(); //存放访问顺序
Stack stack = new Stack<>(); //存放结点,用于回溯
while(root != null || !stack.empty()) { //迭代遍历二叉树
while(root != null) { //使root指向当前子二叉树的最左结点
stack.push(root);
preOrderList.add(root.val); //将当前子二叉树的根节点入栈,并访问
root = root.left;
}
while(root == null && !stack.empty()) {
root = stack.pop().right; //自底向上找到栈中跟结点第一个非空右孩子
}
}
return preOrderList;
}
}

中序遍历二叉树:

中序遍历的访问顺序是 【左 根 右】,所以我们需要从二叉树的左分支入手,依次push入栈,根节点向左更新,直到二叉树的最左边,即到了开始访问的时机,栈中存放的结点方便了我们进行回溯父结点。出栈的顺序就是访问的顺序,为了模拟递归,函数调用是入栈,函数执行是出栈,出栈return到上一个调用它的函数。

/*
1 如图,先从根节点沿着左分支走到最左边,结点4处,由于结点4没有左孩子
/ \ 先访问结点4作为跟结点本身,结点4出栈,接着访问结点4的右孩子(作为整个子二叉树)
2 3 (若右孩子为空则更新结点root为结点2,访问以结点2为根节点的子二叉树:依次访问其根结点和它的右孩子)
/\ /\ 每次循环弹出访问一个结点,更新结点root为结点8,再对以结点8为跟结点的子二叉树进行沿着左分支走到最左边
4 5 6 7 结点8访问完之后,且无右孩子,说明当前子二叉树访问完成,则将root更新为结点2
\ 简单的,root=root.right就可以完成对root的更新,若结点4.right=null, 再下一次循环从栈顶弹出应该回溯到的结点
8 若结点4.right存在,则直接更新就好,综上,一条语句可完成.依次进行,直到根节点为空,并且栈为空。
*/
class Solution {
public List inorderTraversal(TreeNode root) {
List inOrderList = new ArrayList<>(); //存放访问顺序
Stack stack = new Stack<>(); //存放结点,用于回溯
while(root != null || !stack.empty()) { //迭代遍历二叉树
while(root != null) { //使root指向当前子二叉树的最左结点
stack.push(root);
root = root.left;
}
root = stack.pop();
inOrderList.add(root.val); //出栈当前子二叉树的根结点,并访问
root = root.right; //更新root结点:当前子二叉树的右孩子 or 树的父结点
}
return inOrderList;
}
}

后序遍历二叉树:

在做迭代前序遍历和中序遍历的时候,发现root经过的路径都是左根右,对于前序和中序来说,访问路径基本上跟经过路径是一致的。但是在后序遍历中,我们先经过根节点,但是我们不会去访问它,而是会选择先访问它的右子节点。所以在这种情况下,我们会将根节点留在栈中不弹出,等到需要访问它的时候再出。

那我们什么情况下才能访问根节点?是从右节点回溯到根节点,而不是从左节点回溯到根节点,所以我们需要记录之前结点和当前节点的关系,来确定是否访问当前节点。总结起来,我们什么时候才能访问节点。有如下两种情况:

当前经过节点是叶子节点。
当前经过节点的右子节点是上一次访问的节点。

若不满足上述情况,说明是从左孩子回溯到根节点,需要先访问根节点的右孩子,root = root.right

/*
1
/ \
2 3
/\ /\
4 5 6 7

*/
class Solution {
public List postorderTraversal(TreeNode root) {
List postOrderList = new ArrayList<>(); //用于存放访问顺序
Stack stack = new Stack<>(); //存放结点,用于回溯
TreeNode pre = null; //记录之前访问过的结点
while(root != null || !stack.empty()) { //迭代访问二叉树
while(root != null) { //使root指向当前子二叉树的最左结点
stack.push(root);
root = root.left;
}
root = stack.peek();
if(root.right == null || root.right == pre) { //当前结点为叶子结点 或者 当前结点的右孩子是上个访问结点
pre = root; //更新上一次访问的结点
postOrderList.add(stack.pop().val); //出栈,表示访问了当前结点
root = null; //让root到下一次循环再更新,避免发生空栈错误
}else {
root = root.right; //访问当前结点的右孩子
}
}
return postOrderList;
}
}

总结:

1.事实上我们使用迭代对二叉树进行访问,访问的顺序都是左子树、根结点、右子树的顺序。关键在于真正访问根节点的时机(根节点的值加入到列表的时候)
2.事实上用迭代模拟函数调用(入栈)、函数执行返回(出栈),我们就二叉树遍历这个问题上,每个结点都会访问到三次。
3.用栈的后进先出,有效的模拟了递归中的函数调用与执行返回过程

C 0条回复 评论

帖子还没人回复快来抢沙发