惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
月光博客
月光博客
S
SegmentFault 最新的问题
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
人人都是产品经理
人人都是产品经理
K
Kaspersky official blog
Forbes - Security
Forbes - Security
宝玉的分享
宝玉的分享
爱范儿
爱范儿
V
Visual Studio Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
The Cloudflare Blog
Attack and Defense Labs
Attack and Defense Labs
美团技术团队
L
LangChain Blog
NISL@THU
NISL@THU
IT之家
IT之家
T
Tor Project blog
云风的 BLOG
云风的 BLOG
Security Latest
Security Latest
Apple Machine Learning Research
Apple Machine Learning Research
Cisco Talos Blog
Cisco Talos Blog
I
InfoQ
Help Net Security
Help Net Security
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
I
Intezer
Recent Commits to openclaw:main
Recent Commits to openclaw:main
TaoSecurity Blog
TaoSecurity Blog
P
Palo Alto Networks Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
L
Lohrmann on Cybersecurity
The Register - Security
The Register - Security
W
WeLiveSecurity
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念
LeetCode Binary Search Tree 刷题模板
2022-05-25 · via 蛮荆

2022-05-25 算法 LeetCode

📖 概述

在计算机科学中,二叉搜索树(BST)也称为有序或排序二叉树,其中每个节点的值必须大于(或等于)其左子树中的任何值,同时小于(或等于)其右子树中的任何值。

二叉搜索树示例

按照二叉搜索树的定义,使用 中序遍历 来访问树的各个节点,最终可以得到一个有序的数组。

二叉搜索树转换为有序数组

刷题模板

LeetCode 中的二叉搜索树相关题目相对都比较简单,核心的解题思路就是充分利用二叉搜索树的有序性来完成对应的逻辑,这里直接给出一份基础模板代码,作用是通过中序遍历的方式来获取二叉搜索树的所有节点值 (已排序),读者可以在此模板代码上,根据不同的题来完成具体的逻辑部分代码,达到快速刷题的效果。

// 通过中序遍历获取二叉搜索树的所有节点值
func inorderTraversal(root *TreeNode) []int {
	var res []int
	var stack []*TreeNode
	node := root

	for node != nil || len(stack) > 0 {
		for node != nil {
			stack = append(stack, node)
			node = node.Left
		}

		node = stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		res = append(res, node.Val)

		node = node.Right
	}

	return res
}

二叉树的脚手架代码可以使用之前 二叉树刷题模板中的脚手架代码 来辅助刷题和调试代码。


💡 典型题目

二叉搜索树中序遍历得到的值序列是递增有序的,因此这类问题的解决步骤一般是 中序遍历 + 具体处理逻辑,只要得到中序遍历后的值序列 (利用前文中提到的模板),就可以在此基础上快速解题。

1. 二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

图片来源: https://leetcode.cn/

如图所示的二叉搜索树的最小差值为 1 (2 - 1 = 1)。

快速套解题模板:

  1. 最小绝对差来自于 离得最近的两个数
  2. 使用中序遍历获得二叉搜索树排序后的所有节点值
  3. 遍历所有节点值,挨个计算两个数之间的差值,同时更新这个过程中的最小差值
// 题解代码
func getMinimumDifference(root *TreeNode) int {
	vals := inorderTraversal(root)
	if len(vals) == 0 {
		return -1
	}

	// 最小差值初始化为一个超大数
	minDiff := math.MaxInt64
	for i := 1; i < len(vals); i++ {
		// 比较两个数的同时,更新最小差值
		minDiff = min(minDiff, vals[i]-vals[i-1])
	}

	return minDiff
}

// 使用中序遍历获得二叉搜索树排序后的所有节点值
// 这里直接使用前文 刷题模板中 提供的方法
// 为了节省篇幅、折叠方法的具体实现
func inorderTraversal(root *TreeNode) []int {
	...
}

二叉搜索树的最小绝对差 - 代码执行

2. 二叉搜索树中第 K 小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

图片来源: https://leetcode.cn/

k = 3 时,最小的元素等于 3,数组排序后为 [1, 2, 3, 4, 5, 6]。

快速套解题模板:

  1. 使用中序遍历获得二叉搜索树排序后的所有节点值
  2. 获取第 k 个数字

这道题套上模板之后,简直就是送分题。

// 题解代码
func kthSmallest(root *TreeNode, k int) int {
    nums := inorderTraversal(root)
    return nums[k-1]
}

// 使用中序遍历获得二叉搜索树排序后的所有节点值
// 这里直接使用前文 刷题模板中 提供的方法
// 为了节省篇幅、折叠方法的具体实现
func inorderTraversal(root *TreeNode) []int {
	...
}

二叉搜索树中第 K 小的元素 - 执行过程

继续优化

上面的题解代码快速地解决了问题,因为仅仅在模板代码的基础上加了两行代码,但其实方法内部有额外的空间消耗,例如二叉搜索树有很多节点,但是这时参数 k 的值很小,上面的方法依然会遍历整棵二叉树才可以。

根据搜索二叉树的性质,当寻找第 k 个较小元素时,只需要使用中序遍历 (也就是按照节点值大小顺序遍历),遍历到第 k 的元素时,直接返回,这时就可以直接在模板代码的基础上进行修改。

// 优化题解代码
// 只需要在模板代码基础上修改即可
func kthSmallest(root *TreeNode, k int) int {
	var stack []*TreeNode

	for {
		// 中序遍历顺序 
		// 1. 左子树
		// 2. 根节点
		// 3. 右子树

		// 1. 左子树
		for root != nil {
			stack = append(stack, root)
			root = root.Left
		}

		// 2. 根节点
		root = stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		// 每遍历一个数字时,将 k 减去 1
		// 作用: 累加器
		k--
		if k == 0 {
			// 当 k 等于 0 的时候
			// 当前节点值就是整棵搜索二叉树中第 k 小的元素
			return root.Val
		}

		// 右子树
		root = root.Right
	}
}

二叉搜索树中第 K 小的元素 (优化后) - 执行过程

3. 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 二叉搜索树。

解题思路:

这道题不需要查找求值,而是要构建二叉搜索树,所以前文中的刷题模板代码这里用不到。不过没关系,题目中给出的是已经排序完成的数组,根据二叉搜索树的性质,我们很容易通过分治 (递归) 的思路来解题:

  1. 将数组以中间的元素为基准,将数组分成 3 个部分: 左半部分、中间元素、右半部分
  2. 取出数组的中间元素作为根节点的值
  3. 将左半部分作为根节点的左子树 (递归调用)
  4. 将右半部分作为根节点的右子树 (递归调用)
  5. 返回根节点
// 题解代码
func sortedArrayToBST(nums []int) *TreeNode {
    return build(nums, 0, len(nums)-1)
}

func build(nums []int, left, right int) *TreeNode {
    if left > right {
        return nil
    }

	// 将数组以中间的元素为基准,将数组分成 3 个部分: 左半部分、中间元素、右半部分
    mid := left + (right-left) >> 1

	// 取出数组的中间元素作为根节点的值
    root := &TreeNode{Val: nums[mid]}

	// 将左半部分作为根节点的左子树 (递归调用)
    root.Left = build(nums, left, mid-1)

	// 将右半部分作为根节点的右子树 (递归调用)
    root.Right = build(nums, mid+1, right)

    return root
}

将有序数组转换为二叉搜索树 - 执行过程


💡 陷阱题

1. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

图片来源: https://leetcode.cn/

如图所示的二叉树 是 二叉搜索树。

图片来源: https://leetcode.cn/

如图所示的二叉树 不是 二叉搜索树。

根据二叉搜索树的特征,出于直觉,我们很容易写出下面的递归代码:

func isValidBST(root *TreeNode) bool {
	if root == nil {
		return true
	}

	// 如果根节点的值小于等于左节点的值, 不符合二叉搜索树规则
	if root.Left != nil && root.Val <= root.Left.Val {
		return false
	}

	// 如果根节点的值大于等于右节点的值, 不符合二叉搜索树规则
	if root.Right != nil && root.Val >= root.Right.Val {
		return false
	}

	// 递归判断左节点和右节点是否为二叉搜索树
	return isValidBST(root.Left) && isValidBST(root.Right)
}

这里有个陷阱: 我们认为只要左右节点符合二叉搜索树特征,递归下去就可以判断整棵树是否符合二叉搜索树特征,但是 问题的根源在于 我们需要确认的不止是左右两个节点,而是左右两棵子树,所以上面的代码在这个测试用例中就会返回错误的结果。

上图所示的二叉树并不是二叉搜索树,但是代码依然会返回 true, 原因就在于代码只是检测了右节点和其两个子节点是否为二叉搜索树,但是并未检测整个右子树是否为二叉搜索树。

现在刚才的代码基础上进行修正,具体来说:

  • 递归判断左子树时,除了判断 “当前节点值” 是否小于等于左节点值,还需要判断左子树中所有节点值是否小于整个树的根节点值
  • 递归判断右子树时,除了判断 “当前节点值” 是否大于等于右节点值,还需要判断右子树中所有节点值是否大于整个树的根节点值

修正后的方法每次递归时需要两个基准边界值 (low, hi) 作为参数,当前节点的值必须 大于 low 并且小于 hi , 这样才符合二叉搜索树的特征,同时在向树的深层递归时更新 (low, hi) 参数值,具体来说:

  • 递归判断左子树时,low 值保持不变,hi 值变为当前节点的值 (也就是说左节点及其子树中的所有节点值,都不能大于当前节点值)
  • 递归判断左子树时,hi 值保持不变,low 值变为当前节点的值 (也就是说右节点及其子树中的所有节点值,都不能小于当前节点值)

为了可以 LeetCode 官方提供的函数原型上正常提交 AC,这里单独写一个方法作为 “辅助方法”,然后在原型方法中调用即可,最终修正后的代码如下。

func isValidBST(root *TreeNode) bool {
	// 使用 64 位最小值和最大值来模拟 (low, hi) 左右边界值
	return helper(root, math.MinInt64, math.MaxInt64)
}

func helper(root *TreeNode, low, hi int) bool {
	if root == nil {
		return true
	}

	// 题目要求:
	// 节点的左子树只包含小于当前节点的数
	// 节点的右子树只包含大于当前节点的数
	if root.Val <= low || root.Val >= hi {
		return false
	}

	return helper(root.Left, low, root.Val) && helper(root.Right, root.Val, hi)
}

验证二叉搜索树 - 执行过程