JS/TS数据结构---二叉树基础

树是一种非线性表数据结构,树的基本概念如下所列。
  (1)结点高度:结点到叶子结点的最长路径(即边数)。例题:112. 路径总和
  (2)结点深度:根结点到这个结点所经历的边的个数。例题:104. 二叉树的最大深度
  (3)结点层数:结点深度加 1。
  (4)树的高度:根结点的高度。例题:面试题 04.02. 最小高度树
  后面几张这种类型的图都来源于《数据结构与算法之美》。

img

(5)二叉树:只包含左右两个子结点的树(编号1)。
  (6)满二叉树:所有分支结点都存在左右子树,并且所有叶子结点都在同一层上(编号2)。例题:894. 所有可能的满二叉树

​ (7)真二叉树:所有节点的度都要么为0,要么为2。------------满二叉树一定是真二叉树,真二叉树不一定是满二叉树。

img

(8)完全二叉树:叶子结点都在最底下两层,最后一层的叶子结点都靠左排列,并且除了最后一层,其余结点个数都要达到最大(编号3)。例题:222. 完全二叉树的结点个数

img

二叉树

1)实现

有两种方法存储一棵二叉树,第一种是基于指针的链式存储法,如下所示

class Node {
  constructor(data) {
    this.data = data;
    this.left = null;
    this.right = null;
  }
}
class TreeList {
  constructor(datas) {
    this.root = null;
    datas.forEach((value) => {
      const node = new Node(value);
      if (this.root == null) {
        this.root = node;
        return;
      }
      this.insert(this.root, node);
    });
  }
  insert(parent, child) {
    if (parent.data > child.data) {
      parent.left === null
        ? (parent.left = child)
        : this.insert(parent.left, child);
      return;
    }
    parent.right === null
      ? (parent.right = child)
      : this.insert(parent.right, child);
  }
}

第二种是基于数组的顺序存储法。

left = 2 * index + 1;        //左结点下标
right = 2 * index + 2;       //右结点下标

例题:LeetCode的236. 二叉树的最近公共祖先,递归的在左右子树中查找两个指定的结点,最后判断公共祖先所在的位置。在当前结点的左子树,或在其右子树,又或者它就是两种的公共祖先。

2)遍历

二叉树的遍历有四种(示例如下):

前序

先访问当前结点,然后访问左子树,再访问右子树。

  preOrder(root = this.root) {
    //前序
    if (!root) {
      return;
    }
    console.log(root.data);
    this.preOrder(root.left);
    this.preOrder(root.right);
  }

img

1->7->5->3->6->9->8->10->15->13->12->14->20->18->25

​ 面试题28对称二叉树。前序遍历的变种是先访问右结点,再访问左结点,如果其遍历结果与前序遍历结果相同,就说明是对称的。

​ 面试题34二叉树中和为某一值的路径。前序遍历首先访问根结点,在用前序遍历访问结点时,将其压入栈中,遇到叶结点,就求和判断结果是否符合要求。然后将叶结点出栈,回到父节点,继续遍历右子树,递归执行该过程。

中序

先访问左子树,然后访问当前结点,再访问右子树。

  inOrder(root = this.root) {
    //中序
    if (!root) {
      return;
    }
    this.midOrder(root.left);
    console.log(root.data);
    this.midOrder(root.right);
  }

img

面试题7重建二叉树。前序遍历第一个数是根结点,中序遍历以根结点为界其两边分别是左右子树,递归构建左右子树。
  面试题54BST中第 k 大的结点。中序遍历BST,得到的序列是递增的。

后序

先访问左子树,然后访问右子树,再访问当前结点。

  postOrder(root = this.root) {
    //后序
    if (!root) {
      return;
    }
    this.backOrder(root.left);
    this.backOrder(root.right);
    console.log(root.data);
  }

img

面试题33BST的后序遍历序列。序列的最后一个数字是根结点,左子树的结点都比根结点小,右子树的结点都比根结点大,递归执行该过程。

层序

自上而下,自左至右逐层访问树的结点。利用一个辅助队列来完成层序遍历。

  levelOrder(node = this.root) {
    //层序
    let queue = [];
    queue.push(node);               // 根结点入队
    while (queue.length) {
      node = queue.shift();         // 出队
      console.log(node.data);       // 访问该结点
      if (node.left) {
        // 如果它的左子树不为空
        queue.push(node.left);      // 将左子树的根结点入队
      }
      if (node.right) {
        // 如果它的右子树不为空
        queue.push(node.right);     // 将右子树的根结点入队
      }
    }
  }

除了层序遍历之外,其余三种都采用递归的方式来遍历二叉树

有两种图的搜索算法,也适用于树

(1)广度优先搜索算法(Breadth-First Search,BFS)会从根结点开始遍历,先访问其所有的相邻点,就像一次访问树的一层,也就是先宽后深地访问结点,之前的层序遍历就是BFS,如下图左半部分。
  (2)深度优先搜索算法(Depth-First-Search,DFS)会从根结点开始遍历,沿着路径直到这条路径最后一个叶结点被访问,接着原路回退并探索下一条路径,也就是先深度后广度地访问结点,如下图右半部分。

img

在《算法小抄》一文中曾强调先刷二叉树的LeetCode题目,因为很多难题本质上都是基于二叉树的遍历,例如LeetCode的124 题(二叉树中的最大路径和)、105 题(从前序与中序遍历序列构造二叉树)和99 题(恢复二叉搜索树)。

3)递归

递归是一种应用广泛的编程技巧,如果要使用递归,需要满足三个条件。
  (1)一个问题的解可以分解为几个子问题的解。
  (2)这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
  (3)存在递归终止条件,即基线条件(Base Case)。
  注意,递归的关键就是找到将大问题分解为小问题的规律(推荐画出递归树),基于此写出递推公式,然后再推敲终止条件,并且需要警惕重复计算。下面是一个递归的大致模板。

function recursion(level, param1, param2, ...) {
  //递归的终止条件
  if(level > MAX_LEVEL) {
    console.log("result");
    return;
  }
  //数据处理
  processData(level, data1,...);
  //继续递归
  recursion(level + 1, p1, ...);
  //收尾工作
  reverseState(level);
}

递归的数学模型就是归纳法,其过程如下。
  (1)基础情况:证明 P(b)语句成立,该步骤只需带入数字即可。
  (2)声明假设:假设 P(n)语句成立。
  (3)归纳步骤:证明如果 P(n)语句成立,那么 P(n+1) 语句也一定成立。
  例如设计一程序,求自然数 N 的阶乘 N!。
  (1)当 N=1 时,N!=1。
  (2)假设 P(N)=N!,P(N+1)=(N+1)!。
  (3)证明 P(N) 和 P(N+1) 的关系:

P(N+1) = (N+1)! = (N+1)×(N)×…×2×1 = (N+1)×N! = (N+1)×P(N)

根据这个公式可构造一个递归函数:

function factorial(N) {
  return N * factorial(N - 1);   //递归部分
}

在采用数学归纳法设计递归程序后,就能摆脱每一步的递推,直接根据分析就能转化为代码。
  试图想清楚整个递和归过程的做法,实际上是一种思维误区,不符合人脑平铺直叙的思维方式。

leetcode题精选

二叉树遍历---几乎所有二叉树题都需要先进行遍历

  • 前序遍历------先访问当前结点,再访问左子树,再访问右子树
  • 中序遍历------先访问左子树,再访问当前结点,再访问右子树
  • 后序遍历------先访问左子树,再访问右子树,再访问当前结点
  • 层序遍历------自上而下,自左而右,逐个访问

前序

[144] 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:

输入:root = [1,null,2,3]
输出:[1,2,3]

img

示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]
示例 4:

输入:root = [1,2]
输出:[1,2]
示例 5:

输入:root = [1,null,2]
输出:[1,2]

提示:

树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

递归法

/**
 * Definition for a binary tree node.
 * class TreeNode {
 *     val: number
 *     left: TreeNode | null
 *     right: TreeNode | null
 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.left = (left===undefined ? null : left)
 *         this.right = (right===undefined ? null : right)
 *     }
 * }
 */

function preorderTraversal(root: TreeNode | null): number[] {
    if(!root) return [];
    const res : number[] = [];
    helper(root);
    return res;

    //编写递归函数
    function helper(node:TreeNode|null){
        if(!node) return;
        res.push(node.val);
        if(node.left) helper(node.left);
        if(node.right) helper(node.right);
    }

};

迭代法

迭代先序遍历利用了栈的特性,直接先将根节点入栈,然后开始循环:出栈一个元素,存储节点值,若该节点有右节点,入栈,若该节点有左节点,入栈。直到栈空为止。(后进先出)

function preorderTraversal(root: TreeNode | null): number[] {
    if(!root) return [];
    const res : number[] = [];

    const stack :TreeNode[] = [root];

    while(stack.length > 0){
        const node = stack.pop();
        res.push(node.val);
        node.right&&stack.push(node.right);
        node.left&&stack.push(node.left);
    }

    return res;
};

[101] 对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

img

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

img

输入:root = [1,2,2,null,3,null,3]
输出:false

方法一:递归

思路和算法

如果一个树的左子树与右子树镜像对称,那么这个树是对称的。

fig1

因此,该问题可以转化为:两个树在什么情况下互为镜像?

如果同时满足下面的条件,两个树互为镜像:

  • 它们的两个根结点具有相同的值
  • 每个树的右子树都与另一个树的左子树镜像对称

我们可以实现这样一个递归函数,通过「同步移动」两个指针的方法来遍历这棵树,p 指针和 q 指针一开始都指向这棵树的根,随后 p 右移时,q 左移,p 左移时,q 右移。每次检查当前 p 和 q 节点的值是否相等,如果相等再判断左右子树是否对称。

代码如下。

function isSymmetric(root: TreeNode | null): boolean {

    function helper(left:TreeNode|null, right:TreeNode|null){
        if(!left && !right) return true;
        if(!left||!right) return false;
        return left.val === right.val && helper(left.left,right.right) && helper(left.right,right.left);  
    }
    return helper(root,root)
};

方法二:迭代加队列


剑指 Offer 34. 二叉树中和为某一值的路径

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

示例 2:

img

输入:root = [1,2,3], targetSum = 5
输出:[]

示例 3:

输入:root = [1,2], targetSum = 0
输出:[]

//递归回溯:如果条件不符合,回到上一步另一个选择进行递归
function pathSum(root: TreeNode | null, target: number): number[][] {
   
   if(!root) return [];
   const res : number[][] = [];
   helper(root,target,[]);
   return res;
   function helper(node: TreeNode | null,target:number,path:number[]){
        path.push(node.val);
        if(!node.left&&!node.right&&node.val===target){     //该结点是叶子结点
            res.push(path.slice());
        }
        //使用slice进行浅拷贝创建新数组,不改变 左右情况
        node.left&&helper(node.left,target-node.val,path.slice());
        node.right&&helper(node.right,target-node.val,path.slice());
   }   
    
};


中序

[94] 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

img

示例 1:

输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]

递归

function inorderTraversal(root: TreeNode | null): number[] {
    if(!root) return [];
    const res : number[] = [];
    helper(root);
    return res;

    function helper(node:TreeNode|null){
        if(!node) return;
        node.left&&helper(node.left);
        res.push(node.val);
        node.right&&helper(node.right);
    }
};

迭代

迭代中序遍历利用了栈的特性。先将根节点入栈,指针指向根节点的左子节点,然后开始循环:

  1. 指针节点所有左子节点全部依次入栈,
  2. 取出栈顶节点,存储节点值,
  3. 该节点若有右节点,指针指向其右节点。

直到节点栈空并且取出节点没有右子节点为止。

function inorderTraversal(root: TreeNode | null): number[] {
    if(!root) return [];
    const res : number[] = [];
    const stack : TreeNode[] = [root];

    //先将指针指向左结点
    let cur = root.left;

    while(cur||stack.length > 0){
        //将所有左子结点入栈
        while(cur){
            stack.push(cur);
            cur = cur.left;
        }
        //出栈并读取结点值
        cur = stack.pop();
        res.push(cur.val);
        //指针指向右子节点;
        cur = cur.right
    }
    return res;
};

[105] 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

img

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

示例 2:

输入: preorder = [-1], inorder = [-1]
输出: [-1]

方法一:递归

思路

对于任意一颗树而言,前序遍历的形式总是

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。

这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。

细节

在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要 O(1) 的时间对根节点进行定位了。

//前序遍历第一个数是根结点,中序遍历以根结点为界其两边分别是左右子树,递归构建左右子树。
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {

    if(!inorder.length) return null;
    const root = new TreeNode(preorder[0]);
    const left1 = inorder.indexOf(preorder[0])

    //对左右子树分别递归调用
    root.left = buildTree(preorder.slice(1,left1+1),inorder.slice(0,left1));
    root.right = buildTree(preorder.slice(left1+1),inorder.slice(left1+1));

    return root
};

方法二:迭代




后序

[145] 二叉树的后序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。

img

示例 1:

输入:root = [1,null,2,3]
输出:[3,2,1]
示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]

递归法

function postorderTraversal(root: TreeNode | null): number[] {
    if(!root) return[];
    const res:number[] = [];
    helper(root);
    return res;
    function helper(node:TreeNode|null){
        if(!node) return;
        node.left&&helper(node.left);
        node.right&&helper(node.right);
        res.push(node.val);
    }
};

迭代法

迭代后序遍历利用了栈的特性,直接先将根节点入栈,然后开始循环:

  1. 记录当前栈顶节点。
  2. 若该节点为叶子节点,或者该节点的左节点与右节点都已经被遍历过,则存储该节点值,并出栈栈顶节点,使用一个变量记录。
  3. 否则,若该节点有右节点,入栈,若该节点有左节点,入栈。

直到栈空为止。

function postorderTraversal(root: TreeNode | null): number[] {
    if(!root) return[];
    const res:number[] = [];
    const stack :TreeNode[] = [root];
    //永远指向上一次被读取的节点
    let prev = root;

    while(stack.length>0){
        const node = stack[stack.length-1];
        // 当前节点为叶子节点,或者所有子节点都已被访问过(最后访问子树根节点),则读取该节点
        if((!node.left&&!node.right)||node.left === prev||node.right===prev){
            res.push(node.val);
            prev = stack.pop();
        }else{
            node.right&&stack.push(node.right);
            node.left&&stack.push(node.left);
        }

    }

    return res;
};

层序

[102] 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

img

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:

输入:root = [1]
输出:[[1]]
示例 3:

输入:root = []
输出:[]

层序:自上而下,自左至右逐层访问树的结点。利用一个辅助队列来完成层序遍历。

一般使用迭代进行

  • 首先根元素入队
  • 当队列不为空的时候
    • 求当前队列的长度 s_is**i
    • 依次从队列中取 s_is**i 个元素进行拓展,然后进入下一次迭代
function levelOrder(root: TreeNode | null): number[][] {
    const res :number[][] = [];
    if(!root){
        return []
    }
    type tree = {
        node:TreeNode;
        level:number
    }
    let quene : tree[] = [{node:root,level:0}]

    while(quene.length>0){
        let len = quene.length; 
        res.push([]);
        for(let i = 1; i <= len; ++i){
            let index = quene.shift(); //出队
            let node = index.node;
            res[index.level].push(node.val);
            node.left&&quene.push({node:node.left,level:index.level+1});
            node.right&&quene.push({node:node.right,level:index.level+1});
        }
    }
    return res
};

function levelOrder(root: TreeNode | null): number[][] {
    const ret = [];
    if (!root) {
        return ret;
    }

    const q = [root];
    // q.push(root);
    while (q.length !== 0) {
        const currentLevelSize = q.length;
        ret.push([]);
        for (let i = 1; i <= currentLevelSize; ++i) {
            const node = q.shift();
            ret[ret.length - 1].push(node.val); //因为ret.push了一个空数组,所以当前索引是ret.length-1
            if (node.left) q.push(node.left);
            if (node.right) q.push(node.right);
        }
    }
        
    return ret;

};

[103] 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
示例 2:

输入:root = [1]
输出:[[1]]
示例 3:

输入:root = []
输出:[]

跟上题类似

//错误解法,不能简单地改变进入队列的顺序,而是应该改变出队列的顺序

// function zigzagLevelOrder(root: TreeNode | null): number[][] {
//         if(!root) return [];

//         const res:number[][] = [];
//         type tree = {
//             node:TreeNode;
//             level:number;
//         }
//         const q : tree[] = [{node:root,level:0}];

//         while(q.length>0){
//             const len = q.length;
//             res.push([]);
//             for(let i = 1; i <= len; ++i){
//                 let index = q.shift();
//                 let node = index.node;
//                 res[index.level].push(node.val);
//                 if(index.level%2===1){
//                     node.left&&q.push({node:node.left,level:index.level+1});
//                     node.right&&q.push({node:node.right,level:index.level+1});
//                 }else{
//                     node.right&&q.push({node:node.right,level:index.level+1});  
//                     node.left&&q.push({node:node.left,level:index.level+1});                    
//                 }
//             }
            
//         }
//         return res
// };

// 应该改变它们进入res数组的顺序
function zigzagLevelOrder(root: TreeNode | null): number[][] {
        if(!root) return [];

        const res:number[][] = [];
        type tree = {
            node:TreeNode;
            level:number;
        }
        const q : tree[] = [{node:root,level:0}];

        while(q.length>0){
            const len = q.length;
            res.push([]);
            for(let i = 1; i <= len; ++i){

                let index = q.shift();
                let node = index.node;
                if(index.level%2==0){
                    res[index.level].push(node.val);
                }else{
                    res[index.level].unshift(node.val);
                }
                
                node.left&&q.push({node:node.left,level:index.level+1});
                node.right&&q.push({node:node.right,level:index.level+1});
            
            }
            
        }
        return res
};

DFS/BFS----有很多

[111] 二叉树的最小深度------BFS

这道题经典的BFS题型。从BFS的角度来考虑,只要出现某一层中的节点为叶子节点,那么直接返回当前所在层级即可。

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

function minDepth(root: TreeNode | null): number {
  if (root === null) return 0;

  let res = 0;
  const queue: TreeNode[] = [];
  queue.unshift(root);

  while (queue.length > 0) {
    // 对于每一层,进行一次遍历,入队子节点操作
    res += 1;
    let count = queue.length;
    while (count) {
      const curr = queue.pop();
      // 此时为叶子节点,直接返回结果
      if (!curr.left && !curr.right) {
        return res;
      }
      if (curr.left) {
        queue.unshift(curr.left);
      }
      if (curr.right) {
        queue.unshift(curr.right);
      }
      count -= 1;
    }
  }
  return res;
}

剑指 Offer 34. 二叉树中和为某一值的路径

前面使用递归的方法做过一次,这次使用DFS/BFS

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

示例 2:

img

输入:root = [1,2,3], targetSum = 5
输出:[]

示例 3:

输入:root = [1,2], targetSum = 0
输出:[]

DFS

较为直观的做法是使用 DFS,在 DFS 过程中记录路径以及路径对应的元素和,当出现元素和为 target,且到达了叶子节点,说明找到了一条满足要求的路径,将其加入答案。

使用 DFS 的好处是在记录路径的过程中可以使用「回溯」的方式进行记录及回退,而无须时刻进行路径数组的拷贝。

代码:

let ans: number[][]
let t
function pathSum(root: TreeNode | null, target: number): number[][] {
    ans = new Array<Array<number>>()
    t = target
    dfs(root, 0, new Array<number>())
    return ans
};
function dfs(root: TreeNode | null, cur: number, list: Array<number>): void {
    if (root == null) return 
    list.push(root.val)
    if (cur + root.val == t && root.left == null && root.right == null) ans.push(list.slice())
    dfs(root.left, cur + root.val, list)
    dfs(root.right, cur + root.val, list)
    list.pop()
}

时间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)
空间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)

BFS
使用 BFS 的话,我们需要封装一个类/结构体 TNode,该结构体存储所对应的原始节点 node,到达 node 所经过的路径 list,以及对应的路径和 tot。

由于 BFS 过程并非按照路径进行(即相邻出队的节点并非在同一路径),因此我们每次创建新的 TNode 对象时,需要对路径进行拷贝操作。

代码:

class TNode {
    node: TreeNode
    tot: number
    list: Array<number>
    constructor(node: TreeNode, tot: number, list: Array<number>) {
        this.node = node; this.tot = tot; this.list = list.slice();
        this.list.push(node.val)
        this.tot += node.val
    }
}
function pathSum(root: TreeNode | null, target: number): number[][] {
    const ans = new Array<Array<number>>()
    const stk = new Array<TNode>()
    let he = 0, ta = 0
    if (root != null) stk[ta++] = new TNode(root, 0, new Array<number>())
    while (he < ta) {
        const t = stk[he++]
        if (t.tot == target && t.node.left == null && t.node.right == null) ans.push(t.list)
        if (t.node.left != null) stk[ta++] = new TNode(t.node.left, t.tot, t.list)
        if (t.node.right != null) stk[ta++] = new TNode(t.node.right, t.tot, t.list)
    }
    return ans
};

时间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)
空间复杂度:最坏情况所有路径均为合法路径,复杂度为 O(n×h)

[104] 二叉树的最大深度------常考

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

3
/
9 20
/
15 7

返回它的最大深度 3 。

队列+BFS

function maxDepth(root: TreeNode | null): number {
    if (!root) return 0;

    // 队列
    let queue = [root];
    let maxDepth = 0;

    while (queue.length) {
        let len = queue.length;
        //每次遍历一层 用len来控制次数
        while (len > 0) {
            let node = queue.shift();
            if (node.left) {
                queue.push(node.left);
            }
            if (node.right) {
                queue.push(node.right);
            }
            len--;
        }
        maxDepth++;
    }

    return maxDepth;
};

DFS 自底向上递归

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
  if (root === null) {
    return 0;
  }
  return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; 
};

DFS 自顶向下递归

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
  let ans = 0;

  const dfs = function (node, depth) {
    if (node === null) {
      return;
    }
    if (node.left === null && node.right === null) {
      ans = Math.max(ans, depth);
    }

    dfs(node.left, depth + 1);
    dfs(node.right, depth + 1);
  }

  dfs(root, 1);

  return ans;
};

[226] 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:

输入:root = [2,1,3]
输出:[2,3,1]
示例 3:

输入:root = []
输出:[]

万能的递归

function invertTree(root: TreeNode | null): TreeNode | null {

    if(!root) return null;
    
    return new TreeNode(
        root.val,
        invertTree(root.right),
        invertTree(root.left),
    )
};

万能的DFS

function invertTree(root: TreeNode | null): TreeNode | null {

  const dfs = (node: TreeNode | null) => {
    // 叶子节点弹出
    if(!node){
      return;
    }

    // 交换节点
    const nodeLeft = node.left;
    node.left = node.right;
    node.right = nodeLeft;

    // 左树优先入栈
    dfs(node.left);
    // 右树入栈
    dfs(node.right);
  }

  dfs(root);
  return root;

};

[297] 二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例 1:

img

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]
示例 4:

输入:root = [1,2]
输出:[1,2]

BFS 解法
序列化——很典型的 BFS

  • 维护一个队列,初始让根节点入列,考察出列节点:
    • 如果出列的节点是 null,将符号 'X' 推入 res 数组。
    • 如果出列的节点是数值,将节点值推入数组 res,并将它的左右子节点入列。
      • 子节点 null 也要入列,它对应 "X",要被记录,只是它没有子节点可入列。
  • 入列、出列…直到队列为空,就遍历完所有节点,res构建完毕,转成字符串就好。
/*
 * Encodes a tree to a single string.
 */
function serialize(root: TreeNode | null): string {
    //利用bfs+队列,最常用的
  const queue = [root];
  let res = [];
  while (queue.length) {
    const node = queue.shift(); // 考察出列的节点
    if (node) {                 // 是真实节点,带出子节点入列
      res.push(node.val);       // 节点值推入res
      queue.push(node.left);    // 子节点入列,不管是不是null节点都入列
      queue.push(node.right);    
    } else {                    // 是null节点,没有子节点入列------空值用X代替
      res.push('X');            // X 推入res
    }
  }
  return res.join(',');  // 转成字符串

};


反序列化——也是BFS

下图是BFS得到的序列化字符串,和DFS得到的不同,它是一层层的。除了第一个是根节点的值,其他节点值都是成对的,对应左右子节点。

image.png

  • 依然先转成list数组,用一个指针 cursor 从第二项开始扫描。
  • 起初,用list[0]构建根节点,并让根节点入列。
  • 节点出列,此时 cursor 指向它的左子节点值,cursor+1 指向它的右子节点值。
    • 如果子节点值是数值,则创建节点,并认出列的父亲,同时自己也是父亲,入列。
    • 如果子节点值为 'X',什么都不用做,因为出列的父亲的 left 和 right 本来就是 null
  • 可见,所有的真实节点都会在队列里走一遍,出列就带出儿子入列

image.png

/*
 * Decodes your encoded data to tree.
 */
function deserialize(data: string): TreeNode | null {


  if (data == 'X') return null;

  const list = data.split(',');  // 序列化字符串split成数组

  const root = new TreeNode(Number(list[0])); // 获取首项,构建根节点
  const queue = [root];          // 根节点推入队列
  let cursor = 1;                // 初始指向list第二项

  while (cursor < list.length) { // 指针越界,即扫完了序列化字符串
    const node = queue.shift();  // 考察出列的节点

    const leftVal = list[cursor];      // 它的左儿子的值
    const rightVal = list[cursor + 1]; // 它的右儿子的值

    if (leftVal != 'X') {              // 是真实节点
      const leftNode = new TreeNode(Number(leftVal)); // 创建左儿子节点
      node.left = leftNode;                   // 认父亲
      queue.push(leftNode);                   // 自己也是父亲,入列
    }
    if (rightVal != 'X') {
      const rightNode = new TreeNode(Number(rightVal));
      node.right = rightNode;
      queue.push(rightNode);
    }
    cursor += 2; // 一次考察一对儿子,指针加2
  }
  return root;  // BFS结束,构建结束,返回根节点

};

DFS解法---结合递归更加简单

  • 递归遍历一棵树,重点关注当前节点,它的子树的遍历交给递归完成:
  • “serialize函数,请帮我分别序列化我的左右子树,我等你返回的结果,再拼接一下。”
  • 选择前序遍历,是因为 根|左|右根∣左∣右 的打印顺序,在反序列化时更容易定位出根节点的值。
  • 遇到 null 节点也要翻译成特定符号,反序列化时才知道这里是 null。

image.png

const serialize = (root) => {
  if (root == null) {                  // 遍历到 null 节点
    return 'X';
  }
  const left = serialize(root.left);   // 左子树的序列化结果
  const right = serialize(root.right); // 右子树的序列化结果
  return root.val + ',' + left + ','+ right; // 按  根,左,右  拼接字符串
};

反序列化——也是递归

  • 前序遍历的序列化字符串,就像下图右一:

image.png

  • 定义函数 buildTree 用于还原二叉树,传入由序列化字符串转成的 list 数组。
  • 逐个 pop 出 list 的首项,构建当前子树的根节点,顺着 list,构建顺序是根节点,左子树,右子树。
    • 如果弹出的字符为 "X",则返回 null 节点。
    • 如果弹出的字符是数值,则创建root节点,并递归构建root的左右子树,最后返回root。

image.png

const deserialize = (data) => {
  const list = data.split(',');   // split成数组

  const buildTree = (list) => {   // 基于list构建当前子树
    const rootVal = list.shift(); // 弹出首项,获取它的“数据”
    if (rootVal == "X") {         // 是X,返回null节点
      return null;
    }
    const root = new TreeNode(rootVal); // 不是X,则创建节点
    root.left = buildTree(list);        // 递归构建左子树
    root.right = buildTree(list);       // 递归构建右子树
    return root;                        // 返回当前构建好的root
  };

  return buildTree(list); // 构建的入口
};

递归

[100] 相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

img

输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:

img

输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:

img

输入:p = [1,2,1], q = [1,1,2]
输出:false

递归调用 比较每个结点是否都一样

function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean {
  if (!p && !q) return true;
  if ((!p && q) || (p && !q)) return false;
  return p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

[110]*平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:

img

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:

输入:root = []
输出:true

function isBalanced(root: TreeNode | null): boolean {
    
    if(!root) return true;

    const height = (node: TreeNode | null) => {
    if(node === null){
        return 0
    }
    return Math.max(height(node.left), height(node.right)) + 1
    }

    return Math.abs(height(root.left)-height(root.right))<=1&&isBalanced(root.left)&&isBalanced(root.right);
};

DFS


[236] 二叉树的最近公共祖先------常考

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

该题目与 1257. 最小公共区域 相同。

左孩子是null,直接返回右边的孩子值,如果是null,也会返回,然后继续回溯,再向上找父节点
如果左右都有值,那当前节点就是公共祖先节点

function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null {

    if(root ==null || root == p || root == q) {return root}

    let leftNode = lowestCommonAncestor(root.left, p , q);
    let rightNode = lowestCommonAncestor(root.right, p, q);

    if(leftNode ==null) {return rightNode}
    if(rightNode ==null) {return leftNode}
    if(leftNode !=null && rightNode !=null)
    return root;


};

存储父亲节点

pq 向上遍历,第一个相同的节点即为最近公共父节点

let lowestCommonAncestor = function(root, p, q) {
  const parent = new Map(); // 记录所有节点的父节点
  const dfs = (node) => {
    if (node.left) {
      parent.set(node.left, node);
      dfs(node.left);
    }
    if (node.right) {
      parent.set(node.right, node);
      dfs(node.right);
    }
  }
  dfs(root); 
  
  const visited = new Set();
  while (p) {
    visited.add(p);
    p = parent.get(p);
  }
  while (q) {
    if (visited.has(q)) {
      return q;
    }
    q = parent.get(q);
  }
  return null;
};

[654] 最大二叉树

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:

创建一个根节点,其值为 nums 中的最大值。
递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。

示例 1:

img

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:

  • [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    • [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。

      • 空数组,无子节点。
      • [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
        • 空数组,无子节点。
        • 只有一个元素,所以子节点是一个值为 1 的节点。
    • [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。

      • 只有一个元素,所以子节点是一个值为 0 的节点。

      • 空数组,无子节点。

示例 2:

img

输入:nums = [3,2,1]
输出:[3,null,2,null,1]

这道题也是一个简单的树结构还原问题。本质上其实还是将数组反序列化为树形结构。----------[105] [297]也是反序列化构造二叉树的问题

首先,我们需要O(n)的时间找到数组的最大值,生成为一个树节点。并递归其左右子数组,并分别生成为当前节点的左右子树即可。-------递归分治思想

function constructMaximumBinaryTree(nums: number[]): TreeNode | null {
  return traverse(0, nums.length - 1);

  function traverse(left: number, right: number) {
    if (left > right) return null;

    // 找到当前范围中的最大值
    let maxIndex = left;
    let max = nums[left];
    for (let i = left; i <= right; i++) {
      if (nums[i] > max) {
        max = nums[i];
        maxIndex = i;
      }
    }

    const node = new TreeNode(max);
    node.left = traverse(left, maxIndex - 1);
    node.right = traverse(maxIndex + 1, right);

    return node;
  }
}

单调栈

更进一步,根据题目对树的构建的描述可知,nums 中的任二节点所在构建树的水平截面上的位置仅由下标大小决定。

不难想到可抽象为找最近元素问题,可使用单调栈求解。

具体的,我们可以从前往后处理所有的 nums[i],若存在栈顶元素并且栈顶元素的值比当前值要小,根据我们从前往后处理的逻辑,可确定栈顶元素可作为当前 nums[i]对应节点的左节点,同时为了确保最终 nums[i]nums[i] 的左节点为 [0, i - 1]范围的最大值,我们需要确保在构建 nums[i]节点与其左节点的关系时,[0, i - 1] 中的最大值最后出队,此时可知容器栈具有「单调递减」特性。基于此,我们可以分析出,当处理完 nums[i] 节点与其左节点关系后,可明确 nums[i] 可作为未出栈的栈顶元素的右节点。

const stk = new Array<TreeNode>(1010)
function constructMaximumBinaryTree(nums: number[]): TreeNode | null {
    let he = 0, ta = 0
    for (const x of nums) {
        const node = new TreeNode(x)
        while (he < ta && stk[ta - 1].val < x) node.left = stk[--ta]
        if (he < ta) stk[ta - 1].right = node
        stk[ta++] = node
    }
    return stk[0]
};

真二叉树

[894] 所有可能的真二叉树

给你一个整数 n ,请你找出所有可能含 n 个节点的 真二叉树 ,并以列表形式返回。答案中每棵树的每个节点都必须符合 Node.val == 0 。

答案的每个元素都是一棵真二叉树的根节点。你可以按 任意顺序 返回最终的真二叉树列表。

真二叉树 是一类二叉树,树中每个节点恰好有 0 或 2 个子节点。

示例 1:

img

输入:n = 7
输出:[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]]

示例 2:

输入:n = 3
输出:[[0,0,0]]

哈希表加递归

function allPossibleFBT(n: number): Array<TreeNode | null> {

    //偶数肯定不行
    if(n%2===0) return [];
    //创建哈希表,键为n,值为数组列表
    const map: Map<number, Array<TreeNode | null>> = new Map();
    //哈希表传值,直到有n个结点
    if(!map.has(n)){
        if(n===1){
            map.set(1,[new TreeNode(0)])
        } else{
            const res : Array<TreeNode | null> = [];
            for(let left = 0; left<n ;left++){
                //左边的子节点
                const leftNodes = allPossibleFBT(left);
                //右边的子节点
                const rightNodes = allPossibleFBT(n-1-left);

                for(let i = 0;i<leftNodes.length;i++){
                    for(let j = 0; j<rightNodes.length ; j++){
                        res.push(new TreeNode(0,leftNodes[i],rightNodes[j]))
                    }
                }

            }
            map.set(n,res)
        }
    }
    return map.get(n)
};

完全二叉树

[222] 完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

示例 1:

img

输入:root = [1,2,3,4,5,6]
输出:6
示例 2:

输入:root = []
输出:0
示例 3:

输入:root = [1]
输出:1

function countNodes(root: TreeNode | null): number {
    if(!root) return 0;
    const map = new Map();
    let count = 1;
    if(root.left){
        count+=countNodes(root.left);
    }
    if(root.right){
        count+=countNodes(root.right);
    }
    return count;
};

路径总和

[112] 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

示例 2:

输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。

示例 3:

输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。

提示:

树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000

路径总和的方法有很多种,这个题作为简单题应该要记住

递归

可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树

  1. 确定递归函数的参数和返回类型

参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和

再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?

如图所示:

112.路径总和

图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示

 function recur(node: TreeNode, sum: number): boolean
  1. 确定终止条件

首先计数器如何统计这一条路径的和呢?

不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。

如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。

如果遍历到了叶子节点,count不为0,就是没找到。

if (
    node.left === null &&
    node.right === null &&
    sum === 0
) return true;
if (root === null) return false;
  1. 确定单层递归的逻辑

因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。

递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。

if (node.left !== null) {//左 (空节点不遍历)
    // 遇到叶子节点返回true,则直接返回true
    sum -= node.left.val;//递归,处理节点;
    if (recur(node.left, sum) === true) return true;
    sum += node.left.val;//回溯,撤回处理结果
}
if (node.right !== null) {// 右 (空节点不遍历)
    // 遇到叶子节点返回true,则直接返回true
    sum -= node.right.val;
    if (recur(node.right, sum) === true) return true;
    sum += node.right.val;
}

以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。

整体代码:

function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
    function recur(node: TreeNode, sum: number): boolean {
        console.log(sum);
        if (
            node.left === null &&
            node.right === null &&
            sum === 0
        ) return true;
        if (node.left !== null) {
            sum -= node.left.val;
            if (recur(node.left, sum) === true) return true;
            sum += node.left.val;
        }
        if (node.right !== null) {
            sum -= node.right.val;
            if (recur(node.right, sum) === true) return true;
            sum += node.right.val;
        }
        return false;
    }
    if (root === null) return false;
    return recur(root, targetSum - root.val);
};

递归法(精简版):

function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
    if (root === null) return false;
    targetSum -= root.val;
    if (
        root.left === null &&
        root.right === null &&
        targetSum === 0
    ) return true;
    return hasPathSum(root.left, targetSum) ||
        hasPathSum(root.right, targetSum);
};

迭代

如果使用栈模拟递归的话,那么如果做回溯呢?

此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和

迭代法:

function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
    type Pair = {
        node: TreeNode, // 当前节点
        sum: number // 根节点到当前节点的路径数值总和
    }

    const helperStack: Pair[] = [];
    if (root !== null) helperStack.push({ node: root, sum: root.val });
    let tempPair: Pair;
    while (helperStack.length > 0) {
        tempPair = helperStack.pop()!;
        if (
            tempPair.node.left === null &&
            tempPair.node.right === null &&
            tempPair.sum === targetSum
        ) return true;
        if (tempPair.node.right !== null) {
            helperStack.push({
                node: tempPair.node.right,
                sum: tempPair.sum + tempPair.node.right.val
            });
        }
        if (tempPair.node.left !== null) {
            helperStack.push({
                node: tempPair.node.left,
                sum: tempPair.sum + tempPair.node.left.val
            });
        }
    }
    return false;
};

[113] 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:

img

输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:

输入:root = [1,2], targetSum = 0
输出:[]

注意,当满足结果的路径时,深拷贝path副本,而不是path本身
因为在js中,参数是值传递,当参数是引用类型时,是复制参数的地址

function pathSum(root: TreeNode | null, targetSum: number): number[][] {

    const result:number[][] = [];
    const path:number[] = [];

    function resur(node:TreeNode,sum:number){
	
        //注意,当满足结果的路径时,深拷贝path副本,而不是path本身
		//因为在js中,参数是值传递,当参数是引用类型时,是复制参数的地址
        if(!node.left&&!node.right&&sum===0){
            result.push([...path]);
        }

        if(!node.left&&!node.right) return;

        if(node.left){
            path.push(node.left.val);
            sum -= node.left.val;
            resur(node.left,sum);
            //回溯
            sum += node.left.val;
            path.pop();
        }
        if(node.right){
            path.push(node.right.val);
            sum -= node.right.val;
            resur(node.right,sum);
            //回溯
            sum += node.right.val;
            path.pop();
        }
    }

    if(!root) return result;
    path.push(root.val);
    resur(root,targetSum-root.val);
    return result;

};

[124] 二叉树中的最大路径和------常考

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

示例 1:

img

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

img

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

方法1.递归

  • 思路:从根节点递归,每次递归分为走左边、右边、不动 3种情况,用当前节点加上左右子树最大路径和不断更新最大路径和
  • 复杂度:时间复杂度O(n),n为树的节点个数。空间复杂度O(n),递归深度,最差情况下为树的节点数
function maxPathSum(root: TreeNode | null): number {

    let maxSum = Number.MIN_SAFE_INTEGER;

    const dfs = (root) => {
        if(!root){
            return 0;
        }
        const left = dfs(root.left);
        const right = dfs(root.right);

        maxSum = Math.max(maxSum,left+root.val+right);
        //返回路径和
        const pathSum = root.val + Math.max(0,left,right);
        return pathSum < 0 ? 0 : pathSum; 

    }

    dfs(root);

    return maxSum

};

posted @ 2022-07-11 17:40  青川薄  阅读(421)  评论(0编辑  收藏  举报