【代码随想录】二分查找算法总结篇

news2024/12/24 10:19:18

目录

    • 前言
    • 二分查找
      • 例题一
      • 例题二
      • 例题三
      • 例题四


前言

本篇文章记录了代码随想录二分查找算法的总结笔记,下面我们一起来学习吧!!

二分查找

关于二分查找算法,我在之前的这篇博客里面做了非常多的分析,但是后面做题做着发现二分又不会了,还是感觉自己对二分的边界条件不敏感或者说是没完成理解透彻,那么接下来我会通过对例题的逐步分析让大家不再对二分感到困惑!!

在代码随想录中关于二分查找提供了两种写法,这俩种写法其实就是我们解题的关键,下面我们就来逐个分析两种写法的不同与优势!!

第一种写法(左闭右闭):

// 版本一
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1; 
            } else if (nums[middle] < target) {
                left = middle + 1; 
            } else { 
                return middle;
            }
        }
        // 未找到目标值
        return -1;
    }
};

Q:第一种写法的区间是左闭右闭,所以我们的循环条件为while (left <= right)?

当left == right是有意义的,为什么有意义呢?因为我们的设定的区间范围内的元素都是有可能为目标值的,假设我们要查找的target在最后一个位置,那么left一直向右缩小区间,最终left一定 == right,此时mid == left == right,找到了直接返回mid即可。

第二种写法(左闭右开):

// 版本二
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; 
            } else if (nums[middle] < target) {
                left = middle + 1; 
            } else { 
                return middle;
            }
        }
        // 未找到目标值
        return -1;
    }
};

Q:该写法的区间为[left,right)左闭右开,循环条件为while(left < right)?

循环结束条件为left == right,因为此时的left == right是没有意义的,[left, right) == [left, left) == [left, left - 1]显然是没任何意义的。

Q:注意写法二与写法一right的区别,第一种写法为right = mid - 1,第二种写法为right = mid;为何??

其实本质上它们是一样的,因为写法二的right是右开区间它是取不到的,当right == mid,[left, right) == [left, mid) == [left, mid - 1]!!


上述对于俩种写法我们还并不知道它们的优缺点在哪?适用于何种场景?因为上述的二分查找场景是最简单的,下面我们通过例题来进行分析吧。

例题一

搜索插入位置

在这里插入图片描述

给出写法一的代码:

// 左闭右闭
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] < target) {
                l = mid + 1;
            } else if (nums[mid] > target) {
                r = mid - 1;
            } else {
                return mid;
            }
        }

        return r + 1;  // 返回l也是可行的
    }
};

相较于之前的普通二分查找这里就只是返回值改变了,之前的场景是找不到就返回-1,而现在是如果找不到还要返回正确的插入位置。那么对于写法一到底该返回left还是right呢?这里为何最终返回的是right + 1?left与right的位置关系如何呢?下面我们来验证一下:

在这里插入图片描述


另外这里也可以将nums[mid] >= target合并为一步,找到第一个大于等于target元素的位置,注意条件一定不能是nums[mid] <= target, 这样找到的位置是最后一个小于等于target元素的位置,也即第一个大于target元素的位置!!

// 版本二
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] >= target) {
                r = mid - 1;
            } else {
                l = mid + 1;
            } 
        }

        return l;
    }
};

为什么这种合并的写法也可以呢?其实合并的写法就包括了直接在数组中匹配到target对应的元素直接返回的情况,分析如下:

在这里插入图片描述

那么返回 l 或者 r + 1 的话都是符合直接匹配到相应的元素直接返回的,另外还包括了不匹配的情况。

写法二的代码(左闭右开):

// 版本一
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size();
        while (l < r) {
            int mid = l +(r - l) / 2;
            if (nums[mid] < target) {
                l = mid + 1;
            } else if (nums[mid] > target) {
                r = mid;
            } else {
                return mid;
            }
        }

        return l;  // r也可
    }
};
// 版本二
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int l = 0, r = nums.size();
        while (l < r) {
            int mid = l +(r - l) / 2;
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            } 
        }

        return l;  // r也可
    }
};

注意写法二返回left或right都可以,假设直接匹配到不直接返回的话也就是按照写法二的版本二,因为最终left一定 ==right 才能结束循环,所以返回left和right都可以。

从这里就可以看出写法二的优势:相较于写法一left != right需要考虑返回的位置,而写法二返回left与right都可以!后续当然我个人也比较推荐写法二哈哈,当然了其实理解透彻了这两种写法其实就是看哪种方便用哪种了,没必要去纠结这个问题。

例题二

我们接着来看下一道题:34. 在排序数组中查找元素的第一个和最后一个位置

在这里插入图片描述

思路:要解决这道题,首先我们得找到第一个大于等于该元素的位置,假设该位置的值不等于target那么就没必要找下去了,直接返回{-1,-1},因为第一个位置的元素都不相等那么后面肯定是没有的,并且如果该位置的索引为数组的个数的话(也就是要找的元素大于数组中所有的元素),此时也直接返回{-1, -1};如果该位置的元素等于target的话,那么很简单我们就继续向后查找最后一个相等元素的位置即可。

代码如下:

// 代码一:
class Solution {
public:
    int lower_bound(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] >= target) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        return l;  // r + 1
    }

    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0) return {-1, -1};
        
        int start = lower_bound(nums, target);
        if (start == nums.size() || nums[start] != target)  return {-1, -1};
        int end = lower_bound(nums, target + 1) - 1;  // 找到第一个大于等于target+1的元素, 那么它减-1其实就为最后一个等于target的位置

        return {start, end};
    }
};
// 代码二:
class Solution {
public:
    int lower_bound(vector<int>& nums, int target) {
        int l = 0, r = nums.size();
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;  
    }

    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0) return {-1, -1};

        int start = lower_bound(nums, target);
        if (start == nums.size() || nums[start] != target)  return {-1, -1};
        int end = lower_bound(nums, target + 1) - 1;  // 找到第一个大于等于target+1的元素, 那么它减-1其实就为最后一个等于target的位置

        return {start, end};
    }
};
// 代码三:
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0)
            return {-1, -1};

        int l = 0, r = nums.size();
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        if (r == nums.size() || nums[r] != target)  return {-1, -1};
        int pos = r + 1;
        while (pos < nums.size() && nums[pos] == target) {
            pos++;
        }

        return {r, pos - 1};
    }
};
// 代码四: 
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int l = -1, r = nums.size();
        while (l + 1 != r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid;
            }
        }

        if (r == nums.size() || nums[r] != target) return {-1, -1};
        int pos = r + 1;
        while (pos < nums.size() && nums[pos] == target) {
            pos++;
        }

        return {r, pos - 1};
    }
};
// 代码五: STL大法
// lower_bound找到第一个大于等于target的元素, 并返回它的位置
// upper_bound找到第一个大于target的元素, 第一个大于target的位置-1,
// 即为最后一个小于等于target的位置, 因为前面找到了第一个大于等于target的位置, 所以这里一定能找到最后一个等于target的位置
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0)   return {-1, -1};

        int l = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
        if (l == nums.size() || nums[l] != target)  return {-1, -1};

        int r = upper_bound(nums.begin(), nums.end(), target) - nums.begin();
        return {l, r - 1};
    }
};

例题三

下面我们来看这道题:69. x 的平方根

在这里插入图片描述

思路:这道题其实可以从搜索插入位置那道题得到很大的启发,结果返回x的平方根是向下取整的。这里同样的有两种情况,第一情况就是mid * mid刚好与x匹配此时直接返回即可,第二种情况就是找不到刚好匹配的就只能取最后一个小于x的那个位置,即第一个大于等于x的位置-1,所以根据上述一系列结论,我们从搜索插入位置那道题得到启发直接就返回right的位置即可(使用左闭右闭方法)!!

// 方法一: 左闭右闭
class Solution {
public:
    int mySqrt(int x) {
        // 特判, 防止出现除0错误
        if (x <= 1) return x;
		
		// 这里的右区间还能进行优化, 因为x的平方根它必定是小于等于x/2的。
		//所以我们可以将右区间缩小至x / 2, 但是x == 2会出现除零错误, 此时我们要向上取整处理一下, 在外面或者在取mid时都可以
        int l = 0, r = x;   // r = x / 2 + 1也可
        while (l <= r) {
            int mid = l + (r - l) / 2;  
            if (mid > x / mid) {
                r = mid - 1;
            } else if (mid < x / mid) {
                l = mid + 1;
            } else {
                return mid;
            }
        }

        return r;  // l - 1都可
    }
};
// 优化版:
class Solution {
public:
    int mySqrt(int x) {
        // 特判, 防止出现除0错误
        if (x <= 1) return x;

        int l = 0, r = x / 2; 
        while (l <= r) {
            int mid = l + (r - l + 1) / 2;
            if (mid > x / mid) {
                r = mid - 1;
            } else if (mid < x / mid) {
                l = mid + 1;
            } else {
                return mid;
            }
        }

        return r;
    }
};

// 第二种写法: 左闭右开
class Solution {
public:
    int mySqrt(int x) {
        // 特判, 防止出现除0错误
        if (x <= 1) return x;

        int l = 0, r = x + 1;  // 开区间
        while (l < r) {
            int mid = l + (r - l) / 2;  
            if (mid > x / mid) {
                r = mid;
            } else if (mid < x / mid) {
                l = mid + 1;
            } else {
                return mid;
            }
        }

        return l - 1;  // 实际上l与r都是第一个大于等于target的元素,  因此最后一个小于x元素的位置就为 l - 1 or r - 1!!
    }
};

第一种写法较第二种写法的优点:第一种写法的left与right分别代表第一个大于等于target元素的位置、最后一个小于target元素的位置,它能明确代表俩个位置,但缺点就是返回时要考虑清楚返回left还是right;第二种写法的优点就是可以随意返回left与right的位置,但是它们都只能代表第一个大于等于target元素这一个位置,但是我们清楚了它们之间的关系之后,其实第二种写法是更不容易失误的嘿嘿!!

例题四

最后我们来看一道题:367. 有效的完全平方数

这道题乍一看不就是完全平方数嘛,只是返回true还是false的问题,当x的平方根刚好匹配时返回true,不匹配直接就返回false,可以说比上道题简单了不少,也确实是这样,但是我们不能照搬上面的代码:

// 下面这样是错误的
class Solution {
public:
    bool isPerfectSquare(int num) {
        int l = 0, r = num / 2;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (mid > num / mid) {
                r = mid - 1;
            } else if (mid < num / mid) {
                l = mid + 1;
            } else {
                return true;
            }
        }

        return false;
    }
};

为何上述代码是错的呢?首先我们的思路肯定没问题,那么就一定是代码方面出现了问题,我们注意到在上一道题中我们的mid是跟num/mid进行比较的,这样是为了防止整数溢出,那么对于这道题能这么干吗?假设x == 5,此时mid = 2,mid == num / mid,返回true,但实际上5的平方根不为2啊返回false才对,为什么这里出现了错误?原因是num / mid是向下取整的,所以我们不能这么干,那就只有老老实实的判断mid * mid与num的大小了,并且我们不能用int来保存了,应该用long或者long long来保存才不会使得整数溢出,代码如下:

class Solution {
public:
    bool isPerfectSquare(int num) {
        long long l = 1, r = num;
        while (l <= r) {
            long long mid = l + (r - l) / 2;
            if (mid * mid > num) {   // 不能用除法  会向下取整的
                r = mid - 1;
            } else if (mid * mid < num) {
                l = mid + 1;
            } else {
                return true;
            }
        }

        return false;
    }
};
// 左闭右开
class Solution {
public:
    bool isPerfectSquare(int num) {
        long long l = 1;
        long long r = (long long)num + 1;  
        while (l < r) {
            long long mid = l + (r - l) / 2;
            if (mid * mid > num) {   // 不能用除法  会向下取整的
                r = mid;
            } else if (mid * mid < num) {
                l = mid + 1;
            } else {
                return true;
            }
        }

        return false;
    }
};
// 代码三:
class Solution {
public:
    int mySqrt(int x) {
        if(x == 1 || x == 0)
            return x;
        long l = -1, r = x;
        while(l + 1 != r)
        {
            long mid = (l + r) / 2;
            if(mid * mid <= x)
                l = mid;
            else
                r = mid; 
        }
        return l;
    }
};

注意上述代码三是最为巧妙的一种方式,可以解决取整问题以及边界问题,大家还是可以看我之前的这篇博客去了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1692789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode | 5-21| 每日一题

2769. 找出最大的可达成数字 考点: 暴力 数学式子计算 思维 题解 通过式子推导: 第一想法是二分确定区间在区间内进行查找是否符合条件的, 本题最关键的便是 条件确定 , 第二种方法: 一般是通过数学公式推导的,这种题目我称为数学式编程题 代码 条件判断式 class Solution { …

长文处理更高效:一键章节拆分,批量操作轻松搞定,飞速提升工作效率!

在当今信息爆炸的时代&#xff0c;我们时常需要处理大量的文本内容。无论是阅读长篇小说、整理专业资料还是编辑大型文档&#xff0c;TXT文本文件的普遍性不言而喻。然而&#xff0c;当TXT文本内容过于庞大时&#xff0c;阅读、编辑和管理都变得异常繁琐。此时&#xff0c;一款…

齐护K210系列教程(三十一)_视觉小车

视觉小车 齐护编程小车端程序动作说明联系我们 在经常做小车任务项目时会用的K210的视觉与巡线或其它动作结合&#xff0c;这就关系到要将K210的识别结果传送给小车的主控制器&#xff0c;K210为辅助传感器&#xff08;视觉采集&#xff09;。 这节课我们用K210识别图像&#x…

多微信如何高效管理?一台电脑就能搞定!

对于有多个微信号的人来说&#xff0c;管理这些微信无疑是一道难题。 今天&#xff0c;就给大家分享一个能够让你高效管理多个微信号的神器——个微管理系统&#xff0c;下面&#xff0c;就一起来看看它都有哪些功能吧&#xff01; 1、多号同时登录在线 系统支持多个微信号同…

AI助力农田作物智能化激光除草,基于轻量级YOLOv8n开发构建农田作物场景下常见20种杂草检测识别分析系统

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术在各个领域的应用愈发广泛&#xff0c;其中农业领域也不例外。近年来&#xff0c;AI助力农田作物场景下智能激光除草的技术成为了农业领域的一大亮点&#xff0c;它代表着农业智能化、自动化的新趋势。智…

泪目!网络连接中断的原因,终于找到了!

朋友们&#xff0c;出大事了&#xff01; 不知道多少朋友玩过 DNF 这个游戏&#xff0c;这个我从小学玩到大学的 “破” 游戏&#xff0c;昨天竟然出手游了&#xff01; 我都忘了自己曾几何时预约过这个手游通知&#xff0c;昨天给我发了条通知信息说游戏已开服。 老玩家直接…

网络实时安全:构筑数字时代的铜墙铁壁

什么是网络实时安全&#xff1f; 网络实时安全&#xff0c;简而言之&#xff0c;是一种能够在威胁发生的瞬间即刻识别、响应并有效抵御的安全机制。它强调的是速度与效率&#xff0c;确保网络环境能够持续处于安全状态。这背后&#xff0c;离不开高科技的支撑——扩展检测系统…

分类预测 | Matlab实现ZOA-SVM斑马算法优化支持向量机的多变量输入数据分类预测

分类预测 | Matlab实现ZOA-SVM斑马算法优化支持向量机的多变量输入数据分类预测 目录 分类预测 | Matlab实现ZOA-SVM斑马算法优化支持向量机的多变量输入数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现ZOA-SVM斑马算法优化支持向量机的多变量输…

STM32+CubeMX移植SPI协议驱动W25Q16FLash存储器

STM32CubeMX移植SPI协议驱动W25Q16FLash存储器 SPI简介拓扑结构时钟相位&#xff08;CPHA&#xff09;和时钟极性&#xff08; CPOL&#xff09; W25Q16简介什么是Flash&#xff0c;有什么特点&#xff1f;W25Q16内部块、扇区、页的划分引脚定义通讯方式控制指令原理图 CubeMX配…

使用vue3实现右侧瀑布流滑动时左侧菜单的固定与取消固定

实现效果 实现方法 下面展示的为关键代码&#xff0c;想要查看完整流程及代码可参考https://blog.csdn.net/weixin_43312391/article/details/139197550 isMenuBarFixed为控制左侧菜单是否固定的参数 // 监听滚动事件 const handleScroll () > {const scrollTopThreshol…

mac M3芯片 goland 2022.1 断点调试失败(frames are not available)问题,亲测有效

遇到如上问题&#xff0c;解法 步骤1&#xff1a;下载dlv文件 执行 go install github.com/go-delve/delve/cmd/dlvlatest 然后在 $GOPATH/bin里发现多了一个dlv文件 (找不到gopath? 执行 go env 可以看到) 步骤2&#xff1a;配置dlv 将这个dlv文件移到 /Applications/G…

redis中String,Hash类型用法与场景使用

String 用法 1. 设置键值对 &#xff08;1&#xff09;设置键值对使用 set 命令设置 key 的值。 返回值&#xff1a;ok&#xff0c;如果 key 已经存在&#xff0c;set 命令会覆盖旧值。 &#xff08;2&#xff09;使用 setex 命令设置 key 的值并为其设置过期时间&#xff…

MCS-51伪指令

上篇我们讲了汇编指令格式&#xff0c;寻址方式和指令系统分类&#xff0c;这篇我们讲一下单片机伪指令。 伪指令是汇编程序中用于指示汇编程序如何对源程序进行汇编的指令。伪指令不同于指令&#xff0c;在汇编时并不翻译成机器代码&#xff0c;只是会汇编过程进行相应的控制…

『Stable Diffusion 』AI绘画,不会写提示词怎么办?

提示词 有没有想过&#xff0c;为什么你用 SD 生成的猫是长这样的。 而其他人可以生成这样的猫。 虽然生成的都是猫&#xff0c;但猫与猫之间还是有差距的。 如果你的提示词只是“cat”&#xff0c;那大概率就会出现本文第一张图的那个效果。而如果你加上一些形容词&#xff…

【ArcGIS微课1000例】0111:谷歌地球Google Earth下载安装与使用教程

一、谷歌地球安装 双击安装包&#xff0c;默认点击完成即可。 二、谷歌地球使用 打开快捷方式&#xff0c;开始使用谷歌地球。欢迎界面&#xff1a; 软件主界面&#xff1a; 三、谷歌地球下载 软件安装包位于《ArcGIS微课实验1000例(附数据)专栏配套完数据包中的0111.rar中…

小红书云原生 Kafka 技术剖析:分层存储与弹性伸缩

面对 Kafka 规模快速增长带来的成本、效率和稳定性挑战时&#xff0c;小红书大数据存储团队采取云原生架构实践&#xff1a;通过引入冷热数据分层存储、容器化技术以及自研的负载均衡服务「Balance Control」&#xff0c;成功实现了集群存储成本的显著降低、分钟级的集群弹性迁…

STM32硬件接口I2C应用(基于BH1750)

目录 概述 1 STM32Cube控制配置I2C 1.1 I2C参数配置 1.2 使用STM32Cube产生工程 2 HAL库函数介绍 2.1 初始化函数 2.2 写数据函数 2.3 读数据函数 3 光照传感器BH1750 3.1 认识BH1750 3.2 BH1750寄存器 3.3 采集数据流程 4 BH1750驱动实现 4.1 接口函数实现 4.2…

又有人叫嚣:AI取代前端,来给你几张图,看能不能憋死AI。

总有自媒体人&#xff0c;为了些许流量&#xff0c;在大放厥词&#xff0c;说截个图给AI&#xff0c;AI就能输出前端代码&#xff0c;这是啥都敢说&#xff0c;吹牛不上税。 我来给你几张贝格前端工场日常接的大数据项目相关的图&#xff0c;你让AI生成代码&#xff0c;取代前…

WordPress安装插件失败No working transports found

1. 背景&#xff08;Situation&#xff09; WordPress 社区有非常多的主题和插件&#xff0c;大部分人用 WordPress 都是为了这些免费好用的主题和插件。但是今天安装完 WordPress 后安装插件时出现了错误提示&#xff1a;“ 安装失败&#xff1a;下载失败。 No working trans…

WPF之打印与预览

目录 1&#xff0c;打印设置与管理。 1.1&#xff0c;引入程序集&#xff1a; 1.2&#xff0c;主要管理类介绍&#xff1a; 1.3&#xff0c;应用&#xff1a; 1.4&#xff0c;效果。 1.5&#xff0c;Demo链接。 2&#xff0c;打印。 2.1&#xff0c;主要参与打印的类与…