平衡二叉树的判断——怎么在O(n)的时间复杂度内实现?

引言

大家好啊,我是前端拿破轮😁。

跟着卡哥学算法有一段时间了,通过代码随想录的学习,受益匪浅,首先向卡哥致敬🫡。

但是在学习过程中我也发现了一些问题,很多当时理解了并且AC的题目过一段时间就又忘记了,或者不能完美的写出来。根据费曼学习法 ,光有输入的知识掌握的是不够牢靠的,所以我决定按照代码随想录的顺序,输出自己的刷题总结和思考 。同时,由于以前学习过程使用的是JavaScript,而在2025年的今天,TypeScript几乎成了必备项,所以本专题内容也将使用TypeScript,来巩固自己的TypeScript语言能力。

题目信息

平衡二叉树

leetcode题目链接

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

题目分析

首先我们要搞清楚什么是平衡二叉树。所谓平衡二叉树,要满足两个条件

  1. 左右子树高度差不超过1
  2. 左右子树自身也是平衡二叉树

根据这个定义,我们就知道了如何判断一个二叉树是不是平衡的了。这明显要使用递归的方式。首先还是先看看,题目给的原始函数能否满足我们的要求。

这是leetcode自身提供的函数:

ts 复制代码
function isBalanced(root: TreeNode | null): boolean {
    
};

这个函数传入一个根节点,返回一个布尔值,表示是不是平衡二叉树。那这个能否满足我们的需求呢?回顾我们的判断原则,就是上面的两个点。很显然,如果只使用这个函数作为递归,只能判断左右子树是否是平衡二叉树,无法判断左右子树的高度差是否超过1.所以我们可以自己定义一个求二叉树高度的递归函数。

ts 复制代码
const getHeight = (root: TreeNode | null): number => {

}

传入根节点,返回这个二叉树的高度。

还是递归三部曲,我们先考虑getHeight函数。

  1. 函数的参数和返回值以及它们的意义已经确定

  2. 终止条件,如果roor === null则高度是0,终止

  3. 单层递归逻辑,只考虑最外层,二叉树的高度应该是左右子树高度中最大的再加1。

所以我们不难得到getHeight的函数实现如下:

ts 复制代码
const getHeight = (root: TreeNode | null): number => {
  // 终止条件
  if (!root) return 0;

  // 返回左右子树的最大高度 + 1
  return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}

然后我们来分析isBalanced函数。

  1. 函数的参数和返回值已经分析清楚,传入根节点,返回布尔值,表示该二叉树是否平衡。

  2. 确定终止条件,如果root === null则直接返回true,空二叉树肯定平衡。

  3. 确定单层递归逻辑,在单层递归中,需要确保左右子树都是平衡二叉树且高度差不超过1。

题解

ts 复制代码
const getHeight = (root: TreeNode | null): number => {
  // 终止条件
  if (!root) return 0;

  // 返回左右子树的最大高度 + 1
  return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}

function isBalanced(root: TreeNode | null): boolean {
  // 终止条件
  if (!root) return true;

  // 当且仅当左右子树高度差小于等于1,且左右子树都是平衡二叉树时,当前树才是平衡二叉树
  return Math.abs(getHeight(root.left) - getHeight(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
};

这样就可以用顺利AC了,而且也很符合我们的习惯,按照我们的思路先判断两棵子树是不是平衡二叉树,再判断高度差是不是1。感觉起来都挺好的。但是仔细想想,我们就会发现这里怪怪的。isBalenced是一个递归函数,因为我们在他内部调用了自己。getHeigth也是一个递归函数,我们在它内部也调用了自己。然后我们又在isBalanced中调用了getHeight,也就是说,我们在一个递归函数中调用了另一个递归函数!

我们仔细想想,我们在获取高度的时候已经遍历过一次二叉树,但是在判断平衡的时候又进行了遍历。

也就是说每个节点可能会被多次遍历!这样会导致时间复杂度成为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)。那如何优化呢?聪明的你肯定想到了,我们可以再一次遍历中同时返回高度和是否平衡,一旦不平衡,立刻剪枝。这样只需要遍历一次,就可以得到整棵树是否平衡。

那具体操作可以在递归函数总返回一个对象,分别存储高度和是否平衡,但是本题比较简单,也可以只用一个变量。如果是平衡的,就返回其高度。如果不平衡,则返回-1。有的同学可能会说,你不平衡的时候返回-1不就丢失了高度了吗?确实,当我们返回-1时,我们只知道该子树不平衡,而不知道其高度,但是这并没有任何影响。因为一旦出现任何一个子树不平衡,整个二叉树一定不平衡,此时的高度已经失去了意义。所以我们只用一个变量就可以处理。

还是基于递归三部曲进行如下分析:

  1. 确定函数的参数和返回值以及它们的意义:
ts 复制代码
const dfs = (root: TreeNode | null): number => {

}

参数是传入的根节点,返回值是一个数字。如果该二叉树平衡,就返回其高度,如果不平衡就返回-1.

  1. 确定终止条件,当root === null时,返回高度是0

  2. 确定单层递归逻辑,首先判断左右子树递归调用的结果是不是-1,如果是-1,则本层也直接返回-1即可,肯定不平衡。如果不是-1,则比较两者返回值的差是否小于等于1,如果小于等于1,则返回两者中高度较大的+1,如果高度差大于1,则返回-1;

ts 复制代码
const dfs = (root: TreeNode | null): number => {
  // 终止条件
  if (!root) return 0;

  const left = dfs(root.left);
  const right = dfs(root.right);

  // 如果左右子树自身不是平衡二叉树,或者左右子树高度差大于1,返回-1
  if (left === -1 || right === -1 || Math.abs(left - right) > 1) {
    return -1;
  }

  // 返回当前树的高度
  return Math.max(left, right) + 1;
}

function isBalanced(root: TreeNode | null): boolean {
  // 如果平衡则返回高度,肯定不是-1,如果不平衡,返回-1
  return dfs(root) !== -1;
};

通过这种方式,我们实现了只遍历一次二叉树就同时获取到了高度和是否平衡的信息,从而实现在 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)的时间复杂度内解决问题。

总结

关于本题判断一个二叉树是否是平衡二叉树,按照顺着我们正常思路的方式,可以递归解决。但是时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)。通过我们自己定义的遍历函数,可以实现一次遍历同时得到两个信息,从而大大降低时间复杂度。

感兴趣的同学可以订阅本专栏刷爆leetcode,持续更新。

好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。

往期推荐✨✨✨

我是前端拿破轮,关注我,一起学习前端知识,我们下期见!

相关推荐
消失的旧时光-19432 分钟前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
wadesir5 分钟前
Rust中的条件变量详解(使用Condvar的wait方法实现线程同步)
开发语言·算法·rust
console.log('npc')15 分钟前
Table,vue3在父组件调用子组件columns列的方法展示弹窗文件预览效果
前端·javascript·vue.js
用户479492835691523 分钟前
React Hooks 的“天条”:为啥绝对不能写在 if 语句里?
前端·react.js
yugi98783824 分钟前
基于MATLAB实现协同过滤电影推荐系统
算法·matlab
TimberWill24 分钟前
哈希-02-最长连续序列
算法·leetcode·排序算法
Morwit42 分钟前
【力扣hot100】64. 最小路径和
c++·算法·leetcode
我命由我1234542 分钟前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
leoufung42 分钟前
LeetCode 373. Find K Pairs with Smallest Sums:从暴力到堆优化的完整思路与踩坑
java·算法·leetcode
用户47949283569151 小时前
给客户做私有化部署,我是如何优雅搞定 NPM 依赖管理的?
前端·后端·程序员