各位csdn的小伙伴大家好!我胡汉三又回来啦!(惊不惊喜、意不意外!)拖这么久才发布,实话说是因为二分问题的变形好难理解,不论是看视频还是做题都知难而退,这回,我要主动出击!!感兴趣的小伙伴就继续看下去吧!
寻找峰值
这道题的原题在这里162. 寻找峰值 - 力扣(LeetCode),这道题目中说峰值元素是指其值大于左右相邻元素的元素,注意,让我们返回其中一个峰值元素即可。那我们可以假设要找的就是其中一个,还是先设置两个指针left和right分别指向0和n-1,每次过程中我们应该比较nums[mid]和nums[mid+1]的值,如果nums[mid]<nums[mid+1],那么此刻的峰值应该在右半部分,因此我们让left=mid+1,由于题目中提示我们:
- 对于所有有效的
i
都有nums[i] != nums[i + 1]
因此,如果nums[mid]>nums[mid+1],说明峰值元素在左半部分,我们应该让right=mid。最后,当左右指针相遇的时候,找到峰值,循环停止,最后由于left和right指向同一个位置,所以return left 或者return right都是可以的。
为什么右指针更新成mid,左指针更新成mid+1? 为啥左右不对称嘞?
这块代码我改了好久,下面我介绍一下我的思路:比如数组[1,4,2,3,2,6,1],当mid指向3时,nums[mid]>nums[mid+1](即3>2),此时峰值元素可能就是3,但是也可能在3的左侧(也有可能在3的右侧,比如那个6,但是我们此时只找一个峰值,用二分的办法只能考虑一半,此处只不考虑6)。所以我们不能排除mid这个位置,将右指针right更新为mid,蓝色的部分表示目标峰值及右侧,这样下一轮搜索范围就缩小到了[left,mid]。
但是当nums[mid]<nums[mid+1]时,说明在mid位置不可能是峰值元素(因为它小于等于右侧元素),所以可以安全地将mid这个位置排除在后续的搜索范围之外。比如数组[1,2,3,4,5],当mid指向3时,nums[mid]<nums[mid+1](即3<4),3肯定不是峰值,此时峰值元素肯定在mid的右侧,红色的部分表示目标峰顶左侧,所以可以将左指针left更新为mid+1,下一轮搜索范围就缩小到了[mid+1,right]。
因此,灵神的讲解中提到了不论怎样变化数组最右侧的元素一定是蓝色的,我们要找的正是蓝红色分界线处,蓝色里面的第一个元素。
上述思路使用的是二分法闭区间,完整代码如下:
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int n=nums.size();
int left=0,right=n-2;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]>nums[mid+1])
right=mid;
else
left=mid+1;
}
return left;
}
};
此处我还想补充一下,灵神的讲解当中使用的是开区间方法,范围是(-1,n-1)转化成闭区间也就是[0,n-2],(灵神理由就是上述说过的已经知道了数组最右侧的元素必定是蓝色,所以跳过n-1,但是我认为这个理由不充分),我的代码中就是n-1开始,这就是我一直感觉到困惑的点,到现在也没有解决,我认为n-1和n-2本质是一样的,就是相当于在数组中间多加了一个元素?(可以这样理解吗?)但是灵神的代码我改成[n-1]还是弄不通,所以我就自己重新梳理了上述思路。我贴上灵神的代码如下,如果有小伙伴明白的话可以帮我解答一下疑惑吗?
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
if (nums[mid] > nums[mid + 1]) {
right = mid;
} else {
left = mid;
}
}
return right;
}
};
上面是灵神的代码,欢迎各位小伙伴在评论区和我交流,共同探讨这个问题!
寻找旋转排序数组中的最小值
原题在这里153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode),这个题告诉我们让我们找数组中的最小值,同样我们也使用二分方法,但是这回我们是用nums[mid]和数组最后一个元素进行比较,只有两种可能,第一,数组是两个递增段,比如[4,5,6,7,0,1,2],这时候比较7>2,说明最小元素肯定在右半段,并且根据上文解释,7一定不是最小元素,因此直接让left=mid+1,第二,数组整体是一个递增段,比如[0,1,2,4,5,6,7],这时候4<7,说明最小元素可能是4,也可能在4的左侧,因此right=mid,当left指针和right指针相遇,即指向同一个元素的时候循环停止,还是和上题一样,返回nums[left]和nums[right]都是可以的。(不过我竟然发现返回nums[left]花费时间是0ms,返回nums[right]返回时间是3ms,好神奇哦~)
本题完整代码如下:
class Solution {
public:
int findMin(vector<int>& nums) {
int n=nums.size();
int left=0,right=n-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]<nums[n-1])
right=mid;
else
left=mid+1;
}
return nums[right];
}
};
搜索旋转排序数组
原题在这里33. 搜索旋转排序数组 - 力扣(LeetCode),这个题还是告诉我们一个旋转数组,让我们找里面有没有target的元素,我们的思路可以先按照上一题思路,找到旋转数组的最小元素,也就是它的旋转点,然后根据旋转点把数组分成两部分有序子数组,然后用第4节课的二分红蓝染色法在两个子数组中分别查找有没有target。
还是按照上题思路,先找到数组中的最小值(旋转点),代码如下:
int n=nums.size();
int left=0,right=n-1;
while(left<right)//先找出旋转点
{
int mid=left+(right-left)/2;
if(nums[mid]>nums[n-1])
left=mid+1;
else
right=mid;
}
int p=left;//p是旋转点
然后根据旋转点,在两个有序子数组中进行二分查找。首先,别忘了让左右指针初始化一下,此时,我们假设逻辑上的数组已经是排好序的,那我们就可以把问题转化成我们上节课学习的二分红蓝染色法查找target,那现在怎么把逻辑上的排好序的数组和实际上的数组对应起来呢?其实通过第一步我们已经知道偏移量了,我们完全可以使用偏移量换算逻辑下标和实际下标,因此我们只需要每次在比较的时候让实际的数组和target比较,而左右指针是在逻辑上的数组中移动即可。让我们看一个实际的例子:
假设数组nums=[4,5,6,7,0,1,2],旋转点p=4(即最小元素0的索引)。假设当我们在二分查找过程中计算得到逻辑上mid=1时,则有:mid+p=1+4=5;(mid+p)%n=5%7=5,这说明逻辑上下标为1的元素,在原旋转数组中的实际索引是 5,对应的元素是1。也可以理解成逻辑数组就是排好序的[0,1,2,4,5,6,7]和实际数组nums下标的换算。
通过这种方式,我们可以在旋转排序数组中正确地使用二分查找来搜索目标元素。第二部分的代码如下:
left=0;right=n-1;//左右指针重新二分,不要忘记要初始化一下
while (left <= right) {
int mid = left + (right - left) / 2;
int realMid = (mid + p) % n; //用此刻的中间值映射到原来数组的值
if (nums[realMid] == target) {
return realMid;
} else if (nums[realMid] < target) {
left = mid + 1;
} else {
right = mid - 1;//又回到二分红蓝染色找特定值
}
}
return -1;
如果最后left和right交叉了,循环停止了,都没能找到这个元素,那么直接返回-1即可。本题完整的代码如下:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n=nums.size();
int left=0,right=n-1;
while(left<right)//先找出旋转点
{
int mid=left+(right-left)/2;
if(nums[mid]>nums[n-1])
left=mid+1;
else
right=mid;
}
int p=left;//p是旋转点
// 根据旋转点,在两个有序子数组中进行二分查找
left=0;right=n-1;//左右指针重新二分,不要忘记要初始化一下
while (left <= right) {
int mid = left + (right - left) / 2;
int realMid = (mid + p) % n; //用此刻的中间值映射到原来数组的值
if (nums[realMid] == target) {
return realMid;
} else if (nums[realMid] < target) {
left = mid + 1;
} else {
right = mid - 1;//又回到二分红蓝染色找特定值
}
}
return -1;
}
};
好了,亲爱的盆友们,今天的学习终于结束了,时隔这么多天才发出这个学习系列5,我真是感觉心慌慌的~都有点愧对我的26个小粉丝,我的内心be like:
不过,理想之路,绝非坦途,有3分钟的热度就一定会有3分钟的收获!最后还是想说第一题寻找峰值那里对n-1和n-2还是有点困惑,期待有小伙伴可以在评论区和我交流!谢谢大家的支持~我们明天见!