草庐IT

二分查找模板总结

ellisonzhao 2024-01-11 原文

二分查找模板总结(区间、条件不再纠结)

二分查找是一种在 有序数组 中查找某一特定元素的搜索算法。元素集合有顺序,元素性质有分界点,二分法就可以用来求分界点,并不一定要求集合中元素是不重复的。

算法思路:假设目标值在闭区间 [left, right] 中, 每次将区间长度缩小一半,当 left = right 时,我们就找到了目标值。

常见问题:

  1. 查找区间是该选择左开右闭区间 [left, right) 还是 左闭右闭区间 [left, right]
  2. 循环终止条件是 left < right 还是 left <= right
  3. 比较函数该怎么选择

常规写法

二分查找需要注意 查找区间终止条件,稍不留神可能出现死循环。常见的写法如下:

int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1; // 定义 target 在左闭右闭的区间里,[left, right]
  while (left <= right) { // 当 left==right,区间 [left, right] 依然有效,所以用 <=
    int mid = left + (right - left) / 2); // 防止溢出,结果等同于(left + right)/2
    if (nums[mid] > target) {
      right = mid - 1; // target 在左区间,所以更新为 [left, mid - 1]
    } else if (nums[mid] < target) {
      left = mid + 1; // target 在右区间,所以更新为 [mid + 1, right]
    } else {
      // nums[mid] == target
      return mid; // 数组中找到目标值,直接返回下标
    }
  }
  // 未找到目标值
  return -1;
}

上述写法中区间 [left, right] 的更新操作是 right = mid - 1; left = mid + 1;

边界条件需要注意:二分区间直到长度为 1,即 left == right 时,循环条件依然满足,再进行一次比较,所以用 left <= right

例题:

704. 二分查找

对上述写法稍作改造,总结二分模板共有两个:

模板一

当区间 [left, right] 的更新操作是 left = mid + 1; right = mid; 时,二分区间计算 mid 不需要加 1

通用模板写法:

int binarySearch(int left, int right) {
  while (left < right) {
    int mid = (left + right) / 2; // 注意防止溢出
    if (check(mid)) // 判断 mid 是否满足查找条件
      left = mid + 1; // 结果落在 [mid+1, right] 区间
    else
      right = mid; // 结果落在 [left, mid] 区间
  }
  return left;
}

上述例题【704. 二分查找 】套用 模板一 的写法:

int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    // 防止溢出,结果等同于 (left + right)/2
    int mid = left + ((right - left) / 2);
    // 检查 mid,比较 nums[mid] 和 target
    if (nums[mid] < target)
      // 结果落在 [mid+1, right] 区间
      left = mid + 1;
    else
      // 结果落在 [left, mid] 区间
      right = mid;
  }
  return nums[left] == target ? left : -1;
}

模板二

当区间 [left, right] 的更新操作是 left = mid; right = mid - 1; 时,计算 mid 需要加 1

通用模板写法:

int binarySearch(int left, int right) {
  while (left < right) {
    // 注意防止溢出
    int mid = (left + right + 1) / 2;
    if (check(mid))
      // 结果落在 [left, mid-1]
      right = mid - 1;
    else
      // 结果落在 [mid, right]
      left = mid;
  }
  return left;
}

上述例题【704. 二分查找 】套用 模板二 的写法:

int binarySearch(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    // 防止溢出,结果等同于 (left + right + 1)/2
    int mid = left + ((right - left + 1) / 2);
    // 检查 mid,比较 nums[mid] 和 target
    if (nums[mid] > target)
      // target 落在 [left, mid-1]
      right = mid - 1;
    else
      // target 落在 [mid, right]
      left = mid;
  }
  return nums[left] == target ? left : -1;
}

求同存异

使用这两个模板的优势

  1. 不用考虑循环结束时返回 left 还是 right,因为最后退出时 left == right
  2. 不用考虑循环条件是用 < 还是 <=,直接都用 <
  3. 非常适用于求一些分界点的问题

共同点

这两个模板写法的 共同点

  1. 查找区间都是 左闭右闭 区间:[left, right]
  2. 循环判断条件都是用 left < right
  3. 循环退出条件都是 left == right。查找过程一定会在 O ( l o g n ) O(logn) O(logn) 的时间内终止,即使中间遇到 nums[mid] == target 也不会结束,而是继续收缩区间,直到区间长度为 1
  4. 在循环终止(查找结束)之后,判断一下 left 或者 right 是否符合要求即可,如果不符合要求,说明答案不存在

不同点

共同点比较好理解,但更值得注意的是,这两个模板写法的不同点

  1. 更新 mid 的计算方式不同
  2. check(mid) 不同,所以更新 leftright 也会有一些差别

Q & A

1. 为什么模板二计算 mid 会加 1

原因:避免进入死循环

首先,可以明确的是,两个模板在查找过程中都是区间长度不断减半,直到区间长度为 1,即 left == right 时退出循环,然后在循环外面检查最后的查找结果 nums[left] 是否为 target

如果 模板二 更新 mid 采用 mid = (left + right)/2,在最后一次查找时会陷入死循环。例如,对于 nums = [3,4]target 4

  • 第一次查找,left = 0,right = 1,mid = 0,比较得到 nums[mid] < target,更新 left = mid
  • 第二次查找,还是进入相同的条件分支,陷入死循环

而更新 mid 采用 mid = (left + right + 1) / 2 时:

  • 第一次检查 left = 0, right = 1, mid = 1,更新 left = mid退出循环

也可以通过边界条件来理解,这两个模板最后都是要收缩到区间 [left, left+1] 里进行最后一次 check

  • 对于模板一,计算得到 mid = left,区间往左收缩是进入 right = mid ,区间往右收缩是进入 left = mid + 1,结束循环,所以更新 mid 要用 mid = (left + right)/2
  • 对于模板二,计算得到 mid = right,区间往左收缩是进入right = mid - 1 ,区间往右收缩是进入 left = mid,结束循环,所以更新 mid 要用 mid = (left + right + 1)/2

2. 如何选择使用模板

一般写二分的思考顺序是这样的:通过题目背景 确定 check(mid) 的逻辑,判断答案落在左半区间还是右半区间

  • target 属于右半区间,则右半区间是 [mid+1, right],左半区间是 [left, mid],区间更新方式是 left = mid + 1; right = mid;,此时用第一个模板;
  • target 属于左半区间,则左半区间是 [left, mid-1],右半区间是 [mid, right],区间更新方式是 right = mid - 1; left = mid;,此时用第二个模板;

这种区间划分方式将 check(mid) == targrt 分支的逻辑合并到 check(mid) > targrtcheck(mid) < targrt 分支,不断将区间长度减半,具有更好的适用性,尤其适用于求一些分界点的问题。

例题:

解释:

  1. 查找元素 target 的第一个位置,相当于查找 大于等于 target 的第一个元素位置:
  • 如果 nums[mid] < target,此时 target 应该落在 右半区间 [mid+1, right]

  • 如果 nums[mid] >= target,此时 target 应该落在 左半区间 [left, mid],因为 mid 可能就是 target,所以还需要进一步比较;

    因而选择 check(mid)nums[mid] < target,区间更新条件分别是 left = mid + 1; right = mid;

int searchFirst(vector<int> &nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
      int mid = (left + right) >> 1;
      if (nums[mid] < target)
        left = mid + 1;
      else
        right = mid;
    }
    return nums[left] == target ? left : -1;
  }
  1. 查找元素 target 的最后一个位置,相当于查找 小于等于 target 的最后一个元素位置:
  • 如果 nums[mid] > target,此时 target 应该落在 左半区间 [left, mid-1]
  • 如果 nums[mid] <= target,此时 target 应该落在 右半区间 [mid, right],因为 mid 可能就是 target,所以还需要进一步比较

因而选择 check(mid)nums[mid] > target,区间更新条件分别是 right = mid-1; left = mid;

int searchLast(vector<int> &nums, int target) {
  int left = 0, right = nums.size() - 1;
  while (left < right) {
    int mid = (left + right + 1) / 2;
    if (nums[mid] <= target)
      left = mid;
    else
      right = mid - 1;
  }
  return nums[left] == target ? left : -1;
}
  1. 计算 x 的算术平方根,结果只保留整数部分:由于 x 平方根的整数部分是满足 k*k <= x 最大 k 值
  • 如果 mid * mid > x ,那么结果应该落在 左半区间 [left, mid-1]
  • 如果 mid * mid <= x ,那么结果应该落在 右半区间 [mid, right]

因而选择 check(mid)mid*mid > x,区间更新条件分别是 right = mid-1; left = mid;

int mySqrt(int x) {
  int left = 0, right = x;
  while (left < right) {
    // 防止值溢出
    int mid = (right + left + 1LL) / 2;
    if (mid > x / mid)
      // 结果在左半区间 [left, mid-1]
      right = mid - 1;
    else
      // 结果在右半区间 [mid, right]
      left = mid;
  }
  return left;
}

解决问题

题目题解难度
4.寻找两个正序数组的中位数LeetCode 题解困难
33. 搜索旋转排序数组LeetCode 题解中等
34. 在排序数组中查找元素的第一个和最后一个位置LeetCode 题解中等
35. 搜索插入位置LeetCode 题解简单
69. x 的平方根 LeetCode 题解简单
74. 搜索二维矩阵LeetCode 题解中等
81. 搜索旋转排序数组 IILeetCode 题解中等
153. 寻找旋转排序数组中的最小值LeetCode 题解中等
154. 寻找旋转排序数组中的最小值 IILeetCode 题解困难
162. 寻找峰值LeetCode 题解中等
240. 搜索二维矩阵 IILeetCode 题解中等
367. 有效的完全平方数LeetCode 题解简单

有关二分查找模板总结的更多相关文章

  1. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  2. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  3. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  4. ruby-on-rails - Mandrill API 模板 - 2

    我正在使用Mandrill的RubyAPIGem并使用以下简单的测试模板:testastic按照Heroku指南中的示例,我有以下Ruby代码:require'mandrill'm=Mandrill::API.newrendered=m.templates.render'test-template',[{:header=>'someheadertext',:main_section=>'Themaincontentblock',:footer=>'asdf'}]mail(:to=>"JaysonLane",:subject=>"TestEmail")do|format|format.h

  5. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  6. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

  7. ruby-on-rails - 在 Rails 中更高效地查找或创建多条记录 - 2

    我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr

  8. ruby - 查找重叠的正则表达式匹配项 - 2

    我想找到给定字符串中的所有匹配项,包括重叠匹配项。我怎样才能实现它?#Example"a-b-c-d".???(/\w-\w/)#=>["a-b","b-c","c-d"]expected#Solutionwithoutoverlappedresults"a-b-c-d".scan(/\w-\w/)#=>["a-b","c-d"],but"b-c"ismissing 最佳答案 在积极的前瞻中使用捕获:"a-b-c-d".scan(/(?=(\w-\w))/).flatten#=>["a-b","b-c","c-d"]参见Rubyde

  9. ruby - 在 Ruby 中查找多个正则表达式匹配的模式和位置 - 2

    这应该是一个简单的问题,但我找不到任何相关信息。给定一个Ruby中的正则表达式,对于每个匹配项,我需要检索匹配的模式$1、$2,但我还需要匹配位置。我知道=~运算符为我提供了第一个匹配项的位置,而string.scan(/regex/)为我提供了所有匹配模式。如果可能,我需要在同一步骤中获得两个结果。 最佳答案 MatchDatastring.scan(regex)do$1#Patternatfirstposition$2#Patternatsecondposition$~.offset(1)#Startingandendingpo

  10. ruby - 如何通过Middleman安装和使用Slim模板引擎 - 2

    一般来说,我是Middleman和ruby​​的新手。我已经安装了Ruby我已经安装了Middleman和gem以使其运行。我需要使用slim而不是默认的模板系统。所以我安装了Slimgem。Slim的网站只说我需要'slim'才能让它工作。中间人网站说我只需要在config.rb文件中添加模板引擎,但是没有给出例子...对于没有ruby​​背景的人来说,这没有帮助。我在git上找了几个config.rb,它们都有:require'slim'和#Setslim-langoutputstyleSlim::Engine.set_default_options:pretty=>true#Se

随机推荐