530. 二叉搜索树的最小绝对差
题目链接
标签
树 深度优先搜索 广度优先搜索 二叉搜索树 二叉树
思路
对于 二叉搜索树,在 中序遍历 的结果中找 相邻的元素,只有 相邻的元素之差 才可能 成为 最小差值,其他元素之间的差值只会比相邻元素之差大。例如对于以下二叉搜索树:
4
/ \
2 6
/ \
1 3
中序遍历结果为 [1, 2, 3, 4, 6]
,最小差值在 2 - 1, 3 - 2, 4 - 3, 6 - 4
中取得,发现最小差值为 1
。
中序遍历的核心思想为:先遍历左子树,再处理本节点,最后遍历右子树。按照中序遍历的结果,左子树的最后一个节点就是当前节点的“上一个”节点。也就是说,这两个节点在中序遍历的结果中是相邻的。这时就可以计算它们的差值,进而更新最小差值。
不过,有一个特殊情况:根节点一直往左子节点遍历的最终节点(也可以称为中序遍历的第一个节点)是没有前一个节点的,所以该节点无法计算差值。
代码
class Solution {
private int prev; // 前一个节点的值
private int res; // 结果
public int getMinimumDifference(TreeNode root) {
res = Integer.MAX_VALUE;
prev = -1; // 给 prev 赋初始值 -1,表示中序遍历的第一个节点没有前一个节点
dfs(root);
return res;
}
private void dfs(TreeNode curr) { // 中序遍历
if (curr == null) {
return;
}
// 先遍历左子树
dfs(curr.left); // 递归遍历左子树,遍历完之后,prev 就是中序遍历结果中 curr 的上一个节点的值
// 再处理本节点
if (prev != -1) { // 当 prev == -1 时,表示中序遍历的第一个节点,无需进行求差值的操作
res = Math.min(res, curr.val - prev);
}
prev = curr.val;
// 最后遍历右子树
dfs(curr.right); // 递归遍历右子树
}
}
201. 数字范围按位与
题目链接
标签
位运算
思路
对于本题,可以使用一个规律来解决:寻找 左、右边界的二进制数 的公共前缀。
比如说要找 [20, 24]
之间的所有数的按位与的结果,则可以通过 20, 24
这两个数的二进制数来获取:
如果使用普通的解法:
20 = 0001 0100
21 = 0001 0101
21 = 0001 0110
21 = 0001 0111
24 = 0001 1000
^
发现只有一位全为 1,其余位的按位与结果都是 0,最终的结果为 0001 0000,即 16
如果使用规律:
左、右边界的初始值
20 = 0001 0100
24 = 0001 1000
右移一位(两边界不相等):
0000 1010
0000 1100
右移一位(两边界不相等):
0000 0101
0000 0110
右移一位(两边界不相等):
0000 0010
0000 0011
右移一位(两边界相等):
0000 0001
0000 0001
发现公共前缀为 0001,期间向右移动了 4 位,所以也应该让 0001 向左移动 4 位,即得到 0001 0000,也就是 16
所以这个规律奏效
代码
class Solution {
public int rangeBitwiseAnd(int left, int right) {
int shift = 0; // 当 left == right 时,移动的位数
while (left < right) { // 当 left == right 时退出循环
left >>= 1; // left 向右移一位
right >>= 1; // right 向右移一位
shift++; // 移动的位数加一
}
return left << shift; // 让 left 或 right 左移 shift 位
}
}
433. 最小基因变化
题目链接
标签
广度优先搜索 哈希表 字符串
思路
本题需要使用 广度优先搜索,使用广度优先搜索就需要 队列。本题的核心思想是:从 起始基因 开始,一个一个地 变化字符,从而形成 新基因;然后再从这些 新基因 开始,一个一个地 变化字符,从而形成 更新的基因;直到无法继续形成新基因 或 形成的新基因与最终基因相同。
由这个核心思想就引出了本题要讲解的如下几点:
- 新基因的限制条件:新基因不能无法无天,想怎么生成就怎么生成,而是只能生成 基因库 中现有的基因。如果不是现有的基因,那么就跳过它。
- 如何确定无法继续形成新基因:可以通过一个
Set
集合记录已遍历过的基因,如果新基因都是已遍历过的基因,那么就无法形成新基因了。 - 如何确定形成新基因的步数:每次完成 从 一批基因 形成 另一批新的基因 的操作时,步数就加一,直到形成的新基因与最终基因相同。
至于一些细枝末节的处理,请看代码中的注释。
代码
class Solution {
public int minMutation(String startGene, String endGene, String[] bank) {
if (startGene.equals(endGene)) { // 如果 起始基因 与 最终基因 一样
return 0; // 则无需变化,返回 0
}
Set<String> lib = new HashSet<>(bank.length); // 存储 基因 的库
for (String gene : bank) { // 将 所有基因 放入 库 中
lib.add(gene);
}
if (!lib.contains(endGene)) { // 如果 基因库 中不存在 最终基因
return -1; // 则无法完成此基因变化,返回 -1
}
Set<String> vis = new HashSet<>(); // 存储 已遍历过 的基因
int step = 1; // 从 起始基因 到 最终基因 需要转化的步数
LinkedList<String> queue = new LinkedList<>(); // 存储 待遍历 的基因
queue.offer(startGene); // 先将起始基因放入队列,等待遍历
vis.add(startGene); // 并将其放入已遍历过的基因集合
while (!queue.isEmpty()) { // 直到 没有待遍历的基因 才退出循环
int size = queue.size(); // 获取本次遍历的基因数量
for (int cnt = 0; cnt < size; cnt++) { // 循环 size 次,遍历 size 个基因
String curr = queue.poll(); // 取出待遍历的基因,也称作 当前基因
char[] currGene = curr.toCharArray();
// 枚举 从当前基因 发生一次变化 形成的 可能的 新基因
for (int i = 0; i < geneLen; i++) {
char temp = currGene[i]; // 先保存原有的基因
for (char key : keys) { // 取出可能的字符进行替换
if (key == currGene[i]) { // 如果 当前基因的第 i 个字符 与 待替换字符 一样
continue; // 则无法产生新的基因,跳过接下来的替换
}
// 准备构建新的基因
currGene[i] = key; // 将当前基因的第 i 个字符替换为 待替换字符
String newGene = new String(currGene); // 形成新的基因
if (vis.contains(newGene) // 如果新基因已经遍历过了
|| !lib.contains(newGene)) { // 或者新基因不在基因库中
continue; // 则跳过这个新基因
}
if (newGene.equals(endGene)) { // 如果 新基因 与 最终基因 一致
return step; // 则可以返回转化的步数
}
queue.offer(newGene); // 将新基因放入队列,等待遍历
vis.add(newGene); // 并将其放入已遍历过的基因集合
}
currGene[i] = temp; // 将原有的基因还原回来
}
}
step++; // 进行了一次转化,步数加一
}
return -1; // 无法完成此基因变化,返回 -1
}
private static final char[] keys = {'A', 'C', 'G', 'T'}; // 基因序列中可能的字符
private static final int geneLen = 8; // 基因序列的长度为 8
}