BST`
BST,即二叉查找树,对于任意一个节点node,node的左子树的都小于等于node,node的右子树都大于等于node
669. 修剪二叉搜索树(Easy)
给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
示例 1:
输入:
1
/ \
0 2
L = 1
R = 2
输出:
1
\
2
示例 2:
输入:
3
/ \
0 4
\
2
/
1
L = 1
R = 3
输出:
3
/
2
/
1
题解:一开始想到的是用中序遍历二叉树,将符合的节点用数组存储起来,再将这棵二叉树复原,但是这样会破坏原有的结构,所以这种方法行不通。其实可以在遍历的过程中进行剪枝,一般二叉树的题目都可以用递归解决,这道题也不例外,先分析递归的3个重要条件
- 返回值:已经剪枝之和的根节点
- 调用单元做了什么:进行剪枝。
- 首先判断如果root为空,那么直接返回空即可。
- 然后就是要看根结点是否在范围内
- 如果根结点值小于L,那么返回对其右子结点调用递归函数的值
- 如果根结点大于R,那么返回对其左子结点调用递归函数的值。
- 如果根结点在范围内,将其左子结点更新为对其左子结点调用递归函数的返回值
- 终止条件:根节点为空
class Solution {
public TreeNode trimBST(TreeNode root, int L, int R) {
//base case
if (root == null) {
return null;
}
//如果根节点值大于R,返回对其左子节点调用递归函数的值
if (root.val > R) {
return trimBST(root.left, L, R);
}
//根节点值小于L,返回其对右子节点调用递归函数的值
if (root.val < L) {
return trimBST(root.right, L, R);
}
//如果在范围内,将其左节点更新为对其左子节点调用递归函数的值,右节点同理
root.left = trimBST(root.left, L, R);
root.right = trimBST(root.right, L, R);
return root;
}
}
230. 二叉搜索树中第K小的元素(Medium)
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明: 你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 1
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 3
进阶:
- 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?
递归中序遍历解法:这道题让我们求第K个元素,那么可以使用中序遍历来查找,用变量i来记录遍历到第几个节点,当i=k时,返回当前节点的值即可。可以使用递归和非递归解答
class Solution {
private int count = 0;
private int val;
public int kthSmallest(TreeNode root, int k) {
inOrder(root, k);
return val;
}
private void inOrder(TreeNode root, int k) {
//base case
if (root == null) {
return;
}
//中序遍历
//先左
inOrder(root.left, k);
//再根
count++;
if (count == k) {
val = root.val;
return;
}
//后右
inOrder(root.right, k);
}
}
非递归中序遍历解法
class Solution {
public int kthSmallest(TreeNode root, int k) {
//记录变量
int count = 0;
//准备一个栈来进行中序遍历
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
//遍历
while (node != null || !stack.isEmpty()) {
//节点不为空,一直将左节点压入栈
while (node != null) {
stack.push(node);
node = node.left;
}
//节点为空,从栈中弹出一个节点
node = stack.pop();
count++;
//如果count==k,返回node.val
if (count == k) {
return node.val;
}
//往右走
node = node.right;
}
return 0;
}
}
分治解法:这个方法有点类似于二分查找,首先计算出当前节点左子树的节点数量count。
- 如果k<count+1,说明第k小节点在左子树上,对其左节点调用递归
- 如果k>count+1,说明第k小节点在右子树上,对其右节点调用递归,此时k应该更新为k-count-1,因为已经减少了count+1个节点
- 如果k=count+1,说明第k小节点就是当前节点,返回当前节点的值即可
class Solution {
public int kthSmallest(TreeNode root, int k) {
//统计左子树节点个数
int leftCnt = count(root.left);
//如果leftCnt == k - 1,说明第k小是当前节点,返回
if (leftCnt == k - 1) {
return root.val;
}
//如果k-1<leftCnt,说明第k小节点在左子树
if (leftCnt > k - 1) {
return kthSmallest(root.left, k);
}
return kthSmallest(root.right, k - leftCnt - 1);
}
private int count(TreeNode node) {
//base case
if (node == null) {
return 0;
}
return 1 + count(node.left) + count(node.right);
}
}
538. 把二叉搜索树转换为累加树(Easy)
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。
例如:
输入: 二叉搜索树:
5
/ \
2 13
输出: 转换为累加树:
18
/ \
20 13
递归解法:这道题让我把所有大于当前节点值加到当前节点上,根据BST的特点,所有大于当前节点的节点都在右子树上,那么我们可以使用改版的中序遍历:“右根左”,先求出右子树所有节点的和,加到当前节点上,再递归左子树
class Solution {
public TreeNode convertBST(TreeNode root) {
if (root != null) {
dfs(root, 0);
}
return root;
}
private int dfs(TreeNode node, int sum) {
if (node == null) {
return sum;
}
//求出右子树所有大于当前节点的和
sum = dfs(node.right, sum);
//更新当前节点值
node.val += sum;
sum = dfs(node.left, node.val);
return sum;
}
}
同样,这道题还可以使用迭代解法
class Solution {
public TreeNode convertBST(TreeNode root) {
if (root == null) {
return null;
}
int sum = 0;
//准备一个栈
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
//“中序遍历”
while (node != null || !stack.isEmpty()) {
//将右节点全部压入栈
while (node != null) {
stack.push(node);
node = node.right;
}
//为空的时候弹出
node = stack.pop();
//更新当前节点值
node.val += sum;
//更新sum
sum = node.val;
//往左走
node = node.left;
}
return root;
}
}
235. 二叉搜索树的最近公共祖先(Easy)
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中。
递归解法:这道题让我们求出两节点的公共祖先,这是棵搜索二叉树,那么就比较好做了,因为搜索二叉树有个特点:左<根<右,两个给定节点肯定是一大一小,那么就可以做这样的判断:如果当前节点的值大于两个节点,那么说明p,q在左子树,往左子树走;反之,小于两个节点,往右子树走
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//如果走到最后,返回null
if (root == null) {
return null;
}
//大于两个节点,往左子树遍历
if (root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
}
//小于两个节点,往右子树遍历
if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
}
//返回当前节点
return root;
}
}
非递归解法:同样的,这道题也可以用非递归解答,思路一样
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (root != null) {
if (root.val > p.val && root.val > q.val) {
root = root.left;
}else if (root.val < p.val && root.val < q.val) {
root = root.right;
}else {
break;
}
}
return root;
}
}
236. 二叉树的最近公共祖先(Medium)
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
题解:这道题和上道题不同的是这道题不是搜索二叉树,所有就不能利用搜索二叉树的特点来进行解答。在在递归函数中,首先查看当前节点是否为空,若为空则返回空,若为p或者q中的其中一个,则返回当前节点,若都不等于,那就分情况讨论:
- 若p,q不在同一个子树,那么对左右子树调用递归函数之后,会分别返回p,q,那么当前节点就是最近公共祖先,返回即可
- 若p,q在同一棵子树,那么又有两种情况:
- 返回p或者q中比较高位置的那个
- 若p,q在同一层,那么返回两节点的公共祖先(第1种情况)
递归函数的3个条件:
- 返回值:两个节点的最近公共祖先
- 调用单元做了什么:判断当前节点是否为公共祖先,不是的话在左右子树里找(具体分析在上面)
- 终止条件:当前节点为空
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//base case
if (root == null) {
return null;
}
//如果当前节点为p或者q其中一个,直接返回
if (root == p || root == q) {
return root;
}
//当前节点不是公共祖先,在左右子树中找公共祖先
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//如果左右子树都找到了,说明当前节点是最近公共祖先
if (left != null && right != null) {
return root;
}
//如果其中一个为空,返回不为空的那个
return left == null ? right : left;
}
}
108. 将有序数组转换为二叉搜索树(Easy)
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
题解:这道题让我将有序数组转换为二叉搜索树,二叉搜索树的特点是:左<根<右,根节点是中间值,那么数组中间值就是根节点,左右节点又分别是左右数组的中点,这道题实际上考察的是“二分查找”。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return helper(nums, 0, nums.length - 1);
}
private TreeNode helper(int[] nums, int left, int right) {
//base case
if (left > right) {
return null;
}
int mid = left + (right - left) / 2;
//根据当前数组的中间值构造出节点
TreeNode cur = new TreeNode(nums[mid]);
//左节点为左数组的中间值
cur.left = helper(nums, left, mid - 1);
cur.right = helper(nums, mid + 1, right);
return cur;
}
}
109. 有序链表转换二叉搜索树(Medium)
给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
题解:这道题的核心解法和上道题一样,不同点在于这道题是用单链表存储值,而上道题用的是数组,数组可以使用index来访问数组中的元素。单链表没有index,不能通过“二分查找”很快找到中间点。可以使用“快慢指针”来找到中间点
class Solution {
public TreeNode sortedListToBST(ListNode head) {
//用辅助函数来记录首尾
return helper(head, null);
}
private TreeNode helper(ListNode head, ListNode tail) {
//base case
if (head == tail) {
return null;
}
//使用快慢指针找到中点
ListNode slow = head;
ListNode fast = head;
while (fast != tail && fast.next != tail) {
slow = slow.next;
fast = fast.next.next;
}
TreeNode cur = new TreeNode(slow.val);
cur.left = helper(head, slow);
cur.right = helper(slow.next, tail);
return cur;
}
}
653. 两数之和 IV - 输入 BST(Easy)
给定一个二叉搜索树和一个目标结果,如果 BST 中存在两个元素且它们的和等于给定的目标结果,则返回 true。
案例 1:
输入:
5
/ \
3 6
/ \ \
2 4 7
Target = 9
输出: True
案例 2:
输入:
5
/ \
3 6
/ \ \
2 4 7
Target = 28
输出: False
解法一:这道题是典型的TwoSum,可以使用HashSet来做。遍历二叉树就行,然后用一个HashSet,在递归函数函数中,如果node为空,返回false。如果k减去当前结点值在HashSet中存在,直接返回true;否则就将当前结点值加入HashSet,然后对左右子结点分别调用递归函数并且或起来返回即可
class Solution {
public boolean findTarget(TreeNode root, int k) {
HashSet<Integer> set = new HashSet<>();
return helper(set, root, k);
}
private boolean helper(HashSet<Integer> set, TreeNode root, int k) {
//base case
if (root == null) {
return false;
}
//判断当前元素加上set的元素能否等于k
if (set.contains(k - root.val)) {
return true;
}
//不等的话就添加进set
set.add(root.val);
//往左右子树遍历
return helper(set, root.left, k) || helper(set, root.right, k);
}
}
解法二:中序遍历二叉树,将节点存储到数组,然后用“双指针法”
class Solution {
public boolean findTarget(TreeNode root, int k) {
//中序遍历,存储到list
ArrayList<Integer> list = new ArrayList<Integer>();
inOrder(list, root);
//双指针
int i = 0;
int j = list.size() - 1;
while (i < j) {
int sum = list.get(i) + list.get(j);
if (sum == k) {
return true;
}else if (k < sum) {
i++;
}else {
j--;
}
}
return false;
}
private void inOrder(ArrayList<Integer> list, TreeNode root) {
//base case
if (root == null) {
return;
}
//中序遍历
inOrder(list, root.left);
list.add(root.val);
inOrder(list, root.right);
}
}
解法三:也可以借助BST本身的性质,遍历二叉树的时候进行搜索
class Solution {
private boolean search(TreeNode root, TreeNode cur, int y) {
// binary seach on a BST(root), find node.val == y && node != cur
while (root != null) {
if (root.val == y && root != cur) {
return true;
} else if (y < root.val) {
root = root.left;
} else {
root = root.right;
}
}
return false;
}
public boolean findTarget(TreeNode root, int k) {
if (root == null) return false;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
boolean result = search(root, cur, k - cur.val); // cur.val is x, we are here to find y
if (result) return true;
if (cur.left != null) stack.push(cur.left);
if (cur.right != null) stack.push(cur.right);
}
return false;
}
}
530. 二叉搜索树的最小绝对差(Easy)
给定一个所有节点为非负值的二叉搜索树,求树中任意两节点的差的绝对值的最小值。
示例 :
输入:
1
\
3
/
2
输出:
1
解释:
最小绝对差为1,其中 2 和 1 的差的绝对值为 1(或者 2 和 3)。
注意: 树中至少有2个节点。
题解:利用中序遍历BST有序的性质,比较相邻节点的差值即可,因为中序遍历BST是有序的
class Solution {
private int minDiff = Integer.MAX_VALUE;
//根节点没有前节点,所以设置为null
private TreeNode preNode = null;
public int getMinimumDifference(TreeNode root) {
inOrder(root);
return minDiff;
}
private void inOrder(TreeNode root) {
//base case
if (root == null) {
return;
}
//中序遍历
inOrder(root.left);
//如果有前节点,更新minDiff
if (preNode != null) {
minDiff = Math.min(minDiff, Math.abs(preNode.val - root.val));
}
//更新前节点
preNode = root;
inOrder(root.right);
}
}
501. 二叉搜索树中的众数(Easy)
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:
- 结点左子树中所含结点的值小于等于当前结点的值
- 结点右子树中所含结点的值大于等于当前结点的值
- 左子树和右子树都是二叉搜索树 例如:
给定 BST [1,null,2,2],
1
\
2
/
2
返回[2].
提示:如果众数超过1个,不需考虑输出顺序
进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
解法一:答案可能不止一个,也就是有多个值出现的次数一样多,可以使用list记录,用preNode记录前一个节点,并统计当前节点出现的次数
class Solution {
private int curCnt = 1;
private int maxCnt = 1;
private TreeNode preNode = null;
public int[] findMode(TreeNode root) {
List<Integer> maxCntNums = new ArrayList<>();
inOrder(root, maxCntNums);
int[] ret = new int[maxCntNums.size()];
int idx = 0;
for (int num : maxCntNums) {
ret[idx++] = num;
}
return ret;
}
private void inOrder(TreeNode node, List<Integer> nums) {
if (node == null) return;
inOrder(node.left, nums);
//如果有前节点,统计当前节点出现的次数
if (preNode != null) {
if (preNode.val == node.val) curCnt++;
else curCnt = 1;
}
//如果出现次数大于maxCnt,更新,并清空list,重新添加
if (curCnt > maxCnt) {
maxCnt = curCnt;
nums.clear();
nums.add(node.val);
} else if (curCnt == maxCnt) {
//相等的话,就追加
nums.add(node.val);
}
//更新前节点
preNode = node;
inOrder(node.right, nums);
}
}
Trie
Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。Trie树的基本性质可以归纳为:
(1)根节点不包含字符,除根节点之外每个节点只包含一个字符。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符串不相同。

208. 实现 Trie (前缀树)(Medium)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
说明:
- 你可以假设所有的输入都是由小写字母 a-z 构成的。
- 保证所有输入均为非空字符串。
题解:比较简单的做法是每个节点都设置一个大小26的数组,用来表示26个字母
public class Trie {
//前缀树的节点
private class Node {
//每个节点包含26个字母
Node[] childs = new Node[26];
//是否为叶子节点的标志
boolean isLeaf;
}
//根节点为空节点
private Node root = new Node();
public Trie() {}
public void insert(String word) {
if (word == null) {
return;
}
//从根节点出发,迭代添加
Node pre = root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
//如果当前字符没有添加,就新建
if (pre.childs[index] == null) {
pre.childs[index] = new Node();
}
//更新pre
pre = pre.childs[index];
}
//添加完之后,将isLeaf置为true,标识存储完了
pre.isLeaf = true;
}
public boolean search(String word) {
if (word == null) {
return false;
}
//从根节点开始查找
Node pre = root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
//如果遇到一处没有,就返回false
if (pre.childs[index] == null) {
return false;
}
//更新pre
pre = pre.childs[index];
}
//返回最后一个字符的isLeaf
return pre.isLeaf;
}
public boolean startsWith(String word) {
if (word == null) {
return false;
}
//从根节点开始查找
Node pre = root;
for (int i = 0; i < word.length(); i++) {
int index = word.charAt(i) - 'a';
//如果遇到一处没有,就返回false
if (pre.childs[index] == null) {
return false;
}
//更新pre
pre = pre.childs[index];
}
return true;
}
}
677. 键值映射(Medium)
实现一个 MapSum 类里的两个方法,insert 和 sum。
- 对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。
- 对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。
示例 1:
输入: insert("apple", 3), 输出: Null
输入: sum("ap"), 输出: 3
输入: insert("app", 2), 输出: Null
输入: sum("ap"), 输出: 5
题解:这道题的难点在于sum方法,需要返回所有以该前缀开头的键的值的总和,很容易就想到使用上道题的前缀树来做。
class MapSum {
/** Initialize your data structure here. */
public MapSum() {
}
//自定义前缀树节点
private class Node{
//每个节点26个字符
Node[] childs = new Node[26];
boolean isLeaf;
int val;
}
//根节点为空
private Node root = new Node();
public void insert(String key, int val) {
insert(key, val, root);
}
private void insert(String key, int val, Node root) {
if (key == null) {
return;
}
//从根节点出发
Node pre = root;
for (int i = 0; i < key.length(); i++) {
//判断当前字符是否已经存储过,存储过就跳过,否则新建
int index = key.charAt(i) - 'a';
//注意这里的空节点,不能采用Node tmp = pre.childs[index]的形式来判断,会出现空指针异常
if (pre.childs[index] == null) {
pre.childs[index] = new Node();
}
pre = pre.childs[index];
}
//添加完之后,标记置为true
pre.isLeaf = true;
pre.val = val;
}
public int sum(String prefix) {
if (prefix == null) {
return 0;
}
//遍历到prefix的最后一位字符所在的Node
Node pre = root;
for (int i = 0; i < prefix.length(); i++) {
//找到当前字符所在的Node位置
int index = prefix.charAt(i) - 'a';
Node tmp = pre.childs[index];
//如果不存在prefix这个前缀,返回0
if (tmp == null) {
return 0;
}
pre = tmp;
}
return sum(pre);
}
private int sum(Node node) {
//base case
if (node == null) {
return 0;
}
//将当前节点值添加到sum
int sum = node.val;
//遍历当前节点的所有子节点
for (Node child : node.childs) {
//只进入不为空的子节点
if (child != null) {
sum += sum(child);
}
}
return sum;
}
}