一、查找概述
查找算法是一种在数据集中寻找特定数据项的方法。通常,数据集是在计算机程序中存储的,例如数组、链表或散列表。在编写程序时,查找算法是非常重要的,它有助于快速找到所需的数据。
1. 线性查找
线性查找也称为顺序查找,是一种最简单的查找算法。在这种算法中,我们从数据集的开头开始,逐个比较每个数据项,以寻找要查找的数据。如果我们找到了目标数据,查找过程就结束了。如果我们到达数据集的末尾,仍然找不到目标数据,则可以认为它不存在与数据集中。
线性查找的时间复杂度为O(n),其中n是数据集的大小。因此,它在大型数据集中可能会很慢。然而,在小型数据集中,它仍然是一种非常有用的算法。
2. 二分查找
二分查找也称为折半查找,是一种更快速的查找算法。但前提是,数据集必须已经排序。在二分查找中,我们取数据集的中间值,然后将目标与中间值进行比较。如果目标小于中间值,则在左侧子集中继续查找;如果目标大于中间值,则在右侧子集中继续查找。每次比较都会缩小要搜索的数据集的大小。
二分查找的时间复杂度是O(log n),其中n是数据集的大小。这种算法在大型数据集中非常有效,但在小型数据集中可能并不是最快的选择。
3. 哈希表查找
哈希表查找也称为散列表查找,是另一种常见的查找算法。它利用哈希函数将数据项映射到散列表中的位置。在查找过程中,我们只需通过哈希函数计算目标数据的位置,然后检查该位置是否包含目标数据。
哈希表查找的时间复杂为O(1)。这使得它成为大型数据集中最快的查找算法之一。但是,哈希表查找的效率取决于哈希函数的质量。如果两个数据项映射到相同的位置,就会发生哈希冲突,这可能会导致性能下降。
4. 小结
在编写程序时,我们需要选择适合数据集大小和其他要求的最佳查找算法。例如,如果数据集很小,则线性查找可能是最快的选择;如果数据集已经排序,则二分查找是非常有用的。然而,在大型数据集中,哈希表查找通常是最好的选择。了解不同类型的查找算法及其特点可以帮助我们在编写程序时做出明智的选择。
不管是之前学过的数组、链表、队列、还是栈,这些线性结构中,如果想在其中查找一个元素,效率是比较慢的,只有O(N),因此如果你的需求是实现数据的快速查找,那么就需要新的数据结构支持。
还记得最先介绍的那个二分查找算法吗?它的查找效率能够达到 O(logN),是不是还不错?不过呢,它需要对数组事先排好序,而排序的成本是比较高的。那么有没有一个折中的办法呢?有,那就是接下来要给大家介绍的二叉搜索树,它插入元素后,自然就是排好序的,接下来的查询也自然而然可以应用二分查找算法进行高效搜索。
二、二叉搜索树
1. 概述
1.1 历史
二叉搜索树最早是由Bernoulli兄弟在18世纪中提出的,但是真正推广和应用该数据结构的是1960年代的D.L Gries。它的著作《The Science of Programming》中详细介绍了二叉搜索树的实现和应用。
在计算机科学的发展中,二叉搜索树成为了一种非常基础的数据结构,被广泛应用在各种领域,包括搜索、排序、数据库索引等。随着计算机算力的提升和对数据结构的深入研究,二叉搜索树也不断被优化和扩展,例如AVL树,红黑树等。
1.2 特性
二叉搜索树(也称二叉排序树)是符合下面特征的二叉树:
1. 树结点增加key属性,用来比较谁大谁小,key不可以重复
2. 对于任意一个树结点,它的key比左子树的key都大,同时也比右子树的key都小,例如下图所示
可以看出要查找7(从根开始)自然就可应用二分查找算法,只需三次比较
- 与4比,较之大,向右找
- 与6比,较之大,继续向右找
- 与7比,找到
查找的时间复杂度与树高相关,插入、删除也是如此。
- 如果这棵树左右平衡,那么时间复杂度均是O(logN)
- 当然,如果这棵树左右高度相差过大,那么这时是最糟的情况,时间复杂度是O(N)
2. 实现
2.1 定义节点
static class BSTNode {
int key;
Object value;
BSTNode left;
BSTNode right;
public BSTNode(int key) {
this.key = key;
}
public BSTNode(int key, Object value) {
this.key = key;
this.value = value;
}
public BSTNode(int key, Object value, BSTNode left, BSTNode right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
}
2.2 查询
递归实现
/**
* 查找关键字对应的值
* @param key 关键字
* @return 关键字对应的值
*/
public Object get(int key) {
return doGet(root, key);
}
private Object doGet(BSTNode node, int key) {
if(node == null) {
return null; // 没找到
}
if(key < node.key) {
return doGet(node.left, key); // 向左找
} else if(node.key < key) {
return doGet(node.right, key); // 向右找
} else {
return node.value; // 找到了
}
}
非递归实现
/**
* 查找关键字对应的值
* @param key 关键字
* @return 关键字对应的值
*/
public Object get(int key) {
BSTNode node = root;
while(node != null) {
if(key < node.key) {
// 向左找
node = node.left;
} else if(node.key < key) {
// 向右找
node = node.right;
} else {
// 找到了
return node.value;
}
}
return null;
}
2.3 Comparable
如果希望让除int外更多的类型能够作为key,一种方式是key必须实现Comparable接口
package com.itheima.datastructure.BinarySearchTree;
// 二叉搜索树
public class BSTTree1<K extends Comparable<K>, V> { // 泛型上限
BSTNode<K, V> root; // 根节点
static class BSTNode<K, V> {
K key;
V value;
BSTNode<K, V> left;
BSTNode<K, V> right;
public BSTNode(K key) {
this.key = key;
}
public BSTNode(K key, V value) {
this.key = key;
this.value = value;
}
public BSTNode(K key, V value, BSTNode<K, V> left, BSTNode<K, V> right) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
}
/**
* 查找关键字对应的值
* @param key 关键字
* @return 关键字对应的值
*/
public V get(K key) {
if(key == null) {
return null;
}
BSTNode<K, V> node = root;
while(node != null) {
int result = node.key.compareTo(key);
if(result > 0) {
// 向左找
node = node.left;
} else if(result < 0) {
// 向右找
node = node.right;
} else {
// 找到了
return node.value;
}
}
return null;
}
}
2.4 最小Min
递归实现
/**
* 查找最小关键字对应的值
* @return
*/
public V min() {
return doMin(root);
}
private V doMin(BSTNode<K,V> node) {
if(node == null) {
return null;
}
// 左边已经走到头
if(node.left == null) {
return node.value;
}
return doMin(node.left);
}
非递归实现
/**
* 查找最小关键字对应的值
* @return
*/
public V min() {
if(root == null) {
return null;
}
BSTNode<K, V> p = root;
// 左边未走到头
while(p.left != null) {
p = p.left;
}
return p.value;
}
2.5 最大Max
递归实现
/**
* 查找最大关键字对应的值
* @return
*/
public V max() {
return doMax(root);
}
private V doMax(BSTNode<K,V> node) {
if(node == null) {
return null;
}
// 右边已经走到头
if(node.right == null) {
return node.value;
}
return doMax(node.right);
}
非递归实现
/**
* 查找最大关键字对应的值
* @return
*/
public V max() {
if(root == null) {
return null;
}
BSTNode<K, V> p = root;
while(p.right != null) {
p = p.right;
}
return p.value;
}
2.6 新增Put
递归实现
/**
* 存储关键字和对应的值
* @param key
* @param value
*/
public void put(K key, V value) {
root = doPut(root, key, value);
}
private BSTNode<K,V> doPut(BSTNode<K,V> node, K key, V value) {
if(node == null) {
return new BSTNode<>(key, value);
}
int result = node.key.compareTo(key);
if(result > 0) {
// 往左插入
node.left = doPut(node.left, key, value);
} else if(result < 0) {
// 往右插入
node.right = doPut(node.right, key, value);
} else {
// 已经存在则覆盖其value
node.value = value;
}
return node;
}
非递归实现
/**
* 存储关键字和对应的值
* @param key
* @param value
*/
public void put(K key, V value) {
BSTNode<K, V> node = root;
BSTNode<K, V> parent = null;
while(node != null) {
parent = node;
int result = node.key.compareTo(key);
if(result > 0) {
// 向左插入
node = node.left;
}else if(result < 0) {
// 向右插入
node = node.right;
} else {
// 1. key存在则更新
node.value = value;
return;
}
}
// 2. key不存在则新增
int result = parent.key.compareTo(key);
if(parent == null) {
root = new BSTNode<K, V>(key, value);
} else if( result > 0) {
// 向左插入
parent.left = new BSTNode<>(key, value);
} else {
// 向右插入
parent.right = new BSTNode<>(key, value);
}
}
2.7 前驱后继
一个节点的前驱(前任)节点是指比它小的节点中,最大的那个
一个节点的后继(后任)节点是指比它大的节点中,最小的那个
例如上图中
- 1没有前驱,后继是2
- 2前驱是1,后继是3
- 3前驱是2,后继是4
(1)简单的办法是中序遍历,即可获得排序结果,此时很容易找打前驱后继
- 中序遍历规则:先访问左子树,然后是该节点,最后是右子树。
(2)要效率更高,需要研究一下规律,找前驱分成2种情况:
①节点有左子树,此时前驱节点就是左子树的最大值,图中属于这种情况的有
- 2的前驱是1
- 4的前驱是3
- 6的前驱是5
- 7的前驱是6
②节点没有左子树,若离它最近的祖先自从左而来,此祖先即为前驱,如
- 3的祖先自左而来,前驱2
- 5的祖先自左而来,前驱4
- 8的祖先自左而来,前驱7
- 1没有这样的祖先,前驱null
找后继也分成2种情况:
①节点有右子树,此时后继节点即为右子树的最小值,如
- 2的后继3
- 3的后继4
- 5的后继6
- 7的后继8
②节点没有右子树,若离它最近的祖先自从右而来,此祖先即为后继,如
- 1的祖先2自右而来,后继2
- 4的祖先5自右而来,后继5
- 6的祖先7自右而来,后继7
- 8没有这样的祖先,后继null
/**
* 查找关键字的前驱值
* @param key
* @return
*/
public V predecessor(K key) {
BSTNode<K, V> ancestorFromLeft = null; // 祖先自左而来
BSTNode<K, V> p = root;
while(p != null) {
int result = p.key.compareTo(key);
if(result > 0) {
// 向左
p = p.left;
} else if(result < 0) {
// 向右
ancestorFromLeft = p;
p = p.right;
} else {
break; // key相等
}
}
if(p == null) {
return null;
}
// 情况1 - 有左孩子,取左子树最大的
if(p.left != null) {
return doMax(p.left);
}
// 情况2 - 有祖先自从左而来
return ancestorFromLeft != null ? ancestorFromLeft.value : null;
}
/**
* 查找关键字的后继值
* @param key
* @return
*/
public V successor(K key) {
BSTNode<K, V> ancestorFromRight = null; // 祖先自从右而来
BSTNode<K, V> p = root;
while(p != null) {
int result = p.key.compareTo(key);
if(result > 0) {
// 向左
ancestorFromRight = p;
p = p.left;
} else if(result < 0) {
// 向右
p = p.right;
} else {
// key相等
break;
}
}
if(p == null) {
return null;
}
// 情况1 - 有右孩子,取右子树最小的
if(p.right != null) {
return doMin(p.right);
}
// 情况2 - 有祖先自右而来
return ancestorFromRight != null ? ancestorFromRight.value : null;
}
2.8 删除Delete
要删除某节点(称为D),必须先找到被删除节点的父节点,这里称为Parent
1. 删除节点没有左孩子,将右孩子托孤给Parent
2. 删除节点没有右孩子,将左孩子托孤给Parent
3. 删除节点左右孩子都没有,已经被涵盖在情况1、情况2当中,把null托孤给Parent
4. 删除节点左右孩子都有,可以将它的后继节点(称为S)托孤给Parent,设S的父节点为SP,又分为两种情况
- SP就是被删除节点。此时D与S紧邻,只需要将S托孤给Parent(如删除节点2)
- SP不是被删除节点,此时D与S不相邻,此时需要将S的后代托孤给SP,再将S托孤给Parent
例如,要删除上图中的节点7,结果如下图
递归实现
/**
* 根据关键字删除
*
* @param key
* @return
*/
public V delete(K key) {
ArrayList<V> result = new ArrayList<>();
root = doDelete(root, key, result);
return result.isEmpty() ? null : result.get(0);
}
private BSTNode<K, V> doDelete(BSTNode<K, V> node, K key, ArrayList<V> result) {
if (node == null) {
return null;
}
int compare = node.key.compareTo(key);
if (compare > 0) {
// 删除的是左边的节点
node.left = doDelete(node.right, key, result);
return node;
}
if (compare < 0) {
// 删除的是右边的节点
node.right = doDelete(node.right, key, result);
return node;
}
// 待删除的key与当前节点相等
result.add(node.value);
// 待删除节点左右孩子不为空
if (node.left != null && node.right != null) {
// 找后继节点S
BSTNode<K, V> s = node.right;
while (s.left != null) {
s = s.left;
}
// 删除节点S,并将S托孤给Parent
s.right = doDelete(node.right, s.key, new ArrayList<>());
s.left = node.left;
// 返回后继节点
return s;
}
// 对应情况1、2、3
return node.left != null ? node.left : node.right;
}
非递归实现
/**
* 根据关键字删除
*
* @param key
* @return
*/
public V delete(K key) {
BSTNode<K, V> d = root;
BSTNode<K, V> parent = null;
// 1. 找到待删除节点d
while(d != null) {
int compare = d.key.compareTo(key);
if(compare > 0) {
// 左子树
parent = d;
d = d.left;
} else if(compare < 0) {
// 右子树
parent = d;
d = d.right;
} else {
// 相等
break;
}
}
if(d == null) {
return null;
}
// 2. 删除操作
if(d.left == null) {
// 情况1,将右孩子托孤给parent
shift(parent, d, d.right);
} else if(d.right == null) {
// 情况2,将左孩子托孤给parent
shift(parent, d, d.left);
} else {
// 情况4,左右孩子皆有
// 2.2 找被删除节点的后继节点S
BSTNode<K, V> s = d.right;
BSTNode<K, V> sp = d;
while(s.left != null) {
sp = s;
s = s.left;
}
// 2.3 sp不是被删除节点,删除节点d和后继s不相邻,将s的后代托孤给sp,再将s托孤给parent
if(sp != d) {
shift(sp, s, s.right);
s.right = d.right;
}
// 2.4 sp就是被删除节点d,此时d与s紧邻,后继s取代被删除节点d
shift(parent, d, s);
s.left = d.left;
}
return d.value;
}
/**
* 托孤方法
* @param parent 被删除节点的父节点
* @param d 被删除节点
* @param s 被顶上去的节点
*/
private void shift(BSTNode<K,V> parent, BSTNode<K,V> d, BSTNode<K,V> s) {
if(parent == null) {
root = s;
} else if(d == parent.left) {
// 左子树
parent.left = s;
} else {
// 右子树
parent.right = s;
}
}
2.9 找小的
在二叉搜索树中找到所有关键字小于给定值key的节点
/**
* 找小于key的value -> 非递归的中序遍历
* @param key
* @return
*/
public List<V> less(K key) {
ArrayList<V> result = new ArrayList<>();
BSTNode<K, V> p = root;
LinkedList<BSTNode> stack = new LinkedList<>();
while(p != null || !stack.isEmpty()) {
if(p != null) {
// 左子树遍历
stack.push(p); // 记录来时的路
p = p.left;
} else {
// 处理当前节点
BSTNode<K, V> pop = stack.pop();
int compare = pop.key.compareTo(key);
if(compare < 0) {
result.add(pop.value);
} else {
// 如果该节点的key大于等于给定的key,则跳出循环
break;
}
// 指向被弹出节点的右子树,继续处理
p = pop.right;
}
}
return result;
}
2.10 找大的
在二叉搜索树中找到所有关键字大于给定值key的节点
解法一:遍历整个树找到符合条件的节点,但是这样效率不高
/**
* 找大于key的所有value
* @param key
* @return
*/
public List<V> greater(K key) {
ArrayList<V> result = new ArrayList<>();
BSTNode<K, V> p = root;
LinkedList<BSTNode> stack = new LinkedList<>();
while(p != null || !stack.isEmpty()) {
if(p != null) {
// 左子树遍历
stack.push(p);
p = p.left;
} else {
// 处理当前节点
BSTNode<K, V> pop = stack.pop();
int compare = pop.key.compareTo(key);
if(compare > 0) {
result.add(pop.value);
} else {
break;
}
p = pop.right;
}
}
return result;
}
解法二:可以用RNL遍历
注:
- Pre-order, NLR
- In-order, LNR
- Post-order, LRN
- Reverse pre-order, NRL
- Reverse in-order, RNL(反向中序遍历)
- Reverse post-order, RLN
从右往左遍历
/**
* 找大于key的所有value
* @param key
* @return
*/
public List<V> greater(K key) {
ArrayList<V> result = new ArrayList<>();
BSTNode<K, V> p = root;
LinkedList<BSTNode> stack = new LinkedList<>();
while(p != null || !stack.isEmpty()) {
if(p != null) {
stack.push(p);
p = p.right;
} else {
BSTNode<K, V> pop = stack.pop();
int compare = pop.key.compareTo(key);
if(compare > 0) {
result.add(pop.value);
} else {
break;
}
p = pop.left;
}
}
return result;
}
解法三:从根节点开始遍历
- 如果当前节点的键大于给定的关键字,则将该节点的值加入到结果列表,并继续遍历右子树
- 如果当前节点的键小于或等于给定关键字,则直接跳过左子树和当前节点,继续遍历右子树
public List<V> greater(K key) {
ArrayList<V> result = new ArrayList<>();
findGreaterThan(root, key, result);
return result;
}
private void findGreaterThan(BSTNode<K, V> node, K key, List<V> result) {
if (node == null) {
return;
}
// 先检查右子树,因为右子树的节点都可能大于给定的key
findGreaterThan(node.right, key, result);
// 处理当前节点
int compare = node.key.compareTo(key);
if (compare > 0) {
result.add(node.value); // 如果当前节点的key大于给定值,加入结果
findGreaterThan(node.left, key, result); // 然后检查左子树
}
}
2.11 找之间
/**
* 找两个key之间的value集合
* @param key1
* @param key2
* @return
*/
public List<V> between(K key1, K key2) {
ArrayList<V> result = new ArrayList<>();
BSTNode<K, V> p = root;
LinkedList<BSTNode> stack = new LinkedList<>();
while(p != null || !stack.isEmpty()) {
if(p != null) {
stack.push(p);
p = p.left;
} else {
BSTNode<K, V> pop = stack.pop();
int compare1 = pop.key.compareTo(key1);
int compare2 = pop.key.compareTo(key2);
if(compare1 >= 0 && compare2 <= 0) {
result.add(pop.value);
} else if(compare2 > 0) {
break;
}
p = pop.right;
}
}
return result;
}
2.12 小结
优点:
- 如果每个节点的左子树和右子树的大小差距不超过1,可以保证搜索操作的时间复杂度是O(log n),效率高
- 插入、删除节点等操作也比较容易实现,效率也比较高
- 对于有序数据的查询和处理,二叉查找树非常适用,可以适用中序遍历得到有序序列
缺点:
- 如果输入的数据是有序的或者近似有序的,就会出现极度不平衡的情况,可能导致搜索效率下降,时间复杂度退化为O(n)
- 对于频繁地插入、删除操作,需要维护平衡二叉查找树,例如红黑树、AVL树等,否则搜索效率也会下降
- 对于存在大量重复数据的情况,需要做相应的处理,否则会导致树的深度增加,搜索效率下降
- 对于节点过多的情况,由于树的空间开销较大,可能导致内存消耗过大,不适合对内存要求高的场景。
3. 习题
3.1 二叉搜索树中的删除操作
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
示例 1:
输入:root = [5,3,6,2,4,null,7], key = 3 输出:[5,4,6,2,null,null,7] 解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 另一个正确答案是 [5,2,6,null,4,null,7]。
示例 2:
输入: root = [5,3,6,2,4,null,7], key = 0 输出: [5,3,6,2,4,null,7] 解释: 二叉树不包含值为 0 的节点
示例 3:
输入: root = [], key = 0 输出: []
提示:
- 节点数的范围
[0, 10^4]
. -10^5 <= Node.val <= 10^5
- 节点值唯一
root
是合法的二叉搜索树-10^5 <= key <= 10^5
进阶: 要求算法时间复杂度为 O(h),h 为树的高度。
解法一:递归
/**
* Definition for a binary tree node.
* public 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 deleteNode(TreeNode root, int key) {
if(root == null) {
return null;
}
if(key < root.val) {
root.left = deleteNode(root.left, key);
return root;
}
if(root.val < key) {
root.right = deleteNode(root.right, key);
return root;
}
if(root.left == null) {
return root.right;
}
if(root.right == null) {
return root.left;
}
TreeNode s = root.right;
while(s.left != null) {
s = s.left;
}
s.right = deleteNode(root.right, s.val);
s.left = root.left;
return s;
}
}
解法二:非递归
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
// 如果树为空,直接返回 null
if (root == null) return null;
// 定位要删除的节点和它的父节点
TreeNode parent = null;
TreeNode d = root;
while (d != null) {
if (key < d.val) {
parent = d;
d = d.left;
} else if (key > d.val) {
parent = d;
d = d.right;
} else {
break; // 找到要删除的节点
}
}
// 如果找不到该节点,返回原树
if (d == null) return root;
// 处理删除的情况
if (d.left == null && d.right == null) { // 叶子节点
if (parent == null) {
return null; // 删除根节点时树变空
}
if (parent.left == d) {
parent.left = null;
} else {
parent.right = null;
}
} else if (d.left == null) { // 只有右子树
if (parent == null) {
return d.right; // 删除根节点
}
if (parent.left == d) {
parent.left = d.right;
}
else {
parent.right = d.right;
}
} else if (d.right == null) { // 只有左子树
if (parent == null) {
return d.left; // 删除根节点
}
if (parent.left == d) {
parent.left = d.left;
} else {
parent.right = d.left;
}
} else { // 有左右子树
// 找到右子树的最小节点
TreeNode s = d.right;
TreeNode sp = d; // 记录s的父节点
while (s.left != null) {
sp = s;
s = s.left;
}
// 替换节点 d 为 s
d.val = s.val; // 替换值
// 从树中删除 s
if (sp.left == s) {
sp.left = s.right;
} else {
sp.right = s.right;
}
}
return root;
}
}
3.2 二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root
和要插入树中的值 value
,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 1:
输入:root = [4,2,7,1,3], val = 5 输出:[4,2,7,1,3,5] 解释:另一个满足题目要求可以通过的树是:
示例 2:
输入:root = [40,20,60,10,30,50,70], val = 25 输出:[40,20,60,10,30,50,70,null,null,25]
示例 3:
输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5 输出:[4,2,7,1,3,5]
提示:
- 树中的节点数将在
[0, 10^4]
的范围内。 -10^8 <= Node.val <= 10^8
- 所有值
Node.val
是 独一无二 的。 -10^8 <= val <= 10^8
- 保证
val
在原始BST中不存在。
解法一:递归
/**
* Definition for a binary tree node.
* public 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 insertIntoBST(TreeNode root, int val) {
return doInsert(root, val);
}
private TreeNode doInsert(TreeNode node, int val) {
if (node == null) {
return new TreeNode(val);
}
if (val < node.val) {
node.left = doInsert(node.left, val);
} else if (node.val < val) {
node.right = doInsert(node.right, val);
}
return node;
}
}
解法二:非递归
/**
* Definition for a binary tree node.
* public 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 insertIntoBST(TreeNode root, int val) {
// 创建一个新的节点
TreeNode newNode = new TreeNode(val);
// 如果树为空,直接返回新结点作为根
if(root == null) {
return newNode;
}
// 初始化当前节点为根节点
TreeNode current = root;
TreeNode parent = null;
// 找到插入位置
while(current != null) {
parent = current;
if(val < current.val) {
// 插入左子树
current = current.left;
} else {
// 插入右子树
current = current.right;
}
}
// 插入新节点到合适的位置
if(val < parent.val) {
// 插入到父节点的左边
parent.left = newNode;
} else {
// 插入到父节点的右边
parent.right = newNode;
}
return root;
}
}
3.3 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root
和一个整数值 val
。
你需要在 BST 中找到节点值等于 val
的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
。
示例 1:
输入:root = [4,2,7,1,3], val = 2 输出:[2,1,3]
示例 2:
输入:root = [4,2,7,1,3], val = 5 输出:[]
提示:
- 树中节点数在
[1, 5000]
范围内 1 <= Node.val <= 10^7
root
是二叉搜索树1 <= val <= 10^7
解法一:递归
/**
* Definition for a binary tree node.
* public 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) {
return doGet(root, val);
}
private TreeNode doGet(TreeNode node, int val) {
if (node == null) {
return null;
}
if (val < node.val) {
return doGet(node.left, val);
} else if (node.val < val) {
return doGet(node.right, val);
} else {
return node;
}
}
}
解法二:非递归
/**
* Definition for a binary tree node.
* public 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) {
TreeNode node = root;
while(node != null) {
if(val < node.val) {
node = node.left;
} else if(node.val < val) {
node = node.right;
} else {
return node;
}
}
return null;
}
}
3.4 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3] 输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 10^4]
内 -2^31 <= Node.val <= 2^31 - 1
解法一:中序递归
- 为何不能用Long或long?因为它们都是局部变量且不可变,因此每次赋值时,并不会改变其他方法调用时的prev
- 要么把prev设置为AtomicLong,要么把prev设置为全局变量,而不要采用方法参数这样的局部变量
/**
* Definition for a binary tree node.
* public 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 boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
return check(new AtomicLong(Long.MIN_VALUE), root);
}
private boolean check(AtomicLong prev, TreeNode node) {
if (node == null) {
return true;
}
// 先比较左子树和根节点
boolean a = check(prev, node.left);
if(!a) { // 剪枝
return false;
}
if (prev.get() >= node.val) {
return false;
}
// 比较根节点和右子树
prev.set(node.val);
boolean b = check(prev, node.right);
return b;
}
}
解法二:非递归
- 注意,如果相邻两个节点相等,也不应当通过测试
/**
* Definition for a binary tree node.
* public 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 boolean isValidBST(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode prev = null;
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.poll();
if (prev != null && prev.val >= root.val) {
return false;
}
prev = root;
root = root.right;
}
return true;
}
}
解法三:上下限递归
- 设每个节点必须在一个范围内:(min, max),不包含边界,若节点值超过这个范围,则返回false
- 对于node.left范围肯定是(min, node.val)
- 对于node.right范围肯定是(node.val, max)
- 一开始不知道min, max则取java中长整数的最小、最大值
- 本质是前序遍历 + 剪枝
/**
* Definition for a binary tree node.
* public 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 boolean isValidBST(TreeNode root) {
return doValid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean doValid(TreeNode node, long min, long max) {
if(node == null) {
return true;
}
if(node.val <= min || node.val >= max) {
return false;
}
return doValid(node.left, min, node.val) && doValid(node.right, node.val, max);
}
}
3.5 二叉搜索树的范围和
给定二叉搜索树的根结点 root
,返回值位于范围 [low, high]
之间的所有结点的值的和。
示例 1:
输入:root = [10,5,15,3,7,null,18], low = 7, high = 15 输出:32
示例 2:
输入:root = [10,5,15,3,7,13,18,1,null,6], low = 6, high = 10 输出:23
提示:
- 树中节点数目在范围
[1, 2 * 10^4]
内 1 <= Node.val <= 10^5
1 <= low <= high <= 10^5
- 所有
Node.val
互不相同
解法一:中序非递归实现
/**
* Definition for a binary tree node.
* public 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) {
int sum = 0;
TreeNode p = root;
LinkedList<TreeNode> stack = new LinkedList<>();
while(p != null || !stack.isEmpty()) {
if(p != null) {
stack.push(p);
p = p.left;
} else {
TreeNode pop = stack.pop();
if(pop.val >= low && pop.val <= high) {
sum += pop.val;
} else if(pop.val > high) { // 剪枝
break;
}
p = pop.right;
}
}
return sum;
}
}
中序递归实现:
/**
* Definition for a binary tree node.
* public 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 a = rangeSumBST(root.left, low, high);
int b = 0;
if(root.val >= low && root.val <= high) {
b = root.val;
}
return a + b + rangeSumBST(root.right, low, high);
}
}
解法三:上下限递归实现
/**
* Definition for a binary tree node.
* public 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;
}
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);
}
}
3.6 前序遍历结果构造二叉搜索树
给定一个整数数组,它表示BST(即 二叉搜索树 )的 先序遍历 ,构造树并返回其根。
保证 对于给定的测试用例,总是有可能找到具有给定需求的二叉搜索树。
二叉搜索树 是一棵二叉树,其中每个节点, Node.left
的任何后代的值 严格小于 Node.val
, Node.right
的任何后代的值 严格大于 Node.val
。
二叉树的 前序遍历 首先显示节点的值,然后遍历Node.left
,最后遍历Node.right
。
示例 1:
输入:preorder = [8,5,1,7,10,12] 输出:[8,5,10,1,7,null,12]
示例 2:
输入: preorder = [1,3] 输出: [1,null,3]
提示:
1 <= preorder.length <= 100
1 <= preorder[i] <= 10^8
preorder
中的值 互不相同
解法一:递归
/**
* Definition for a binary tree node.
* public 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) {
TreeNode root = insert(null, preorder[0]);
for(int i = 1; i < preorder.length; i++) {
insert(root, preorder[i]);
}
return root;
}
private TreeNode insert(TreeNode node, int val) {
if(node == null) {
return new TreeNode(val);
}
if(val < node.val) {
// 左子树
node.left = insert(node.left, val);
} else if(val > node.val) {
// 右子树
node.right = insert(node.right, val);
}
return node;
}
}
解法二:上限法(动态更新上限)。O(n)
依次处理prevorder中每个值,返回创建好的节点或null
1. 如果超过上限,返回null作为孩子返回
2. 如果没超过上限,创建节点,并设置其左右孩子
/**
* Definition for a binary tree node.
* public 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 insert(preorder, Integer.MAX_VALUE);
}
int i = 0;
private TreeNode insert(int[] preorder, int max) {
if(i == preorder.length) {
return null;
}
int val = preorder[i];
if(val > max) {
return null;
}
TreeNode node = new TreeNode(val);
i++;
node.left = insert(preorder, node.val);
node.right = insert(preorder, max);
return node;
}
}
解法三:分治法
/**
* Definition for a binary tree node.
* public 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 partition(preorder, 0, preorder.length - 1);
}
private TreeNode partition(int[] preorder, int start, int end) {
if(start > end) {
return null;
}
TreeNode root = new TreeNode(preorder[start]);
int index = start + 1;
while(index <= end) {
if(preorder[index] > preorder[start]) {
break;
}
index++;
}
// index就是右子树的起点
root.left = partition(preorder, start + 1, index - 1);
root.right = partition(preorder, index, end);
return root;
}
}
3.7 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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在ancestor的两侧,则ancestor就是它们的最近公共祖先
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode ancestor = root;
while(ancestor.val > p.val && ancestor.val > q.val || ancestor.val < p.val && ancestor.val < q.val) {
// 两个节点在祖先的同侧
if(ancestor.val > p.val) {
ancestor = ancestor.left;
} else {
ancestor = ancestor.right;
}
}
return ancestor;
}
}
解法二:递归。
- 如果两个节点的值均小于当前节点的值,说明这两个节点位于当前节点的左侧,因此继续在左子树中查找
- 如果两个节点的值均大于当前节点的值,说明这两个节点位于当前节点的右侧,因此继续在右子树中查找
- 如果一个节点的值小于当前节点的值,而另一个节点的值大于当前节点的值,或者其中一个节点的值与当前节点的值相等,则当前节点就是这两个节点的最近公共祖先
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 情况1
if(root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
}
// 情况2
if(root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
}
// 情况3
return root;
}
}
3.8 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 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 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2 输出:1
提示:
- 树中节点数目在范围
[2, 10^5]
内。 -10^9 <= Node.val <= 10^9
- 所有
Node.val
互不相同
。 p != q
p
和q
均存在于给定的二叉树中。
解法一:递归
- 基准情况:如果当前节点为空,返回null。如果当前节点等于目标节点p或q,返回当前节点
- 递归查找:在左子树和右子树中查找p和q
- 判断返回值:如果在左右子树都找到了p或q,当前节点就是LCA。如果只在某一侧找到了p或q,则返回那一侧的结果。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 情况1:当前节点为空,或等于p,或等于q
if(root == null || 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 ? left : right;
}
}
3.9 二叉树展开为链表
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6] 输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = [] 输出:[]
示例 3:
输入:root = [0] 输出:[0]
提示:
- 树中结点数在范围
[0, 2000]
内 -100 <= Node.val <= 100
进阶:你可以使用原地算法(O(1)
额外空间)展开这棵树吗?
解法一:栈
右子树指向链表的下一个节点,左子树保持为空
/**
* Definition for a binary tree node.
* public 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 void flatten(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
TreeNode current;
while (!stack.isEmpty()) {
current = stack.pop();
// 右孩子先入栈,后出栈
if (current.right != null) {
stack.push(current.right);
}
// 左孩子后入栈,先出栈
if (current.left != null) {
stack.push(current.left);
}
// 指向栈顶元素
if (!stack.isEmpty()) {
current.right = stack.peek();
}
// 单链表 -> 左孩子为空
current.left = null;
}
}
}
解法二:递归
- 将当前节点的左子树连接到右子树上,并将左子树的右指针指向原来的右子树
- 找到左子树展开后的最后一个节点,然后将右子树连接到末尾
/**
* Definition for a binary tree node.
* public 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 void flatten(TreeNode root) {
if (root == null) {
return;
}
// 先存储当前节点的左右子树
TreeNode left = root.left;
TreeNode right = root.right;
// 将左子树展开
flatten(left);
// 将右子树展开
flatten(right);
// 把左子树连接到右子树上
root.left = null; // 左子树为空
root.right = left; // 将左子树放在右子树的位置
// 找到左子树的末端,将右子树连接到末端
TreeNode current = root;
while(current.right != null) {
current = current.right;
}
// 将右子树连接到末端
current.right = right;
}
}
3.10 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
示例 1:
输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-10,null,5] 解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3] 输出:[3,1] 解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums
按 严格递增 顺序排列
解法一:递归。二分法
/**
* Definition for a binary tree node.
* public 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 sortedArrayToBST(int[] nums) {
if (nums.length == 0) {
return null;
}
return helper(nums, 0, nums.length - 1);
}
private TreeNode helper(int[] nums, int start, int end) {
if (start > end) {
return null;
}
// 二分法
int mid = start + (end - start) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = helper(nums, start, mid - 1);
root.right = helper(nums, mid + 1, end);
return root;
}
}
3.11 将二叉搜索树变为平衡
给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。如果有多种构造方法,请你返回任意一种。
如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1
,我们就称这棵二叉搜索树是 平衡的 。
示例 1:
输入:root = [1,null,2,null,3,null,4,null,null] 输出:[2,1,3,null,null,null,4] 解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。
示例 2:
输入: root = [2,1,3] 输出: [2,1,3]
提示:
- 树节点的数目在
[1, 10^4]
范围内。 1 <= Node.val <= 10^5
解法一:
- 中序遍历:首先,通过中序遍历BST,可以生成一个有序的节点值数组(由于BST的特性,中序遍历能够得到升序序列)
- 构建平衡树:将有序数组转化为平衡二叉搜索树。通过选择中间元素作为根节点,可以确保右子树的高度差不超过1,从而形成平衡树。
/**
* Definition for a binary tree node.
* public 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 void inorderTraversal(TreeNode root, List<Integer> values) {
if(root == null) {
return;
}
inorderTraversal(root.left, values);
values.add(root.val);
inorderTraversal(root.right, values);
}
// 按照有序节点值构造平衡的二叉树
private TreeNode sortedArrayToBST(List<Integer> values, int start, int end) {
if(start > end) {
return null;
}
int mid = start + (end - start) / 2;
TreeNode node = new TreeNode(values.get(mid));
node.left = sortedArrayToBST(values, start, mid - 1);
node.right = sortedArrayToBST(values, mid + 1, end);
return node;
}
public TreeNode balanceBST(TreeNode root) {
List<Integer> values = new ArrayList<>();
inorderTraversal(root, values);
return sortedArrayToBST(values, 0, values.size() - 1);
}
}