茴香豆的茴有四种写法,那二分有几种写法?

news/2024/10/19 23:29:14

《编程珠玑》一书的作者 Jon Bentley 曾经说过:“90%的程序员无法正确实现二分查找算法...”,今天,本文将带领你会写二分。

经典写法

现在我们来求解这样一个通用的二分查找问题:有一个不下降序列 $ a $,我们要从其中所有找到大于等于 $ k $ 的数的最小的下标。

bool check(int index) {return a[index] >= k;}while (l < r) {int m = (l + r) / 2; // 或 l + (r - l) / 2 防止溢出check(m) ? (r = m) : (l = m + 1);
} return l;

序列一定可以划分为两部分,左半部分全部小于 k, 右半部分全部大于等于 k,我们找到右半部分的第一个元素。

l 和 r 通常赋初值为 0 和 数组末下标。接下来,已知答案在 \([l, r]\) 内,首先选取一个靠中的位置 $ m $,若此处已经满足大于等于条件,则他位于右半边,答案应位于 m 左侧或 m 本身,所以我们修改新的可能区间为 \([l, m]\);反过来,若此处不满足大于等于条件,则他位于左半边,答案应位于 m 右侧(不可能包含其本身),所以我们修改新的可能区间为 \([m+1, r]\)

当 l 距离 r 仅 1 时,int m = (l + r) / 2将向零取整得到 l, 若其可行则 r 会被赋值 l,否则 l 会被赋值 r,无论情况,最终都会使得 l 和 r 相等,循环结束。此时 l (或 r)就是正确答案。

处理例外情况

需要特别说明的是,当 \(k\) 比序列中最大的数还大时,不存在符合条件的数,得到的结果无效,二分查找的结果需要进行验证。通常来说,如果下标 0 开,初始 l = 0, r = len,查找到的 \(l\) 就会超出序列的范围。因此,在使用二分查找后,可以检查 \(l\) 是否在有效范围内并且 \(a[l] \ge k\),以确保找到的答案有效,或者在二分查找前先将 $ k $ 和末数对比。

另一种经典写法

接下来,我们讨论另一种经典写法。假设问题变为:我们仍然有一个不下降序列 \(a\),但是这一次我们需要找到其中小于等于 \(k\) 的最大下标。

bool check(int index) {return a[index] <= k;}while (l < r) {int m = (l + r + 1) / 2; // l + (r - l + 1) / 2check(m) ? (r = m - 1) : (l = m);
} return l;

和上文类似,在每次循环中,如果位置 \(m\) 满足条件 \(a[m] \le k\),说明 \(m\) 是可行的,因此我们将 \(l\) 赋值为 \(m\),即答案可能出现在当前 \(m\) 位置或更右侧;如果 \(m\) 不满足条件,则说明 \(m\) 太大,因此将 \(r\) 赋值为 \(m - 1\),在下一次循环中继续查找左侧部分。

但是,在更新中间位置 \(m\) 时,使用 int m = (l + r + 1) / 2,即向上取整。当 l 距离 r 仅 1 时,m 将被赋为 r, 若其可行则 l 会被赋值 r,否则 r 会被赋值 l,循环可以正常结束。

两个问题的根本区别在于可行域所在的位置不一样,一个是可行域在左侧,求其右端;另一个则是可行域在右侧,求其左端。这导致了检查 m 后对 l 和 r 修改的不同,又进一步导致 m 不在正中央时的取整条件不同,确保循环在最后一步正确结束而不是无限循环。因此,两套方案一一对应,不可以混用。

下标取整问题

我们现在知道“若 l = m + 1,则取 m 应向下;若 r = m - 1,则取 m 时应向上”。不过 (l + r) / 2 在 C++ 中是向零取整的。这就意味着在 l 和 r 可取负数时,结果将不正确,m 会向上取造成死循环。解决方案之一是改用 l + (r - l) / 2,而对于写法二,或者其他取整规则不同的语言,也可自行推导。关键不是死记硬背形式,而是记住如何取整。

带记录答案的写法

int ans;
while (l <= r) {int m = (l + r) / 2;check(m) ? (ans = m, r = m - 1) : (l = m + 1);
} return ans;

无论 m 是否可行,新的区间里总是不取它。这样的话 m 向任何一方取整都可以。同时,无论如何取整都会出现,r - l 跳过 1 直接达到 0 的情况,因此 l = r 时还需要再循环一次,最终结束时 l > r,ans 是答案。

这种写法无视可行域的方向,但是需要一个额外的变量。

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

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

相关文章

session测试

jsp1 <%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>session测试</title> </…

704.二分查找

题目 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。示例 1:输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4 示例 2:输入: nums = [-…

win11微软拼音输入法变繁体字

0. 设置→时间和语言 1. 时间和语言→语言和区域2. 中文简体→语言选项3. 键盘→微软拼音→键盘选项4. 常规5. 选择字符集→简体中文

泰山学堂选拔游记

泰山学堂选拔游记 前言:由于相关保密协议,所有与选拔试题与详细细节有关的内容将被剔除。 Tips:由于神秘因素,我在中学阶段的各个平台部分文章与笔记已经进行了隐藏。 插曲:等通知大学的经典通知方式 通过笔试后,要加对应取向面试群了解消息,但各个取向过笔试预留加面试…

mongo基本命令(一)

一 前言 环境: win10 mongo6.0.1 记录一些基本的mongo查询命令 二 查询命令 1 进入命令行 进入mongo命令行,我这里是mongo是装在docker里面的 需要先在docker里面启动mongo容器 docker exec -it xxx bash 进入mongo容器,xxx为mongo容器名 mongosh 进入mongo命令行,我安装…

Java21虚拟线程:我的锁去哪儿了?

0 前言 最近的文章中,我们详细介绍了当我们迁移到 Java 21 并将代际 ZGC 作为默认垃圾收集器时,我们的工作负载是如何受益的。虚拟线程是我们在这次迁移中兴奋采用的另一个特性。 对虚拟线程新手,它们被描述为“轻量级线程,大大减少编写、维护和观察高吞吐量并发应用程序的…

ManualResetEventManualResetEventSlim

ManualResetEvent ManualResetEvent有三个重要的方法,分别为:waiteone(),set(),reset(),其含义如下: 1.WaitOne()即等待信号发出,即可往下运行。 2.set()发出信号,让线程方法继续往下运行,并允许其他线程(如有)一并往下运行。 3.reset()重新初始化(即:去掉票据)变为…