java二叉树堆栈遍历_专题:二叉树的遍历(迭代)

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

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

前序遍历二叉树:LeetCode-144. Binary Tree Preorder Traversal​zhuanlan.zhihu.com7cf0ad07845685480a2743d9bf0951d9.png

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

/*1 如图,先访问二叉树的根节点1,同时结点1入栈,/ \ 再访问二叉树的左子树,左子树的跟结点2被访问,同时入栈,2 3 重复上述过程,直到到达二叉树的最左结点的left指针域,为null/\ /\ 需要回溯到栈中第一个弹出的结点且那根节点具有右孩子,则更新root4 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;

}

}

中序遍历二叉树:LeetCode-94. Binary Tree Inorder Traversal​zhuanlan.zhihu.com7cf0ad07845685480a2743d9bf0951d9.png

中序遍历的访问顺序是 【左 根 右】,所以我们需要从二叉树的左分支入手,依次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;

}

}

后序遍历二叉树:LeetCode-145. Binary Tree Postorder Traversal​zhuanlan.zhihu.com7cf0ad07845685480a2743d9bf0951d9.png

在做迭代前序遍历和中序遍历的时候,发现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;

}

}

总结:事实上我们使用迭代对二叉树进行访问,访问的顺序都是左子树、根结点、右子树的顺序。关键在于真正访问根节点的时机(根节点的值加入到列表的时候)

事实上用迭代模拟函数调用(入栈)、函数执行返回(出栈),我们就二叉树遍历这个问题上,每个结点都会访问到三次。

用栈的后进先出,有效的模拟了递归中的函数调用与执行返回过程。

此外,欢迎到我的博客网站做客,请点击以恒的个人博客​tangyiheng.top4e626c8887dbfbd65b6f1f3cfe59e7b6.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值