当前位置: 首页 > news >正文

关于二分查找的简单思考

关于二分查找的简单思考

二分查找是排列组合?总是写出死循环?笔者给出自认为的正确的二分查找思考方式。

标准 Binary-Search 的两种形式

参考 C++ <algorithm> 库,我们发现有两种二分查找的常用算法[1]

  • lower_bound(ranges::lower_bound): 寻找到范围内第一个不小于给定值的值。
  • upper_bound(ranges::upper_bound): 寻找到范围内第一个大于给定值的值。

这样我们就明确了我们的目标:返回一个区间,这个区间有且仅有一个数,满足我们所给定的上述条件。我们将我们的区间定义为 \([l, r]\), 那么目标就是返回其中的一个指针,我们一般将会返回 \(l\)

寻找循环不变量

接下来,对于我们的区间,我们需要寻找一个称为循环不变量的东西。这个东西告诉我们我们在进行二分查找时,所选定的范围应该满足什么条件,这个条件将会贯穿我们整个二分查找的所有环节,直到我们的区间为空(无法继续二分查找)。

我们可以利用逆向思维,在区间为空的情况下,从结果导出我们的循环不变量。

  • 针对lower_bound: 我们的left最终需要指向第一个不小于目标值的值。区间为空的时候,right == left - 1。因此我们需要
    nums[l-1] < target && nums[r+1] >= target.
  • 针对upper_bound: 我们的left最终需要指向第一个大于目标值的值。区间为空的时候,right == left - 1。因此我们需要:
    nums[l-1] <= target && nums[r+1] > target.

这样就是我们在更新区间时所必须要遵守的规则。在区间更新后,所有的新区间[l',r']都应该对应满足以上的条件。

更新区间

对于区间的描述,我们将具有四种区间的描述方法:
[l, r], [l, r), (l, r], (l, r)。四种描述方法都可以作为我们进行二分查找的方式。下面我们具体进行分析。[2]

lower_bound 区间更新

在寻找到中间量mid后,我们将会使用 nums[mid]target 进行比较。很明显,比较结果具有以下三种情况。我们再次重申我们的循环不变量:

nums[l-1] < target && nums[r+1] >= target

区间修改方式:lower_bound

  • nums[mid] < target. 这时我们需要有:
    l' = mid + 1, r' = r。这样保证了我们
    nums[l'-1] = nums[mid] < target, nums[r + 1] >= target,
    虽然r+1可能已经越界,但是单调非递减数组的情况下这是成立的。
  • nums[mid] == target. 这时我们需要有:
    l' = l, r' = mid - 1。这样保证了我们
    nums[l'-1] = nums[mid] < target, nums[r + 1] >= target,
    虽然l-1可能已经越界,但是单调非递减数组的情况下这是成立的。
  • nums[mid] > target. 这时我们需要有:
    l' = l, r' = mid - 1。这样保证了我们
    nums[l'-1] = nums[mid] < target, nums[r + 1] >= target,
    虽然l-1可能已经越界,但是单调非递减数组的情况下这是成立的。

区间终点: left > right

我们的区间终点就是空集。因此我们需要在left > right的情况下终止。此时根据循环不变量,我们的nums[l-1] < target && nums[r+1] >= target。在上述情况下,如果target大于所有的数,最终l将会在指向nums.length()时终止(r不会越界)。在target小于所有数的时候,最终r会指向-1(l不会越界)。因此结果是符合预期的。

这样我们就有:

int left = 0, right = nums.size() - 1;
while (left <= right) {int mid = (left + right) / 2;if (nums[mid] < target) {left = mid + 1;} else {right = mid - 1;}
}
return left;

那么针对其他的区间,我们就可以分析:

  • [l, r),则实际上是 [l, r - 1]。我们就有:
    l = 0, r = nums.size() -> 起始条件。
    left <= right - 1 -> while 条件
    int mid = (left + right - 1) / 2 -> 中点。
    l = mid + 1, r - 1 = mid - 1 -> l不变, r = mid.
    返回l.
  • (l, r],则实际上是 [l + 1, r]。我们有:
    l = -1, r = nums.size() - 1 -> 起始条件。
    left + 1 <= right -> while 条件
    int mid = (left + 1 + right) / 2 -> 中点。
    l + 1 = mid + 1, r = mid - 1 -> r不变, l = mid.
    返回l + 1.
  • (l, r),则实际上是 [l+1, r-1]我们留给读者作为练习 我最讨厌这样的作者了,还是认真写吧。
    l = -1, r = nums.size() -> 起始条件。
    left + 1 <= right - 1 -> while 条件
    int mid = (left + 1 + right - 1) / 2 -> 中点。
    l + 1 = mid + 1, r - 1 = mid - 1 -> r = midl = mid.

这样我们直接给出代码。

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

这样我们的lower_bound就可以在四种范式下写出。再也不用担心写出死循环了! 我们发现,我们只需要记住:循环不变量+根据循环不变量变换区间的方法即可。

Upper_bound 区间更新

在寻找到中间量mid后,我们将会使用 nums[mid]target 进行比较。很明显,比较结果具有以下三种情况。我们再次重申我们的循环不变量:

nums[l-1] <= target && nums[r+1] > target

我们就只写出[l,r]的所有情况,剩下的情况按照上述方式推导即可。

  • nums[mid] < target. 这时我们需要有:
    l' = mid + 1, r' = r。这样保证了我们
    nums[l'-1] = nums[mid] <= target, nums[r + 1] > target,
    虽然r+1可能已经越界,但是单调非递减数组的情况下这是成立的。
  • nums[mid] == target. 这时我们需要有:
    l' = mid + 1, r' = r。这样保证了我们
    nums[l'-1] = nums[mid] <= target, nums[r + 1] > target,
    虽然r+1可能已经越界,但是单调非递减数组的情况下这是成立的。
  • nums[mid] > target. 这时我们需要有:
    l' = l, r' = mid - 1。这样保证了我们
    nums[l'-1] = nums[mid] <= target, nums[r + 1] > target,
    虽然l-1可能已经越界,但是单调非递减数组的情况下这是成立的。

这样我们有:

int left = 0, right = nums.size() - 1;
while (left <= right) {int mid = (left + right) / 2;if (nums[mid] <= target) {left = mid + 1;} else {right = mid - 1;}
}
return left;

非标准二分查找

非标准的二分查找采用“区间判别法”。
例如,二分查找可以泛化成:寻找第一个/最后一个符合条件的。我们记为 check(i)

那么,我们有如下的示例:

img

img

如果后者我们继续沿用左闭右开区间呢?稍微改一下循环不变量就好了。

img


  1. cpp_reference, Binary search operations (on partitioned ranges) ↩︎

  2. 灵茶山艾府的题解 ↩︎

http://www.jsqmd.com/news/649717/

相关文章:

  • Flowable流程定义存MySQL还是MongoDB?我选混合存储的5个实战理由
  • 数学建模国赛C题避坑指南:模拟退火与NSGA-II算法选型、调参与结果对比分析
  • 深聊酒店布草推荐厂家,哪家口碑好、价格合理值得关注 - mypinpai
  • Qt国际化实战:从零构建一个支持动态语言切换的桌面应用
  • 广告敏感词过滤-敏感词-文本审核-敏感词过滤-敏感词检测 - Jumdata
  • Prism对话框实战:从注册到封装的完整指南
  • Windows Defender彻底移除工具:专业解决方案与完整操作指南
  • 告别群晖音乐无歌词时代:打造你的私人卡拉OK音乐站
  • 别再只用@Scheduled了!Quartz-Scheduler的JobDataMap和并发控制,让你的定时任务更强大
  • 2026年新疆新能源汽车漆面防护与轻改升级深度横评:隐形车衣、底盘护板、电动踏板选购避坑指南 - 精选优质企业推荐榜
  • 这个“漂亮老男人”的社交法则,你掌握了吗?——BGP邻居关系深度解析
  • 酒店布草四件套厂家盘点,靠谱供应商哪家比较靠谱 - 工业设备
  • 终极指南:八大网盘直链下载助手的完整使用教程
  • 总结美妆培训选购要点,彩妆培训哪家口碑好有妙招 - 工业品牌热点
  • SpringWeb项目中越权漏洞的实战检测与防御策略
  • Spring AI 1.0.0实战:用MCP协议5分钟给你的大模型装上“手和脚”
  • 如何用DownKyi在10分钟内构建个人B站学习资料库?
  • 告别示波器!用Python+Arduino低成本模拟AK协议轮速传感器(附代码)
  • 全球合规外汇交易平台哪家好 技术维度排行实测与解析 - 速递信息
  • AWS NAT Gateway 费用优化实战 — S3 Gateway Endpoint 路由缺失导致月损万元
  • Tesseract OCR 字库优化实战:从数据准备到模型部署
  • LaTeX写论文:遇到网页、报告、学位论文这些‘非标准’文献,BibTeX该怎么写?(避坑指南)
  • 2026年全国定制儿童箱包厂家排名,靠谱的定制学生箱包厂家推荐 - 工业品网
  • Spring Boot项目里,如何优雅地打开H2数据库的Web控制台(附安全配置建议)
  • 2026年SD-WAN核心阵营标杆品牌深度分析 - 博客万
  • 5G网络卡顿的元凶?深入浅出聊聊CSI-RS配置不当对手机速率的影响与排查思路
  • 深聊电池电眼设计厂家怎么选,哪家性价比高 - 工业推荐榜
  • 2026年靠谱的化妆培训公司推荐,师资口碑双优的专业机构选择指南 - 工业品网
  • 小红书数据采集终极指南:3步快速获取海量公开数据
  • AutoDL新手避坑指南:从零到一完成YOLOv5模型训练(附高效工具链)