前序遍历
递归法
// 所有遍历函数的入参都是树的根结点对象function preorder(root){// 递归边界,root 为空if(!root){return;}// 输出当前遍历的结点值console.log('当前遍历的结点值是:', root.val);preorder(root.left);preorder(root.right);}
迭代法
思路:
- 将根结点入栈
- 取出栈顶结点,将结点值 push 进结果数组
- 若栈顶结点有右孩子,则将右孩子入栈
- 若栈顶结点有左孩子,则将左孩子入栈
/*** @param {TreeNode} root* @return {number[]}*/const preorderTraversal = function(root) {// 定义结果数组const res = []// 处理边界条件if(!root) {return res}// 初始化栈结构const stack = []// 首先将根结点入栈stack.push(root)// 若栈不为空,则重复出栈、入栈操作while(stack.length) {// 将栈顶结点记为当前结点const cur = stack.pop()// 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部res.push(cur.val)// 若当前子树根结点有右孩子,则将右孩子入栈 根左右 右离根远则先判断右边cur.right && stack.push(cur.right)// 若当前子树根结点有左孩子,则将左孩子入栈cur.left && stack.push(cur.left)}// 返回结果数组return res};
后序遍历
递归法
// 所有遍历函数的入参都是树的根结点对象function preorder(root){// 递归边界,root 为空if(!root){return;}preorder(root.left);preorder(root.right);// 输出当前遍历的结点值console.log('当前遍历的结点值是:', root.val);}
迭代法
后序遍历的出栈序列,按照规则应该是 左 -> 右 -> 根 。这个顺序相对于先序遍历,最明显的变化就是根结点的位置从第一个变成了倒数第一个。
和先序遍历二叉树类似,唯一区别是向数组中unshift元素,先push左再push右/*** @param {TreeNode} root* @return {number[]}*/const preorderTraversal = function(root) {// 定义结果数组const res = []// 处理边界条件if(!root) {return res}// 初始化栈结构const stack = []// 首先将根结点入栈stack.push(root)// 若栈不为空,则重复出栈、入栈操作while(stack.length) {// 将栈顶结点记为当前结点const cur = stack.pop()// 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部res.unshift(cur.val)// 若当前子树根结点有左孩子,则将左孩子入栈 左右根 左离根远则先判断左边cur.left && stack.push(cur.left)// 若当前子树根结点有右孩子,则将右孩子入栈cur.right && stack.push(cur.right)}// 返回结果数组return res};
中序遍历
递归法
// 所有遍历函数的入参都是树的根结点对象function preorder(root){// 递归边界,root 为空if(!root){return;}preorder(root.left);// 输出当前遍历的结点值console.log('当前遍历的结点值是:', root.val);preorder(root.right);}
迭代法
/*** @param {TreeNode} root* @return {number[]}*/const inorderTraversal = function(root) {// 定义结果数组const res = []// 初始化栈结构const stack = []// 用一个 cur 结点充当游标let cur = root// 当 cur 不为空、或者 stack 不为空时,重复以下逻辑while(cur || stack.length) {// 这个 while 的作用是把寻找最左叶子结点的过程中,途径的所有结点都记录下来while(cur) {// 将途径的结点入栈stack.push(cur)// 继续搜索当前结点的左孩子cur = cur.left}// 取出栈顶元素cur = stack.pop()// 将栈顶元素入栈res.push(cur.val)// 尝试读取 cur 结点的右孩子cur = cur.right}// 返回结果数组return res};
- 两个 while :内层的 while 的作用是在寻找最左叶子结点的过程中,把途径的所有结点都记录到 stack 里。记录工作完成后,才会走到外层 while 的剩余逻辑里——这部分逻辑的作用是从最左的叶子结点开始,一层层回溯遍历左孩子的父结点和右侧兄弟结点,进而完成整个中序遍历任务。
- 外层while 的两个条件:cur 的存在性和stack.length 的存在性,各自是为了限制什么?
- stack.length 的存在性比较好理解, stack 中存储的是没有被推入结果数组 res 的待遍历元素。只要 stack 不为空,就意味着遍历没有结束, 遍历动作需要继续重复。
- cur 的存在性就比较有趣了。它对应以下几种情况:
- 初始态, cur 指向 root 结点,只要 root 不为空, cur 就不为空。此时判断了 cur 存在后,就会开始最左叶子结点的寻找之旅。这趟“一路向左”的旅途中, cur 始终指向当前遍历到的左孩子。
- 第一波内层 while 循环结束, cur 开始承担中序遍历的遍历游标职责。 cur 始终会指向当前栈的栈顶元素,也就是“一路向左”过程中途径的某个左孩子,然后将这个左孩子作为中序遍历的第一个结果元素纳入结果数组。假如这个左孩子是一个叶子结点,那么尝试取其右孩子时就只能取到 null ,这个 null 的存在,会导致内层循环 while 被跳过,接着就直接回溯到了这个左孩子的父结点,符合 左->根 的序列规则
- 假如当前取到的栈顶元素不是叶子结点,同时有一个右孩子,那么尝试取其右孩子时就会取到一个存在的结点。 cur 存在,于是进入内层 while 循环,重复“一路向左”的操作,去寻找这个右孩子对应的子树里最靠左的结点,然后去重复刚刚这个或回溯、或“一路向左”的过程。如果这个右孩子对应的子树里没有左孩子,那么跳出内层 while 循环之后,紧接着被纳入 res 结果数组的就是这个右孩子本身,符合 根->右 的序列规则
