二叉树遍历思维实战
在算法面试中,二叉树是绕不开的基础核心,而「遍历」更是解决二叉树问题的「万能钥匙」。无论是根到叶的路径统计、层间节点的特殊处理,还是路径值的计算与判断,只要题目围绕二叉树的「树枝」做文章,用遍历思维解题都会无比自然、高效。
很多新手在面对二叉树题时,容易陷入「一题一解」的困境,每道题都重新设计遍历逻辑,既浪费时间又容易出错。实际上,二叉树的遍历题高度同质化,只需掌握两套通用模板,再根据题目需求修改核心业务逻辑,就能轻松搞定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种,覆盖所有路径类题:
-
累计值/中间结果:如路径转数字(129题)、二进制和(1022题),核心更新逻辑:新状态 = 上一层状态 * 基数 + 当前节点值(基数为10、2等);
-
层数/深度:如二叉树右视图(199题DFS解法),传递当前节点层数,实现按层处理;
-
特征统计状态:如伪回文路径(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 递归遍历通用避坑点
-
切勿重复遍历子树(如两次traverse(node.left)),导致右子树无法访问;
-
可修改的结果变量(如res=0、minStr='')需用let声明,const不可重新赋值;
-
path数组+手动回溯时,push和pop必须成对出现,避免路径轨迹混乱;
-
核心业务逻辑仅在叶子节点处理(题目均要求「根到叶路径」);
-
字典序判断需直接比较字符串,不可用节点值和替代。
3.3 通用优化技巧
-
路径优化:路径转数字类问题,优先用动态值计算替代显式path数组,降低空间开销;
-
特征统计优化:节点值范围小时(如1-9),优先用位掩码替代数组/哈希表,空间复杂度降至O(1);
-
提前终止:叶子节点处理完后提前return,避免无意义的递归和回溯;
-
自动回溯:用基本类型(数字、字符串、位掩码)做带参遍历,利用值传递实现自动回溯,简化代码。
3.4 核心解题心法
「模板定结构,业务填细节」------ 二叉树遍历题的核心逻辑高度统一,无需为每道题重新设计遍历框架。只需根据题目场景选择对应的模板,在「核心业务区」编写少量代码即可解决问题。
真正的难点不在于遍历本身,而在于:
-
把题目要求转化为「叶子节点/层内节点的处理规则」;
-
根据数据特征选择最优的轨迹记录方式(显式path/动态值/位掩码)。
四、最后:从模板到灵活运用
本文的模板是「入门抓手」,背会模板能解决90%的二叉树遍历题,但真正的算法能力在于理解模板背后的逻辑,并能灵活调整:
-
递归遍历不仅限于前序,可根据需求在中序/后序位置编写业务逻辑(如利用中序遍历的有序性解决二叉搜索树问题);
-
层序遍历可增加层容器,收集每层的所有节点(如二叉树层序遍历输出二维数组);
-
复杂问题可结合两种遍历思维(如先层序确定树的高度,再递归处理路径)。
通过本文的6道实战题,你已经掌握了遍历思维的核心用法。后续只需多做同类题,熟练运用「模板+优化技巧」,就能轻松搞定所有二叉树遍历相关的高频考点,面试时遇到这类题也能从容应对!