j7~头像
关注
【算法】专题三:二分查找到底怎么学?底层原理 + LeetCode 多道题目实战,搞定边界坑点封面图

【算法】专题三:二分查找到底怎么学?底层原理 + LeetCode 多道题目实战,搞定边界坑点

一.二分查找算法简介

1. 特点

二分查找细节繁多,边界条件处理不当极易出现死循环;但只要吃透边界逻辑,本质上是原理简单的算法。


2. 学习侧重点

  1. 算法原理(重点) 核心思想:利用数组有序特性,不断取区间中点,通过比较中点值和目标值,折半缩小查找范围,快速定位目标。
  2. 二分模板(重点) 不要死记硬背,理解区间定义(左闭右闭 / 左闭右开)之后再记忆模板。 一共 3 套常用模板:
    1. 朴素二分模板:查找是否存在目标值,简单,但有使用局限;
    2. 查找左边界二分模板:找到目标元素第一次出现的位置,边界细节多;
    3. 查找右边界二分模板:找到目标元素最后一次出现的位置,边界细节多。

补充小说明

  • 朴素二分:适合数组元素不重复,只需要判断元素是否存在的场景。
  • 左右边界二分:适合数组存在重复元素,需要查找目标的左右边界的场景,面试笔试高频。
  • 坑点:mid 的溢出问题、while循环条件、left/right 更新语句,这三处是死循环高发位置。

二.算法练习

1.二分查找

a.题目描述:


b.题⽬链接:

704. 二分查找 - 力扣(LeetCode)


c.算法流程:

朴素二分查找(左闭右闭区间)流程

  • a. 定义 left、right 指针,分别指向数组查找区间的左右边界。
  • b. 求取当前待查找区间的中间点 mid,对比 arr[mid] 与目标值 target,分为三种情况:
  1. i. arr[mid] == target:已找到目标元素,返回下标 mid;
  2. ii. arr[mid] > target:区间 [mid, right] 内所有值均大于 target,舍弃右半区间,更新区间为 [left, mid - 1],设置 right = mid - 1,重复步骤 b;
  3. iii. arr[mid] < target:区间 [left, mid] 内所有值均小于 target,舍弃左半区间,更新区间为 [mid + 1, right],设置 left = mid + 1,重复步骤 b;
  • c. 当 left > right(左右指针错开),代表全区间查找完毕,不存在目标元素,返回 -1。

d.代码实现:


2.在排序数组中查找元素的第⼀个和最后⼀个位置

a.题目描述:


b.题目链接:

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)


c.算法流程

整体思路是把这件事拆成两个独立的问题:找左端点一次二分,找右端点再二分一次。两次的循环条件相同,但划分依据、中点取整和收缩方式都不一样。

d.代码实现:

⼆分查找算法模板总结:

二分查找最难的不是原理,而是边界的细节。真正常用的其实就只有下面两个模板:

// 模板 A:找左边界
while (left < right)
{
    int mid = left + (right - left) / 2;
    if (......) left = mid + 1;
    else        right = mid;
}
// 模板 B:找右边界
while (left < right)
{
    int mid = left + (right - left + 1) / 2;
    if (......) left = mid;
    else        right = mid - 1;
}

两个模板的循环条件都是 left < right,这一点很关键:它意味着二分结束时 left 和 right 一定相等,那个位置就是候选答案。所以循环里不需要判断 left == right,也就避开了死循环。


它们只差两处

模板 A(左边界)模板 B(右边界)
中点计算left + (right - left) / 2(下取整)left + (right - left + 1) / 2(上取整)
保留 mid 的那一支else --> right = midif --> left = mid

除了这两处,其余完全一样。


为什么这两处必须这样配对

根源是防死循环。

mid ± 1 那一支是安全的,因为它必然让区间至少缩小一个位置。但裸 mid 那一支有可能原地不动——当区间只剩两个元素、中点恰好落在那一端时,区间就不会缩小。

举个最小例子,假设此刻区间是 left = 0, right = 1:

模板 A 用的是 right = mid
    下取整 --> mid = 0 = left
        走 else 这一支:right = mid = 0
        --> 区间从 [0,1] 变成 [0,0]   缩小了,安全

    上取整 --> mid = 1 = right
        走 else 这一支:right = mid = 1
        --> 区间还是 [0,1]   没变!死循环

所以模板 A 必须用下取整。模板 B 的情况正好相反,它用的是 left = mid,必须用上取整,否则同样会卡住。

这就引出一句很好用的口诀:

让下面出现 -1 的时候,上面就加 +1。

这里"下面"指 if-else 分支,"上面"指中点计算。对照一下:

  • 模板 A 的分支是 mid + 1 和裸 mid,没有 -1 → 中点不加 1,用下取整 
  • 模板 B 的分支是裸 mid 和 mid - 1,出现了 -1 → 中点要加 1,用上取整 

3.搜索插⼊位置

a.题目描述:


b.题目链接:

35. 搜索插入位置 - 力扣(LeetCode)


c.算法流程:

  • a. 分析插入位置两侧区间元素特征: 设插入位置下标为 index,区间满足:
  1. [left, index - 1]:区间内全部元素小于 target;
  2. [index, right]:区间内全部元素大于或等于 target。
  • b. 定义 left、right 作为查找区间的左右边界,根据 nums[mid] 与目标值 target 的大小关系,更新查找区间:
  1. i. 若 nums[mid] >= target:说明 mid 落在 [index, right] 区间,mid 及其左侧有可能是目标插入点,更新查找区间为 [left, mid],即令 right = mid,继续循环;
  2. ii. 若 nums[mid] < target:说明 mid 落在 [left, index - 1] 区间,插入点一定在 mid 的右侧,更新查找区间为 [mid + 1, right],即令 left = mid + 1,继续循环。
  • c. 循环终止条件:当区间收缩至 left == right(区间长度为 1),此时 left(或 right)的值,就是目标元素的插入位置。

d.代码实现:


4.x 的平方根

a.题目描述:


b.题目链接:

69. x 的平方根 - 力扣(LeetCode)


c.算法流程:

设 x 的平⽅根的最终结果为 index :

  • a. 分析 index 左右两次数据的特点:
  1.  [0, index] 之间的元素,平⽅之后都是⼩于等于 x 的;
  2.  [index + 1, x] 之间的元素,平⽅之后都是⼤于 x 的。

因此可以使⽤⼆分查找算法。


d.代码实现:


5.山峰数组的峰顶索引

a.题目描述:


b.题目链接:

852. 山脉数组的峰顶索引 - 力扣(LeetCode)


c.算法流程

a. 分析峰顶以及山峰两侧区间的元素特征:

  • 峰顶元素:arr[i] > arr[i - 1] && arr[i] > arr[i + 1];
  • 峰顶左侧区间:元素呈上升趋势,满足 arr[i] > arr[i - 1] && arr[i] < arr[i + 1];
  • 峰顶右侧区间:元素呈下降趋势,满足 arr[i] < arr[i - 1] && arr[i] > arr[i + 1]。

b. 定义left、right为查找区间左右边界,根据mid位置元素趋势,更新查找区间:

  1. i. 若mid位置处于上升趋势:峰值一定在mid右侧,更新查找区间为[mid + 1, right],即left = mid + 1,继续查找;
  2. ii. 若mid位置处于下降趋势:峰值一定在mid左侧,更新查找区间为[left, mid - 1],即right = mid - 1,继续查找;
  3. iii. 若mid就是峰顶元素,直接返回mid下标。

c. 循环结束,找到峰值位置。


d.代码实现:


6.寻找峰值

a.题目描述:


b.题目链接:

162. 寻找峰值 - 力扣(LeetCode)


c.算法流程:

a. 分析数组二段性: 任取下标 i,对比 arr[i] 和 arr[i+1],存在两种情况:

  • arr[i] > arr[i + 1]:左侧区间一定存在峰值(数组两端等效负无穷),目标在左侧;
  • arr[i] < arr[i + 1]:右侧区间一定存在峰值(数组两端等效负无穷),目标在右侧。 具备二段性,因此可以采用二分查找求解。

b. 定义 left、right 作为查找区间的左右边界,取中间点 mid,根据 arr[mid] 与arr[mid+1] 的大小关系更新区间:

  1. i. 若 arr[mid] > arr[mid + 1]:峰值存在于左半区间,更新查找区间为 [left, mid],即 right = mid,继续查找;
  2. ii. 若 arr[mid] < arr[mid + 1]:峰值存在于右半区间,更新查找区间为 [mid + 1,right],即 left = mid + 1,继续查找。

c. 直到查找区间收缩至 left == right,此时 left(或 right)即为峰值下标。


d.代码实现:


7.搜索旋转排序数组中的最⼩值

a.题目描述:


b.题目链接:

153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)


c.算法流程

a. 二分查找核心本质:找到判定条件,将查找区间一分为二。 C 点为待查找目标点。由图像可得区间性质:

  • [A,B]区间内所有点的值,严格大于 D 点的值;
  • [C,D]区间内所有点的值,小于等于 D 点的值;
  • 特殊说明:当[C,D]区间仅剩一个元素时,C 点的值可以等于 D 点的值。

b. 定义left、right为查找区间的左右边界,取中间点mid,根据mid落点划分下一轮查找区间:

  1. i. 若mid落在[A,B]区间,即nums[mid] > D:目标在mid右侧,更新查找区间为[mid + 1,right],即left = mid + 1,继续查找;
  2. ii. 若mid落在[C,D]区间,即nums[mid] <= D:目标在mid及左侧,更新查找区间为[left,mid],即right = mid,继续查找。

c. 直到查找区间收缩至长度为 1(left == right),此时left(或right)即为所求 C 点。


d.代码实现:


8.0〜n-1 中缺失的数字

a.题目描述:


b.题目链接:

LCR 173. 点名 - 力扣(LeetCode)


c.算法流程:

a. 分析数组二段性: 本题存在 O (N) 的暴力解法,这里只介绍最优二分解法。数组为升序排列:

  • 第一个缺失位置的左侧:数组元素值与数组下标相等;
  • 第一个缺失位置的右侧:数组元素值与数组下标不相等。 具备二段性,可以使用二分查找求解。

b. 定义left、right作为查找区间的左右边界,取中间点mid,根据nums[mid]与下标mid的关系更新查找区间:

  1. i. 若nums[mid] == mid:缺失位置一定在mid右侧,更新查找区间为[mid + 1, right],即left = mid + 1,继续查找;
  2. ii. 若nums[mid] != mid:缺失位置在mid及其左侧,更新查找区间为[left, mid],即right = mid,继续查找。

c. 直到查找区间收缩至长度为 1(left == right),此时left(或right)即为第一个缺失的正整数。


d.代码实现:

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_93351213/article/details/167393267

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--