二叉树(纲领篇)
- 是否可以通过遍历一颗二叉树得到答案?
如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。 - 是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?
如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。 - 如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?
其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
二叉树遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
先不管所谓前中后序,单看 traverse 函数,你说它在做什么事情?
- 它就是一个能够遍历二叉树所有节点的一个函数,和遍历数组或者链表本质没有任何区别
/* 迭代遍历数组 */
void traverse(int[] arr) {
for (int i = 0; i < arr.length; i++) {
}
}
/* 递归遍历数组 */
void traverse(int[] arr, int i) {
if (i == arr.length) {
return;
}
// 前序位置
traverse(arr, i + 1);
// 后序位置
}
/* 迭代遍历单链表 */
void traverse(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
}
}
/* 递归遍历单链表 */
void traverse(ListNode head) {
if (head == null) {
return;
}
// 前序位置
traverse(head.next);
// 后序位置
}
- 单链表和数组的遍历可以是迭代的,也可以是递归的,二叉树这种结构无非就是二叉链表 - 由于没办法简单改写成迭代形式,所以一般说二叉树的遍历框架都是指递归的形式。
- 只要是递归形式的遍历,都可以有前序位置和后序位置,分别在递归之前和递归之后。
- 所谓前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,那么进一步,你把代码写在不同位置,代码执行的时机也不同。
- 比如说:让你倒序打印一条单链表,怎么搞?- 可以利用后序位置来操作
/* 递归遍历单链表,倒序打印链表元素 */
void traverse(ListNode head) {
if (head == null) {
return;
}
traverse(head.next);
// 后序位置
print(head.val);
}
结合上面那张图,你应该知道为什么这段代码能够倒序打印单链表了吧,本质上是利用递归的堆栈帮你实现了倒序遍历的效果。
那么说回二叉树也是一样的,只不过多了一个中序位置罢了。
对前后序遍历的深度理解
前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点,绝不仅仅是三个顺序不同的 List:
- 前序位置的代码在刚刚进入一个二叉树节点的时候执行;
- 后序位置的代码在将要离开一个二叉树节点的时候执行;
- 中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
使用图来进行理解:
- 你可以发现每个节点都有「唯一」属于自己的前中后序位置,所以说前中后序遍历是遍历二叉树过程中处理每一个节点的三个特殊时间点。
- 这里你也可以理解为什么多叉树没有中序位置,因为二叉树的每个节点只会进行唯一一次左子树切换右子树,而多叉树节点可能有很多子节点,会多次切换子树去遍历,所以多叉树节点没有「唯一」的中序遍历位置。
注意:
二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作。
二叉树(纲领篇)
104. 二叉树的最大深度
题目描述
解决方案
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
// 定义:输入一个节点,返回以该节点为根的二叉树的最大深度
var maxDepth = function(root) {
if(root == null) return 0
let leftMax = maxDepth(root.left)
let rightMax = maxDepth(root.right)
// shi
return Math.max(leftMax, rightMax) + 1
};
144. 二叉树的前序遍历
题目描述
解决方案
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
let res = []
var traverse = function(node) {
if(node == null) return []
res.push(node.val)
traverse(node.left)
traverse(node.right)
}
traverse(root)
return res
};
543.二叉树的直径
题目描述
解决方案
所谓二叉树的直径,就是左右子树的最大深度之和,那么直接的想法是对每个节点计算左右子树的最大高度,得出每个节点的直径,从而得出最大的那个直径。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var diameterOfBinaryTree = function(root) {
let maxDiameter = 0 // 定义一个变量 后续每次操作需要进行相应的更新
var maxDepth = function(root) {
if(root == null) return 0
let leftMax = maxDepth(root.left)
let rightMax = maxDepth(root.right)
maxDiameter = Math.max(maxDiameter, leftMax+rightMax) // 二叉树的最大直接直径,在程序运行的过程中就进行相应更新
return 1 + Math.max(leftMax, rightMax)
}
maxDepth(root)
return maxDiameter
};
二叉树(思路篇)
二叉树解题的思维模式
- 是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。
- 是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
无论使用哪一种思维模式,你都需要思考
- 如果单独抽出一个二叉树节点,他需要做什么,需要在什么时候(前/中/后序位置) 做?
其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
226. 翻转二叉树(简单)
题目描述
解决方案1
1、这题能不能用「遍历」的思维模式解决?
- 可以,我写一个 traverse 函数遍历每个节点,让每个节点的左右子节点颠倒过来就行了。
- 单独抽出一个节点,需要让它做什么?让它把自己的左右子节点交换一下。
- 需要在什么时候做?好像前中后序位置都可以。
- 综上,可以写出如下解法代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
// 遍历的思路,清楚每一个节点应该做什么,清楚每一个节点应该做些什么
var traverse = function(root) {
if(root == null) return null
// 交换每个节点的左右节点
let temp = root.left
root.left = root.right
root.right = temp
// 遍历框架
traverse(root.left)
traverse(root.right)
}
traverse(root)
return root
};
解决方案2
2、这题能不能用「分解问题」的思维模式解决?
- 我们尝试给 invertTree 函数赋予一个定义:
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root);
- 然后思考,对于某一个二叉树节点 x 执行 invertTree(x),你能利用这个递归函数的定义做点啥?
- 我可以用 invertTree(x.left) 先把 x 的左子树翻转,再用 invertTree(x.right) 把 x 的右子树翻转,最后把 x 的左右子树交换,这恰好完成了以 x 为根的整棵二叉树的翻转,即完成了 invertTree(x) 的定义。
- 直接写出解法代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
if(root == null) return null
let left = invertTree(root.left)
let right = invertTree(root.right)
// 将左右子树进行调换操作
root.left = right
root.right = left
return root
};
这种「分解问题」的思路,核心在于你要给递归函数一个合适的定义,然后用函数的定义来解释你的代码;如果你的逻辑成功自恰,那么说明你这个算法是正确的。
116. 填充每个节点的下一个右侧节点指针
题目描述
解决方案
- 可以把二叉树的相邻节点抽象成一个【三叉树节点】,这样二叉树就变成了一颗三叉树,
- 然后遍历这颗三叉树,把每个三叉树节点中的两个节点连接就行
代码实现
/**
* // Definition for a Node.
* function Node(val, left, right, next) {
* this.val = val === undefined ? null : val;
* this.left = left === undefined ? null : left;
* this.right = right === undefined ? null : right;
* this.next = next === undefined ? null : next;
* };
*/
/**
* @param {Node} root
* @return {Node}
*/
var connect = function(root) {
if(root == null) return null
// 遍历三叉树
traverse(root.left,root.right)
return root
};
// 三叉树的遍历框架
var traverse = function(node1, node2) {
if(node1 == null || node2 == null) {
return
}
// 前序位置,将传入的两个节点连接起来
node1.next = node2
// 连接相同父节点的两个子节点
traverse(node1.left, node1.right)
traverse(node2.left, node2.right)
// 连接跨越父节点的两个子节点
traverse(node1.right, node2.left)
}
二叉树(构造篇)
二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。
654. 最大二叉树
题目链接
题目链接
题目描述
解决方案
- 每个二叉树节点都可以认为是一棵子树的根节点,对于根节点,首先要做的当然是把想办法把自己先构造出来,然后想办法构造自己的左右子树。
- 所以,我们要遍历数组把找到最大值 maxVal,从而把根节点 root 做出来,然后对 maxVal 左边的数组和右边的数组进行递归构建,作为 root 的左右子树。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
return build(nums, 0, nums.length-1)
};
var build = function(nums, lo, hi) {
if(lo > hi) {
return null // 不符合情况的话,返回null
}
// 找到数组中的最大值,以及其对应的索引
let index = -1, maxVal = -100
for(let i = lo; i <= hi; i++) {
if(nums[i] > maxVal) {
index = i
maxVal = nums[i]
}
}
// 先构造出根节点
let root = new TreeNode(maxVal)
root.left = build(nums, lo, index-1)
root.right = build(nums, index+1, hi)
return root
}
105.从前序与中序遍历序列构造二叉树
题目链接
题目链接
题目描述
解决方案
- 二叉树的前序和中序遍历结果的特点如下:
- 前序遍历结果第一个就是根节点的值,然后再根据中序遍历结果确定左右子树的节点。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
return buildChildrenTree(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1)
};
// 构造树的时候,因为要递归调用所以要把索引都列举出来
var buildChildrenTree = function(preorder, preStart, preEnd, inorder, inStart, inEnd) {
// 判断一个即可,因为前序和中序都是表示一个树的遍历情况
if(preStart > preEnd) return null
// 根节点其实就是前序遍历中的第一个节点
let index = -1 // 找节点
let rootVal = preorder[preStart]
for(let i = 0; i < inorder.length; i++) {
if(inorder[i] == rootVal) {
index = i
break
}
}
// 先构造出当前根节点
let leftSize = index - inStart
let root = new TreeNode(rootVal)
// 递归构造左右子树
root.left = buildChildrenTree(preorder, preStart+1, preStart+leftSize, inorder, inStart, index-1)
root.right = buildChildrenTree(preorder, preStart+leftSize+1, preEnd, inorder, index+1, inEnd)
return root
}
- 也可以使用Map的形式来来存储inorder中的值与索引之间的映射
var buildTree = function(preorder, inorder) {
return buildChildrenTree(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1)
};
// 构造树的时候,因为要递归调用所以要把索引都列举出来
var buildChildrenTree = function(preorder, preStart, preEnd, inorder, inStart, inEnd) {
// 判断一个即可,因为前序和中序都是表示一个树的遍历情况
if(preStart > preEnd) return null
// 根节点其实就是前序遍历中的第一个节点
// let index = -1 // 找节点
let rootVal = preorder[preStart]
// 存储inorder中值到索引的映射
let m = new Map()
for(let i = 0; i < inorder.length; i++) {
// m.push([inorder[i], i])
m[inorder[i]] = i
}
console.log(m)
let index = m[rootVal]
// for(let i = 0; i < inorder.length; i++) {
// if(inorder[i] == rootVal) {
// index = i
// break
// }
// }
// 先构造出当前根节点
let leftSize = index - inStart
let root = new TreeNode(rootVal)
// 递归构造左右子树
root.left = buildChildrenTree(preorder, preStart+1, preStart+leftSize, inorder, inStart, index-1)
root.right = buildChildrenTree(preorder, preStart+leftSize+1, preEnd, inorder, index+1, inEnd)
return root
}
- 此处的Map中的内容,用的是对象的键值对的形式存储的,
106.从中序与后序遍历序列构造二叉树
题目描述
解决方案
- 和前一题一样,都是根据二叉树的后序和中序遍历结果的特点来找到相应的下标属性的,然后根据下标来递归构建二叉树
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} inorder
* @param {number[]} postorder
* @return {TreeNode}
*/
var buildTree = function(inorder, postorder) {
return buildPostChildren(inorder, 0, inorder.length-1, postorder, 0, postorder.length-1)
};
var buildPostChildren = function(inorder, inStart, inEnd, postorder, postStart, postEnd){
if(inStart > inEnd) return null
// 根节点就是后续遍历的最后一个节点
let index = -1
let rootVal = postorder[postEnd]
for(let i = inStart; i <= inEnd; i++) {
if(inorder[i] == rootVal) {
index = i
}
}
let leftSize = index - inStart
// 先构造初根节点
let root = new TreeNode(rootVal)
root.left = buildPostChildren(inorder, inStart, index-1, postorder, postStart, postStart+leftSize-1)
root.right = buildPostChildren(inorder, index+1, inEnd, postorder, postStart+leftSize, postEnd-1)
return root
}
二叉树(序列化篇)
297. 二叉树的序列化与反序列化
题目描述
序列化与反序列化举例 - 比如这么一颗二叉树
- serialize 方法也许会把它序列化成字符串 2,1,#,6,#,#,3,#,#,其中 # 表示 null 指针,
- 那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。
- 也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。
- 想象一下,二叉树结该是一个二维平面内的结构,而序列化出来的字符串是一个线性的一维结构。所谓的序列化不过就是把结构化的数据「打平」,本质就是在考察二叉树的遍历方式。
- **二叉树的遍历方式有哪些?**递归遍历方式有前序遍历,中序遍历,后序遍历;迭代方式一般是层级遍历。
序列化问题其实就是遍历问题 - 你能遍历,顺手把遍历的结果转化成字符串的形式,不就是序列化了么?
前序遍历的思路
- 前序遍历的特点是根节点在开头,然后接着左子树的前序遍历结果,然后接着右子树的前序遍历结果:
- 所以如果按照前序遍历顺序进行序列化,反序列化的时候,就知道第一个元素是根节点的值,然后递归调用反序列化左右子树,接到根节点上即可,上述思路翻译成代码即可解决本题。
归并排序详解及应用
就这么说吧,所有递归的算法,你甭管它是干什么的,本质上都是在遍历一棵(递归)树,然后在节点(前中后序位置)上执行代码,你要写递归算法,本质上就是要告诉每个节点需要做什么。
归并排序的代码框架
// 定义:排序 nums[lo..hi]
void sort(int[] nums, int lo, int hi) {
if (lo == hi) {
return;
}
int mid = (lo + hi) / 2;
// 利用定义,排序 nums[lo..mid]
sort(nums, lo, mid);
// 利用定义,排序 nums[mid+1..hi]
sort(nums, mid + 1, hi);
/****** 后序位置 ******/
// 此时两部分子数组已经被排好序
// 合并两个有序数组,使 nums[lo..hi] 有序
merge(nums, lo, mid, hi);
/*********************/
}
// 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi]
// 合并为有序数组 nums[lo..hi]
void merge(int[] nums, int lo, int mid, int hi);
经典总结 : 归并排序就是先把左半边数组排好序,再把右半边数组排好序,然后把两半数组合并。
上述代码和二叉树的后序遍历很像:
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) {
return;
}
traverse(root.left);
traverse(root.right);
/****** 后序位置 ******/
print(root.val);
/*********************/
}
再进一步,你联想一下求二叉树的最大深度的算法代码:
// 定义:输入根节点,返回这棵二叉树的最大深度
int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 利用定义,计算左右子树的最大深度
int leftMax = maxDepth(root.left);
int rightMax = maxDepth(root.right);
// 整棵树的最大深度等于左右子树的最大深度取最大值,
// 然后再加上根节点自己
int res = Math.max(leftMax, rightMax) + 1;
return res;
}
之前讲过
- 说二叉树问题可以分为两类思路,一类是遍历一遍二叉树的思路,另一类是分解问题的思路**,根据上述类比,显然归并排序利用的是分解问题的思路( 分治算法)。**
归并排序的思路
- 归并排序的过程可以在逻辑上抽象成一棵二叉树,树上的每个节点的值可以认为是 nums[lo…hi],叶子节点的值就是数组中的单个元素:
- 然后,在每个节点的后序位置(左右子节点已经被排好序)的时候执行 merge 函数,合并两个子节点上的子数组:
- 这个 merge 操作会在二叉树的每个节点上都执行一遍,执行顺序是二叉树后序遍历的顺序。
- 后序遍历二叉树大家应该已经烂熟于心了,就是下图这个遍历顺序:
- 结合上述基本分析,我们把 nums[lo…hi] 理解成二叉树的节点,sort 函数理解成二叉树的遍历函数,整个归并排序的执行过程就是以下 GIF 描述的这样:
归并算法模板
class Merge {
// 用于辅助合并有序数组
private static int[] temp;
public static void sort(int[] nums) {
// 先给辅助数组开辟内存空间
temp = new int[nums.length];
// 排序整个数组(原地修改)
sort(nums, 0, nums.length - 1);
}
// 定义:将子数组 nums[lo..hi] 进行排序
private static void sort(int[] nums, int lo, int hi) {
if (lo == hi) {
// 单个元素不用排序
return;
}
// 这样写是为了防止溢出,效果等同于 (hi + lo) / 2
int mid = lo + (hi - lo) / 2;
// 先对左半部分数组 nums[lo..mid] 排序
sort(nums, lo, mid);
// 再对右半部分数组 nums[mid+1..hi] 排序
sort(nums, mid + 1, hi);
// 将两部分有序数组合并成一个有序数组
merge(nums, lo, mid, hi);
}
// 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组
private static void merge(int[] nums, int lo, int mid, int hi) {
// 先把 nums[lo..hi] 复制到辅助数组中
// 以便合并后的结果能够直接存入 nums
for (int i = lo; i <= hi; i++) {
temp[i] = nums[i];
}
// 数组双指针技巧,合并两个有序数组
int i = lo, j = mid + 1;
for (int p = lo; p <= hi; p++) {
if (i == mid + 1) {
// 左半边数组已全部被合并
nums[p] = temp[j++];
} else if (j == hi + 1) {
// 右半边数组已全部被合并
nums[p] = temp[i++];
} else if (temp[i] > temp[j]) {
nums[p] = temp[j++];
} else {
nums[p] = temp[i++];
}
}
}
}
二叉搜索树(特性篇)
BST的特性
- 对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。
- 对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。
- 从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)。
// 输入一个BST,下面代码可以将BST中每个节点的值升序打印出来
void traverse(TreeNode root) {
if (root == null) return;
traverse(root.left);
// 中序遍历代码位置
print(root.val);
traverse(root.right);
}
230. 二叉搜索树中第K小的元素
题目描述
代码思路
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
var kthSmallest = function(root, k) {
// res书写在外面会有作用域的问题
let res = 0
let rank = 0
function traverse(root, k) {
if(root == null) return
traverse(root.left, k)
rank++
if(k == rank) {
// 找到第k小的元素
res = root.val
return
}
traverse(root.right, k)
}
traverse(root, k)
return res
};
538. 将二叉树转换为累加树
题目描述
- 维护一个外部累加变量 sum,在遍历 BST 的过程中增加 sum,同时把 sum 赋值给 BST 中的每一个节点,就将 BST
转化成累加树了。 - 但是注意顺序,正常的中序遍历顺序是先左子树后右子树,这里需要反过来,先右子树后左子树。
代码实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var convertBST = function(root) {
let sum = 0
function traverse(root) {
if(root == null) return
traverse(root.right)
sum += root.val
root.val = sum
traverse(root.left)
}
traverse(root)
return root
};
二叉搜索树(基操篇)
- 本篇来实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中「删」和「判断合法性」略微复杂。
- BST 的基础操作主要依赖**「左小右大」**的特性,可以在二叉树中做类似二分搜索的操作,寻找一个元素的效率很高。比如下面这就是一棵合法的二叉树:
- 对于 BST 相关的问题,你可能会经常看到类似下面这样的代码逻辑:
void BST(TreeNode root, int target) {
if (root.val == target)
// 找到目标,做点什么
if (root.val < target)
BST(root.right, target);
if (root.val > target)
BST(root.left, target);
}
98. 验证二叉搜索树
题目描述
注意:
但是根据 BST 的定义,root 的整个左子树都要小于 root.val,整个右子树都要大于 root.val。(这一点非常关键)
代码实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
return isValidBSTPlus(root, null, null)
};
var isValidBSTPlus = function(root, min, max) {
if(root == null) return true
// 里面在进行比较的时候,是root.val进行比较操作
if(min != null && root.val <= min) return false
if(max != null && root.val >= max) return false
return isValidBSTPlus(root.left, min, root.val) && isValidBSTPlus(root.right, root.val, max)
}
700. 二叉搜索树中的搜索
题目描述
代码实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var searchBST = function(root, val) {
if(root == null) return null
if(root.val > val) return searchBST(root.left, val) // 当前值给定值,根据BST特性,去左子树中查找 肯定需要return 返回其中结果
if(root.val < val) return searchBST(root.right, val)
return root
};
701. 二叉搜索树中的插入操作
题目描述
代码实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var insertIntoBST = function(root, val) {
// 找到空位置就插入新节点 直接就是创建一个新的节点
// 有空位置的话,就进行接收即可
if(root == null) return new TreeNode(val)
// 根据BST的性质来
if(root.val < val) {
// 一旦涉及到改,就类似二叉树的构造问题,函数要返回TreeNode类型,并且要对递归调用的返回值进行接收
// 进行接收以后,左右两边才能被构造出来
root.right = insertIntoBST(root.right, val)
}
if(root.val > val) {
root.left = insertIntoBST(root.left, val)
}
return root
};
450. 删除二叉搜素树中的节点
题目描述
删除节点有三种情况
情况 1:A 恰好是末端节点,两个子节点都为空,那么它可以当场去世了。
if (root.left == null && root.right == null)
return null;
情况 2:A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。
// 排除了情况 1 之后
if (root.left == null) return root.right;
if (root.right == null) return root.left;
情况 3:A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。
if (root.left != null && root.right != null) {
// 找到右子树的最小节点
TreeNode minNode = getMin(root.right);
// 把 root 改成 minNode
root.val = minNode.val;
// 转而去删除 minNode
root.right = deleteNode(root.right, minNode.val);
}
代码实现
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} key
* @return {TreeNode}
*/
var deleteNode = function(root, key) {
if(root == null) return null
if(root.val == key) {
if(root.left == null) return root.right // 直接返回root.right的值,反正最后需要返回构造出来的root
if(root.right == null) return root.left
// 第三种情况的第二种解决方案:找到右子树中最大的
let minNode = getMinNode(root.right)
root.right = deleteNode(root.right, minNode.val) // key 为数值,minNode应该为minNode.val
minNode.left = root.left
minNode.right = root.right
root = minNode
}else if(root.val > key) {
// 在进行构造的时候,构造的是哪一部分,就需要用相应内容进行接收
root.left = deleteNode(root.left, key)
}else {
root.right = deleteNode(root.right, key)
}
return root
};
var getMinNode = function(node) {
while(node.left != null) node = node.left // BST中最左边的就是最小的
return node
}