(二分) 逼近类二分搜索

前言

前置知识,需要先掌握最基本的二分查找704. 二分查找

本文讲解题目为:278. 第一个错误的版本


本文讲解的是逼近类的二分(当然这是我自己取的名字),什么意思?

例如我们有一串数字1,2,3,3,3,3,3,4;若要求找到第一个大于2的数值的位置

显然第三个数是我们需要的数,但如何通过二分搜索到,这就是本文需要研究的。


纯二分搜索其实不难,但其中的各种细节实在让人抓狂


一般如何辨别一道题能用二分,我总结了一些规律

  • 首先拥有连续的序列
  • 能够通过暴力 O ( n ) O(n) O(n)的方法是搜索出答案
  • 该序列具有二段性

模板题

力扣:278. 第一个错误的版本

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        
    }
};

其他练习题:

34. 在排序数组中查找元素的第一个和最后一个位置

理解题意

有很多人不懂这个题意,这里简单抽象说一下

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

现有一个给定api isBadVersion函数,返回布尔值

且随着给定的数值递增,返回值将由 false ---> true


因此出现序列false false false false true true true true true true true

要求我们找到第一个返回true的位置,这就要求我们如何用二分去逼近这个位置


样例中每个测试样例是两个数,但实际借口只接受一个数

n:表示序列长度 [1, n]

bad:表示第几个是第一true,就是我们需要搜索的值,但这个数是隐藏起来的,类似于猜数游戏一样让你猜出来

解题思想

下面,我将以三种写法来理解性的解决该题


对于本题的一个核心搜索思想是:

设当前搜索idx位置,答案是ans位置

  • 若**idx为 true,**则ans必在 包括idx的左侧
  • 若**idx为false,**则ans必在 不包括idx的右侧

因此我们只考虑一段,另一端不断缩进


先说一个规律

  • 若循环判断无等号, 则只有一边需要±1
  • 若循环判断有等号,则两边都需要±1

循环判断无等号

若mid是true,则有可能mid就是第一个true,因此不能偏移。

因为这个位置可能就是我们要找的第一个true,因此直接让right接替mid的位置


false的话,必然需要偏移才能到达true的状态,至少是该位置的下一个位置,才可能是true


退出循环的条件是:当且仅当 left == right

因此我们return 任意即可

class Solution {
public:
    int firstBadVersion(int n) {
        int left = 1;
        int right = n;

        while (left < right) {
            int mid = (right-left)/2 + left;
            if (isBadVersion(mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        // 退出循环时必然是 left == right
        return left; // return right;一样
    }
};

循环判断有等号

还是上面的原理,若mid为false

闭区间[left, mid]之间全为false,不合格,left = mid + 1;


其余什么都不用想

让另一边right = mid - 1即可(因为循环中是有=的,不然就是出现死循环)


这种写法与最普通的二分查找完全一样,但是难点就在于如何return

退出循环的时候,要么是left == right+1,要么是left+1 == right,因此不能随意return

我们从偏移的原理入手

left 向右偏移一个单位,极端情况下,会让false发生质的变化,变为true 可行

right 向左偏移一个单位,极端情况下,会让true发生质的变化,变为false 不可行

所以说,我们这里只能return left;

class Solution {
public:
    int firstBadVersion(int n) {
        int left = 1;
        int right = n;

        while (left <= right) {
            int mid = (right-left)/2 + left;
            if (!isBadVersion(mid)) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return left;
    }
};

综合写法

回顾一下上面两种方法的优缺点

  • 无等号
    • 缺点:要明确哪一个需要偏移±1
    • 优点:随意return left或者right,不用顾虑
  • 有等号
    • 缺点:要明确return 哪一个
    • 优点:循环中两边都要偏移,不用顾虑

我们来取其精华,取其糟粕一下

循环中若要都±1则必须循环判断中有等号,这个无法改变

而关于return方面,不想纠结return哪一个,我们就引入第三个变量

  1. 我们将这第三个变量始终接受我们需要的值

  2. 且在循环中,我们不断的缩小区间,而这区间就是答案所在区间,退出循环前的那一轮,会夹击成区间长度为1的空间

我们在保证上面两个条件的情况下,必然可以得到我们想要的答案。


大家觉得这种写法是否有点无脑且耍赖的感觉呢 ///(^v^)\\\

class Solution {
public:
    int firstBadVersion(int n) {
        int left = 1;
        int right = n;
        
        int ans = -1;
        while (left <= right) {
            int mid = (right-left)/2 + left;
            if (isBadVersion(mid)) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        return ans;
    }
};



END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值