LeetCode 98 | 判断二叉搜索树是否合法
今天是LeetCode专题第63篇文章,我们一起来聊聊LeetCode中的第98题,二叉搜索树的合法性判断问题。和之前介绍过的几道题类似,也是一道关于二叉搜索树的问题。
本题的官方难度是Medium,点赞4167,反对552,差评率比之前的两道题稍稍高了一点点,称得上是一道多半好评的题目。通过率27.7%相比之前40%以上的通过率,这道题的通过率算是Medium难度的题目中偏低的,也就意味着这道题相对困难一些,但题意还是很正的,和之前一样没有奇怪的逻辑,考察的就是朴实的理解。
题意
题意很简单,给定一棵二叉树要求判断它是否是一棵合法的二叉搜索树(BST)。
一棵合法的二叉搜索树需要满足三个条件:
- 左子树中的所有节点小于根节点
- 右子树中的所有节点大于根节点
- 不存在两个节点的值相等
样例
2
/
1 3
Input: [2,1,3]
Output: true
5
/
1 4
/
3 6
Input: [5,1,4,null,null,3,6]
Output: false
Explanation: The root node's value is 5 but its right child's value is 4.
题意
我们第一反应就是通过递归来解这道题,递归的思路当然是可以的,我们把判断的逻辑一层层嵌套,通过递归的形式让它自己调用自己,从而大大简化代码的编写。但是这其中有一个问题,就是这道题当中的逻辑关系相对不太容易传递。
我们举一个很简单的例子:
这是一颗二叉树,为了简化,我们把左子树简化成了A,右子树简化成了B。对于A来说,我们需要保证A当中的所有元素都小于root,然而对于B来说,它需要保证其中所有的元素都大于root。如果我们希望递归来实现这个判断的话,我们需要通过递归来遍历A和B当中的所有元素,来一一判断是否是满足条件的。
这当然是可行的,但是有一个很大的问题是效率很低。因为对于root来说我们需要遍历一次A和B,对于A和B当的根节点来说,我们又需要遍历一次。这其中有很多的逻辑和计算是重复的,这会带来大量的浪费,我们要做的是优化算法的流程,让它变得更优。
其实也很好分析,虽然二叉搜索树限制了左子树和右子树上所有的元素,但是我们仔细想想,其实不需要每个元素都考虑,只需要关注左子树当中最大值和右子树当中最小值就行了。如果左子树当中的最大值也小于根节点,右子树当中的最小值也大于根节点,那么就一定是合法的。反过来如果左子树存在比根节点大的数或者是右子树存在比根小的数就都是非法的。
我们可以通过递归来同时维护当前子树当中的最大值和最小值,只要用左子树的最大值和右子树的最小值和根节点进行校验即可。
附上代码:
import sys
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
def dfs(node):
if node is None:
return True, -sys.maxsize, sys.maxsize
# 递归获取左右子树的最大值以及最小值以及是否合法
left, lmax, lmin = dfs(node.left)
right, rmax, rmin = dfs(node.right)
# 如果左右子树存在非法,直接返回
if (not left) or (not right):
return False, lmax, rmin
# 否则比较左子树最大值与右子树最小值
if lmax >= node.val or rmin <= node.val:
return False, lmax, rmin
return True, max(node.val, rmax), min(node.val, lmin)
return dfs(root)[0]
这段代码看起来不长,但是其实不太好写,尤其是由于Python支持多值返回,所以一些操作和C++的递归不太一致,对于C++选手来说可能不是非常友好。但核心的原理是我们在递归求子树的最大值和最小值的同时也判断了子树是否是一棵合法的子树,递归不难写但要把这两个逻辑整合在一起对新手来说可能不太容易,推荐大家最好自己亲手写一次,加深一下理解。
- END -
- FLAnimatedImage -ios gif图片加载框架介绍
- linux学习第八篇:文件或目录权限chmod,更改所有者和所属组chown,umask,隐藏权限lsattr_chattr
- linux学习第九篇:特殊权限set_uid,set_gid,stick_bit以及软连接文件,硬链接文件
- C#事件(event)的一个实例
- linux学习第十篇:find命令,文件名后缀
- linux学习第十一篇:linux和Windows互传文件,用户配置文件和密码配置文件,用户组管理以及用户管理
- linux学习第十二篇:usermod命令,用户密码管理,mkpasswd命令
- 一斤代码深入理解系列(三):微信小程序和服务器通信
- C#CreateGraphics方法的三种实现方式
- 一斤代码深入理解系列(四):微信小程序和服务器通信-WebSocket
- linux学习第十四篇:查看磁盘,文件大小命令:df,du;磁盘分区
- 二叉树的性质和常用操作代码集合
- linux学习第十五篇:磁盘格式化,磁盘挂载,手动增加swap空间
- 《Java程序设计基础》 第8章手记Part 2
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法