二分算法研究

二分算法研究

此文主旨是彻底搞清楚二分算法。

在处理查找问题时,如果数据结构是数组,则可考虑二分查找

下面将用两个模板来讲解二分查找,理解这两个模板之后,遇到问题只需稍加变化即可。

模板一

int search(vector<int>& nums, int target)
{
    int left = 0;
    int right = nums.size() - 1;      // 1. 注意这里
    int mid = 0;
    
    while (left <= right)             // 2. 注意这里
    {
        mid = left + (right - left) / 2;    // 防止mid溢出
         if (nums[mid] == target)
         {
             return mid;
         } else if (nums[mid] < target) 
         {
             left = mid + 1;        // 3. 注意这里
         } else if (nums[mid] > target)   // 好习惯
         {
             right = mid - 1;       // 4.注意这里
         }
    }
    
    return -1;
}

建议刷题:leetcode 704。

此种模板是确保每一次循环查找都是在左闭右闭的区间,这种可保证最后退出时,整个区间为空。

1号注意点是进行初始的区间设置,如果数组中一个5个数,则初始的区间为[0, 4]。

2号注意点是设定程序退出条件,即当left = right + 1时才会退出,此时区间为[5, 4]整个区间为空的,即整个要查找的区间未有遗漏的数。

3和4注意点是当未找到对应的数时,对区间进行划分。当未找到时,区间便被mid划分为两个区间,即[left, mid - 1], [mid + 1, right]。所以下一次查找时,leftright均要加一。

一般在写二分查找时,有一个好习惯,便是用else if代替else。因为困难的二分问题,将会有很多边界判断条件,使用else if更容易看边界条件,容易找出错误。想挑战较为困难的同学可以尝试一下这道题leetcode 33。

模板二

int search(vector<int>& nums, int target)
{
    int left = 0;
    int right = nums.size();    // 1. 注意这里
    int mid = 0;
    
    while (left < right)        // 2. 注意这里
    {
        mid = left + (right - left) / 2;
        if (nums[mid] == target)
        {
            return mid;
        } else if (nums[mid] < target) 
        {
            left = mid + 1;
        } else if (nums[mid] > target)
        {
            right = mid;    // 3. 注意这里
        }
    }
    
    return left;
}

建议刷题:leetcode 35

此模板主要解决当数组中没有要查找的数时返回它应该插入的地方

1号注意点用来设定初始的查找区间,最初的区间即为[0, nums.size()),若数组有5个数,则为[0, 5)

2号注意点用来改变退出条件,当left == right时,程序退出。而这样会导致漏掉一个数,例如当left = 3时,整个区间为[3, 3),在索引3的位置这个数便未被判断,而程序就会退出,所以此时只需要打一个补丁即可:return nums[left] == target ? left : -1;

3号注意点是进行区间划分,当未找到是,划分的两个此间为:[left, mid)[mid + 1, right)。所以left需要等于mid + 1。而right只需要等于mid

此模板最大的作用是数组中没有该元素,可以返回需要插入的位置,即第一个比该数大的数的索引。此模板中,右边界指向的数是一定比目标数字大的(不论数组中是否存在这个数),因为要想右边界移动,中间的值一定要比目标值大。所以当循环结束时,left == right此时均一定指向第一个比目标值大的位置。

那么上一个模板可不可以返回插入的位置呢?也是可以的,由于right = mid - 1,如果mid的位置刚好是要插入的位置,则right就会容易跑到要插入的位置的前一个。所以只需要最后返回right + 1即可。或者由于循环结束条件为left = righ + 1,所以直接返回left也可。

例题一

leetcode 34

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

你的算法时间复杂度必须是 O(log n) 级别。

如果数组中不存在目标值,返回[-1, -1]

示例:

输入:nums = [5, 7, 7, 8, 8, 10], target = 8
输出:[3, 4]

此题可以在找到target之后,往前往后一个一个找,但这样就不能完全发挥二分查找的威力,并且时间复杂度最坏情况下也成了O(n)

目标的左边界,可以理解为向数组中,插入一个比目标小一点点的数,这样插入这个数的位置就是左边界。所以当nums[mid]等于目标值的时候,就相当于找的值大了一点点,所以让右边界移动

目标的右边界,可以理解为向数组中,插入一个比目标大一点点的数,这样插入这个数的位置就是右边界的后一个。所以当nums[mid]等于目标值的时候,就相当于找的值小了一点点,所以让左边界移动

全部代码就是这样:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        // 存放最后结果
        vector<int> ans(2);
        // 找左边界
        ans[0] = searchLeftRight(nums, target, true);
        // 找有边界
        ans[1] = searchLeftRight(nums, target, false) - 1; // 1. 注意

        // 左边大说明没找到
        if (ans[0] > ans[1])    // 2. 注意
        {
            ans[0] = ans[1] = -1;
        } 

        return ans;
    } 

    // 边界就相当于返回插入的位置,所以用左开右闭
    int searchLeftRight(vector<int>& nums, int target, bool isleft)
    {
        int left = 0;
        int right = nums.size();
        int mid = 0;

        while (left < right)
        {
            mid = left + (right - left) / 2;
            if (nums[mid] == target && isleft)    // 3. 注意
            {
                right = mid;
            } else if (nums[mid] == target && !isleft){   // 4. 注意
                left = mid + 1;
            } else if (nums[mid] < target) {
                left = mid + 1;

            } else if (nums[mid] > target) {
                right = mid;
            }
        }

        return left;    // 5. 注意
    }

};

1号注意点,因为右边边界的位置是插入一个比这个大的数的位置的前一个,所以要减1。

2号注意点,当数组中,没有目标值的时候,此时函数返回的左边界和右边界应该相等,而由于右边界减一了,所以如果最后左边界大于右边界,则说明数组中无目标值。

3,4号注意点,当找左边界时,当nums[mid]等于目标值时,相当于nums[mid]比要找的数大了一点点,所以应该是right移动。查找右边界同理,应该是left移动。

5号注意点,因为采用的是模板二,所以最后返回leftright等价,如果使用模板一也完全可以,最后返回leftright + 1


参考文章:

[ 我作了首诗,保你闭着眼睛也能写对二分查找](我作了首诗,保你闭着眼睛也能写对二分查找 (qq.com))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值