每日算法-250413
记录一下今天解决的几道 LeetCode 算法题。
2476. 二叉搜索树最近节点查询
题目
思路
主要思路是将二叉搜索树(BST)通过中序遍历转换为一个升序排列的数组,然后对每个查询值,利用二分查找在这个有序数组中找到最接近的小于等于和大于等于它的节点值。
解题过程
- 中序遍历:首先,定义一个
traversal
方法,使用中序遍历(左子树 -> 根节点 -> 右子树)将 BST 的所有节点值存入一个List<Integer>
。因为中序遍历 BST 会得到一个升序序列。 - 转换为数组:将得到的
List<Integer>
转换为一个int[]
数组nums
。选择先用 List 再转 Array 是因为在遍历开始时无法确定节点的总数,List 可以动态扩容。 - 处理查询:遍历
queries
列表中的每一个查询值x
。 - 二分查找:为每个
x
调用一个check
方法(二分查找)。这个check
方法旨在找到nums
数组中第一个 大于等于x
的元素的下标index
。 - 确定最小值 (min):
- 小于等于
x
的最大值 (min_i
) 对应的元素应该在index
的左侧。 - 如果
nums[index]
正好等于x
,那么min_i
就是nums[index]
,其下标为index
。 - 如果
nums[index]
大于x
(或者index
等于数组长度n
,表示所有元素都小于x
),那么小于等于x
的最大值应该在index - 1
的位置。 - 所以,我们先判断
index == n
或者nums[index] != x
的情况,如果是,说明目标x
不在数组中或者nums[index]
是大于x
的最小值,此时我们需要找index - 1
作为min_i
的候选下标。 - 处理边界:如果最终确定的
min_i
的下标小于 0(比如x
比数组中所有元素都小,index
为 0,然后index--
变为 -1),则min_i
不存在,设为 -1。否则min_i
为nums[index]
(经过调整后的index
)。
- 小于等于
- 确定最大值 (max):
- 大于等于
x
的最小值 (max_i
) 对应的元素下标就是二分查找直接返回的index
。 - 处理边界:如果
index
等于数组长度n
(表示x
大于数组中所有元素),则max_i
不存在,设为 -1。否则max_i
为nums[index]
。
- 大于等于
- 存储结果:将找到的
min_i
和max_i
存入一个临时的List<Integer>
,然后添加到最终的结果列表ret
中。 - 返回结果:返回
ret
。
复杂度
- 时间复杂度: O(N + Q log N)
- O(N) 用于中序遍历构建有序数组,其中 N 是树中的节点数。
- O(Q log N) 用于处理 Q 个查询,每个查询进行一次二分查找。
- 如果简化考虑 Q 和 N 可能的大小关系,有时也写作 O(N log N) 或 O(Q log N),但 O(N + Q log N) 更精确。
- 空间复杂度: O(N)
- 需要 O(N) 的空间存储中序遍历的结果数组。
- 递归栈空间在最坏情况下(链状树)也可能达到 O(N),平均情况下是 O(log N)。
Code
/**
* 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 List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
List<List<Integer>> ret = new ArrayList<>();
List<Integer> arr = new ArrayList<>();
traversal(root, arr);
int n = arr.size();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = arr.get(i);
}
int min = 0, max = 0;
for (Integer x : queries) {
List<Integer> tmp = new ArrayList<>();
int index = check(nums, x);
max = index == n ? -1 : nums[index];
if (index == n || max != x) {
index--;
}
min = index < 0 ? -1 : nums[index];
tmp.add(min);
tmp.add(max);
ret.add(tmp);
}
return ret;
}
private void traversal(TreeNode root, List<Integer> arr) {
if (root == null) {
return;
}
traversal(root.left, arr);
arr.add(root.val);
traversal(root.right, arr);
}
private int check(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < t) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
74. 搜索二维矩阵
题目
思路
利用题目给出的两个特性(行内递增、下一行首元素大于上一行尾元素),可以将整个二维矩阵视为一个一维有序数组,从而使用二分查找。或者更直观地,先进行一次二分查找确定目标值 target
可能在哪一行,再对该行进行一次二分查找。
解题过程 (两次二分查找)
- 确定行:
- 对矩阵的行进行二分查找(基于每行的第一个元素或最后一个元素)。
- 使用
left
和right
指针表示当前搜索的行范围,初始为0
到matrix.length - 1
。 - 计算中间行
mid
。 - 比较
target
与matrix[mid][0]
(行首元素)和matrix[mid][m-1]
(行尾元素,m
为列数)。 - 如果
target < matrix[mid][0]
,说明target
如果存在,必定在mid
行之前,令right = mid - 1
。 - 如果
target > matrix[mid][m-1]
,说明target
如果存在,必定在mid
行之后,令left = mid + 1
。 - 如果
matrix[mid][0] <= target <= matrix[mid][m-1]
,说明target
可能在mid
行,跳出循环或直接对该行进行下一步查找。
- 确定列:
- 如果上一步确定了目标可能所在的行
targetRow
,则对matrix[targetRow]
这个一维数组进行标准的二分查找。 - 定义一个
check
方法(或直接内联实现),在matrix[targetRow]
中查找target
。 - 使用
left
和right
指针表示列范围,初始为0
到m - 1
。 - 计算
mid
列。 - 比较
arr[mid]
与t
(target
)。 - 如果
arr[mid] < t
,令left = mid + 1
。 - 如果
arr[mid] >= t
,令right = mid - 1
。(这里使用>=
是为了找到第一个可能等于t
的位置,或者t
的插入位置) - 循环结束后,
left
指向第一个大于等于t
的位置。检查left
是否在数组范围内且arr[left]
是否等于t
。
- 如果上一步确定了目标可能所在的行
- 返回结果:如果在行查找中没有找到合适的行,或者在列查找中没有找到
target
,返回false
。否则返回true
。
复杂度
- 时间复杂度: O(log M + log N)
- 第一次二分查找确定行,复杂度为 O(log M),其中 M 是行数。
- 第二次二分查找在确定行内查找列,复杂度为 O(log N),其中 N 是列数。
- 空间复杂度: O(1)
- 只使用了常数级别的额外空间。
Code
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int left = 0, right = matrix.length - 1, m = matrix[0].length;
while (left <= right) {
int mid = left + (right - left) / 2;
if (matrix[mid][0] > target) {
right = mid - 1;
} else if (matrix[mid][m - 1] < target) {
left = mid + 1;
} else {
return check(matrix[mid], target);
}
}
return false;
}
private boolean check(int[] arr, int t) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < t) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return arr[left] == t;
}
}
240. 搜索二维矩阵 II
题目
思路
这道题的矩阵特性与上一题不同:每行递增,每列递增,但下一行的首元素不一定大于上一行的尾元素。因此不能直接视为一维数组进行二分。
可以采用一种称为 Z 型查找 的方法。利用矩阵的行列单调性,从矩阵的一个角(例如右上角或左下角)开始搜索。
解题过程 (以右上角为例)
- 起始位置:将指针
(x, y)
初始化为矩阵的右上角,即x = 0
(第一行),y = m - 1
(最后一列),其中n
是行数,m
是列数。 - 比较与移动:
- 获取当前位置的元素
num = matrix[x][y]
。 - 比较
target
和num
:- 如果
target == num
,说明找到了目标值,返回true
。 - 如果
target < num
,说明target
不可能在当前列y
的下方(因为列是递增的),所以需要向左移动,减小列索引y--
。 - 如果
target > num
,说明target
不可能在当前行x
的左方(因为行是递增的),所以需要向下移动,增加行索引x++
。
- 如果
- 获取当前位置的元素
- 终止条件:
- 重复步骤 2,直到找到
target
或指针移出矩阵边界。 - 如果指针移出边界仍未找到
target
,则说明矩阵中不存在该值,返回false
。
- 重复步骤 2,直到找到
复杂度
- 时间复杂度: O(M + N)
- 指针
x
最多向下移动 M 次,指针y
最多向左移动 N 次。每次移动都是 O(1)。总的移动次数不超过 M + N。
- 指针
- 空间复杂度: O(1)
- 只使用了常数级别的额外空间存储指针
x
和y
。
- 只使用了常数级别的额外空间存储指针
Code
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int n = matrix.length, m = matrix[0].length;
int x = 0, y = m - 1;
while ((x >= 0 && x <= n - 1) && (y >= 0 && y <= m - 1)) {
int num = matrix[x][y];
if (target < num) {
y--;
} else if (target > num) {
x++;
} else {
return true;
}
}
return false;
}
}