JavaScript刷LeetCode拿offer-树的遍历

2021.5.21开始的兔系刷题之路 根据LeetCode分类进行逐个击破 培养出自己的套路~ 从前辈们的学习经验中获得建议 并开始一点点地实践 争取一天可以刷一道题 慢慢地把这些专题都刷过来! 阅读详情

什么是树

  • 一种分层数据的抽象模型。
  • 前端工作中常见的树包括:DOM树,级联选择,树形控件
  • JS中没有树,可以用Object和Array构建树
  • 树的常用操作:深度/广度优先遍历,先中后序遍历

深度优先遍历

  • 访问根节点
  • 对根节点的children挨个进行深度优先遍历

代码展示:

const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: []
                },
                {
                    val: 'e',
                    children: []
                }
            ]
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: []
                },
                {
                    val: 'g',
                    children: []
                }
            ]
        }
    ],
};

const dfs = (root) => {
  console.log(root.val);
  root.children.forEach(dfs);
}

dfs(tree);

输出顺序:a -> b -> d -> e -> c -> f -> g

广度优先遍历

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的children分别入队
  • 重复二,三步,直到队列为空

代码展示:

const bfs = (root) => {
    const q = [root];
    while (q.length) {
        const n = q.shift();
        console.log(n.val);
        n.children.forEach((child) => {
            q.push(child);
        })
    }
}

bfs(tree);  

输出顺序:a -> b -> c -> d -> e -> f -> g

先序遍历

  • 访问根节点
  • 对根节点的左子树进行先序遍历
  • 对根节点的右子树进行先序遍历

代码展示:

const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null
        },
        right: {
            val: 5,
            left: null,
            right: null
        }
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null
        },
        right: {
            val: 7,
            left: null,
            right: null
        }
    }
};
// 递归
const preorder = (root) => {
    if (!root) return;
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
}

preorder(tree);

// 迭代
const preorder = (root) => {
    if (root) return;
    const stack = [root];
    while (stack.length) {
        const n = stack.pop();
        console.log(top.val);
        if (n.right) stack.push(n.right);
        if (n.left) stack.push(n.left);
    }
  }
}

preorder(tree);

参考视频:传送门

输出顺序:1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7

中序遍历

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历

代码展示:

// 递归
const inorder = (root) => {
    if (!root) return;
    preorder(root.left);
    console.log(root.val);
    preorder(root.right);
}

inorder(tree);

// 迭代
const inorder = (root) => {
    if (!root) return;
    const stack = [];
    let p = root;
    while (stack.length || p) {
        while (p) {
            stack.push(p);
            p = p.left;
        }
        const n = stack.pop();
        console.log(n.val);
        p = n.right;
    }
}

inorder(tree);

输出顺序:4 -> 2 -> 5 -> 1 -> 6 -> 3 -> 7

后序遍历

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点

代码展示:

// 递归
const postorder = (root) => {
    if (!root) return;
    preorder(root.left);
    preorder(root.right);
    console.log(root.val);
}

postorder(tree);

// 迭代
const postorder = (root) => {
    if (!root) return;
    const stack = [root];
    const outputStack = [];
    while (stack.length) {
        const n = stack.pop();
        outputStack.push(n);
        if (n.left) stack.push(n.left);
        if (n.right) stack.push(n.right);
    }
    while (outputStack) {
        const n = outputStack.pop();
        console.log(n.val);
    }
}

postorder(tree);

输出顺序:4 -> 5 -> 2 -> 6 -> 7 -> 3 -> 1

leetcode题目

难度:简单

  1. 二叉树的最大深度

思路:

  • 求最大深度,优先考虑深度优先遍历
  • 在深度优先遍历过程中,记录每个节点所在的层级,找到最大的层级即可

代码展示:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
  let maxDepth = 0;
  const dfs = (n, l) => {
    if (!n) return;
    dfs(n.left, l + 1);
    dfs(n.right, l + 1);
    if (!n.left && !n.right) maxDepth = Math.max(maxDepth, l);
  }
  dfs(root, 1);
  return maxDepth;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点个数。

  • 空间复杂度:O(height):height为二叉树的最大深度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。

  1. 翻转二叉树

思路:

  • 方法一使用广度优先遍历,在遍历树的过程中,交换当前层级下的左右子树
  • 方法二使用递归解决,递归最重要的是定义子问题。

代码展示:

方法一:广度优先遍历

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    if (!root) root;
    const bfs = (root) => {
        const q  = [root];
        while (q.length) {
            const n = q.shift();
            const temp = n.right;
            n.right = n.left;
            n.left = temp;
            if (n.left) q.push(n.left);
            if (n.right) q.push(n.right);
        }
    }
    bfs(root);
    return root;
}

方法二:递归

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
  if (!root) return null;
  let newTree = new TreeNode(root.val);
  newTree.left = invertTree(root.right);
  newTree.right = invertTree(root.left);
  return newTree;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点个数。

  • 空间复杂度:O(height):height为二叉树的最大深度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。

  1. 对称二叉树

思路:

  • 通过遍历比较两个相同根节点的左子树和右子树的值是否相等
  • 如果每次都相等,直到两个节点都不存在,说明是对称的
  • 如果两个节点不相等,则说明不对称

代码展示:

/** * @param {TreeNode} root * @return {boolean} */
var isSymmetric = function(root) {
    if (!root) return false;
    const checkSym = (p, q) => {
        if (!p && !q) return true;
        if (!p || !q) return false;
        return p.val === q.val && checkSym(p.left, q.right) && checkSym(q.left, p.right);
    }
    return checkSym(root, root);
}

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点数,最差情况为O(n)。

  1. 二叉树的直径

在这里插入图片描述

思路:

  • 考虑深度优先遍历
  • 寻找两个最深的节点距离之和

代码展示:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var diameterOfBinaryTree = function(root) {
  if (!root) return 0;
  let res = 1; // 默认为根节点的路径长度
  const dfs = (root) => {
    if (!root) return 0;
    let L = dfs(root.left);  // 左子树深度,上图为例,最长L为2
    let R = dfs(root.right);  // 右子树深度,上图为例,最长R为1
    res = Math.max(res, L + R + 1); // 最大L+R+1,+1为根节点,总共深度为4,即节点树为4
    return Math.max(L, R) + 1;  // 包含根节点的深度,上图为例,最长L为2,最长R为1
  }
  dfs(root);
  return res - 1; // 最终结果要得到直径为3
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(height):height为二叉树的最大深度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。

剑指 Offer 27. 二叉树的镜像

思路:

  • 逐层递归互换左右子树节点的位置

代码展示:

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var mirrorTree = function(root) {
    if (!root) return root;
    const temp = root.left;
    root.left = root.right;
    root.right = temp;
    mirrorTree(root.left);
    mirrorTree(root.right);
    return root;
}

优化后:

/** * @param {TreeNode} root * @return {TreeNode} */
var mirrorTree = function(root) {
    if (!root) return root;
    [root.left, root.right] = [mirrorTree(root.right), mirrorTree(root.left)];
    return root;
}

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点树。

  1. 二叉树的最近公共祖先

剑指 Offer 68 - II. 二叉树的最近公共祖先

思路:

  • 递归

代码展示:

/** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */
var lowestCommonAncestor = function(root, p, q) {
  // 当传入递归的树等于 p 或者等于 q,说明找到了 p 或者 q,返回给递归调用的 l 或 r
  if (!root || p === root || q === root) return root;
  let l = lowestCommonAncestor(root.left, p, q); // 递归调用,寻找 p 和 q
  let r = lowestCommonAncestor(root.right, p, q); // 递归调用,寻找 p 和 q
  return l && r ? root : l || r; 
  // 如果 p 和 q 分别在 root.left 和 root.right 中找到,则根节点 root 成为最近的公共祖先返回。
  // 假如 p 和 q 在 root.left 中找到,递归会把 p 和 q 的公共祖先返回给 l。
  // 假如,p 和 q 在 root.right 中找到,递归会把 p 和 q 的公共祖先返回给 r。
  // 根节点root,l 或 r 最终成为当前函数的返回值。
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(n):n为二叉树节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。

剑指 Offer 55 - I. 二叉树的深度

思路:

  • 考虑深度优先遍历

代码展示:

var maxDepth = function (root) {
    if (!root) return 0;
    let max = 0;
    const dfs = (root, l) => {
        if (root.left) dfs(root.left, l + 1);
        if (root.right) dfs(root.right, l + 1);
        if (!root.left && !root.right) max = Math.max(max, l);
    }
    dfs(root, 1);
    return max;
}

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数,计算树的深度需要遍历所有节点。

  • 空间复杂度:O(n):最差情况下,空间复杂度为O(n)。

  1. 二叉树的所有路径

思路:

  • 本题考虑使用深度优先遍历。
  • 如果当前节点有左子树或右子树,就递归调用函数,直到左右子树都不存在,此时就是我们要找的的路径。

代码展示:

/**
 * @param {TreeNode} root
 * @return {string[]}
 */
var binaryTreePaths = function(root) {
  if (!root) return [];
  const res = [];
  const dfs = (root, path) => {
      if (root) {
          path += root.val;
          if (!root.left && !root.right) {
              res.push(path);
          } else {
              path += '->';
              dfs(root.left, path);
              dfs(root.right, path);
          }
      }
  }
  dfs(root, "");
  return res;
};

复杂度分析:

  • 时间复杂度:O(n^2):n为二叉树节点数。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N^2)。
  • 空间复杂度:O(n^2):n为二叉树节点数。

剑指 Offer 32 - I. 从上到下打印二叉树

剑指 Offer 32 - II. 从上到下打印二叉树 II

  • 解题方法同二叉树的层序遍历
  1. 平衡二叉树

思路:

  • 考虑深度优先遍历

  • 算出最大深度和最小深度的差值,即可判断是否为平衡二叉树 (本题和求二叉树直径做法类似)

代码展示:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isBalanced = function(root) {
  if (!root) return true;
  let diff = 0;
  const dfs = (root) => {
    if (!root) return 0;
    let L = dfs(root.left);
    let R = dfs(root.right);
    diff = Math.max(diff, Math.abs(R - L));
    return Math.max(L, R) + 1;
  }
  dfs(root);
  return diff - 1 <= 0;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点数,最差情况为O(n)。

  1. 合并二叉树

思路:

  • 递归对两个相同位置的节点相加

代码展示:

/**
 * @param {TreeNode} root1
 * @param {TreeNode} root2
 * @return {TreeNode}
 */
var mergeTrees = function(root1, root2) {
  if (!root1 || !root2) return root1 || root2;
  let newRoot = new TreeNode(root1.val + root2.val);
  newRoot.left = mergeTrees(root1.left, root2.left);
  newRoot.right = mergeTrees(root1.right, root2.right);
  return newRoot;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点数。

  1. 路径总和

思路:

  • 考虑深度优先遍历
  • 记录从根节点到当前节点的和,与target比较。

代码展示:

/** * @param {TreeNode} root * @param {number} targetSum * @return {boolean} */
var hasPathSum = function(root, targetSum) {
  if (!root) return false;
  let res = false;
  const dfs = (root, val) => {
    if (root.left) dfs(root.left, val + root.left.val);
    if (root.right) dfs(root.right, val + root.right.val);
    if (!root.left && !root.right && val === targetSum) res = true;
  }
  dfs(root, root.val);
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点数。

  1. 二叉树的最小深度

思路:

  • 方法一考虑使用广度优先遍历

  • 方法二考虑使用深度优先遍历

代码展示:

方法一:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    if (!root) return 0;
    let minDep = Infinity;
    const bfs = (root, l) => {
        const q = [[root, l]];
        while (q.length) {
            const [n, l] = q.shift();
            if (n.left) q.push([n.left, l + 1]);
            if (n.right) q.push([n.right, l + 1]);
            if (!n.left && !n.right) minDep = Math.min(minDep, l);
        }
    }
    bfs(root, 1);
    return minDep;
};

方法二:

/** * @param {TreeNode} root * @return {number} */
var minDepth = function(root) {
  if (!root) return 0;
  if (root.left && root.right) {
    return 1 + Math.min(minDepth(root.left), minDepth(root.right));
  } else if (root.left) {
    return 1 + minDepth(root.left);
  } else if (root.right) {
    return 1 + minDepth(root.right);
  } else {
    return 1;
  }
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n):n为二叉树节点数。

  1. N 叉树的前序遍历

思路:

  • 类似于二叉树的前序遍历

代码展示:

// 递归
var preorder = function(root) {
  if (!root) return [];
  const res = [];
  const preord = (n) => {
    if (n) res.push(n.val);
    n.children.forEach(preord);
  }
  preord(root);
  return res;
};

// 迭代
var preorder = function(root) {
  if (!root) return [];
  const stack = [root];
  const res = [];
  while (stack.length) {
    const n = stack.pop();
    res.push(n.val);
    n.children.reverse().forEach(child => {
      stack.push(child);
    });
  }
  return res;
}

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(height):height为二叉树最大深度。

剑指 Offer 54. 二叉搜索树的第k大节点

思路:

  • 根据树的特点,采用中序遍历,按右子树 - 根节点 - 左子树的顺序遍历,就可以由大到小找到第k大节点

代码展示:

/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {number}
 */
var kthLargest = function(root, k) {
  if (!root || k <= 0) return null;
  const stack = [];
  const res = null;
  let p = root;
  while (stack.length || p) {
    while (p) {
      stack.push(p);
      p = p.right;
    }
    const top = stack.pop();
    if (--k === 0) return top.val;
    p = top.left;
  }
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(n):n为二叉树节点数。

难度:中等

  1. 二叉树的前序遍历

代码展示:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
  if (!root) return [];
  const stack = [root];
  const res = [];
  while (stack.length) {
    const n = stack.pop();
    res.push(n.val);
    if (n.right) stack.push(n.right);
    if (n.left) stack.push(n.left);
  }
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 二叉树的中序遍历

代码展示:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
  if (!root) return [];
  const stack = [];
  const res = [];
  let p = root;
  while (stack.length || p) {
    while (p) {
      stack.push(p);
      p = p.left;
    }
    const n = stack.pop();
    res.push(n.val);
    p = n.right;
  }
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 二叉树的后序遍历

代码展示:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
  if (!root) return [];
  const stack = [root];
  const outputStack = [];
  const res = [];
  while (stack.length) {
      const n = stack.pop();
      outputStack.push(n);
      if (n.left) stack.push(n.left);
      if (n.right) stack.push(n.right);
  }
  while (outputStack.length) {
      const n = outputStack.pop();
      res.push(n.val);
  }
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 二叉树的层序遍历

代码展示:

方法一:深度优先遍历

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  if (!root) return [];
  const res = [];
  const dfs = ([root, l]) => {
    if (!res[l]) {
      res[l] = [root.val];
    } else {
      res[l].push(root.val)
    }
    if (root.left) dfs([root.left, l + 1]);
    if (root.right) dfs([root.right, l + 1]);
  }
  dfs([root, 0]);
  return res;
};

方法二:广度优先遍历

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  if (!root) return [];
  let res = [];
  const bfs = (root, l) => {
    const q = [[root, l]];
    while (q.length) {
      const [n, l] = q.shift();
      if (!res[l]) res[l] = [];
      res[l].push(n.val);
      if (n.left) q.push([n.left, l + 1]);
      if (n.right) q.push([n.right, l + 1]);
    }
  };
  bfs(root, 0);
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 从前序与中序遍历序列构造二叉树

  2. 从中序与后序遍历序列构造二叉树

剑指 Offer 07. 重建二叉树

思路:

  • 递归

代码展示:

参考:多种解法,逐渐优化

/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function(preorder, inorder) {
  if (!preorder.length || !inorder.length) return null;
  const map = new Map();
  for (let i = 0; i < inorder.length; i++) {
    map.set(inorder[i], i);
  }
  const builder = (p_start, p_end, i_start, i_end) => {
    if (p_start > p_end) return null;
    let rootVal = preorder[p_start]; // 找到根节点
    let root = new TreeNode(rootVal); // 设置二叉树的根节点
    let mid = map.get(rootVal);  // 找到根节点在inorder中的位置
    let leftNum = mid - i_start; // 左子树的个数
    root.left = builder(p_start + 1, p_start + leftNum, i_start, mid - 1);
    root.right = builder(p_start + leftNum + 1, p_end, mid + 1, i_end);
    return root;
  }
  return builder(0, preorder.length - 1, 0, inorder.length - 1);
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 二叉树的锯齿形层序遍历

思路:

  • 同二叉树层序遍历

代码展示:

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var zigzagLevelOrder = function(root) {
  if (!root) return [];
  const res = [];
  const bfs = ([root, index]) => {
    if (!root) return;
    if (!res[index]) {
      res[index] = [root.val];
    } else {
      index % 2 === 0 ? res[index].push(root.val) : res[index].unshift(root.val);
    }
    if (root.left) bfs([root.left, index + 1]);
    if (root.right) bfs([root.right, index + 1]);
  }
  bfs([root, 0]);
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 二叉树的右视图

思路:

  • 方法一考虑广度优先遍历,每层保留最后一个元素

  • 方法二考虑使用类似先序遍历,根 - 右 - 左的方式遍历,每层第一个出现的元素保留下来即可

代码展示:

方法一:广度优先遍历

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var rightSideView = function(root) {
  if (!root) return [];
  const res = [];
  const bfs = (root, l) => {
    const q = [[root, l]]
    while (q.length) {
      const [n, l] = q.shift();
      res[l] = n.val;
      if (n.left) q.push([n.left, l + 1]);
      if (n.right) q.push([n.right, l + 1]);
    }
  }
  bfs(root, 0);
  return res;
};

方法二:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var rightSideView = function(root) {
  if (!root) return [];
  const res = [];
  const stack = [[root, 0]];
  while (stack.length) {
    const [n, l] = stack.pop();
    if (res[l] == undefined) res.push(n.val);
    if (n.left) stack.push([n.left, l + 1]);
    if (n.right) stack.push([n.right, l + 1]);
  }
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(n)

剑指 Offer 26. 树的子结构

思路:

  • 递归比较

代码展示:

/** * @param {TreeNode} A * @param {TreeNode} B * @return {boolean} */
var isSubStructure = function(A, B) {
  if (!A || !B) return false;
  const isSameSub = (p, q) => {
    if (!q) return true;
    if (!p) return false;
    if (p.val !== q.val) return false;
    return isSameSub(p.left, q.left) && isSameSub(p.right, q.right);
  }
  return isSameSub(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
};

复杂度分析:

  • 时间复杂度:O(mn):m,n分别为A和B的节点数。

  • 空间复杂度:O(m)

  1. 验证二叉搜索树

代码展示:

/** * @param {TreeNode} root * @return {boolean} */
var isValidBST = function(root) {
  const helper = (root, lower, upper) => {
    if (root === null) {
      return true;
    }
    if (root.val <= lower || root.val >= upper) {
      return false;
    }
    return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
  }
  return helper(root, -Infinity, Infinity);
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(n)

  1. 不同的二叉搜索树

思路:

  • 卡塔兰数公式

代码展示:

var numTrees = function(n) {
    let C = 1;
    for (let i = 0; i < n; ++i) {
        C = C * 2 * (2 * i + 1) / (i + 2);
    }
    return C;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。

  • 空间复杂度:O(1)

  1. 完全二叉树的节点个数

代码展示:

/** * @param {TreeNode} root * @return {number} */
var countNodes = function(root) {
  return root ? countNodes(root.left) + countNodes(root.right) + 1 : 0;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(1)

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

代码展示:

/**
 * @param {TreeNode} root
 * @param {number} target
 * @return {number[][]}
 */
var pathSum = function(root, target) {
  if (!root) return [];
  const res = [];
  let temp = [];
  const dfs = (root, v) => {
    if (!root) return null;
    temp.push(root.val);
    if (root.left) dfs(root.left, root.left.val + v);
    if (root.right) dfs(root.right, root.right.val + v);
    if (!root.left && !root.right && v === target) {
      res.push([...temp]);
    }
    temp.pop();
  }
  dfs(root, root.val);
  return res;
};

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(n)

难度:困难

  1. 二叉树中的最大路径和

代码展示:

参考解法

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxPathSum = function(root) {
  let maxNum = Number.MIN_SAFE_INTEGER;
  const dfs = (root) => {
    if (!root) return 0;
    const left = dfs(root.left);
    const right = dfs(root.right);
    const curMaxSum = left + root.val + right; // 当前子树内部最大路径和
    maxNum = Math.max(maxNum, curMaxSum);
    const outputMaxSum = root.val + Math.max(left, right); // 当前子树对上一层输出的最大路径和
    return outputMaxSum > 0 ? outputMaxSum : 0; // 大于0有输出的必要,小于0就返回0
  };
  dfs(root);
  return maxNum;
}

复杂度分析:

  • 时间复杂度:O(n):n为二叉树节点数。
  • 空间复杂度:O(n)

剑指 Offer 37. 序列化二叉树

总结

继续对树的深度/广度优先遍历,先中后序遍历,层序遍历等遍历和递归的方法,有更深入的理解和学习。

资治通鉴:文言文、白话文对照读本 介绍一处可以免费下载资治通鉴的网址,该文献共20卷,排版为左边是文言文,右边是白话文翻译,对照阅读非常方便。下面是1-20卷的链接地址:第一卷:http://www.docin.com/p-9705067.html 第二卷:http://www.docin.com/p-9705979.html 第三卷:http://www.docin.com/p-9709382.html 第四卷: 阅读详情

相关推荐

1020空心杯电机驱动设计与ESP32实战

空心杯电机是一种高动态响应的直流微特电机,其驱动本质是解决电感负载下的大di/dt启动、反电动势耦合与电流纹波抑制问题。核心原理在于建立包含内阻、电感和反电动势的等效电路模型,并据此匹配开关器件、PWM参数与采样闭环。技术价值体现在小体积下实现毫秒级响应、低噪声(>30kHz)、高精度电流保护(±50mA)及热鲁棒性。典型应用于微型航模、手持云台与精密执行器等对功率密度和动态性能敏感的嵌入式系统。本文以1020规格空心杯电机为对象,基于ESP32平台完成从MOSFET选型、32kHz硬件PWM生成到INA2

weixin_42298164的博客 583

JavaScript刷LeetCode拿offer-二叉树层序遍历篇

博主最近在刷leetcode,做到二叉树套题的时候发现很多题的解题思路都是基于二叉树的层序遍历来完成的,因此写下这篇文章,记录一下二叉树层序遍历这件"神器"在实战的运用。

m0_67615103的博客 220

leetcode刷题总结「JavaScript」(3.27已更新

JS每日一题 数组 两数之和 删除有序数组中的重复项 数的操作 整数反转 回文数 字符串 最长公共前缀 常用函数 Array.prototype.splice() #数组 #两数之和 题目传送门:两数之和(opens new window) 题意:在数组中挑两个数,使得target等于两数之和,输出两数的坐标。 方法一: 暴力枚举: 点击查看代码 var twoSum = function(nums, target) { for(va.

weixin_60789461的博客 1790

ROS2学习笔记(二)-- 多机通讯原理简介及配置方法

简单介绍ROS2多机通信原理以及配置注意事项

aibingjin的博客 1万+

网络安全实战:从Wireshark到BurpSuite的抓包技巧解析

本文深入解析了Wireshark和BurpSuite在网络安全抓包中的实战技巧,从基础对比到高级过滤、核心模块应用及安全测试避坑指南。通过详细的操作步骤和案例分析,帮助读者掌握数据包抓取与分析的核心技能,提升网络安全防护能力。

weixin_29207533的博客 573

LeetCode 剑指Offer 数据结构之 树 总结 Part1

LeetCode 剑指Offer 数据结构之 树 总结 Part1 经典问题 1)遍历 2)树深度 3)重建树 4)对称树 5)子树匹配 6)二叉查找树 遍历问题 对应的题目是剑指 Offer 32 - I,剑指 Offer 32 - II,剑指 Offer 32 - III,剑指 Offer 34 DFS(一般采用递归) 深度优先的遍历 三种: 先序 根 左 右 (目前感觉用DFS先序走起比较多) def preorderTraversal(root): """ :type root: T

mimazitx的博客 227

Trae IDE与Playwright MCP:5分钟实现零代码网页数据抓取

网页数据抓取是数据采集和自动化处理中的基础技术,其核心原理是通过程序模拟浏览器行为,定位并提取页面中的结构化信息。传统方式依赖编写脚本处理网络请求、解析HTML和应对反爬机制,技术门槛较高。而结合AI与自动化工具的新范式,通过自然语言交互即可完成复杂操作,极大降低了技术门槛并提升了开发效率。这种技术价值在于将开发者从繁琐的编码中解放,使其更专注于数据分析和业务逻辑。在数据监控、市场调研、竞品分析等应用场景中,快速获取网页数据是关键需求。本文以Trae IDE集成Playwright MCP为例,演示如何通过

culi3118的博客 498

JavaScript刷LeetCode拿offer-滑动窗口

《JavaScript刷LeetCode拿offer-双指针技巧》中,简单地介绍了双指针技巧相比较单指针的优点,以及结合 Easy 难度的题目带大家进一步了解双指针的应用。进入 Medium 难度之后,解题的关键在于如何构造双指针以及确定指针移动的规则,解题方法可以归纳为以下两类:滑动窗口算法(Sliding Window Algorithm);对数组进行预处理(如:排序,前缀和等等),再利用双指针遍历;这两种方法都可以将双循环问题转化为单循环问题,从而有效地降低算法的时间复杂度。本篇主要介绍滑动窗口算法以

m0_67615103的博客 393

一文梳理金融风控建模全流程(Python)

。借助A卡模型,银行或金融机构能够根据申请人的个人信息和历史数据,对其进行风险评估。这种评估能够帮助金融机构判断借款人是否具备偿还贷款的能力和意愿。

weixin_43420291的博客 4776

E28 LoRa模块透传 定点传输 RSSI测试与MicroPython应用

测试说明 测试模块数:3 测试模式:配置模式和传输模式 如果没有检测到串口,首先检查是否安装串口驱动CH341SER,下载链接,提取码:vaw2 工作模式 模式(0-3) M2 M1 M0 模式介绍 备注 0 传输模式 1 0 0 串口打开,无线打开,连续透明传输 空速支持手动配置和自适应波特率自动调节;连续模式双方波特率必须一致 1 RSSI 1 0 1 串口打开,无线打开,RSSI功能打开 模块每100ms输出RSSI强度值 2 测距模式 1 1 0 保留 - 3 配置模式

GniLAY1022的博客 4243

JavaScript刷LeetCode拿offer-js版字典

与集合类似,字典也是一种存储唯一值的数据结构,但是它以键值对的形式来存储ES6中有字典,名为Map字典的常用操作:键值对的增删改查。

m0_67615103的博客 183

E2B Agent Runtime Sandbox 沙箱技术架构研究

本实现指南基于E2B的技术架构,提供了完整的基于Firecracker和Docker的Agent Runtime Sandbox后端服务实现方案。架构设计:参考E2B的分层架构,实现API层、管理层和虚拟化层Firecracker集成:深度集成Firecracker提供硬件级安全隔离完整功能:支持文件读写、shell执行、浏览器访问等核心功能生产就绪:提供Docker部署、监控、测试和安全配置可扩展性:设计支持水平扩展和高并发场景。

AI天才研究院 748

力扣刷题全攻略:从入门到进阶的算法提升

算法是程序员的核心竞争力,而力扣(LeetCode)作为全球知名算法平台,提供了系统提升算法能力的绝佳途径。理解基础数据结构如数组、链表、哈希表,掌握递归、动态规划等算法思想,是高效刷题的前提。通过分类刷题、模式识别和时间复杂度优化等技巧,可以显著提升解题效率。这些算法能力不仅能帮助通过技术面试,更能在实际工作中应用于性能优化、系统设计等场景。Python因其简洁语法常被推荐为刷题入门语言,而哈希表等数据结构的高效运用则是算法优化的关键。

weixin_34321753的博客 356

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

LeetCode 剑指 Offer 27. 二叉树的镜像(层序遍历-BFS)(JavaScript 树)

题链接:https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof/ 题描述: 解题思路: 第一种方法:层序遍历树,将每个节点的左右子节点读出来并变成另一棵树的子节点。最后返回新树根节点即可。 /** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.ri

ySFRaabbcc的博客 272

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

JavaScript刷LeetCode拿offer-栈相关题目

后出来的余数反而要排到前面把余数依次入栈,然后出栈,就可以实现余数倒叙输出。最后调用的函数,最先执行完JS解释器使用栈来控制函数调用的顺序栈是一个后进先出的数据结构JavaScript没有栈的结构;可以用array实现栈的功能栈常用操作:入栈 push(x);出栈 pop();最后元素 stack[stack.length - 1]

js2030code的博客 442
上一篇: 前端工程师leetcode算法面试必备-简单的二叉树
下一篇: JavaScript刷LeetCode拿offer-栈相关题目
js2030code
博客等级 码龄4年 4粉丝 97原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值