初学者练习代码准备机试(五)二分查找解决寻找峰值、寻找旋转排序数组中的最小值、搜索旋转排序数组

       各位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还是有点困惑,期待有小伙伴可以在评论区和我交流!谢谢大家的支持~我们明天见!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值