二分查找相关问题

二分查找主要就是搜索区间的正确更新,不能落下或者重复计算某些区间

为什么while(left<=right) 如果 right = middle会发生什么

版本一的正确写法(right = middle - 1)的步长推导

假设初始区间是 [left, right]middle 的计算公式为:

middle = left + (right - left) / 2

每次根据 nums[middle]target 的比较,我们有三种情况:

  1. 如果 nums[middle] == target,返回 middle
  2. 如果 nums[middle] > target,说明目标值在 middle 的左边,因此搜索范围缩小为 [left, middle - 1],即 right = middle - 1
  3. 如果 nums[middle] < target,目标值在 middle 的右边,搜索范围缩小为 [middle + 1, right],即 left = middle + 1

通过使用 right = middle - 1left = middle + 1,每次迭代都在缩小搜索区间,并保证不会重复检查 middle 位置,从而能够保证算法收敛。

修改为 right = middle 后的情况

如果将 right = middle - 1 改为 right = middle,则在 nums[middle] > target 的情况下,你会将右边界调整为 middle,即搜索区间变成 [left, middle]。问题在于,在下一次迭代时,如果 middle 仍然是相同的值(因为 leftright 都没有明显缩小),则会导致无限循环,因为搜索区间没有明显缩小。

为了更具体地说明这一点,假设 left = 0right = 2,计算得到 middle = 1

  • 如果 nums[middle] > target,使用 right = middle,右边界变成 1,区间变为 [0, 1],再计算 middle,还是 1
  • 这样即使在下一次迭代,middle 仍然会是 1,从而导致死循环,区间 [0, 1] 永远不会收敛到目标值,算法无法结束。

步长分析

  • 正确的步长:当 nums[middle] > target 时,步长为 right = middle - 1,保证每次区间缩小,从而能最终收敛。
  • 错误的步长:当 right = middle 时,可能会导致步长为 0 的情况,即 right 不会真正缩小,导致死循环。

总结

  1. right = middle - 1 是为了确保中间点不再包含在接下来的搜索区间内,确保算法收敛。
  2. right = middle 可能导致搜索区间没有明显缩小,从而陷入死循环。

因此,将 right = middle - 1 改为 right = middle 是不正确的,会导致算法无法终止。

为什么不能是left = middle,right = middle-1

分析更新方式

我们先看看这两种更新方法的效果:

  1. left = middle + 1:当 nums[middle] < target 时,目标值一定在 middle 右边,因此我们排除掉 middle,从 middle + 1 开始搜索。
  2. right = middle:当 nums[middle] >= target 时,目标值可能就是 middle 或者在 middle 的左边,因此我们保留 middle,将搜索范围缩小到 [left, middle]
你提到的更新方式

你建议将 left = middleright = middle - 1,也就是:

  1. left = middle:如果 nums[middle] < target,将 left 更新为 middle,表示我们把当前的 middle 包含在接下来的搜索区间中。
  2. right = middle - 1:如果 nums[middle] >= target,将 right 更新为 middle - 1,排除 middle 这个位置。

问题 1:区间无法正确缩小

如果你使用 left = middleright = middle - 1,在某些情况下,搜索区间可能无法有效缩小,从而导致问题。例如:

  • 如果 leftright 相差 1,即区间为 [left, right] = [0, 1],并且 middle = 0,这时你设置 left = middle 使得 left = 0,同时 right = middle - 1 使得 right = -1
  • 这会导致区间变成 [0, -1],这种区间不合理,程序可能提前结束并且未能正确找到目标值。

问题 2:遗漏目标值

使用 left = middleright = middle - 1 可能会遗漏目标值,因为有时候我们并没有正确保留 middle。举个例子:

假设数组为 nums = [1, 2, 3, 4, 5],目标值为 4

  1. 初始 left = 0right = 4middle = 2(对应值 3)。
  2. 由于 nums[middle] < target,你会更新 left = middle,即 left = 2
  3. 再次迭代时,right = middle - 1 = 1,此时 left = 2right = 1,导致搜索区间无效,程序提前退出,目标值 4 未被检查到。

这种情况下,middle 的值实际上是我们可能要查找的目标值之一,因此不应该被直接排除。

正确的做法

为了避免这些问题,我们在设计二分查找时,必须确保每次迭代能够有效缩小搜索区间,并且不会遗漏任何可能的目标值。这就是为什么我们使用以下更新方式:

  1. left = middle + 1:如果 nums[middle] < target,那么 middle 肯定不是目标值,且目标值在 middle 右边。因此,我们可以安全地排除 middle,从 middle + 1 继续搜索。
  2. right = middle:如果 nums[middle] >= target,那么 middle 可能是目标值,因此不能排除它。我们需要将 right 保持为 middle,继续在左侧区间搜索。

区间如何正确缩小

这两种更新方式的共同点在于,每次迭代后区间都能缩小

  • left = middle + 1 时,左边界向右移动,排除掉 middle
  • right = middle 时,右边界向左收缩,保留 middle 位置。

因此,我们可以确保算法每次迭代都能缩小区间,并最终收敛到目标值。

总结

  • 使用 left = middleright = middle - 1 不正确,因为可能导致区间无法有效缩小,或错过目标值,甚至提前终止循环。
  • 正确的更新方式应该是 left = middle + 1right = middle,这样可以保证区间每次迭代都能正确缩小,并确保不会遗漏目标值。

这就是为什么我们不能简单地使用 left = middleright = middle - 1,而必须仔细设计区间更新策略。

陷入死循环的情况

是的,陷入死循环的条件之一就是 middle 不再变化。具体来说,当 leftright 的更新方式不能有效缩小区间时,middle 在连续的迭代中保持不变,这会导致算法进入死循环。

让我们再详细看看这个现象的原因:

什么时候 middle 不再变化?

middle 的计算公式是:

middle = left + (right - left) / 2;

如果 leftright 的值在连续的迭代中没有发生足够的变化,middle 的值就会重复,这意味着算法无法缩小搜索区间,从而无法继续收敛。

例如:

  • 如果 left = 0right = 1,计算得到 middle = 0
  • 在这种情况下,如果 right 更新为 middle(即 right = middle),那么 right 仍然等于 0
  • 接下来的迭代中,left = 0right = 0,再次计算 middle 仍然是 0

此时,middle 没有变化,搜索区间始终是 [0, 0],算法陷入死循环。

死循环的根本原因

死循环的根本原因是搜索区间没有有效缩小。在二分查找算法中,区间的缩小是依赖于 leftright 的更新。如果在迭代过程中 leftright 的更新没有让区间缩小,middle 的值就不会改变,算法将不断重复这一过程,永远无法退出。

具体条件

  1. 区间不缩小:如果每次迭代后,leftright 没有有效缩小(即没有明确排除当前的 middle),算法无法找到目标值。

  2. middle 重复:如果计算出的 middle 在连续迭代中不发生变化,意味着搜索区间没有得到进一步缩小,因此会导致无限循环。

举个例子

假设我们使用错误的写法 right = middle,并且数组 nums = [1, 3, 5],目标值是 2

  1. 初始left = 0right = 2,计算 middle

    middle = 0 + (2 - 0) / 2 = 1;
    
  2. 比较nums[middle] = 3,由于 nums[middle] > target,按照错误的逻辑 right = middle,即 right = 1

  3. 下一步left = 0right = 1,重新计算 middle

    middle = 0 + (1 - 0) / 2 = 0;
    
  4. 比较nums[middle] = 1,由于 nums[middle] < target,设置 left = middle + 1,即 left = 1

  5. 再次迭代left = 1right = 1,计算 middle

    middle = 1 + (1 - 1) / 2 = 1;
    

    现在 middle = 1left == right == 1,并且 middle 一直是 1。由于你在前面的迭代中设置了 right = middle,区间没有缩小,并且 middle 将保持为 1,导致无限循环。

正确做法:避免死循环

为了避免死循环,你需要确保每次迭代时搜索区间都能有效缩小。也就是说,当排除掉某些不可能的范围时,你需要将边界正确地调整

  • nums[middle] > target 时,应该使用 right = middle - 1,以排除掉 middle 这个位置,保证区间缩小。
  • nums[middle] < target 时,应该使用 left = middle + 1,排除掉 middle 这个位置。

通过这样修改,leftright 每次迭代都会向中间靠近,最终使得 left 超过 right,算法能够正确终止。

结论

陷入死循环的条件主要是因为**middle 不再变化**,这是由区间没有正确缩小导致的。为了避免这种情况,必须在每次迭代时确保 leftright 被正确地更新,从而让 middle 随着区间的变化而不断调整,最终收敛到解或者终止搜索。

数学问题

二分查找背后的思想和逻辑确实可以与某些数学问题相对应,特别是在以下几个方面:

1. 二分法求解方程(Bisection Method)

二分查找与二分法求解方程有直接的数学对应。二分法(Bisection Method)是一种用于数值求解方程的基础方法,主要用于寻找连续函数在某个区间内的零点(即函数值为零的点)。

二分法求解方程的基本思想:

给定一个连续函数 ( f(x) ),在区间 ([a, b]) 上,如果 ( f(a) ) 和 ( f(b) ) 的符号相反(即 ( f(a) \cdot f(b) < 0 )),那么根据中间值定理,函数在 ([a, b]) 之间必定有一个零点(解)。

二分法通过不断地将区间对半分,检查中点处的函数值,来逐步缩小包含零点的区间,直到找到零点或使区间足够小。

步骤:

  1. 计算中点 ( c = \frac{a + b}{2} )。
  2. 如果 ( f© = 0 ),则 ( c ) 是零点,返回。
  3. 如果 ( f(a) \cdot f© < 0 ),则零点位于 ([a, c]),更新区间为 ([a, c])。
  4. 如果 ( f© \cdot f(b) < 0 ),则零点位于 ([c, b]),更新区间为 ([c, b])。
  5. 重复上述步骤,直到区间足够小。

这个过程与二分查找极其类似——我们通过检查区间的中点,逐步缩小区间的大小。

关联:
  • 二分查找是在一个有序数组中通过对比中间元素与目标值,来缩小目标可能存在的范围。
  • 二分法求根是在一个连续函数中通过检查中点处的函数值,来缩小零点所在的区间。

两者的核心思想都是通过“分而治之”,每次把问题规模减半。

2. 对数时间复杂度(Logarithmic Time Complexity)

二分查找的时间复杂度是 ( O(\log n) ),这在计算机科学中是非常典型的“对数级别”问题。数学上,对数函数的特性决定了通过对半划分的过程可以很快缩小问题的规模。

在二分查找中,每次我们将搜索空间缩小为原来的一半,问题的规模随着递归的深度呈对数递减。假设初始问题的规模为 ( n ),那么我们大约只需要 (\log_2(n)) 次比较就能找到目标。

这与数学中的对数性质紧密相关:

[
\log_2(n) = k \implies 2^k = n
]

也就是说,当我们将问题规模减少到 ( 1 ) 时,我们进行了大约 ( \log_2(n) ) 次步骤,这就是二分查找背后的数学原理之一。

3. 离散搜索 vs 连续搜索

二分查找可以看作是离散版本的搜索问题,而二分法求解方程则是连续版本的搜索问题。两者都是通过逐步缩小解所在的区间,来逼近目标值:

  • 离散搜索(Discrete Search):在有序数组中搜索特定值,这是二分查找的应用。数组是离散的,元素是有限的,每次搜索都在有限个候选项中进行。

  • 连续搜索(Continuous Search):在一个函数的连续区间中搜索零点,这对应于二分法的应用。区间是连续的,函数值可以是任意实数。

尽管二者的对象不同(离散 vs 连续),但二分查找和二分法的核心思想相同,都是通过每次对半分割搜索区间,逐渐逼近目标。

4. 对数函数的反解问题

二分查找还可以类比于解决对数方程的过程。例如,对于一个方程 ( 2^x = n ),我们可以通过二分查找的方式来逼近 ( x )。通过逐步尝试不同的 ( x ) 值,并根据结果调整 ( x ) 的区间,直到找到合适的 ( x )。

这个过程与使用二分查找在一个对数分布中搜索目标值相似。因为每次的比较都将搜索空间减半,所以这种问题的复杂度也是对数级别的。

模板

在二分查找中,区间的边界定义有两种常见的方式:左闭右闭左闭右开。这两种定义决定了循环的边界条件以及 leftright 的更新方式。下面我们分别介绍这两种方式的实现。

1. 左闭右闭区间([left, right])

定义:区间是左闭右闭,即 leftright 都是闭区间的边界,表示搜索区间是 ([left, right]),包括 leftright 两端的元素。

实现:
int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1;  // 注意right初始化为nums.size() - 1

    while (left <= right) {  // 循环条件是left <= right,表示[left, right]是闭区间
        int middle = left + (right - left) / 2;  // 防止溢出,等价于(left + right) / 2
        if (nums[middle] == target) {
            return middle;  // 找到目标值,直接返回索引
        } else if (nums[middle] < target) {
            left = middle + 1;  // 排除middle,搜索区间变为[middle + 1, right]
        } else {
            right = middle - 1;  // 排除middle,搜索区间变为[left, middle - 1]
        }
    }
    return -1;  // 如果循环结束仍未找到目标值,返回-1
}
关键点:
  1. 初始区间left = 0right = nums.size() - 1,表示整个数组的索引范围。
  2. 循环条件left <= right,因为是左闭右闭区间,当 left == right 时,区间仍然有效,需要检查。
  3. 更新规则
    • left = middle + 1:排除 middle,因为 nums[middle] < target,目标值在右边。
    • right = middle - 1:排除 middle,因为 nums[middle] > target,目标值在左边。

2. 左闭右开区间([left, right))

定义:区间是左闭右开,即 left 是闭区间的边界,right 是开区间的边界,表示搜索区间是 ([left, right)),包含 left,但不包含 right

实现:
int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size();  // 注意right初始化为nums.size()

    while (left < right) {  // 循环条件是left < right,表示[left, right)是左闭右开区间
        int middle = left + (right - left) / 2;  // 防止溢出,等价于(left + right) / 2
        if (nums[middle] == target) {
            return middle;  // 找到目标值,直接返回索引
        } else if (nums[middle] < target) {
            left = middle + 1;  // 排除middle,搜索区间变为[middle + 1, right)
        } else {
            right = middle;  // 搜索区间变为[left, middle),right是开区间,所以不排除middle
        }
    }
    return -1;  // 如果循环结束仍未找到目标值,返回-1
}
关键点:
  1. 初始区间left = 0right = nums.size(),表示区间 ([0, nums.size())`,整个数组范围。
  2. 循环条件left < right,因为是左闭右开区间,当 left == right 时,区间为空,循环终止。
  3. 更新规则
    • left = middle + 1:排除 middle,因为 nums[middle] < target,目标值在右边。
    • right = middle:不排除 middle,因为 nums[middle] >= target,目标值可能是 middle

左闭右闭 vs 左闭右开

方式区间形式循环条件right 初始化更新 left更新 right
左闭右闭([left, right])left <= rightnums.size() - 1left = middle + 1right = middle - 1
左闭右开([left, right))left < rightnums.size()left = middle + 1right = middle

总结

  • 左闭右闭:包含 leftright,即 ([left, right])。循环条件是 left <= right,在最后一次循环中检查 left == right 的情况。
  • 左闭右开:包含 left 但不包含 right,即 ([left, right))。循环条件是 left < right,当 left == right 时,区间为空,停止循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值