二叉树的非递归前序遍历

逻辑浮尘
• 阅读 2006

前序遍历

「前序遍历」指先访问节点,再遍历节点的左子树,最后遍历节点的右子树,按照这种规则不重复地访问树中所有节点的过程。

模拟过程

二叉树的非递归前序遍历

过程中,用「打印节点值」表示对节点的访问,「访问结束」表示该节点完成了访问节点遍历节点的左子树遍历节点的右子树的所有过程。

  1. 到达节点A,访问节点A(打印节点值),开始遍历A的左子树
  2. 到达节点B,访问节点B(打印节点值),开始遍历B的左子树
  3. 到达节点D,访问节点D(打印节点值),开始遍历D的左子树
  4. 到达节点H,访问节点H(打印节点值),H仅有右子树,开始遍历H的右子树
  5. 到达节点K,访问节点K(打印节点值),K无子树,节点K的访问结束

    • 节点K的访问结束同时意味着节点H的右子树遍历结束,且节点H仅有右子树,节点H的访问结束
    • 节点H的访问结束同时意味着节点D的左子树遍历结束,且节点D仅有左子树,节点D的访问结束
    • 节点D的访问结束同时意味着节点B的左子树遍历结束,开始遍历节点B的右子树
  6. 到达节点E,访问节点E(打印节点值),E无子树,节点E的访问结束

    • 节点E的访问结束同时意味着节点B的右子树遍历结束,且节点B的左子树也遍历结束,节点B的访问结束
    • 节点B的访问结束同时意味着节点A的左子树遍历结束,开始遍历节点A的右子树
  7. 到达节点C,访问节点C(打印节点值),开始遍历C的左子树
  8. 到达节点F,访问节点F(打印节点值),开始遍历F的左子树
  9. 到达节点I,访问节点I(打印节点值),I无子树,节点I的访问结束

    • 节点I的访问结束同时意味着节点F的左子树遍历结束,且节点F仅有左子树,节点F的访问结束
    • 节点F的访问结束同时意味着节点C的左子树遍历结束,开始遍历节点C的右子树
  10. 到达节点G,访问节点G(打印节点值),G仅有右子树,开始遍历G的右子树
  11. 到达节点J,访问节点J(打印节点值),J无子树,节点J的访问结束

    • 节点J的访问结束同时意味着节点G的右子树遍历结束,且节点G仅有右子树,节点G的访问结束
    • 节点G的访问结束同时意味着节点C的右子树遍历结束,且节点C的左子树也遍历结束,节点C的访问结束
    • 节点C的访问结束同时意味着节点A的右子树遍历结束,且节点A的左子树也遍历结束,节点A的访问结束

至此,树中所有节点都被访问

分析过程

上述过程中节点的数据结构如下

function Node(value) {
  this.value = value
  this.left = null
  this.right = null
}

上述过程中树的结构如下,被当作变量root保存

root {
  value: 'A',
  left: {
      value: 'B',
      left: {
          value: 'D',
          left: {
              value: 'H',
              left: null,
              right: {
                  value: 'K',
                  left: null,
                  right: null
              }
          },
          right: null
      },
      right: {
          value: 'E',
          left: null,
          right: null
      }
  },
  right: {
      value: 'C',
      left: {
          value: 'F',
          left: {
              value: 'I',
              left: null,
              right: null
          },
          right: null
      },
      right: {
          value: 'G',
          left: null,
          right: {
              value: 'J',
              left: null,
              right: null
          }
      }
  }
}

分析过程:每到达一个节点,先访问节点(打印节点的值),然后寻找下一个节点并前往,直至所有节点都被访问完成,且初始的节点为树的根节点(root)。草写代码如下

const preOrderTraverse = root => {
  let node = root // 初始的节点为树的根节点
  while (node) { // 若不存在下一个节点,循环终止
    console.log(node.value) // 访问节点(打印节点的值)
    ...nextNode // 寻找下一个前往的节点
    node = nextNode || null // 前往下一个节点,若找不到下一个节点,则所有节点都被访问完成
  }
}

寻找下一个节点并前往,前往下一个节点的方式有多种,其中一种是若节点存在左子树,则下一个节点就是该左子树的根,如上述过程中 A->B, B->D, D->H, C->F, F->I

二叉树的非递归前序遍历

翻译成代码就是

const preOrderTraverse = root => {
  let node = root // 初始的节点为树的根节点
  while (node) { // 若不存在下一个节点,循环终止
    console.log(node.value) // 访问节点(打印节点的值)
    let nextNode
    if(node.left){ // 若节点存在左子树
      nextNode = node.left // 则下一个节点就是该左子树的根
    }
    ... // 未完待续
    node = nextNode || null // 前往下一个节点,若找不到下一个节点,则所有节点都被访问完成
  }
}

寻找下一个节点并前往,前往下一个节点的另一种方式是,若节点仅存在右子树,则下一个节点就是该右子树的根,如上述过程中 H->K, G->J

二叉树的非递归前序遍历

翻译成代码就是

const preOrderTraverse = root => {
  let node = root // 初始的节点为树的根节点
  while (node) { // 若不存在下一个节点,循环终止
    console.log(node.value) // 访问节点(打印节点的值)
    let nextNode
    if(node.left){ // 若节点存在左子树
      nextNode = node.left // 则下一个节点就是该左子树的根
    }else if(!node.left&&node.right){ // 若节点仅存在右子树
      nextNode = node.right // 则下一个节点就是该右子树的根
    }
    ... // 未完待续
    node = nextNode || null // 前往下一个节点,若找不到下一个节点,则所有节点都被访问完成
  }
}

前面提到的寻找下一个节点并前往的方式中,其下一个节点都是当前节点的左孩子或右孩子,分析前序遍历的过程,发现还有一种前往的下一个节点并不是当前节点的孩子的情况,如上述过程中 K->E, E->C, I->J

二叉树的非递归前序遍历

当节点仅含一颗子树时,就遍历该子树,当节点含左右子树时,先遍历其左子树,而将其右子树的根保存。当其左子树遍历完成时(触发的时机是到达叶节点),前往其右子树的根,开始遍历其右子树。
例如,到节点A时,将其右子树的根节点C保存起来,当左子树遍历结束时,才取出节点C开始遍历右子树。到达节点B,将其右子树的根节点E保存起来。当到达叶节点K,节点B的左子树遍历结束,取出节点E开始遍历右子树。将节点E弹出,此时仅剩节点A的右子树待遍历。节点C先于节点E保存,但后于节点E取出,故采用栈保存待遍历的右子树的根。

翻译成代码就是

const preOrderTraverse = root => {
  let node = root, // 初始的节点为树的根节点
      stack = [] // 用栈保存待遍历的右子树的根
  while (node) { // 若不存在下一个节点,循环终止
    console.log(node.value) // 访问节点(打印节点的值)
    let nextNode
    if(node.left){ // 若节点存在左子树
      node.right && stack.push(node.right) // 若是含左右子树的节点,遍历左子树,保存右子树
      nextNode = node.left // 则下一个节点就是该左子树的根
    }else if(!node.left&&node.right){ // 若节点仅存在右子树
      nextNode = node.right // 则下一个节点就是该右子树的根
    }else{ // 叶节点,说明最近的含有左右子树的节点的左子树遍历结束,开始遍历含有左右子树的节点的右子树
      nextNode = stack.pop() // 前往那个右子树的根,被存于栈中,若栈空,说明没有右子树待遍历,遍历结束
    }
    node = nextNode // 前往下一个节点,若找不到下一个节点,则所有节点都被访问完成
  }
}

整理最终代码如下

const preOrderTraverse = root => {
  let node = root, // 初始的节点为树的根节点
    stack = [] // 用栈保存待前往的节点
  while (node) { // 若不存在下一个节点,循环终止
    console.log(node.value) // 访问节点(打印节点的值)
    if (node.left) { // 若节点存在左子树
      node.right && stack.push(node.right) // 若是含左右子树的节点,将其右孩子保存
      node = node.left // 则下一个节点就是该左子树的根
    } else if (!node.left && node.right) { // 若节点仅存在右子树
      node = node.right // 则下一个节点就是该右子树的根
    } else { // 叶节点,说明最近的含有左右子树的节点的左子树遍历结束,开始遍历含有左右子树的节点的右子树
      node = stack.pop() // 前往那个右子树的根,被存于栈中,若栈空,说明没有右子树待遍历,遍历结束
    }
  }
}
preOrderTraverse(root)
// A B D H K C F I G J

为什么我总想的那么复杂

const preOrderTraverse = root => {
  let current = root,
    stack = []
  while (current || stack.length !== 0) {
    if (current) {
      console.log(current.value) // 访问节点
      stack.push(current)
      current = current.left // 遍历左子树
    } else {
      current = stack.pop()
      current = current ? current.right : null // 遍历右子树
    }
  }
}
preOrderTraverse(binaryTree.root)
点赞
收藏
评论区
推荐文章
九路 九路
4年前
前端学数据结构与算法:二叉树的四种遍历方式及其应用
前言上一章我们从0到1的实现了一颗二叉搜索树,以及理解了二叉搜索树的特性与基本操作,这一章介绍关于二叉树的更多操作,也就是树的遍历,对树的每个节点进行访问。主要包括前序遍历、中序遍历、后序遍历、层序遍历,前面三种也叫深度优先遍历(DFS),最后的层序遍历也叫广度优先遍历(BFS),理解这四种遍历方式的不同,再遇到树相关的算法问题时,也就能更加游刃有余。这
Wesley13 Wesley13
3年前
JAVA递归实现线索化二叉树
JAVA递归实现线索化二叉树基础理论首先,二叉树递归遍历分为先序遍历、中序遍历和后序遍历。先序遍历为:根节点左子树右子树中序遍历为:左子树根节点右子树后序遍历为:左子树右子树根节点(只要记住根节点在哪里就是什么遍历,且都是先左再右)线索化现在有这么一棵二叉树,它的数据结
Wesley13 Wesley13
3年前
OSG节点访问和遍历
遍历节点树:osg::Node类中有两个辅助函数:voidascend(NodeVisitor&nv)//虚函数,向上一级节点推进访问器voidtraverse(NodeVisitor&nv)//虚函数,向下一级节点推进访问器NodeVisitor的traverse()函数实现如下:in
Wesley13 Wesley13
3年前
04.重建二叉树 (Java)
题目描述输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。(https://www.oschina.net/action/GoToLink?urlht
Wesley13 Wesley13
3年前
6.重建二叉树(代码未完成)
 题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出图2.6所示的二叉树并输出它的头结点。!(https://static.oschina.net/uploads
Wesley13 Wesley13
3年前
JZ18 二叉树镜像
JZ18二叉树镜像题目操作给定的二叉树,将其变换为源二叉树的镜像。思路先遍历,节点入栈,再依次出栈调换左右节点遍历的过程中调换左右节点代码coding:utf8classTreeNode:def__init__
小万哥 小万哥
1年前
DOM 节点遍历:掌握遍历 XML文档结构和内容的技巧
遍历是指通过或遍历节点树遍历节点树通常,您想要循环一个XML文档,例如:当您想要提取每个元素的值时。这被称为"遍历节点树"。下面的示例循环遍历所有的子节点,并显示它们的名称和值:htmlvarx,i,xmlDoc;vartxt"";vartext"""E
贾蔷 贾蔷
2个月前
力扣145题:二叉树的后序遍历, 解题思路与C++实现
题目介绍力扣第145题要求实现一个函数,该函数接收一个二叉树的根节点,并返回该树的后序遍历结果。后序遍历是一种遍历二叉树的算法,其顺序为:先遍历左子树,是右子树,是根节点。解题思路分析解题时,我们可以使用递归或迭代的方法。递归方法较为直观,但可能导致栈溢出
贾蔷 贾蔷
2星期前
洛谷P3365 改造二叉树:从问题分析到代码实现
一、问题分析题目要求我们计算将修改为(BST)所需的最少修改次数。二叉搜索树的性质是:对于任意节点,其左子树所有节点的值都小于该节点的值,右子树所有节点的值都大于该节点的值。二、解题思路1.‌序列‌:BST的中序遍历结果是一个严格1.‌问题转化‌:将原二叉
菜园前端 菜园前端
2年前
什么是二叉树?
原文链接:什么是二叉树?树中每个节点最多只能有两个子节点,在JavaScript中一般都是通过Object来模拟二叉树。常用操作前序遍历中序遍历后序遍历前序遍历根左右。口诀:1.访问根节点2.对根节点的左子树进行前序遍历3.对根节点的右子树进行前序遍历通过
菜园前端 菜园前端
2年前
什么是堆?
原文链接:什么是堆?堆是一种特殊的完全二叉树。完全二叉树的含义就是每层节点都完全填满,除了最后一层外只允许最右边缺少若干个节点。在JavaScript中通常用数组表示堆(按照广度优先遍历顺序)。最大堆最小堆特性所有的节点都大于等于它的子节点(最大堆)或者所