Leetcode练习题:二分查找与二叉树
二分查找很重要,掌握思想之后实现起来也比较简单,注意条件终止的条件,以及mid的变化,变形会有点难下面有题目。
二叉树也是最常使用的树形结构,一般的树也能转换,还有二叉排序树等等。
这章节实际使用价值很高。
74:搜索二维矩阵
问题描述
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。要求使用二分查找。
该矩阵具有如下特性:
每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
输出: true
示例 2:
输入:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 13
输出: false
解题思路
题目说:每行的第一个整数大于前一行的最后一个整数。
这就给了我们解题思路,使用二分法,可以将二维的数组有一维的下标来表示,只需要进行处理即可。
- row=index/column
- column=index%column
代码实现
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m=matrix.size();
if(m==0)
{
return false;
}
int n=matrix[0].size();
int begin=0,end=n*m-1,mid,i,j;
while(begin<=end)
{
mid=(begin+end)/2;
i=mid/n;
j=mid%n;
if(matrix[i][j]==target)
{
return true;
}
else if(matrix[i][j]>target)
{
end=mid-1;
}else
{
begin=mid+1;
}
}
return false;
}
反思与收获
二维数组可以转换成一维数组下标来表示。
#81:搜索旋转排序数组Ⅱ
问题描述
假设我们的数组是一个按照升序排序的数组在预先未知的某个点上进行了旋转得到的。
( 例如,数组 [2,5,6,0,0,1,2] ,可以知道是从[0,0,1,2,2,5,6] 旋转变来的 )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。要求使用二分查找。
示例 1:
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
示例 2:
输入: nums = [2,5,6,0,0,1,2], target = 3
输出: false
进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
解题思路
首先将搜索旋转排序数组,不考虑重复元素的情况。
- 2 5 6 0 1 mid之前的元素有序
- 5 6 0 1 2 mid之后的元素有序
只有这两种情况,这时候再考虑target在哪
如果在前面有序部分,这时有两个条件begin<=target<mid,之所以begin也要比较原因是如果找的是元素1呢
后面有序,也是一样的道理 mid<target<=end
再考虑有重复元素的情况
其实还是先找有序的部分,只是在判断的时候可能出现mid==begin的情况
6 6 6 1 2
3 3 3 4 1
这时就没有办法判断哪里是有序的
解决方法是:
直接让begin往前一位,无视掉这一重复元素,这并不影响是否存在的答案,在继续循环
其实还有一种相对稍微复杂的办法,就是不断去判断target,begin mid之间的关系,用If else去找存在的区间,但反倒是复杂了,先找到有序区间,再判断target是否在这里面更简单。
代码实现
bool search(vector<int>& nums, int target) {
int begin=0,end=nums.size()-1,mid;
if(nums.empty()||nums.size()==0)
{
return false;
}
while(begin<=end)
{
mid=begin+(end-begin)/2;
if(target==nums[mid])
{
return true;
}
//ⅠⅡ之间的区别
if(nums[begin]==nums[mid])
{
begin++;
continue;
}
//前半部分有序
if(nums[begin]<nums[mid])
{
if(nums[mid]>target&&nums[begin]<=target)
{
end=mid-1;
}
else
{
begin=mid+1;
}
}else
{
if(nums[mid]<target&&target<=nums[end])
{
begin=mid+1;
}else
{
end=mid-1;
}
}
}
return false;
}
};
反思与收获
虽然旋转听上去会难,但把可能存在的情况分析出来,就能解决问题,分情况讨论在哪里就能实现,去找target与mid分析后,开始分析mid与begin之间的三种关系。写了很多if else之后可以想想如何简化,这一次直接寻找有序区间,可以节省很多判断
98:验证二叉搜索树
问题描述
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2 / \ 1 3输出: true
示例 2:
输入:
5 / \ 1 4 / \ 3 6输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
解题思路
直接递归实现
首先当前为空,则为真
左子树是否存在,存在的话是不是比该节点小
右子树是否存在,存在的话是不是比该节点大
以及左右子树是否为二叉搜索树
这样想就错了…
比如10 5 15 null null 6 20
因为二叉搜索树的意义更深的在于,不仅仅是左右节点与根节点的比较,而是左边的所有节点都比根节点小,右边所有的节点都比根节点大。
所以我们需要记录最大最小值,然后不断更新
递归左子树时,将最大值该为根节点,递归右子树时将最小值改为根节点
代码实现
bool recursion(TreeNode* root,long long left,long long right)
{
if(!root)
{
return true;
}
if(root->val<=left||root->val>=right)
{
return false;
}
return recursion(root->left,left,root->val)&&recursion(root->right,root->val,right);
}
bool isValidBST(TreeNode* root) {
return recursion(root,LONG_MIN,LONG_MAX);
}
反思与收获
二叉搜索树,是左边所有的节点都小于根节点小于右边所有的节点,实际上可以看成是大小区间,中序遍历的时候只能是一个上升序列。
153:寻找旋转排序数组中的最小值
问题描述
假设我们的数组是一个按照升序排序的数组在预先未知的某个点上进行了旋转得到的。
( 例如,数组 [3,5,6,0,1,2] ,可以知道是从[0,1,2,3,5,6] 旋转变来的 )。
请找出其中最小的元素。要求使用二分查找。
你可以假设数组中不存在重复元素。
示例 1:
输入: [3,4,5,1,2]
输出: 1
示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0
解题思路
之前做了一道也是旋转区间的寻找目标值的题,有了一定的基础,这次找到旋转点就可以,有几个特殊的情况,可以直接考虑,节省时间。
只有一个元素时,直接返回该元素
最后一个元素大于第一个元素时,说明实际上没有旋转,直接返回第一个元素。
比较中间值 与前后的关系
如果mid大于mid+1,则说明mid+1就是最小的元素,如 4 5 6 7 1 2 3
如果mid小于mid-1,则说明mid是最小的元素,如5 6 7 1 2 3 4
否则就比较mid和第一个元素的值,选择左或者右区间
代码实现
int findMin(vector<int>& nums) {
int begin=0,end=nums.size()-1,mid;
//int ans=INT_MAX;
if(nums.size()==1)
{
return nums[0];
}
if(nums[end]>nums[0])
{
return nums[0];
}
while(end>=begin)
{
mid=begin +(end-begin)/2;
//相当于找到旋转的点
if(nums[mid]>nums[mid+1])
{
return nums[mid+1];
}
if(nums[mid]<nums[mid-1])
{
return nums[mid];
}
//说明最小的还是在右边 4 5 6 7 1 2 3
if(nums[mid]>nums[0])
{
begin=mid+1;
}else
{
//则在左边
end=mid-1;
}
}
return -1;
}
反思与收获
这又是一道旋转区间的题,考虑特殊情况,mid与其左右元素之间的大小比较,有点巧妙,提高了效率。
154:寻找旋转排序数组中的最小值Ⅱ
问题描述
假设我们的数组是一个按照升序排序的数组在预先未知的某个点上进行了旋转得到的。
( 例如,数组 [3,5,6,0,1,2] ,可以知道是从[0,1,2,3,5,6] 旋转变来的 )。
请找出其中最小的元素。要求使用二分查找。
注意数组中可能存在重复的元素。
示例 1:
输入: [1,3,5]
输出: 1
示例 2:
输入: [2,2,2,0,1]
输出: 0
说明:
这道题是 寻找旋转排序数组中的最小值 的延伸题目。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
解题思路
又是上一题的升级版,有了重复元素的影响,根据之前两道题目的学习,我们肯定可以考虑无视掉重复元素,begin++来实现,但这一次我们尝试用end–来实现。
以及由于有重复元素,上一题考虑mid有左右元素就没有了意义。
- mid<end
3 4 5 1 【2】 2 2 2 3
说明右边最小的元素是mid 忽略右边
- mid>end
2 3 4 [5] 1 2 2 忽略左边
- mid==end
2 2 【2】 1 2
2 1 【2】 2 2
那就不知道了,但是end跟mid的值是一样的,因此end可以往前走一步
代码实现
int findMin(vector<int>& nums) {
int begin=0,end=nums.size()-1,mid;
while(begin<end)
{
mid=begin+(end-begin)/2;
if(nums[mid]<nums[end])
{
end=mid;
}else if(nums[mid]>nums[end])
{
begin=mid+1;
}else
{
end--;
}
}
//这样写更简洁 也考虑只有一个元素等等
return nums[begin];
}
反思与收获
旋转区间有重复元素,可以使用begin++或者end–的方法来实现,跳过重复的元素。
240:搜索二维矩阵Ⅱ
问题描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。要求使用二分查找。
该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
说明:以上所说的升序,由于中间存在重复元素,因此严格来说,“升序”应该理解成“非递减”
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
解题思路
跟Ⅰ不同的是,不再是每行最后一个元素小于下一行的第一个元素了,因此不能简单的将其拉直来实现。
当然能想到的是,找到符合情况的列或行,再进行二分来搜索。
但我看题解是发现一个很巧妙发方法,参考地址
从右往左思考,从右上角开始,
往左是递减,往下是递增,合在一起就变成单调了,不过这不是二分…
代码实现
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.size()==0||matrix[0].size()==0)
{
return false;
}
int r=matrix.size();
int c=matrix[0].size();
int i=0,j=c-1;
while(i<r&&j>=0)
{
if(target<matrix[i][j])
{
j--;
}else if(target>matrix[i][j])
{
i++;
}else{
return true;
}
}
return false;
}
反思与收获
对于每行递增每列递增的矩阵,可以学习从右上角开始思考。
363:矩阵区域不超过K的最大数组和
问题描述
给定一个非空二维矩阵 matrix 和一个整数 k,找到这个矩阵内部不大于 k 的最大矩形和。
示例:
输入: matrix = [[1,0,1],[0,-2,3]], k = 2
输出: 2
解释: 矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
说明:
矩阵内的矩形区域面积必须大于 0。
如果行数远大于列数,你将如何解答呢?
解题思路
参考题解我看的是方法三,在暴力解决的基础是使用二分查找实现优化。
暴力解决的办法就是算出每一个矩阵的和,然后判断,事实上也好实现,要注意的是先固定列,再每行每行带来计算,原因是题目提示说行数远远大于列数。
这个思想很像是前缀和,此时每一行的总和已经算好了,一点点往下把下一行加入不断扩大矩阵,同时更新总和并记录,那每一个矩阵都可以看成是大的-小的要满足<=K,转换后变为小>=K-大的,用lower_bound可以找到第一个满足要求的。
代码实现
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
int row=matrix.size();
if(row==0)
{
return 0;
}
int c=matrix[0].size();
int ans=INT_MIN;
//先固定列
for(int left=0;left<c;left++)
{
vector<int> r_sum(row,0);
for(int right=left;right<c;right++)
{
//更新每行总和
for(int i=0;i<row;i++)
{
r_sum[i]+=matrix[i][right];
}
set<int> helper;
helper.insert(0);
//有点像前缀和 从上往下将每一行相加,扩大矩阵,记录每一个矩阵的总和数值
int pre_row_sum=0;
for(int i=0;i<row;i++)
{
pre_row_sum+=r_sum[i];
//大-小<=K,可以看成
//第一个大于等于 小>=大-k
auto p=helper.lower_bound(pre_row_sum-k);
helper.insert(pre_row_sum);
//如果没找到
if(p==helper.end())
{
continue;
}
//如果找到了,进行更新
else
{
int temp=pre_row_sum-(*p);
if(temp>ans)
{
ans=temp;
}
}
}
//找到等于k的直接退出
if(ans==k)
{
return k;
}
}
}
return ans;
}
反思与收获
前缀和的思想也可以用在二维,向列或者向行不断扩大矩阵的范围,更新数值。
找第一个大于等于/找第一个大于,常常要想到lower_bound和upper_bound
436:寻找右区间
问题描述
给定一组区间(包含起始点和终点),对于每一个区间 i,检查是否存在一个区间 j,它的起始点大于或等于区间 i 的终点,这可以称为 j 在 i 的“右侧”。
对于任何区间,你需要存储的满足条件的区间 j 的最小索引,这意味着区间 j 有最小的起始点可以使其成为“右侧”区间。如果区间 j 不存在,则将区间 i 存储为 -1。最后,你需要输出一个值为存储的区间值的数组。
注意:
你可以假设区间的终点总是大于它的起始点。
你可以假定这些区间都不具有相同的起始点。
示例 1:
输入: [ [1,2] ]
输出: [-1]
解释:集合中只有一个区间,所以输出-1。
示例 2:
输入: [ [3,4], [2,3], [1,2] ]
输出: [-1, 0, 1]
解释:对于[3,4],没有满足条件的“右侧”区间。
对于[2,3],区间[3,4]具有最小的“右”起点;
对于[1,2],区间[2,3]具有最小的“右”起点。
示例 3:
输入: [ [1,4], [2,3], [3,4] ]
输出: [-1, 2, -1]
解释:对于区间[1,4]和[3,4],没有满足条件的“右侧”区间。
对于[2,3],区间[3,4]有最小的“右”起点。
解题思路
判断标准是:区间j的起始点是否在区间i终止点的右侧可以重叠。
显然我们需要将左边节点进行排序,才能方便二分查找,但会破坏顺序,因此需要哈希表来记录一开始的下标,并且题目也提示了可以假定每个区间起始位置不同。
首先哈希记录下标,关键字为左端点
根据左端点大小进行排序
对于每个区间的右端点开始进行二分查找,另写一个函数实现,这函数设置了右端点为target,最后返回的是left节点,无论target是否存在,它返回的都是大于或等于target的第一个值。
代码实现
int binary(vector<int> nums,int target)
{
int len=nums.size();
if(nums[len-1]<target)
{
return -1;
}
int left=0,right=len-1;
while(left<right)
{
int mid=(left+right)/2;
if(nums[mid]<target)
{
left=mid+1;
}else
{
right=mid;
}
}
return left;
}
vector<int> findRightInterval(vector<vector<int>>& intervals) {
int len=intervals.size();
unordered_map<int,int> hashmap;
vector<int> left(len);
for(int i=0;i<len;i++)
{
hashmap[intervals[i][0]] = i;
left[i]=intervals[i][0];
}
sort(left.begin(),left.end());
vector<int> ans(len,-1);
for(int i=0;i<len;i++)
{
int index=binary(left,intervals[i][1]);
if(index!=-1)
{
ans[i]=hashmap[intervals[index][0]];
}
}
return ans;
}
反思与收获
可以利用二分查找的方法找到大于等于某元素的第一个值,只需要进行一点改变,循环终止条件为left<right,最后返回left即可。
1038:从二叉搜索树到更大和树
问题描述
给出二叉 搜索 树的根节点,该二叉树的节点值各不相同,修改二叉树,使每个节点 node 的新值等于原树中大于或等于 node.val 的所有节点的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。
节点的右子树仅包含键 大于 节点键的节点。
左右子树也必须是二叉搜索树。
示例:

输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,33,8]
解题思路
将该节点更新为大于等于它的值的总和,结合二叉搜索树的特点,大于就是在右子树,相当于将该节点更新为,其右子树节点的总和+其自己的值。
前序遍历成一维序列的含义就很类似于前缀和,只不过是从后往前看。
因此先递归调用右子树,然后对当前节点进行更新,再调用左子树
代码实现
int sum=0;
TreeNode* bstToGst(TreeNode* root) {
if(root)
{
bstToGst(root->right);
sum+=root->val;
root->val=sum;
bstToGst(root->left);
}
return root;
}
反思与收获
结合搜索二叉搜索树的特点来解题,灵活运用中序遍历的原理。
1373:二叉搜索子树的键值和
问题描述
给你一棵以 root 为根的 二叉树 (注意:不一定是二叉搜索树),请你返回任意二叉搜索子树的最大键值和。
二叉搜索树的定义如下:
任意节点的左子树中的键值都 小于 此节点的键值。
任意节点的右子树中的键值都 大于 此节点的键值。
任意节点的左子树和右子树都是二叉搜索树。
示例 1:

输入:root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]
输出:20
解释:因为以1为根的二叉树不是二叉搜索树,所以键值为 3 的子树是和最大的二叉搜索树。
示例 2:

输入:root = [4,3,null,1,2]
输出:2
解释:因为以3或4为根的二叉树不是二叉搜索树,所以键值为 2 的单节点子树是和最大的二叉搜索树。
示例 3:
输入:root = [-4,-2,-5]
输出:0
解释:所有节点键值都为负数,和最大的二叉搜索树为空
。
示例 4:
输入:root = [2,1,3]
输出:6
示例 5:
输入:root = [5,4,8,3,null,6,3]
输出:7
说明:
每棵树最多有 20000 个节点。
每个节点的键值在 [-10^4 , 10^4] 之间。
解题思路
该题复杂在,你需要一边更新数值,一边判断这棵树是不是二叉搜索树。
判断二叉搜索树,之前提到了,设置左右边界值,进行更新判断。
将函数返回值设置为int类型的vector
我们需要四个参数,左右大小边界值即最小最大值,总和,还有是否为二叉树的判断。
代码实现
int ans=0;
vector<int> dfs(TreeNode *p)
{
//不存在,虽然最大值和最小值无影响,但还是返回为intmax和Intmin
if(!p)
{
return {true,INT_MAX,INT_MIN,0};
}
vector<int> left=dfs(p->left);
vector<int> right=dfs(p->right);
int sum=0,minnum,maxnum;
//如果左右不是二叉搜索树,或者当前小于左子树最大值,当前大于右子树最小值,则为false
if(!left[0]||!right[0]||p->val>=right[1]||p->val<=left[2])
{
return {false,0,0,0};
}
//更新最小值,最大值
minnum=p->left? left[1]:p->val;
maxnum=p->right? right[2]:p->val;
sum+=(p->val+left[3]+right[3]);
ans=max(ans,sum);
return {true,minnum,maxnum,sum};
}
int maxSumBST(TreeNode* root) {
if(!root)
{
return 0;
}
dfs(root);
return ans;
}
反思与收获
将函数多个参数需求设计为函数返回值,并用vector来存储重要的信息。
——————————————————————————————
二分查找和二叉搜索树真的很重要,二分查找会遇到一些变形:比如矩阵简单的变一维,负责从右上角开始考虑,旋转数组,寻找目标值或者是寻找第一个比目标值大的值。
二叉搜索树主要是判断,我理解成为区间比较,以及其特点,前序遍历是单调递增的序列。
(〃´皿`)q
248

被折叠的 条评论
为什么被折叠?



