#41
今日计划
1.写uniapp 示例代码 2*h 10-12.
再看会视频 1h 19.
:3h
2.写点Java 算法
二叉搜素树习题
avl树
中午先看视频课 1h
14.30 - 17.30
3h
:4h
3.Java 技术栈
4h
spring aop 看完 ssm框架下那个
maven 高级视频 20h的那个
:4h
4.小兔鲜儿day1
::10h
规划 aop 小兔项目 maven高级
今日源码
uniapp
demo2
以下是针对 **0 基础** 学习者的详细代码说明,包含 **Vue 基础语法** 讲解 + 逐行注释,用最通俗的语言拆解每个知识点:
---
### 完整代码(带 0 基础友好注释)
```html
<!--
1. <template>:Vue 模板区域,写 HTML 结构的地方
- 所有要渲染到页面的内容都写在这里
- 可以理解为“Vue 里的 HTML 部分”
-->
<template>
<!-- 最外层容器,相当于 HTML 里的 <div>,用来包裹页面内容 -->
<view class="container">
<!--
2. 文本渲染:{{ 变量 }} 是 Vue 的“插值语法”
- 把 JavaScript 里的变量显示到页面上
- 类似“把变量的值打印到这里”
-->
<text class="title">{{ title }}</text>
<!--
3. 条件渲染:v-if
- 类似“如果 showMessage 是 true,就显示这个 <view>”
- 反之则隐藏(Vue 会直接从 DOM 里删掉)
-->
<view v-if="showMessage" class="message">
{{ message }}
</view>
<!--
4. 事件绑定:@click
- 给按钮绑定点击事件,点击时执行 changeTitle 函数
- 类似 HTML 里的 onclick,但 Vue 用 @ 符号更简洁
-->
<button @click="changeTitle" type="primary">
修改标题
</button>
<!-- 商品列表区域 -->
<view class="list">
<!-- 加载状态提示:数据没加载完时显示 -->
<view v-if="isLoading" class="loading">
正在加载更多数据...
</view>
<!--
5. 列表渲染:v-for
- 遍历 displayedItems 数组,把每个元素渲染成 <view>
- :key="index" 是为了让 Vue 高效更新列表(必须写)
- 可以理解为“循环创建多个相同结构的 DOM”
-->
<view
v-for="(item, index) in displayedItems"
:key="index"
class="list-item"
>
{{ index + 1 }}. {{ item.name }} - {{ item.price }}元
<!-- 操作按钮:点击时执行 operateItem 函数,把当前 item 传过去 -->
<button class="operate-btn" @click="operateItem(item)">操作</button>
</view>
<!-- 加载更多按钮:如果还有数据没加载,就显示 -->
<button
v-if="hasMoreData"
@click="loadMore"
type="default"
class="load-more-btn"
>
加载更多
</button>
</view>
<!-- 计算属性展示:调用 calculateTotalPrice 函数显示总价格 -->
<view class="total-price">
商品总价格:{{ calculateTotalPrice() }}元
</view>
<!-- 表单区域 -->
<view class="form">
<!--
6. 双向绑定:v-model
- 输入框内容和 inputValue 变量“实时同步”
- 输入框变,变量变;变量变,输入框也变
-->
<u-input v-model="inputValue" placeholder="请输入内容" />
<button @click="addItem" type="default">添加</button>
</view>
</view>
</template>
<!--
7. <script setup>:Vue 的逻辑区域,写 JavaScript 代码的地方
- 所有变量、函数都写在这里
- 可以理解为“Vue 里的 JavaScript 部分”
-->
<script setup>
// 8. 导入 Vue 提供的响应式工具
// - ref:让变量变成“响应式”(数据变了,页面自动更新)
// - onMounted:页面加载完成后执行的钩子(类似 DOM 加载完)
// - computed:计算属性(高级用法,当前代码里用了函数替代,可忽略)
import { ref, onMounted } from 'vue';
// 9. 定义“响应式变量”
// - 用 ref 包裹后,变量变化会触发页面更新
const title = ref('Hello uni-app'); // 页面标题
const message = ref('欢迎学习uniapp Vue3开发'); // 欢迎消息
const showMessage = ref(true); // 控制欢迎消息是否显示
const items = ref([ // 商品列表原始数据
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 },
{ id: 3, name: '橙子', price: 4 }
]);
const inputValue = ref(''); // 表单输入框内容
const displayedItems = ref(items.value.slice(0, 3)); // 当前展示的商品(分页用)
const isLoading = ref(false); // 是否正在加载更多
const hasMoreData = ref(true); // 是否还有更多数据
// 10. 生命周期钩子:onMounted
// - 页面渲染完成后自动执行
onMounted(() => {
console.log('页面加载完成'); // 控制台打印日志
// 模拟异步请求:2秒后给商品列表加一条数据
setTimeout(() => {
items.value.push({ id: 4, name: '葡萄', price: 8 });
}, 2000);
});
// 11. 定义函数(方法)
// - 点击按钮时会调用这些函数
// 修改标题和隐藏消息
const changeTitle = () => {
title.value = '狗叫'; // 直接修改响应式变量的值
showMessage.value = false; // 隐藏欢迎消息
};
// 添加新商品
const addItem = () => {
// 去除输入框首尾空格,如果为空就不执行
if (!inputValue.value.trim()) return;
// 创建新商品对象
const newItem = {
id: items.value.length + 1, // id 自增
name: inputValue.value, // 用输入框的内容
price: Math.floor(Math.random() * 20) // 随机价格
};
items.value.push(newItem); // 加到原始数据
displayedItems.value.push(newItem); // 加到当前展示的数据
inputValue.value = ''; // 清空输入框
};
// 加载更多商品(模拟分页)
const loadMore = () => {
isLoading.value = true; // 显示“加载中”状态
// 模拟异步请求:1秒后执行
setTimeout(() => {
const startIndex = displayedItems.value.length; // 当前最后一条的索引
const endIndex = startIndex + 3; // 加载3条新数据
const newItems = items.value.slice(startIndex, endIndex); // 从原始数据里截取
displayedItems.value.push(...newItems); // 加到当前展示的数据
isLoading.value = false; // 隐藏“加载中”状态
// 判断是否还有更多数据
if (endIndex >= items.value.length) {
hasMoreData.value = false; // 没有更多了
}
}, 1000);
};
// 操作商品(示例:删除)
const operateItem = (item) => {
console.log('操作商品:', item); // 控制台打印当前商品
// 找到商品在原始数据里的索引
const index = items.value.findIndex(i => i.id === item.id);
if (index!== -1) { // 如果找到
items.value.splice(index, 1); // 从原始数据删除
// 从当前展示的数据删除
displayedItems.value.splice(displayedItems.value.findIndex(i => i.id === item.id), 1);
}
};
// 计算商品总价格(遍历求和)
const calculateTotalPrice = () => {
// reduce 是数组方法,遍历 items 求和
return items.value.reduce((sum, item) => sum + item.price, 0);
};
</script>
<!--
12. <style scoped>:Vue 的样式区域,写 CSS 的地方
- scoped:样式只作用于当前组件(不会影响其他组件)
- 可以理解为“Vue 里的 CSS 部分”
-->
<style scoped>
/* 最外层容器样式 */
.container {
padding: 30rpx; /* rpx 是 uni-app 适配多端的单位,类似 px */
background-color: #f8f8f8;
}
/* 标题样式 */
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
/* 欢迎消息样式 */
.message {
background-color: #e6f7ff;
color: #1890ff;
padding: 20rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
}
/* 列表容器样式 */
.list {
margin-top: 30rpx;
}
/* 列表项样式 */
.list-item {
padding: 20rpx;
border-bottom: 1rpx solid #eee;
font-size: 28rpx;
display: flex; /* 弹性布局,让内容和按钮左右排列 */
justify-content: space-between; /* 内容和按钮两端对齐 */
align-items: center; /* 垂直居中 */
}
/* 操作按钮样式 */
.operate-btn {
font-size: 24rpx;
padding: 10rpx 20rpx;
}
/* 加载中提示样式 */
.loading {
text-align: center;
padding: 20rpx;
color: #999;
}
/* 加载更多按钮样式 */
.load-more-btn {
display: block; /* 让按钮独占一行 */
margin: 20rpx auto; /* 水平居中 */
}
/* 总价格样式 */
.total-price {
margin-top: 20rpx;
font-size: 28rpx;
color: #ff5722;
font-weight: bold;
}
/* 表单样式 */
.form {
display: flex; /* 让输入框和按钮并排 */
margin-top: 30rpx;
}
/* 输入框样式 */
.form u-input {
flex: 1; /* 输入框占满剩余空间 */
margin-right: 20rpx;
}
</style>
```
---
### 0 基础核心语法拆解(Vue 基础 + uni-app 特色)
#### 1. **模板语法(`<template>` 里的内容)**
- **插值语法**:`{{ 变量 }}`
- 作用:把 JavaScript 里的变量显示到页面上。
- 示例:`{{ title }}` 会显示 `title` 变量的值(比如 `Hello uni-app`)。
- **指令(以 `v-` 开头的特殊属性)**
- `v-if="条件"`:条件为 `true` 时显示该元素,否则隐藏。
- 示例:`v-if="showMessage"` 控制欢迎消息是否显示。
- `v-for="(item, index) in 数组"`:循环渲染数组里的每个元素。
- 示例:`v-for="(item, index) in displayedItems"` 把 `displayedItems` 里的每个商品渲染成 `<view>`。
- `v-model="变量"`:双向绑定,输入框内容和变量实时同步。
- 示例:`v-model="inputValue"` 输入框输入的内容会直接存到 `inputValue` 里。
#### 2. **逻辑区域(`<script setup>` 里的内容)**
- **响应式变量**:用 `ref` 包裹的变量
- 作用:变量变化时,页面会自动更新。
- 示例:`const title = ref('Hello uni-app')`,修改 `title.value` 时,页面上的 `{{ title }}` 会同步变化。
- **生命周期钩子**:`onMounted`
- 作用:页面加载完成后自动执行的代码。
- 示例:`onMounted(() => { ... })` 里的代码会在页面渲染完成后执行(比如模拟异步请求)。
- **函数定义**:直接写 `const 函数名 = () => { ... }`
- 作用:处理用户交互(比如点击按钮)。
- 示例:`changeTitle` 函数在按钮点击时执行,修改 `title` 和 `showMessage`。
#### 3. **样式区域(`<style scoped>` 里的内容)**
- **`scoped` 属性**:样式只作用于当前组件,不会影响其他组件。
- **`rpx` 单位**:uni-app 专用单位,自动适配不同设备屏幕(类似 `px`,但更智能)。
- **Flex 布局**:用 `display: flex` 实现弹性布局,方便排列元素(比如让按钮和文字并排)。
#### 4. **uni-app 特色**
- **`view`、`text`、`button` 等组件**:uni-app 封装的跨平台组件,类似 HTML 的 `div`、`span`、`button`,但能更好地适配小程序、App 等。
- **`u-input` 组件**:`uView` 组件库的输入框(需要项目引入 `uView` 库才能用,普通 `input` 可用 `<input>` 或 uni-app 的 `<uni-input>`)。
---
### 学习路径建议(0 基础友好)
1. **先理解核心流程**:
- 页面结构写在 `<template>`(Vue 的 HTML)。
- 变量、函数写在 `<script setup>`(Vue 的 JavaScript)。
- 样式写在 `<style scoped>`(Vue 的 CSS)。
2. **逐个突破语法**:
- 先掌握 `{{ 变量 }}`、`v-if`、`v-for`、`v-model`(最常用)。
- 再学 `ref`、`onMounted`(理解响应式和生命周期)。
3. **动手改代码**:
- 试着修改 `title` 的初始值、`changeTitle` 的逻辑,看页面变化。
- 删改商品列表,观察 `v-for` 的渲染效果。
4. **结合实际需求扩展**:
- 比如给“操作”按钮加个确认删除的提示(用 `uni.showModal`)。
- 把模拟的异步请求换成真实的接口调用(用 `uni.request`)。
这样拆分后,0 基础也能一步步理解 **Vue 基础语法** 和 **uni-app 组件逻辑**,遇到不懂的可以再针对性提问~
算法
Leetcode450
以下是添加了详细注释的Java版本代码:
```java
class Solution {
/**
* 删除二叉搜索树中的指定节点,并保持BST性质
*
* @param root 当前子树的根节点
* @param key 要删除的节点值
* @return 删除节点后的新子树根节点
*/
public TreeNode deleteNode(TreeNode root, int key) {
// 1. 基本情况:如果当前节点为空,直接返回null
if (root == null) return null;
// 2. 递归查找要删除的节点
if (key < root.val) {
// 目标节点在左子树中
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
// 目标节点在右子树中
root.right = deleteNode(root.right, key);
} else {
// 3. 找到目标节点,处理删除逻辑
// 情况1:无左子树,直接返回右子树
if (root.left == null) {
return root.right;
}
// 情况2:无右子树,直接返回左子树
else if (root.right == null) {
return root.left;
}
// 情况3:有两个子节点
else {
// 3.1 找到右子树中的最小节点(中序后继节点)
TreeNode minNode = findMin(root.right);
// 3.2 用后继节点的值替换当前节点的值
root.val = minNode.val;
// 3.3 递归删除右子树中的后继节点
root.right = deleteNode(root.right, minNode.val);
}
}
// 返回更新后的当前子树根节点
return root;
}
/**
* 查找以node为根的子树中的最小节点
* BST的最小节点是最左侧节点
*/
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}
// 二叉树节点定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
```
### 核心逻辑解释
1. **递归查找策略**:
- BST特性:左子树节点值 < 根节点值 < 右子树节点值
- 通过比较key与当前节点值的大小,决定往左/右子树递归
2. **删除节点的三种情况**:
```
情况1:待删除节点无左子树
root
/ \
null R
→ 直接返回右子树R
情况2:待删除节点无右子树
root
/ \
L null
→ 直接返回左子树L
情况3:待删除节点有两个子树
root
/ \
L R
→ 找到右子树R的最小节点S(中序后继)
→ 用S的值替换root的值
→ 递归删除R中的S节点
```
3. **中序后继节点的作用**:
- 中序遍历BST得到升序序列,当前节点的后继节点是刚好大于它的最小节点
- 后继节点位于右子树的最左侧,保证替换后仍满足BST性质
4. **递归的回溯过程**:
- 递归删除子节点后,上层节点的left/right指针会自动更新
- 无需显式处理父节点指针,递归返回值会自动完成链接
### 复杂度分析
- **时间复杂度**:O(h),h为树的高度
- 最坏情况(退化为链表):O(n)
- 平均情况(平衡树):O(log n)
- **空间复杂度**:O(h),递归栈深度
- 尾递归优化后可降至O(1)(需使用迭代实现)
这个算法巧妙地利用了BST的性质,通过递归和节点替换保持了树的结构完整性,是处理BST删除操作的经典方法。
Leetcode 701
LeetCode 701题是**二叉搜索树中的插入操作**,题目链接为:https://leetcode.cn/problems/insert-into-a-binary-search-tree/ 。下面为你详细讲解这道题。
### 题目描述
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果 。
### 示例
```
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是 [5,2,7,1,3,null,4] ,
这里给出其中一种情况。
```
### 解题思路
- **递归法**:
- 二叉搜索树的特性是左子树的所有节点值都小于根节点值,右子树的所有节点值都大于根节点值。利用这个特性,从根节点开始,将待插入的值 `val` 与当前节点的值进行比较。
- 如果 `val` 小于当前节点的值,那么就递归地在当前节点的左子树中插入;如果左子树为空,直接将新节点作为左子节点插入。
- 如果 `val` 大于当前节点的值,那么就递归地在当前节点的右子树中插入;如果右子树为空,直接将新节点作为右子节点插入。
- 递归结束的条件是遇到空节点,此时创建一个新节点并返回。
- **迭代法**:
- 同样基于二叉搜索树的特性,从根节点开始,使用一个循环来找到插入位置。
- 循环中,将待插入的值 `val` 与当前节点的值进行比较,决定向左或向右移动。
- 当找到一个空的子节点位置时,创建新节点并插入。
### 代码实现
**递归法(Java)**:
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
// 遇到空节点,创建新节点并返回
return new TreeNode(val);
}
if (val < root.val) {
// 插入值小于当前节点值,递归插入左子树
root.left = insertIntoBST(root.left, val);
} else {
// 插入值大于当前节点值,递归插入右子树
root.right = insertIntoBST(root.right, val);
}
return root;
}
}
```
**迭代法(Java)**:
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
TreeNode current = root;
while (true) {
if (val < current.val) {
if (current.left == null) {
// 左子树为空,插入新节点
current.left = new TreeNode(val);
break;
}
current = current.left;
} else {
if (current.right == null) {
// 右子树为空,插入新节点
current.right = new TreeNode(val);
break;
}
current = current.right;
}
}
return root;
}
}
```
### 复杂度分析
- **时间复杂度**:
- 递归法和迭代法的时间复杂度都是 $O(h)$,其中 $h$ 是二叉搜索树的高度。在平均情况下,二叉搜索树是平衡的,高度为 $O(\log n)$,$n$ 是树中节点的数量;在最坏情况下,树退化为链表,高度为 $O(n)$。
- **空间复杂度**:
- 递归法的空间复杂度取决于递归调用栈的深度,也就是树的高度,为 $O(h)$。在平均情况下是 $O(\log n)$,最坏情况下是 $O(n)$。
- 迭代法的空间复杂度为 $O(1)$,因为只使用了常数级别的额外空间。
E03. 查询节点-Leetcode 700
### LeetCode 700:二叉搜索树中的搜索
#### 题目描述
给定二叉搜索树(BST)的根节点和一个值 `val`,在树中查找是否存在值为 `val` 的节点,并返回该节点的子树;如果不存在,则返回 `null`。
#### 示例
```
输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]
输入:root = [4,2,7,1,3], val = 5
输出:null
```
#### 解题思路
由于二叉搜索树(BST)具有以下性质:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 左右子树也都是BST
因此查找过程可以利用BST的有序性,通过以下方式实现:
1. **递归法**:从根节点开始,若当前节点值等于目标值则返回;若目标值更小则在左子树查找;若更大则在右子树查找
2. **迭代法**:使用循环代替递归,避免栈溢出问题,适合处理深树结构
#### 代码实现
**递归解法(Java)**
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
// 基本情况:当前节点为空或找到目标值
if (root == null || root.val == val) {
return root;
}
// 根据BST性质决定搜索方向
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
}
```
**迭代解法(Java)**
```java
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
// 从根节点开始迭代查找
while (root != null) {
if (root.val == val) {
return root; // 找到目标节点
} else if (val < root.val) {
root = root.left; // 目标值更小,搜索左子树
} else {
root = root.right; // 目标值更大,搜索右子树
}
}
return null; // 未找到目标节点
}
}
```
#### 复杂度分析
- **时间复杂度**:O(h),其中h为树的高度
- 平均情况:BST平衡时,h = log(n),n为节点数
- 最坏情况:树退化为链表时,h = n
- **空间复杂度**:
- 递归解法:O(h),递归栈深度
- 迭代解法:O(1),仅使用常数级额外空间
#### 算法优化点
1. **提前终止**:找到目标值后立即返回,无需继续搜索
2. **非递归实现**:对于深树结构,迭代法可避免栈溢出风险
3. **尾递归优化**:若语言支持(如Python的`functools.lru_cache`),可优化递归性能
#### 常见错误处理
1. **空指针检查**:在访问`root.left`或`root.right`前,需确保`root`不为空
2. **边界条件**:处理只有根节点、只有左/右子树的特殊情况
3. **值比较逻辑**:严格遵循BST的大小关系,避免方向判断错误
该问题是BST的基础操作,掌握搜索、插入、删除这三大操作是理解BST数据结构的核心。
验证二叉搜索树-Leetcode 98
### LeetCode 98: 验证二叉搜索树
#### 题目描述
给定一个二叉树,判断其是否是一个有效的二叉搜索树(BST)。
有效二叉搜索树定义如下:
- 左子树上所有节点的值均小于根节点的值
- 右子树上所有节点的值均大于根节点的值
- 左右子树也必须是二叉搜索树
#### 示例
```
输入: [2,1,3]
输出: true
输入: [5,1,4,null,null,3,6]
输出: false
解释: 根节点值为5,右子节点值为4,但4小于5,因此不是有效BST
```
#### 解题思路
##### 方法一:中序遍历法
二叉搜索树的重要特性是:**中序遍历结果为严格递增序列**。因此可以通过以下步骤验证:
1. 对二叉树进行中序遍历
2. 检查遍历结果是否严格递增
##### 方法二:递归边界法
为每个节点维护允许的数值范围:
1. 根节点的初始范围是(-∞, +∞)
2. 左子节点的范围是(父节点最小值, 父节点值)
3. 右子节点的范围是(父节点值, 父节点最大值)
4. 如果当前节点值不在允许范围内,则不是有效BST
#### 代码实现
##### 中序遍历法(Java)
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class Solution {
// 存储中序遍历结果
private List<Integer> inorderList = new ArrayList<>();
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
// 1. 中序遍历
inorderTraversal(root);
// 2. 检查是否严格递增
for (int i = 1; i < inorderList.size(); i++) {
if (inorderList.get(i) <= inorderList.get(i - 1)) {
return false;
}
}
return true;
}
// 中序遍历辅助方法
private void inorderTraversal(TreeNode root) {
if (root == null) return;
inorderTraversal(root.left);
inorderList.add(root.val);
inorderTraversal(root.right);
}
}
```
##### 递归边界法(Java)
```java
class Solution {
public boolean isValidBST(TreeNode root) {
// 初始范围:最小值为Integer.MIN_VALUE,最大值为Integer.MAX_VALUE
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST(TreeNode root, long min, long max) {
// 空节点视为有效BST
if (root == null) return true;
// 检查当前节点值是否在允许范围内
if (root.val <= min || root.val >= max) {
return false;
}
// 左子树范围:[min, root.val)
// 右子树范围:(root.val, max)
return isValidBST(root.left, min, root.val) &&
isValidBST(root.right, root.val, max);
}
}
```
#### 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|------|------------|------------|
| 中序遍历法 | O(n) - 遍历所有节点 | O(n) - 存储中序遍历结果 |
| 递归边界法 | O(n) - 遍历所有节点 | O(h) - 递归栈深度,h为树高 |
#### 关键技巧
1. **避免整数溢出**:
- 在递归边界法中使用`long`类型存储边界值,避免`Integer.MIN_VALUE`和`Integer.MAX_VALUE`导致的溢出问题
2. **严格递增判断**:
- 必须确保每个节点值严格大于左子树所有值,严格小于右子树所有值
- 注意不能使用"小于等于"或"大于等于"
3. **边界传递逻辑**:
- 左子树的最大值是父节点值
- 右子树的最小值是父节点值
- 这个边界会随着递归深度逐层传递
#### 常见错误
1. **只比较直接子节点**:
```java
// 错误示例:仅比较直接子节点,忽略了子树的整体约束
return root.left.val < root.val && root.right.val > root.val;
```
2. **未正确处理边界值**:
```java
// 错误示例:使用Integer范围可能导致溢出
return isValidBST(root, Integer.MIN_VALUE, Integer.MAX_VALUE);
```
3. **混淆"小于"和"小于等于"**:
```java
// 错误示例:允许节点值等于边界值
if (root.val <= min || root.val >= max) {
```
这道题是二叉搜索树的基础验证问题,理解BST的性质和验证方法对解决更复杂的树问题非常重要。
求范围和-Leetcode 938
### LeetCode 938: 二叉搜索树的范围和
#### 题目描述
给定二叉搜索树的根节点 `root`,返回值位于范围 `[low, high]` 之间的所有节点的值的和。
#### 示例
```
输入: root = [10,5,15,3,7,null,18], low = 7, high = 15
输出: 32
输入: root = [10,5,15,3,7,13,18,1,null,6], low = 6, high = 10
输出: 23
```
#### 解题思路
由于二叉搜索树(BST)具有以下性质:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
因此可以利用BST的有序性优化搜索过程:
1. **递归法**:根据当前节点值与范围的关系决定搜索方向
- 若当前节点值 < low:只需要搜索右子树
- 若当前节点值 > high:只需要搜索左子树
- 若在范围内:累加当前节点值,并搜索左右子树
2. **迭代法**:使用栈模拟递归过程,避免栈溢出
#### 代码实现
**递归解法(Java)**
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class Solution {
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
int sum = 0;
// 当前节点值在范围内,累加并搜索左右子树
if (root.val >= low && root.val <= high) {
sum = root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);
}
// 当前节点值小于low,只搜索右子树
else if (root.val < low) {
sum = rangeSumBST(root.right, low, high);
}
// 当前节点值大于high,只搜索左子树
else {
sum = rangeSumBST(root.left, low, high);
}
return sum;
}
}
```
**迭代解法(Java)**
```java
class Solution {
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
int sum = 0;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node != null) {
// 当前节点值在范围内,累加到sum
if (node.val >= low && node.val <= high) {
sum += node.val;
}
// 根据BST性质决定入栈顺序:先压右子节点,再压左子节点,保证栈顶是左子节点先处理
if (node.val < high) {
stack.push(node.right);
}
if (node.val > low) {
stack.push(node.left);
}
}
}
return sum;
}
}
```
#### 复杂度分析
- **时间复杂度**:O(n),其中n为树中节点数量。最坏情况下需要遍历所有节点。
- **空间复杂度**:
- 递归解法:O(h),h为树的高度,递归栈的深度。
- 迭代解法:O(h),栈的最大深度为树的高度。
#### 优化技巧
1. **剪枝策略**:
- 当当前节点值 < low时,左子树所有节点值一定都 < low,无需搜索左子树
- 当当前节点值 > high时,右子树所有节点值一定都 > high,无需搜索右子树
2. **遍历顺序**:
- 迭代解法中采用"先右后左"的入栈顺序,确保左子树先被处理,符合中序遍历逻辑
- 也可以采用中序遍历方式,只累加在范围内的节点值
#### 代码优化版本
```java
class Solution {
// 优化后的递归解法,代码更简洁
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
// 根据当前节点值与范围的关系决定返回值
if (root.val < low) {
return rangeSumBST(root.right, low, high);
}
if (root.val > high) {
return rangeSumBST(root.left, low, high);
}
// 当前节点值在范围内,累加左右子树结果
return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);
}
}
```
#### 常见错误
1. **边界条件处理**:
- 忘记处理`root == null`的情况
- 范围判断使用`<`和`>`而非`<=`和`>=`
2. **迭代栈操作顺序**:
- 错误地先压左子节点再压右子节点,导致遍历顺序错误
- 未根据节点值与范围的关系选择性入栈
3. **性能优化忽略**:
- 未利用BST性质进行剪枝,导致遍历了不需要的节点
该问题充分体现了二叉搜索树的有序性优势,通过合理利用BST性质可以减少不必要的节点访问,提高算法效率。
根据前序遍历结果构造二叉搜索树-Leetcode 1008
### LeetCode 1008: 根据前序遍历构造二叉搜索树
#### 题目描述
给定一个整数数组 `preorder`,它是二叉搜索树(BST)的前序遍历结果,请构造并返回该二叉搜索树的根节点。
#### 示例
```
输入: preorder = [8,5,1,7,10,12]
输出: [8,5,10,1,7,null,12]
输入: preorder = [1,3]
输出: [1,null,3]
```
#### 解题思路
二叉搜索树的前序遍历结果具有以下特性:
- **根节点**:数组的第一个元素
- **左子树**:所有小于根节点的值组成的子数组
- **右子树**:所有大于根节点的值组成的子数组
因此,可以采用以下步骤递归构造BST:
1. **确定根节点**:前序遍历的第一个元素即为根节点
2. **划分左右子树**:找到第一个大于根节点的元素位置,将数组分为左右两部分
3. **递归构造**:分别对左右子数组递归构造左右子树
#### 代码实现
**递归解法(Java)**
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class Solution {
public TreeNode bstFromPreorder(int[] preorder) {
return buildTree(preorder, 0, preorder.length - 1);
}
private TreeNode buildTree(int[] preorder, int start, int end) {
// 递归终止条件:起始索引大于结束索引
if (start > end) {
return null;
}
// 根节点为当前区间的第一个元素
TreeNode root = new TreeNode(preorder[start]);
// 找到第一个大于根节点值的位置,划分左右子树
int index = start + 1;
while (index <= end && preorder[index] < preorder[start]) {
index++;
}
// 递归构造左右子树
root.left = buildTree(preorder, start + 1, index - 1);
root.right = buildTree(preorder, index, end);
return root;
}
}
```
**优化解法(递归+上界约束)**
```java
class Solution {
private int index = 0;
public TreeNode bstFromPreorder(int[] preorder) {
return buildTree(preorder, Integer.MAX_VALUE);
}
private TreeNode buildTree(int[] preorder, int upperBound) {
// 遍历完所有元素或当前元素超过上界时返回null
if (index >= preorder.length || preorder[index] > upperBound) {
return null;
}
// 创建当前节点
TreeNode root = new TreeNode(preorder[index++]);
// 递归构造左右子树:左子树的上界为当前节点值,右子树的上界为当前上界
root.left = buildTree(preorder, root.val);
root.right = buildTree(preorder, upperBound);
return root;
}
}
```
#### 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|------|------------|------------|
| 递归划分法 | O(n²) | O(n) |
| 上界约束法 | O(n) | O(n) |
**优化点说明**:
- **递归划分法**:每次需要遍历数组寻找划分点,导致时间复杂度为O(n²)
- **上界约束法**:通过维护当前子树的上界,只需一次遍历即可完成构造,时间复杂度优化至O(n)
#### 关键技巧
1. **上界约束**:
- 利用BST的特性,左子树所有节点值必须小于根节点值,右子树所有节点值必须小于父节点的上界
- 通过递归传递上界参数,确保节点值符合BST约束
2. **全局索引**:
- 使用全局变量`index`记录当前处理的元素位置,避免在递归过程中传递过多参数
3. **剪枝策略**:
- 当元素值超过当前上界时,说明该元素不属于当前子树,直接返回null
#### 常见错误
1. **数组越界**:
```java
// 错误示例:未检查index是否越界
while (preorder[index] < preorder[start]) {
index++;
}
```
2. **左右子树划分错误**:
```java
// 错误示例:错误的递归区间
root.left = buildTree(preorder, start, index - 1); // 应为start+1
root.right = buildTree(preorder, index, end + 1); // 应为end
```
3. **上界传递错误**:
```java
// 错误示例:右子树的上界应为父节点的上界,而非父节点值
root.right = buildTree(preorder, root.val); // 错误
root.right = buildTree(preorder, upperBound); // 正确
```
该问题通过前序遍历序列构造BST,充分体现了BST的有序性特性。掌握递归构造和上界约束的技巧,对于解决其他树构造问题也非常有帮助。
二叉搜索树的最近公共祖先-Leetcode 235
### LeetCode 235: 二叉搜索树的最近公共祖先
#### 题目描述
给定一个二叉搜索树(BST), 找到该树中两个指定节点的最近公共祖先(LCA)。
最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
#### 示例
```
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
```
#### 解题思路
由于二叉搜索树(BST)的特性:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
因此,对于任意节点,其左右子树的节点值分布具有明确的有序性。可以利用这一特性快速找到最近公共祖先:
1. **递归法**:
- 若当前节点值同时大于p和q的值,则LCA在左子树中
- 若当前节点值同时小于p和q的值,则LCA在右子树中
- 否则(p和q分别位于当前节点两侧或其中一个等于当前节点),当前节点即为LCA
2. **迭代法**:
- 从根节点开始,根据当前节点值与p、q值的大小关系,决定向左或向右移动
- 当找到第一个节点值位于p和q之间时,该节点即为LCA
#### 代码实现
**递归解法(Java)**
```java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 如果当前节点值大于p和q的值,LCA在左子树
if (root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
}
// 如果当前节点值小于p和q的值,LCA在右子树
if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
}
// 否则当前节点即为LCA
return root;
}
}
```
**迭代解法(Java)**
```java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode current = root;
while (current != null) {
// 如果当前节点值大于p和q的值,向左移动
if (current.val > p.val && current.val > q.val) {
current = current.left;
}
// 如果当前节点值小于p和q的值,向右移动
else if (current.val < p.val && current.val < q.val) {
current = current.right;
}
// 否则当前节点即为LCA
else {
return current;
}
}
return null; // 理论上不会执行到这里,因为题目保证存在解
}
}
```
#### 复杂度分析
- **时间复杂度**:O(h),其中h为树的高度。最坏情况下需要遍历树的高度。
- **空间复杂度**:
- 递归解法:O(h),递归栈的深度为树的高度。
- 迭代解法:O(1),只需要常数级的额外空间。
#### 关键技巧
1. **利用BST的有序性**:
- 无需遍历整棵树,只需根据节点值的大小关系决定搜索方向
- 当p和q分别位于当前节点两侧时,当前节点即为LCA
2. **迭代优化**:
- 由于不需要回溯,迭代解法更高效,避免了递归栈的开销
3. **边界条件处理**:
- 当p或q等于当前节点时,直接返回当前节点
- 确保处理p和q的顺序不影响结果
#### 常见错误
1. **忽略BST特性**:
```java
// 错误示例:未利用BST特性,遍历整棵树
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// ...
}
```
2. **方向判断错误**:
```java
// 错误示例:混淆大于小于符号
if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.left, p, q); // 错误:应向右移动
}
```
3. **未处理p和q的顺序**:
```java
// 错误示例:仅考虑p<q的情况
if (root.val > p.val && root.val < q.val) { // 错误:未考虑p>q的情况
return root;
}
```
该问题通过利用BST的有序性,将LCA的查找时间复杂度优化到O(h),是BST特性的典型应用。掌握这种高效的查找方法对于解决其他树相关问题也非常有帮助。
题目汇总
package com.itheima.BST;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class BstLeetCode {
//E01 450. 删除二叉搜索树中的节点
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) {
return null;
}
if (key < root.val) {
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
root.right = deleteNode(root.right, key);
} else {
if (root.left == null) {
return root.right;
} else if (root.right == null) {
return root.left;
} else {
TreeNode minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteNode(root.right, minNode.val);
}
}
return root;
}
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
//Leetcode 701 二叉搜索树中的插入操作
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (val < root.val) {
root.left = insertIntoBST(root.left, val);
} else {
root.right = insertIntoBST(root.right, val);
}
return root;
}
public TreeNode insertIntoBST2(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
TreeNode current = root;
while (true) {
if (val < current.val) {
if (current.left == null) {
current.left = new TreeNode(val);
break;
} else {
current = current.left;
}
} else {
if (current.right == null) {
current.right = new TreeNode(val);
break;
} else {
current = current.right;
}
}
}
return root;
}
//Leetcode 700 二叉搜索树中的搜索
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
public TreeNode searchBST2(TreeNode root, int val) {
while (root != null) {
if (root.val == val) {
return root;
} else if (val < root.val) {
root = root.left;
} else {
root = root.right;
}
}
return null;
}
//Leetcode 98 验证二叉搜索树
private List<Integer> inOrderList = new ArrayList<>();
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
inOrderTraversal(root);
for (int i = 1; i < inOrderList.size(); i++) {
if (inOrderList.get(i) <= inOrderList.get(i - 1)) {
return false;
}
}
return true;
}
private void inOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
inOrderTraversal(root.left);
inOrderList.add(root.val);
inOrderTraversal(root.right);
}
public boolean isValidBST2(TreeNode root) {
return isValidBST2(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST2(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
if (root.val < min || root.val > max) {
return false;
}
return isValidBST2(root.left, min, root.val) && isValidBST2(root.right, root.val, max);
}
//Leetcode 938 二叉搜索树的范围和
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
int sum = 0;
if (root.val >= low && root.val <= high) {
sum = root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);
} else if (root.val < low) {
sum += rangeSumBST(root.left, low, high);
} else {
sum += rangeSumBST(root.right, low, high);
}
return sum;
}
public int rangeSumBST2(TreeNode root, int low, int high) {
if (root == null) {
return 0;
}
int sum = 0;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node != null) {
if (node.val >= low && node.val <= high) {
sum += node.val;
}
if (node.val < high) {
stack.push(node.right);
}
if (node.val > low) {
stack.push(node.left);
}
}
}
return sum;
}
//LeetCode 1008: 根据前序遍历构造二叉搜索树
public TreeNode bstFromPreorder(int[] preorder) {
return buildTree(preorder, 0, preorder.length - 1);
}
private TreeNode buildTree(int[] preorder, int start, int end) {
if (start > end) {
return null;
}
TreeNode root = new TreeNode(preorder[start]);
int index = start + 1;
while (index < end && preorder[index] < root.val) {
index++;
}
root.left = buildTree(preorder, start + 1, index - 1);
root.right = buildTree(preorder, index, end);
return root;
}
private int index = 0;
public TreeNode bstFromPreorder2(int[] preorder) {
return buildTree2(preorder, Integer.MAX_VALUE);
}
private TreeNode buildTree2(int[] preorder, int upperBound) {
if (index >= preorder.length || preorder[index] > upperBound) {
return null;
}
TreeNode root = new TreeNode(preorder[index++]);
root.left = buildTree2(preorder, root.val);
root.right = buildTree2(preorder, upperBound);
return root;
}
//LeetCode 235: 二叉搜索树的最近公共祖先
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
} else if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
} else {
return root;
}
}
public TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q) {
TreeNode current = root;
while (current!= null) {
if (current.val > p.val && current.val > q.val) {
current = current.left;
} else if (current.val < p.val && current.val < q.val) {
current = current.right;
} else {
return current;
}
}
return null;
}
}
详细解答
以下是添加了详细注释的完整代码,解释了每个方法的核心逻辑、复杂度分析和关键点:
```java
package com.itheima.BST;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* 二叉搜索树(BST)相关LeetCode题解
* 包含搜索、插入、删除、验证、构造等操作
*/
public class BstLeetCode {
// E01 450. 删除二叉搜索树中的节点
/**
* 删除BST中的指定节点,保持BST性质
* 时间复杂度:O(h),h为树的高度
* 空间复杂度:O(h),递归栈深度
*/
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
// 递归查找要删除的节点
if (key < root.val) {
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
root.right = deleteNode(root.right, key);
} else {
// 找到节点,处理删除逻辑
if (root.left == null) return root.right;
if (root.right == null) return root.left;
// 有两个子节点的情况:找到右子树的最小节点(中序后继)
TreeNode minNode = findMin(root.right);
root.val = minNode.val; // 用后继节点值替换当前节点
root.right = deleteNode(root.right, minNode.val); // 删除后继节点
}
return root;
}
// 查找以node为根的子树中的最小节点
private TreeNode findMin(TreeNode node) {
while (node.left != null) node = node.left;
return node;
}
// Leetcode 701 二叉搜索树中的插入操作
/**
* 递归插入新节点,保持BST性质
* 时间复杂度:O(h)
* 空间复杂度:O(h)
*/
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
if (val < root.val) {
root.left = insertIntoBST(root.left, val);
} else {
root.right = insertIntoBST(root.right, val);
}
return root;
}
/**
* 迭代插入新节点,保持BST性质
* 时间复杂度:O(h)
* 空间复杂度:O(1)
*/
public TreeNode insertIntoBST2(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
TreeNode current = root;
while (true) {
if (val < current.val) {
if (current.left == null) {
current.left = new TreeNode(val);
break;
}
current = current.left;
} else {
if (current.right == null) {
current.right = new TreeNode(val);
break;
}
current = current.right;
}
}
return root;
}
// Leetcode 700 二叉搜索树中的搜索
/**
* 递归搜索指定值的节点
* 时间复杂度:O(h)
* 空间复杂度:O(h)
*/
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) return root;
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
/**
* 迭代搜索指定值的节点
* 时间复杂度:O(h)
* 空间复杂度:O(1)
*/
public TreeNode searchBST2(TreeNode root, int val) {
while (root != null) {
if (root.val == val) return root;
root = val < root.val ? root.left : root.right;
}
return null;
}
// Leetcode 98 验证二叉搜索树
private List<Integer> inOrderList = new ArrayList<>();
/**
* 中序遍历验证BST(非最优解)
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
// 中序遍历生成有序列表
inOrderTraversal(root);
// 检查列表是否严格递增
for (int i = 1; i < inOrderList.size(); i++) {
if (inOrderList.get(i) <= inOrderList.get(i - 1)) {
return false;
}
}
return true;
}
private void inOrderTraversal(TreeNode root) {
if (root == null) return;
inOrderTraversal(root.left);
inOrderList.add(root.val);
inOrderTraversal(root.right);
}
/**
* 递归边界验证BST(最优解)
* 时间复杂度:O(n)
* 空间复杂度:O(h)
*/
public boolean isValidBST2(TreeNode root) {
return isValidBST2(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST2(TreeNode root, long min, long max) {
if (root == null) return true;
// 当前节点值必须在(min, max)范围内
if (root.val <= min || root.val >= max) return false;
// 递归验证左右子树
return isValidBST2(root.left, min, root.val) &&
isValidBST2(root.right, root.val, max);
}
// Leetcode 938 二叉搜索树的范围和
/**
* 递归计算范围内节点和
* 时间复杂度:O(n)
* 空间复杂度:O(h)
*/
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) return 0;
if (root.val >= low && root.val <= high) {
// 当前节点在范围内,累加左右子树
return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);
} else if (root.val < low) {
// 当前节点小于下界,只搜索右子树
return rangeSumBST(root.right, low, high);
} else {
// 当前节点大于上界,只搜索左子树
return rangeSumBST(root.left, low, high);
}
}
/**
* 迭代计算范围内节点和
* 时间复杂度:O(n)
* 空间复杂度:O(h)
*/
public int rangeSumBST2(TreeNode root, int low, int high) {
if (root == null) return 0;
int sum = 0;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node != null) {
if (node.val >= low && node.val <= high) {
sum += node.val;
}
// 根据BST性质选择性入栈
if (node.val < high) stack.push(node.right);
if (node.val > low) stack.push(node.left);
}
}
return sum;
}
// LeetCode 1008: 根据前序遍历构造二叉搜索树
/**
* 递归构造BST(方法一:划分左右子树)
* 时间复杂度:O(n²)
* 空间复杂度:O(h)
*/
public TreeNode bstFromPreorder(int[] preorder) {
return buildTree(preorder, 0, preorder.length - 1);
}
private TreeNode buildTree(int[] preorder, int start, int end) {
if (start > end) return null;
TreeNode root = new TreeNode(preorder[start]);
// 找到第一个大于根节点的位置,划分左右子树
int index = start + 1;
while (index <= end && preorder[index] < root.val) {
index++;
}
root.left = buildTree(preorder, start + 1, index - 1);
root.right = buildTree(preorder, index, end);
return root;
}
private int index = 0;
/**
* 递归构造BST(方法二:上界约束)
* 时间复杂度:O(n)
* 空间复杂度:O(h)
*/
public TreeNode bstFromPreorder2(int[] preorder) {
return buildTree2(preorder, Integer.MAX_VALUE);
}
private TreeNode buildTree2(int[] preorder, int upperBound) {
if (index >= preorder.length || preorder[index] > upperBound) {
return null;
}
TreeNode root = new TreeNode(preorder[index++]);
// 左子树的上界是当前节点值
root.left = buildTree2(preorder, root.val);
// 右子树的上界是当前上界
root.right = buildTree2(preorder, upperBound);
return root;
}
// LeetCode 235: 二叉搜索树的最近公共祖先
/**
* 递归查找LCA
* 时间复杂度:O(h)
* 空间复杂度:O(h)
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
} else if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
} else {
// 当前节点即为LCA
return root;
}
}
/**
* 迭代查找LCA
* 时间复杂度:O(h)
* 空间复杂度:O(1)
*/
public TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q) {
TreeNode current = root;
while (current != null) {
if (current.val > p.val && current.val > q.val) {
current = current.left;
} else if (current.val < p.val && current.val < q.val) {
current = current.right;
} else {
return current;
}
}
return null;
}
}
// 二叉树节点定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
```
### 核心方法解释
1. **删除节点(deleteNode)**
- 三种情况处理:无子节点、一个子节点、两个子节点
- 两个子节点时用右子树最小节点(中序后继)替换
2. **插入节点(insertIntoBST)**
- 递归找到合适的空位置插入新节点
- 迭代版本通过循环避免递归栈开销
3. **验证BST(isValidBST2)**
- 最优解法通过递归传递上下界约束
- 每个节点的值必须在 (min, max) 范围内
4. **前序构造BST(bstFromPreorder2)**
- 时间优化版利用上界约束,O(n)时间复杂度
- 全局索引变量跟踪当前处理的元素
5. **最近公共祖先(lowestCommonAncestor)**
- 利用BST特性,快速定位LCA
- 递归和迭代两种实现方式
### 复杂度总结
| 方法 | 时间复杂度 | 空间复杂度 |
|------|------------|------------|
| 删除节点 | O(h) | O(h) |
| 插入节点 | O(h) | O(h)/O(1) |
| 搜索节点 | O(h) | O(h)/O(1) |
| 验证BST | O(n) | O(n)/O(h) |
| 范围和 | O(n) | O(h)/O(h) |
| 前序构造 | O(n²)/O(n) | O(h) |
| 最近公共祖先 | O(h) | O(h)/O(1) |
这些方法充分体现了二叉搜索树的有序性特性,合理利用BST性质可以高效解决各种树操作问题。
平衡二叉树
package com.itheima.AVLTree;
public class AVLTree {
static class Node {
int key;
int height;
Node left, right;
public Node(int key) {
this.key = key;
this.height = 1;
}
}
private Node root;
private int height(Node node) {
return node == null ? 0 : node.height;
}
private int getBalance(Node node) {
return node == null ? 0 : height(node.left) - height(node.right);
}
private Node rightRotate(Node y) {
Node x = y.left;
Node T2 = x.right;
x.right = y;
y.left = T2;
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
private Node leftRotate(Node x) {
Node y = x.right;
Node T2 = y.left;
y.left = x;
x.right = T2;
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
return y;
}
public void insert(int key) {
root = insert(root, key);
}
private Node insert(Node node, int key) {
if (node == null) {
return new Node(key);
}
if (key < node.key) {
node.left = insert(node.left, key);
} else if (key > node.key) {
node.right = insert(node.right, key);
} else {
return node;
}
node.height = Math.max(height(node.left), height(node.right)) + 1;
int balance = getBalance(node);
if (balance > 1 && key < node.left.key) {
return rightRotate(node);
}
if (balance < -1 && key > node.right.key) {
return leftRotate(node);
}
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
private Node minValueNode(Node node) {
Node current = node;
while (current.left != null) {
current = current.left;
}
return current;
}
public void delete(int key) {
root = delete(root, key);
}
private Node delete(Node root, int key) {
if (root == null) {
return root;
}
if (key < root.key) {
root.left = delete(root.left, key);
} else if (key > root.key) {
root.right = delete(root.right, key);
} else {
if (root.left == null || root.right == null) {
Node temp = root.left != null ? root.left : root.right;
} else {
Node temp = minValueNode(root.right);
root.key = temp.key;
root.right = delete(root.right, temp.key);
}
if (root == null) {
return root;
}
root.height = Math.max(height(root.left), height(root.right)) + 1;
int balance = getBalance(root);
if (balance > 1 && key < root.left.key) {
return rightRotate(root);
}
if (balance < -1 && key > root.right.key) {
return leftRotate(root);
}
if (balance > 1 && key > root.left.key) {
root.left = leftRotate(root.left);
return rightRotate(root);
}
if (balance < -1 && key < root.right.key) {
root.right = rightRotate(root.right);
return leftRotate(root);
}
}
return root;
}
}
注释
以下是添加了更详细注释的AVL树实现代码,包含每个方法的算法思路、关键点、复杂度分析和示例说明:
```java
package com.itheima.tree;
/**
* AVL树实现示例
* 自平衡二叉搜索树,通过旋转操作保持树的平衡,确保所有操作的时间复杂度为O(log n)
*/
public class AVLTree {
// AVL树节点定义
static class Node {
int key; // 节点键值,用于比较和排序
int height; // 节点高度,用于计算平衡因子
Node left, right; // 左右子节点引用
public Node(int key) {
this.key = key;
this.height = 1; // 新节点高度默认为1(叶子节点)
}
}
private Node root; // 根节点
// 获取节点高度
private int height(Node node) {
return node == null ? 0 : node.height; // 空节点高度为0
}
// 获取平衡因子:左子树高度 - 右子树高度
// 平衡因子范围应为[-1, 1],否则需要调整
private int getBalance(Node node) {
return node == null ? 0 : height(node.left) - height(node.right);
}
// 右旋操作(RR旋转)
// 用于修复LL型不平衡(左左情况)
// 示例:
// y x
// / \ / \
// x T3 右旋(y) z y
// / \ ---------> /| / \
// z T2 T1 T2 T3
// /
// T1
private Node rightRotate(Node y) {
Node x = y.left;
Node T2 = x.right;
// 执行旋转:将x提升为根节点,y变为x的右子节点
x.right = y;
y.left = T2;
// 更新高度:先更新子节点y,再更新父节点x
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
// 返回新的根节点
return x;
}
// 左旋操作(LL旋转)
// 用于修复RR型不平衡(右右情况)
// 示例:
// x y
// / \ / \
// T1 y 左旋(x) x z
// / \ ---------> / \ |\
// T2 z T1 T2 T3
// |
// T3
private Node leftRotate(Node x) {
Node y = x.right;
Node T2 = y.left;
// 执行旋转:将y提升为根节点,x变为y的左子节点
y.left = x;
x.right = T2;
// 更新高度:先更新子节点x,再更新父节点y
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
// 返回新的根节点
return y;
}
// 插入操作
public void insert(int key) {
root = insert(root, key);
}
// 递归插入新节点,并在插入后调整平衡
private Node insert(Node node, int key) {
// 1. 执行标准BST插入
if (node == null)
return new Node(key); // 找到插入位置,创建新节点
if (key < node.key)
node.left = insert(node.left, key);
else if (key > node.key)
node.right = insert(node.right, key);
else // 不允许重复键
return node;
// 2. 更新当前节点的高度:子树高度最大值+1
node.height = 1 + Math.max(height(node.left), height(node.right));
// 3. 获取平衡因子,检查是否失衡
int balance = getBalance(node);
// 4. 如果失衡,执行四种旋转之一
// LL型:左左情况 -> 右旋
// 失衡条件:balance > 1 且 新节点插入到左子树的左子树中
if (balance > 1 && key < node.left.key)
return rightRotate(node);
// RR型:右右情况 -> 左旋
// 失衡条件:balance < -1 且 新节点插入到右子树的右子树中
if (balance < -1 && key > node.right.key)
return leftRotate(node);
// LR型:左右情况 -> 先左旋后右旋
// 失衡条件:balance > 1 且 新节点插入到左子树的右子树中
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left); // 先对左子树进行左旋
return rightRotate(node); // 再对当前节点进行右旋
}
// RL型:右左情况 -> 先右旋后左旋
// 失衡条件:balance < -1 且 新节点插入到右子树的左子树中
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right); // 先对右子树进行右旋
return leftRotate(node); // 再对当前节点进行左旋
}
// 未失衡,返回未修改的节点指针
return node;
}
// 查找最小节点:辅助方法,用于删除有两个子节点的情况
private Node minValueNode(Node node) {
Node current = node;
// 循环查找左子树的最左节点(最小值)
while (current.left != null)
current = current.left;
return current;
}
// 删除操作
public void delete(int key) {
root = delete(root, key);
}
// 递归删除节点,并在删除后调整平衡
private Node delete(Node root, int key) {
// 1. 执行标准BST删除
if (root == null)
return root;
if (key < root.key)
root.left = delete(root.left, key);
else if (key > root.key)
root.right = delete(root.right, key);
else {
// 找到要删除的节点
// 情况1:只有一个子节点或无子节点
if (root.left == null || root.right == null) {
root = (root.left != null) ? root.left : root.right;
}
// 情况2:有两个子节点
else {
// 获取右子树的最小节点(中序后继)
Node temp = minValueNode(root.right);
root.key = temp.key; // 用后继节点值替换当前节点
root.right = delete(root.right, temp.key); // 删除后继节点
}
// 如果树只有一个节点,直接返回
if (root == null)
return root;
}
// 2. 更新当前节点的高度
root.height = 1 + Math.max(height(root.left), height(root.right));
// 3. 获取平衡因子,检查是否失衡
int balance = getBalance(root);
// 4. 如果失衡,执行四种旋转之一
// LL型:左子树过高,且左子树平衡因子≥0
if (balance > 1 && getBalance(root.left) >= 0)
return rightRotate(root);
// LR型:左子树过高,且左子树平衡因子<0
if (balance > 1 && getBalance(root.left) < 0) {
root.left = leftRotate(root.left); // 先左旋左子树
return rightRotate(root); // 再右旋当前节点
}
// RR型:右子树过高,且右子树平衡因子≤0
if (balance < -1 && getBalance(root.right) <= 0)
return leftRotate(root);
// RL型:右子树过高,且右子树平衡因子>0
if (balance < -1 && getBalance(root.right) > 0) {
root.right = rightRotate(root.right); // 先右旋右子树
return leftRotate(root); // 再左旋当前节点
}
return root;
}
// 中序遍历:按升序输出节点
public void inorder() {
inorder(root);
System.out.println();
}
private void inorder(Node root) {
if (root != null) {
inorder(root.left);
System.out.print(root.key + " ");
inorder(root.right);
}
}
// 前序遍历:按根-左-右顺序输出,用于展示树的结构
public void preorder() {
preorder(root);
System.out.println();
}
private void preorder(Node root) {
if (root != null) {
System.out.print("[" + root.key + " h:" + root.height + "] ");
preorder(root.left);
preorder(root.right);
}
}
// 测试示例
public static void main(String[] args) {
AVLTree tree = new AVLTree();
// 插入测试数据
tree.insert(9);
tree.insert(5);
tree.insert(10);
tree.insert(0);
tree.insert(6);
tree.insert(11);
tree.insert(-1);
tree.insert(1);
tree.insert(2);
/* 插入后的树结构(高度标注):
9(h:4)
/ \
1(h:3) 10(h:2)
/ \ \
0(h:2) 5(h:2) 11(h:1)
/ / \
-1(h:1) 2(h:1) 6(h:1)
*/
System.out.println("插入后的中序遍历(升序):");
tree.inorder(); // 输出: -1 0 1 2 5 6 9 10 11
System.out.println("插入后的前序遍历(带高度信息):");
tree.preorder(); // 输出: [9 h:4] [1 h:3] [0 h:2] [-1 h:1] [5 h:2] [2 h:1] [6 h:1] [10 h:2] [11 h:1]
// 删除测试:删除10,触发RL旋转
tree.delete(10);
/* 删除10后的树结构:
1(h:4)
/ \
0(h:2) 9(h:3)
/ / \
-1(h:1) 5(h:2) 11(h:1)
/ \
2(h:1) 6(h:1)
*/
System.out.println("删除10后的中序遍历:");
tree.inorder(); // 输出: -1 0 1 2 5 6 9 11
System.out.println("删除10后的前序遍历:");
tree.preorder(); // 输出: [1 h:4] [0 h:2] [-1 h:1] [9 h:3] [5 h:2] [2 h:1] [6 h:1] [11 h:1]
}
}
```
### 核心算法详解
1. **平衡因子与失衡条件**:
- 平衡因子 = 左子树高度 - 右子树高度
- 平衡条件:平衡因子 ∈ [-1, 1]
- 失衡条件:平衡因子 > 1 或 < -1
2. **四种旋转操作**:
- **LL旋转(右旋)**:修复左左型失衡(插入点在左子树的左子树)
- **RR旋转(左旋)**:修复右右型失衡(插入点在右子树的右子树)
- **LR旋转(先左旋后右旋)**:修复左右型失衡(插入点在左子树的右子树)
- **RL旋转(先右旋后左旋)**:修复右左型失衡(插入点在右子树的左子树)
3. **插入操作步骤**:
1. 执行标准BST插入
2. 回溯更新节点高度
3. 计算平衡因子
4. 根据失衡类型执行对应旋转
4. **删除操作步骤**:
1. 执行标准BST删除(找到并删除节点,用中序后继替换)
2. 回溯更新节点高度
3. 计算平衡因子
4. 根据失衡类型执行对应旋转
### 复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 关键说明 |
|------|------------|------------|----------|
| 插入 | O(log n) | O(log n) | 递归栈深度为树高 |
| 删除 | O(log n) | O(log n) | 递归栈深度为树高 |
| 查找 | O(log n) | O(log n) | 递归栈深度为树高 |
AVL树通过严格保持平衡(每个节点的平衡因子绝对值不超过1),确保树的高度始终为O(log n),从而保证所有操作的高效性。
### 测试用例说明
测试数据插入顺序:9 → 5 → 10 → 0 → 6 → 11 → -1 → 1 → 2
1. **插入过程中的平衡调整**:
- 插入9:根节点
- 插入5:9的左子节点
- 插入10:9的右子节点
- 插入0:5的左子节点,触发LL旋转,根节点变为5
- 插入6:10的左子节点
- 插入11:10的右子节点
- 插入-1:0的左子节点
- 插入1:0的右子节点
- 插入2:1的右子节点,触发LR旋转
2. **删除过程中的平衡调整**:
- 删除10:触发RL旋转,根节点变为1
通过详细的注释和测试用例,这个AVL树实现可以帮助理解自平衡二叉搜索树的核心原理和实现细节。