二叉树题目汇总🎄( 一 、何时使用后序遍历,中断递归)

228 阅读16分钟

本章节主要归纳总结有关二叉树以及二叉搜索树的相关题目🎄。

在此之前,无论是深度优先遍历的递归方法还是迭代方法,亦或是广度优先搜索(层序遍历)的迭代方法,都要能够熟练的掌握。这里可以看我的上一篇文章👉 二叉树的遍历

总结

这里先总结是为了大家能一开始就可以看到学完本节所能感受到的知识,故而把总结放在这里。

  • 何时使用后序遍历:当我们需要在处理当前节点单层递归逻辑时,需要左右孩子节点 或者 需要左右子树所提供的信息时,直接使用后序递归
  • 中断递归:在递归中,如果我们需要当某类情况发生时,将这个结果直接在当前调用栈中一步步返回出去,就像for循环的break一样,那么我们就需要在这次递归后 判断某类情况+return
  • 二叉树的高度和深度概念不同,深度是根节点到该节点,高度是该节点到叶子节点。
    • 二叉树的最大深度 = 根节点的高度
    • 二叉树的最小深度 = 根节点到最近叶子节点的深度,递归求解时需要注意一个子树为空的情况
  • 完全二叉树中总包含满二叉树
    • 满二叉树可以通过从根节点触发,一直向左和向右走的步数是否相等来判断
    • 满二叉树的节点个数等于2h12^h-1
  • 层序遍历是一种迭代的方法,往往层序遍历的代码和思路构思起来会比较简单,通常可以先考虑是否可以通过层序遍历解决。

话不多说,开始看题!👇

LeetCode-226.翻转二叉树

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

看看示例,什么是翻转: invert1-tree.jpg 实际上就是交换两个节点值,但是交换的规则是什么呢? 思路:将每个节点的左右子树进行替换。

那如何遍历二叉树呢?我们访问每个节点时,都需要访问其左右节点,没有其他特殊需求了,因此这里使用深度优先和广度优先都可以。

递归

下面给出前序递归的代码,分析递归:

  • 返回值和参数:每轮只需要交换左右孩子因此,入参为当前节点,不需要返回值;
  • 终止条件:当前节点为空时返回;
  • 当前节点处理逻辑:交换当前节点左右子树(节点)
// 前序递归
function preorderRecursive(node) {
  if (!node) return node;
  //中
  const temp = node.left;
  node.left = node.right;
  node.right = temp;
  //左
  invertTree(node.left);
  //右
  invertTree(node.right);
}
var invertTree = function (root) {
  preorderRecursive(root);
  return root;
};

层序遍历:

var invertTree = function (root) {
  if (!root) return root;
  const quene = [];
  quene.push(root);
  while (quene.length) {
    const size = quene.length;
    for (let i = 0; i < size; i++) {
      const node = quene.shift();
      //替换左右孩子
      const tmep = node.left;
      node.left = node.right;
      node.right = tmep;
      node.left && quene.push(node.left);
      node.right && quene.push(node.right);
    }
  }
  return root;
};

LeetCode-101.对称二叉树

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

1698026966-JDYPDU-image.png

迭代

第一次看感觉和上题有些类似,但是仔细思考一下就会发现,左右节点之间的比较并没有作用,那他是如何比较的呢? 我的第一想法就是层序遍历,因为层序遍历可以获取每一层的节点,这样就变成了比较每一层数组是否是对称数组。但是需要注意一个问题,看下面这个二叉树: image.png

他不对称,但是像我们之前写层序遍历那样的代码就不会判断出问题,是因为我们没有将空节点加入到每一层导致的,因此需要将每一层的空节点也加入到当前层中。 代码如下:

var isSymmetric = function (root) {
  const quene = [];
  if (!root) return true;
  quene.push(root);
  while (quene.length) {
    const size = quene.length;
    const layer = [];
    for (let i = 0; i < size; i++) {
      const node = quene.shift();
      layer.push(node?.val ?? null);
      if (!node) continue;
      quene.push(node.left);
      quene.push(node.right);
    }
    for (let i = 0, j = layer.length - 1; i < j; i++, j--) {
      if (layer[i] !== layer[j]) return false;
    }
  }
  return true;
};

除此之外,还可以借助队列,有序入队。从头节点的左右子节点开始,每次成对地将子节点入队,从第二层开始观察,实际上每次比较的都是左子树的左节点和右子树的右节点,左子树的右节点和右子树的左节点,即外侧与外侧,内侧与内侧进行比较的。

具体代码如下:

var isSymmetric = function (root) {
  const quene = [];
  if (!root) return true;
  quene.push(root.left);
  quene.push(root.right);
  while (quene.length) {
    const leftChild = quene.shift();
    const rightChild = quene.shift();

    //左右子树都为空,继续
    if (!leftChild && !rightChild) continue;
    //左右子树有一个为空,另一个不为空,则不对称;
    if (!leftChild || !rightChild) return false;
    //左右子树都存在,节点值不同
    if (leftChild.val !== rightChild.val) return false;

    //此时已经判断了两个节点了,需要按照顺序成对地入队其子节点
    //外侧节点
    quene.push(leftChild.left);
    quene.push(rightChild.right);
    //内侧节点
    quene.push(leftChild.right);
    quene.push(rightChild.left);
  }
  return true;
};

递归

实际上递归的思路与上述迭代中第二种方法相似,我们每轮去判断的都是内侧或外侧的两个节点,起始也是从根节点的左右子节点开始。

分析递归:

  • 返回值和参数:返回当前两个子节点是否对称(true|false),入参为成对的两个节点
  • 终止条件:
    • 两个节点均为空 true
    • 两个节点一个为空一个不为空 false
    • 值不相同 false
  • 单层递归逻辑:此时两个节点均不空且值相同,那么就需要递归遍历其子节点,并将结果返回;

实现:

function compare(left, right) {
  if (!left && !right) return true;
  if (!left || !right) return false;
  if (left.val !== right.val) return false;
  //左/右,递归外侧节点
  const outside = compare(left.left, right.right);
  //右/左,递归内侧节点
  const inside = compare(left.right, right.left);
  //中
  const res = outside && inside;
  return res;
}
var isSymmetric = function (root) {
  if (!root) return true;
  return compare(root.left, root.right);
};

仔细观察会发现,实际上本次递归为后序遍历,我们也只能通过后序遍历完成此题。 此外,你可以调试一下代码,想想后序遍历能够带给我们什么便利?

我们可以在处理当前节点单层递归逻辑时,获取到其左右节点递归而来的答案,进一步利用。

LeetCode-104.二叉树的最大深度

给定一个二叉树 root ,返回其最大深度。

首先我们要分清二叉树的深度和高度的定义:

  • 深度:对于某节点,根节点到该节点的最大简单路径的长度为深度。path(root->node)
  • 高度:对于某节点,该节点到其子树的叶子节点的最大简单路径为高度。path(node->叶子)

所以,二叉树的最大深度 = 最远叶子节点的深度 = 根节点的高度

知道了这个后,这里主要给出递归法,使用深度优先遍历。对于层序遍历,这道题就十分简单了,我们每层深度加一就可以实现了。

递归

如何获得最大深度,采用什么遍历方式? 上面提到了实际我们可以求的是根节点的高度,我们只需要知道根节点左子树和右子树的最大高度+1即可,那么子树的高度又需要子树的子树上获得,重点来了:需要从子树中获得信息,那不就是和上面后序遍历中我们总结的一样了:我们可以在处理当前节点单层递归逻辑时,获取到其左右节点递归而来的答案,进一步利用。

分析递归:

  • 返回值和入参:返回的是当前节点的高度,入参为当前节点
  • 终止条件:空节点,返回高度0;
  • 单层循环逻辑:获取左孩子节点高度,右孩子节点高度,最终返回左子树和右子树的最大高度。

实现:

function getDepthRecursive(node) {
  if (!node) return 0;
  //左
  const leftDepth = getDepthRecursive(node.left);
  //右
  const rightDepth = getDepthRecursive(node.right);
  //中
  const curNodeDepth = 1 + Math.max(leftDepth, rightDepth);
  return curNodeDepth;
}
var maxDepth = function (root) {
  return getDepthRecursive(root);
};

LeetCode-111.二叉树的最小深度

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

跟上一题很像,层序遍历可以解决,我们只要再每层遍历该层节点时,判断是不是叶子节点,一旦遇到叶子节点就可以停止计算了。

递归

实际上懂了上一题,这道题调整为获取左子树和右子树的最小值,但是如图所示的树呢?

image.png

图中虚线箭头表示该节点递归后的返回值,然而这棵树的真正最小深度应该为3,这是为什么呢? 因为我们不能一味的返回左右子树最小高度,例如这里的节点2,他没有右子树,且他不是叶子节点,他的高度应该是左子树的高度,如果一个节点有左右子树,那么此时他的高度才是左右子树最小高度。

至此,可以递归分析了: 分析递归:

  • 返回值和入参:返回的是当前节点的高度,入参为当前节点
  • 终止条件:空节点,返回高度0;
  • 单层递归逻辑:获取左孩子节点高度,右孩子节点高度。
    • 如果有只有一个子节点,那么返回那个子节点的高度
    • 如果两个都存在,返回最小值

实现:

function getDepthRecursive(node) {
  if (!node) return 0;
  const leftDepth = getDepthRecursive(node.left);
  const rightDepth = getDepthRecursive(node.right);

  let curMinDepth;
  if (node.left && !node.right) {
    curMinDepth = leftDepth + 1;
  } else if (!node.left && node.right) {
    curMinDepth = rightDepth + 1;
  } else {
    curMinDepth = Math.min(leftDepth, rightDepth) + 1;
  }
  return curMinDepth;
}
var minDepth = function (root) {
  return getDepthRecursive(root);
};

LeetCode-222.完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。 首先明确完全二叉树的定义:除了最底层节点没有被填满,其他节点都被填满,且最底层的节点都尽可能地靠左。

如果我们不利用完全二叉树的特点,那么直接使用一种遍历方法全部遍历一遍就完事儿了,但是这样这道题就变得很没有意义了。

满二叉树的高度与节点的关系:节点个数 = 2h12^h-1

如何利用完全二叉树的特性:

  • 满二叉树,可以利用公式计算节点个数,而完全二叉树上有满二叉树,如果是满二叉树就可以直接计算,不是满二叉树就分别计算;
    • 如何判断是否是满二叉树?从当前节点一直向左和一直向右的步数相同即为满二叉树。

举例说明:

complete.jpg

  1. 节点1,一直向左可以走两步,一直向右可以走一步,因此不是满二叉树。
  2. 不是满二叉树,那么分别判断其子节点是不是满二叉树:
    1. 节点2,是满二叉树,根据步数(高度)计算节点个数
    2. 节点3,不是满二叉树,分别判断其子节点
      1. 节点6是满二叉树,公式计算

ok了,这样一分析是不是很清楚,实际就是在满二叉树中寻找满二叉树。

递归

上面一顿分析后,你觉得用什么遍历方法?

如果向左步数等于向右,那么可以计算。 如果不相等,那么需要分别求左右子树的节点个数,然后将节点个数返回相加。那说明了我们计算一棵树的节点时,需要知道其子树的节点数,这不又和前面题一样了,使用后序遍历。

递归分析:

  • 返回值和入参:每次返回的是该节点所表示的子树节点个数,入参为当前节点
  • 终止条件:
    • 空节点 0
    • 判断当前节点是否为满二叉树,是则返回公式计算的节点个数
  • 单层递归逻辑:此时当前节点已经确定了不是满二叉树
    • 递归获取左子树,右子树节点个数
    • 返回左子树节点个数+右子树节点个数+1

实现:

function countNodesRecursive(node) {
  //-----终止条件
  if (!node) return 0;
  let leftNode = node, rightNode = node;
  let leftHeight = 0, rightHeight = 0;
  //当前节点向左的高度
  while (leftNode.left) {
    leftHeight++;
    leftNode = leftNode.left;
  }
  //当前节点向右的高度
  while (rightNode.right) {
    rightHeight++;
    rightNode = rightNode.right;
  }
  if (leftHeight === rightHeight) {
    return (2 << leftHeight) - 1;
  }

  //-----单次循环逻辑
  //左
  const leftCount = countNodesRecursive(node.left);
  //右
  const rightCount = countNodesRecursive(node.right);
  //中
  return leftCount + rightCount + 1;
}
var countNodes = function (root) {
  return countNodesRecursive(root);
};

LeetCode-110.平衡二叉树

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

思路:我们需要获取每个节点左右孩子节点的高度,进而计算高度差,决定一个节点的高度需要左右孩子节点的高度,高度差也需要,那这不又是妥妥的后序遍历。

递归

知道后序遍历了,那我们来分析一下递归三大要素:

  • 返回值和参数:返回节点的高度,入参为当前节点
  • 终止条件:
    • 空节点 0
    • 如果当前节点的左右节点高度差大于1,则返回-1
  • 单层递归逻辑:
    • 获取左右节点高度
    • 计算当前节点左右子树的高度差,大于1则返回-1
    • 小于等于1则返回 最大高度+1

按照上述逻辑我们先来实现以下代码:

image.png

乍一看没啥问题,但是提交就会出错,为什么呢? 这时候我们需要debug一下,你会发现,在某个节点的左子树处已经判断出非平衡二叉树了,返回了-1,但是当前节点并没有保留这次结果,而是接着去判断左右子树的高度差。而我们期望的是,一旦遇到了不平衡的二叉树,那么代码应该一直返回-1,最终返回出去。

知道了这个我们就可以着手修改了,也就是获取子节点高度时,如果是-1,就直接返回。

实现如下:

function getHeightRecursive(node) {
  if (!node) return 0;
  //左
  const leftHeight = getHeightRecursive(node.left);
  if (leftHeight === -1) return leftHeight;
  //右
  const rightHeight = getHeightRecursive(node.right);
  if (rightHeight === -1) return rightHeight;
  //中
  const isBalanced = Math.abs(leftHeight - rightHeight) <= 1;
  if (!isBalanced) {
    return -1;
  }
  return Math.max(leftHeight, rightHeight) + 1;
}
var isBalanced = function (root) {
  return getHeightRecursive(root) !== -1;
};

本题可以总结出:在递归中,如果我们需要当某类情况发生时,将这个结果直接在当前调用栈中一步步返回出去,就像for循环的break一样,那么我们就需要在这次递归后 判断某类情况+return

LeetCode-404.左叶子之和

给定二叉树的根节点 root ,返回所有左叶子之和。

这道题关键在于判断左叶子:node.left && !node.left.left && !node.left.right 那么这道题其实就已经解决了,至于用什么遍历方式,因为总是要都遍历一遍的,那无所谓了。

递归

首先是前序遍历的递归分析:

  • 返回值和参数:无返回值,入参为当前节点和当前总和
  • 终止条件:
    • 空节点 0
  • 先序单层递归逻辑:
    • 判断当前节点的左孩子是不是叶子节点,是的话求和
    • 不是左叶子就继续递归寻找其左子树和右子树

实现:

function getSumOfLeftLeavesRecursive(node, sum) {
  if (!node) return;
  //中
  if (node.left && !node.left.left && !node.left.right) {
    sum.leftSum += node.left.val;
  }
  //左
  getSumOfLeftLeavesRecursive(node.left, sum);
  getSumOfLeftLeavesRecursive(node.right, sum);
}
var sumOfLeftLeaves = function (root) {
  let sum = { leftSum: 0 };
  getSumOfLeftLeavesRecursive(root, sum);
  return sum.leftSum;
};

注意:这里初始化sum为一个对象,而不是数字,是因为原始类型通过参数传递的是引用,而原始类型的引用无法修改改变了,而对象作为引用类型可以被修改。

由于前序遍历出现的问题👆,我们考虑不断获取左右子树的左叶子合并逐步向上返回,因此使用后序遍历实现 后序遍历的递归分析:

  • 返回值和参数:返回值当前节点代表子树的左叶子节点和,入参为当前节点
  • 终止条件:
    • 空节点 0
  • 后序单层递归逻辑:
    • 获取左子树左叶子和
    • 获取右子树左叶子和
    • 如果当前节点有左叶子,就返回左子树左叶子和+右子树左叶子和+当前节点左叶子
    • 当前节点没有左叶子就返回 左子树左叶子和+右子树左叶子和

实现:

function getSumOfLeftLeavesRecursive(node) {
  if (!node) return 0;

  //左
  const leftSum = getSumOfLeftLeavesRecursive(node.left);
  //右
  const rightSum = getSumOfLeftLeavesRecursive(node.right);
  //中
  if (node.left && !node.left.left && !node.left.right) {
    return node.left.val + leftSum + rightSum;
  }
  return leftSum + rightSum;
}
var sumOfLeftLeaves = function (root) {
  return getSumOfLeftLeavesRecursive(root);
};

层序遍历

var sumOfLeftLeaves = function (root) {
  let sum = 0;
  if (!root) return sum;
  const quene = [];
  quene.push(root);
  while (quene.length) {
    const size = quene.length;
    for (let i = 0; i < size; i++) {
      const node = quene.shift();
      node.left && quene.push(node.left);
      node.right && quene.push(node.right);
      //如果node是左叶子节点,则求和
      if (node.left && !node.left.left && !node.left.right) {
        sum += node.left.val;
      }
    }
  }
  return sum;
};

层序遍历秒杀题

以下几道题我们都可以使用层序遍历加细微改动AC,这里给出题单,以两道为例。

LeetCode-637.二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

在层序遍历中,我们借助队列,每轮循环都会获得当前层的所有节点,我们只需要在每次循环后计算平均值即可,是不是很简单。 如果你忘了层序遍历怎么写,还是先去看看我的上一篇文章再来喔。

代码如下:

/**
 * 获取nums数组的平均值
 * @param {number[]} nums 
 * @returns 
 */
function getAverage(nums) {
  let sum = 0;
  for (let num of nums) {
    sum += num;
  }
  return sum / nums.length;
}
var averageOfLevels = function (root) {
  if (!root) return [];
  const quene = [];
  const res = [];
  quene.push(root);
  while (quene.length) {
    const size = quene.length;
    const layer = [];
    for (let i = 0; i < size; i++) {
      const node = quene.shift();
      layer.push(node.val);
      node.left && quene.push(node.left);
      node.right && quene.push(node.right);
    }
    //计算平均值
    res.push(getAverage(layer));
  }
  return res;
};

LeetCode-429.N 叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

了解二叉树层序遍历的思路后,实际上N叉树的层序遍历是一样的,只是我们每次遍历到一个节点后,针对当前树节点的数据结构所涉及的添加子节点的代码发生了变化而已,直接看代码:

/**
 * 
 * function Node(val,children) {
 *    this.val = val;
 *    this.children = children;
 * };
 */
var levelOrder = function (root) {
  const quene = [];
  const res = [];
  if (!root) return res;
  quene.push(root);
  while (quene.length) {
    const layer = [];
    const size = quene.length;
    for (let i = 0; i < size; i++) {
      const node = quene.shift();
      layer.push(node.val);
      //变化,每个节点的子节点为children数组
      if (node.children) {
        for (const child of node.children) {
          quene.push(child);
        }
      }
    }
    res.push(layer);
  }
  return res;
};

其他层序遍历题都可以通过在层序遍历的基础上完成,都很简单。