二叉树遍历思维实战

二叉树遍历思维实战

在算法面试中,二叉树是绕不开的基础核心,而「遍历」更是解决二叉树问题的「万能钥匙」。无论是根到叶的路径统计、层间节点的特殊处理,还是路径值的计算与判断,只要题目围绕二叉树的「树枝」做文章,用遍历思维解题都会无比自然、高效。

很多新手在面对二叉树题时,容易陷入「一题一解」的困境,每道题都重新设计遍历逻辑,既浪费时间又容易出错。实际上,二叉树的遍历题高度同质化,只需掌握两套通用模板,再根据题目需求修改核心业务逻辑,就能轻松搞定90%的高频考点。

本文将从「模板提炼→实战拆解→避坑优化」三个维度,手把手教你用遍历思维破解二叉树路径/层序类高频题,结合6道力扣经典真题,让你从「套模板」到「灵活用」,彻底掌握二叉树遍历的解题精髓。

一、先背会两套通用模板:递归+层序,覆盖所有场景

二叉树遍历题可分为两大核心类型,对应两套通用模板,无需记忆复杂逻辑,背会就能直接套用,重点在于理解「模板结构」和「业务填充点」。

1.1 递归遍历模板(前序+回溯):适配根到叶路径类问题

递归遍历的核心是「前序入栈、后序出栈」的回溯思想,适合需要跟踪「根节点到叶子节点完整路径」的场景(如路径收集、路径值计算、路径特征判断)。其优势在于能自然记录节点访问轨迹,代码简洁且逻辑清晰。

以下是优化后的通用模板(两种实现方式,按需选择):

方式1:path数组+手动回溯(直观易懂,新手首选)

javascript 复制代码
function fn(root) {
  // 1. 处理空树边界
  if (root === null) return [];
  // 2. 定义结果容器(视题目需求:数组/字符串/数字)
  const res = [];
  // 3. 定义路径容器,用于回溯记录根到当前节点的轨迹
  const path = [];
  // 4. 启动递归遍历
  traverse(root);
  // 5. 返回最终结果
  return res;

  // 递归遍历辅助函数(闭包共享外层res、path)
  function traverse(node) {
    // 递归终止:空节点直接返回,防止无限递归
    if (node === null) return;

    // 前序位置:进入节点,执行「入栈」操作(核心:记录轨迹)
    path.push(node.val);

    // 【核心业务区】视题目需求编写逻辑(如叶子节点判断、路径处理)
    if (node.left === null && node.right === null) {
      // 叶子节点:通常是路径处理的关键节点(收集/计算/判断)
      res.push([...path]); // 浅拷贝,避免后续回溯修改路径
    }

    // 递归遍历左右子树(必须先左后右,不可重复遍历)
    traverse(node.left);
    traverse(node.right);

    // 后序位置:离开节点,执行「出栈」操作(核心:回溯,恢复轨迹)
    path.pop();
  }
}

方式2:带参遍历+自动回溯(简洁高效,进阶首选)

核心原理:JavaScript的基本类型(数字、字符串、位掩码)是「值传递」,递归时传递的是值的副本,子树修改不会影响上一层,递归返回后自动恢复状态,无需手动执行pop回溯,代码更简洁。

javascript 复制代码
function fn(root) {
  // 1. 处理空树边界
  if (root === null) return [];
  // 2. 定义结果容器(视题目需求:数组/字符串/数字)
  const res = [];
  // 3. 启动递归遍历:初始路径为空,传递给根节点
  traverse(root, []);
  // 4. 返回最终结果
  return res;

  // 递归遍历辅助函数:参数传递「根到当前节点的路径」
  function traverse(node, path) {
    // 递归终止:空节点直接返回
    if (node === null) return;

    // 前序位置:更新路径 → 浅拷贝原路径,添加当前节点值(避免修改上一层路径)
    const newPath = [...path, node.val];

    // 【核心业务区】叶子节点:路径处理(收集/计算/比较/格式化)
    if (node.left === null && node.right === null) {
      res.push(newPath); // 叶子节点处理逻辑,按需修改
      return;
    }

    // 递归遍历左右子树:传递更新后的新路径,自动回溯
    traverse(node.left, newPath);
    traverse(node.right, newPath);
    // 无需手动回溯:newPath是浅拷贝的新数组,不影响上一层的path
  }
}

递归模板核心要点

  • 闭包特性:辅助函数traverse共享外层res和path,无需额外传参,简化代码;

  • 回溯逻辑:方式1需保证push和pop成对出现,方式2借助值传递实现自动回溯;

  • 关键节点:叶子节点(node.left === null && node.right === null)是路径类题的核心处理点,几乎所有路径题都在此处编写核心业务逻辑;

  • 状态传递:带参遍历的核心是传递「上一层节点的状态」(如累计值、路径、位掩码),下一层基于该状态继续更新。

常见状态传递类型(记熟直接套用)

带参遍历的参数本质是「跨节点的状态」,常见类型只有3种,覆盖所有路径类题:

  1. 累计值/中间结果:如路径转数字(129题)、二进制和(1022题),核心更新逻辑:新状态 = 上一层状态 * 基数 + 当前节点值(基数为10、2等);

  2. 层数/深度:如二叉树右视图(199题DFS解法),传递当前节点层数,实现按层处理;

  3. 特征统计状态:如伪回文路径(572题),用位掩码或数组统计路径中节点值的奇偶性、出现次数等特征。

1.2 层序遍历模板(BFS+队列):适配层间节点类问题

层序遍历(广度优先搜索BFS)的核心是「按层处理节点」,通过队列控制「一层一遍历」,适合需要按层操作节点的场景(如取每层最后一个节点、层内节点统计、每层平均值)。其优势在于逻辑直观,符合人类「从上到下、从左到右」的观察习惯。

javascript 复制代码
function fn(root) {
  // 1. 处理空树边界
  if (root === null) return [];
  // 2. 定义结果容器
  const res = [];
  // 3. 初始化队列,存入根节点(队列是层序遍历的核心载体)
  const queue = [root];

  // 层数 
  let level = 0 

  // 4. 层序遍历主循环:队列非空则继续
  while (queue.length) {

    // 遍历到哪层了,如果需要就用
    level++

    // 5. 记录当前层节点数(关键:确定每层的遍历边界)
    const curLevelSize = queue.length;

    // 6. 遍历当前层所有节点
    for (let i = 0; i < curLevelSize; i++) {
      // 7. 取出队首节点(当前层的待处理节点)
      const curNode = queue.shift();

      // 【核心业务区】视题目需求编写逻辑(如取层内特定节点、层内统计)
      // 例:取每层最后一个节点 → if (i === curLevelSize - 1) { ... }

      // 8. 子节点入队:左子节点先入,右子节点后入(保证层内顺序)
      if (curNode.left) queue.push(curNode.left);
      if (curNode.right) queue.push(curNode.right);
    }

    // 可选:整层遍历完成后统一处理(如层内节点汇总)
  }

  // 9. 返回最终结果
  return res;
}

层序模板核心要点

  • 层边界控制:curLevelSize = queue.length是层序遍历的灵魂,确保每次循环只处理「当前层」的节点,不与下一层混淆;

  • 队列操作:shift(取队首)和push(队尾入子节点)配合,实现节点的按层传递;

  • 层内顺序:左子节点先入队,保证层内节点遍历顺序与「从左到右」的直观顺序一致。

二、模板实战:6道力扣高频题手把手拆解

以下6道题覆盖二叉树路径、层序的所有高频考法,全部基于上述模板实现,保留模板核心结构,仅修改「核心业务区」代码,让你直观感受「套模板解题」的高效性。同时标注易踩坑点和优化技巧,兼顾正确性和解题效率。

2.1 递归实战1:二叉树的所有路径(力扣257)

题目要求

给你二叉树的根节点,按任意顺序返回所有从根节点到叶子节点的路径,路径格式为"1->2->3"。

解题思路

  • 模板:直接套用「递归遍历模板(path数组+手动回溯)」;

  • 核心业务:叶子节点处将path数组格式化为"->"连接的字符串,存入结果容器;

  • 优化:叶子节点处提前回溯+return,避免后续无意义的代码执行。

套模板实现代码(直接通关)

javascript 复制代码
var binaryTreePaths = function(root) {
  if (root === null) return [];
  const res = [];
  const path = [];
  traverse(root);
  return res;

  function traverse(node) {
    if (node === null) return;
    path.push(node.val); // 前序入栈

    // 核心业务:叶子节点格式化路径并收集
    if (node.left === null && node.right === null) {
      res.push(path.join('->'));
      path.pop(); // 叶子节点提前回溯,优化效率
      return;
    }

    traverse(node.left);
    traverse(node.right);
    path.pop(); // 后序出栈
  }
};

2.2 递归实战2:求根节点到叶节点数字之和(力扣129)

题目要求

树中每个节点存0-9的数字,根到叶的路径代表一个数字(如1→2→3代表123),计算所有根到叶数字的和。

解题思路

  • 模板:基于「递归遍历模板(带参遍历)」优化,无需显式path数组;

  • 核心优化:路径值通过curNum = curNum * 10 + node.val动态计算(值传递,无需回溯),替代path数组,降低空间复杂度;

  • 核心业务:叶子节点处将动态计算的路径值累加到结果。

套模板优化代码(最优解)

javascript 复制代码
function sumNumbers(root) {
  // 1. 处理空树边界:空树返回0(数字,匹配题目要求)
  if (root === null) return 0;
  // 2. 定义结果容器:数字类型,初始为0
  let res = 0;
  // 3. 启动递归遍历:初始前序和为0(无节点时的基础值)
  traverse(root, 0);
  // 4. 返回最终结果
  return res;

  // 带参遍历函数:prevSum=根到当前节点父节点的路径和(上一层状态)
  function traverse(node, prevSum) {
    // 递归终止:空节点直接返回
    if (node === null) return;

    // 前序位置:更新状态,计算根到当前节点的路径和(核心)
    const curSum = 10 * prevSum + node.val;

    // 【核心业务区】叶子节点:累加路径和到结果
    if (node.left === null && node.right === null) {
      res += curSum; // 累加
      return; // 提前返回,避免无意义的递归
    }

    // 递归遍历左右子树:传递当前状态给下一层
    traverse(node.left, curSum);
    traverse(node.right, curSum);

    // 无需后序回溯:prevSum/curSum是值传递,递归返回后自动恢复上一层状态
  }
}

2.3 层序实战:二叉树的右视图(力扣199)

题目要求

想象站在二叉树右侧,按从上到下顺序返回能看到的节点值(左子树更高时,能看到左子树高出的部分)。

解题思路

  • 模板:直接套用「层序遍历模板」;

  • 核心业务:每层遍历的最后一个节点(i === curLevelSize - 1)即为右侧能看到的节点,直接存入结果。

套模板实现代码(最优解)

javascript 复制代码
var rightSideView = function(root) {
  if (root === null) return [];
  const res = [];
  const queue = [root];

  while (queue.length) {
    const curLevelSize = queue.length;
    for (let i = 0; i < curLevelSize; i++) {
      const curNode = queue.shift();

      // 核心业务:取每层最后一个节点
      if (i === curLevelSize - 1) {
        res.push(curNode.val);
      }

      if (curNode.left) queue.push(curNode.left);
      if (curNode.right) queue.push(curNode.right);
    }
  }

  return res;
};

2.4 递归实战3:从叶结点开始的最小字符串(力扣988)

题目要求

节点值为0-25(对应a-z),返回从叶节点到根节点的字典序最小字符串(如"z" < "ab")。

解题思路

  • 模板:套用「递归遍历模板(带参遍历)」,用字符串传递路径;

  • 核心技巧:头部拼接字符串(curChar + path),天然生成叶→根的路径,无需反转;

  • 核心业务:叶子节点处将生成的字符串与当前最小值比较,更新最小字符串;

  • 避坑点:切勿用节点值和判断字典序,需直接比较字符串本身。

套模板实现代码(避坑版)

javascript 复制代码
function smallestFromLeaf(root) {
  // 1. 处理空树边界:题目要求空树返回空字符串
  if (root === null) return '';
  // 2. 定义结果容器:初始为空字符串,用于存储字典序最小的叶→根字符串
  let res = '';
  // 3. 启动递归遍历:初始路径为空字符串(根节点无父节点)
  traverse(root, '');
  // 4. 返回最终结果
  return res;

  // 带参遍历函数:path - 父节点到根节点的字符拼接字符串(叶→根格式)
  function traverse(node, path) {
    // 递归终止:空节点直接返回
    if (node === null) return;

    // 前序位置:头部拼接,天然生成当前节点到根的叶→根字符串
    const curChar = String.fromCharCode(node.val + 97); // 0→a,25→z
    const newPath = curChar + path;

    // 【核心业务区】叶子节点:比较并更新字典序最小的字符串
    if (node.left === null && node.right === null) {
      // 首次赋值 或 当前字符串字典序更小,更新结果
      if (res === '' || newPath < res) {
        res = newPath;
      }
      return; // 提前返回,避免无意义的递归
    }

    // 递归遍历左右子树:传递新路径,自动回溯
    traverse(node.left, newPath);
    traverse(node.right, newPath);
  }
}

2.5 递归实战4:从根到叶的二进制数之和(力扣1022)

题目要求

节点值为0或1,根到叶的路径代表二进制数(最高有效位开始),计算所有二进制数的十进制和。

解题思路

  • 模板:同129题,基于「递归遍历模板(带参遍历)」,动态计算二进制值;

  • 核心优化:二进制转十进制通过curNum = curNum * 2 + node.val实现(左移1位+当前值),效率高于幂运算;

  • 核心业务:叶子节点处将动态计算的十进制值累加到结果。

套模板优化代码(最优解)

javascript 复制代码
function sumRootToLeaf(root) {
  // 1. 处理空树边界
  if (root === null) return 0;
  // 2. 定义结果容器
  let res = 0;
  // 3. 启动递归遍历:初始前序和为0
  traverse(root, 0);
  // 4. 返回最终结果
  return res;

  // 带参遍历函数:prevSum=根到当前节点父节点的二进制转十进制和
  function traverse(node, prevSum) {
    if (node === null) return;

    // 前序位置:更新二进制转十进制的和(左移1位+当前值)
    const curSum = 2 * prevSum + node.val;

    // 【核心业务区】叶子节点:累加结果
    if (node.left === null && node.right === null) {
      res += curSum;
    }

    // 递归遍历左右子树
    traverse(node.left, curSum);
    traverse(node.right, curSum);
  }
}

2.6 递归实战5:二叉树中的伪回文路径(力扣572)

题目要求

节点值为1-9,根到叶路径为「伪回文」指路径值的排列能形成回文,统计伪回文路径的数目。

解题思路

  • 模板:基于「递归遍历模板(带参遍历)」,用位掩码替代path统计路径特征;

  • 核心知识点:伪回文判断条件 → 路径中奇数次数的数字≤1(偶数长度为0,奇数长度为1);

  • 位掩码原理:用9位二进制数统计1-9的出现奇偶性(0=偶,1=奇),叶子节点处通过mask & (mask-1) === 0判断;

  • 核心业务:叶子节点处判断位掩码是否满足伪回文条件,满足则结果+1。

套模板优化代码(位掩码最优解)

javascript 复制代码
function pseudoPalindromicPaths(root) {
  // 1. 处理空树边界
  if (root === null) return 0;
  // 2. 定义结果容器(统计伪回文路径数量)
  let res = 0;
  // 3. 启动递归遍历:初始位掩码为0(全偶)
  traverse(root, 0);
  // 4. 返回最终结果
  return res;

  // 带参遍历函数:mask=位掩码(统计1-9出现次数的奇偶性)
  function traverse(node, mask) {
    if (node === null) return;

    // 前序位置:更新位掩码(异或翻转对应位,0↔1)
    const newMask = mask ^ (1 << (node.val - 1));

    // 【核心业务区】叶子节点:判定伪回文
    if (node.left === null && node.right === null) {
      // 二进制中1的数量≤1,即为伪回文
      if ((newMask & (newMask - 1)) === 0) {
        res++;
      }
      return;
    }

    // 递归遍历左右子树,自动回溯
    traverse(node.left, newMask);
    traverse(node.right, newMask);
  }
}

基础版替代方案(数组统计,适合对位掩码不熟悉的新手)

javascript 复制代码
function pseudoPalindromicPaths(root) {
  if (root === null) return 0;
  let res = 0;
  traverse(root, new Array(10).fill(0), true);
  return res;

  function traverse(node, recordArr, isEven) {
    if (node === null) return;

    // 前序位置:更新数组统计奇偶性
    const newRecordArr = [...recordArr];
    newRecordArr[node.val] = (newRecordArr[node.val] + 1) % 2;
    isEven = !isEven;

    // 【核心业务区】叶子节点:判定伪回文
    if (node.left === null && node.right === null) {
      if (isEven) {
        if (newRecordArr.every(item => item === 0)) res++;
        return;
      }
      let oneCount = 0;
      for (let i = 0; i < newRecordArr.length; i++) {
        if (newRecordArr[i] === 1) {
          oneCount++;
          if (oneCount > 1) return;
        }
      }
      if (oneCount === 1) res++;
      return;
    }

    traverse(node.left, newRecordArr, isEven);
    traverse(node.right, newRecordArr, isEven);
  }
}

三、遍历思维解题核心总结(避坑+优化)

3.1 两大模板适用场景速判

遍历模板 核心适用场景 典型题目 核心优势
递归遍历(前序+回溯) 根到叶路径类问题(需跟踪节点轨迹) 257、129、988、1022、572 天然记录路径,回溯逻辑简单,可灵活优化
层序遍历(BFS+队列) 层间节点类问题(需按层处理节点) 199、层序遍历、每层平均值 按层处理,逻辑直观,符合人类观察习惯

3.2 递归遍历通用避坑点

  1. 切勿重复遍历子树(如两次traverse(node.left)),导致右子树无法访问;

  2. 可修改的结果变量(如res=0、minStr='')需用let声明,const不可重新赋值;

  3. path数组+手动回溯时,push和pop必须成对出现,避免路径轨迹混乱;

  4. 核心业务逻辑仅在叶子节点处理(题目均要求「根到叶路径」);

  5. 字典序判断需直接比较字符串,不可用节点值和替代。

3.3 通用优化技巧

  1. 路径优化:路径转数字类问题,优先用动态值计算替代显式path数组,降低空间开销;

  2. 特征统计优化:节点值范围小时(如1-9),优先用位掩码替代数组/哈希表,空间复杂度降至O(1);

  3. 提前终止:叶子节点处理完后提前return,避免无意义的递归和回溯;

  4. 自动回溯:用基本类型(数字、字符串、位掩码)做带参遍历,利用值传递实现自动回溯,简化代码。

3.4 核心解题心法

「模板定结构,业务填细节」------ 二叉树遍历题的核心逻辑高度统一,无需为每道题重新设计遍历框架。只需根据题目场景选择对应的模板,在「核心业务区」编写少量代码即可解决问题。

真正的难点不在于遍历本身,而在于:

  • 把题目要求转化为「叶子节点/层内节点的处理规则」;

  • 根据数据特征选择最优的轨迹记录方式(显式path/动态值/位掩码)。

四、最后:从模板到灵活运用

本文的模板是「入门抓手」,背会模板能解决90%的二叉树遍历题,但真正的算法能力在于理解模板背后的逻辑,并能灵活调整:

  • 递归遍历不仅限于前序,可根据需求在中序/后序位置编写业务逻辑(如利用中序遍历的有序性解决二叉搜索树问题);

  • 层序遍历可增加层容器,收集每层的所有节点(如二叉树层序遍历输出二维数组);

  • 复杂问题可结合两种遍历思维(如先层序确定树的高度,再递归处理路径)。

通过本文的6道实战题,你已经掌握了遍历思维的核心用法。后续只需多做同类题,熟练运用「模板+优化技巧」,就能轻松搞定所有二叉树遍历相关的高频考点,面试时遇到这类题也能从容应对!

相关推荐
宝贝儿好3 小时前
第二章: 图像处理基本操作
算法
鹏多多3 小时前
移动端H5项目,还需要react-fastclick解决300ms点击延迟吗?
前端·javascript·react.js
小陈phd3 小时前
多模态大模型学习笔记(二)——机器学习十大经典算法:一张表看懂分类 / 回归 / 聚类 / 降维
学习·算法·机器学习
@––––––4 小时前
力扣hot100—系列4-贪心算法
算法·leetcode·贪心算法
不想秃头的程序员4 小时前
Vue3 封装 Axios 实战:从基础到生产级,新手也能秒上手
前端·javascript·面试
CoovallyAIHub4 小时前
让本地知识引导AI追踪社区变迁,让AI真正理解社会现象
深度学习·算法·计算机视觉
爱装代码的小瓶子4 小时前
【C++与Linux基础】进程间通讯方式:匿名管道
android·c++·后端
CoderCodingNo4 小时前
【GESP】C++ 二级真题解析,[2025年12月]第一题环保能量球
开发语言·c++·算法
yumgpkpm4 小时前
预测:2026年大数据软件+AI大模型的发展趋势
大数据·人工智能·算法·zookeeper·kafka·开源·cloudera