引言
在计算机科学中,二分搜索(Binary Search)算法是一种在有序数组中查找特定元素的基本搜索技术。其优点在于高效的搜索速度,时间复杂度为 ( O(log n) ),这一点与时间复杂度为O(n) 的线性搜索法相比,效率提高了数个数量级。二分搜索的核心思想是“分而治之”,即将大问题分解为小问题来逐步解决。
二分搜索适用场景
二分搜索不仅适用于简单的有序数组查找,它也适用于任何可以通过单调性质进行决策的问题,如实数范围内的最优解搜索、概率问题中的阈值确定,以及在计算机图形学中的光线追踪技术等。掌握二分搜索不仅能提升算法效率,也有助于培养分析和解决问题的思维能力。
二分搜索的两种主流写法
传统上,二分搜索有两种流行的写法:左闭右闭 [left, right]
和左闭右开 [left, right)
。左闭右闭写法的循环条件为 while (left <= right)
,而左闭右开写法的循环条件为 while (left < right)
。这两种写法虽然在实现上略有不同,但都广泛应用于实践中。
左闭右闭写法
在这种写法中,搜索范围包括 left
和 right
指向的元素。每次循环将区间分为三部分:小于 mid
的元素、等于 mid
的元素和大于 mid
的元素。根据与 target
的比较结果,我们可以排除其中的一个区间。当 left
和 right
相遇时,循环结束,此时 left
和 right
指向相同的可能是 target
的位置。
左闭右开写法
而在左闭右开的写法中,right
指向的元素不包含在搜索范围内。这种方法的结束条件是 left
和 right
相邻,此时 left
指向的是最后一个不符合条件的元素,right
指向的是第一个符合条件的元素。
left + 1 != right
写法的探讨
这种写法本质上是左闭右开的一种变体。其优势在于处理边界的清晰性和减少了在循环中对 mid
的过度检查。
写法原理
这种写法的基本原理是保持 left
和 right
指针之间至少有一个元素的间隔。这样,left
永远指向不满足条件的最后一个元素,而 right
指向的是满足条件的第一个元素。当 left
和 right
相邻时,即 left + 1 == right
,它们之间的元素就是我们要找的元素。
二分搜索写法的具体问题优势
left + 1 != right
方法在处理特定类型的问题时显示出其优势,如“查找第一个大于等于目标值的元素”或“查找最后一个小于等于目标值的元素”。在这些问题中,通过维护一个明确的未满足条件和已满足条件的界限,left + 1 != right
写法减少了对边界的额外检查,从而提高了代码的执行效率和可读性。
对边界的处理
使用 left + 1 != right
的方法,边界处理变得直观而简单。在传统的左闭右闭写法中,我们需要小心翼翼地处理 left
和 right
的更新条件,以避免出现无限循环或者错过目标元素。而在 left + 1 != right
的方法中,我们避免了这种直接更新 left
或 right
至 mid
的操作,因此减少了对边界处理的担忧。
对区间的考量
在 left + 1 != right
的二分搜索中,对区间的考量显得尤为重要。这种写法天然地支持左闭右开的区间操作,即 [left, right)
。在每次循环中,我们将区间从 mid
处分割,并根据比较结果决定是将 left
移动到 mid
,还是将 right
移动到 mid
。由于我们总是保持 left
和 right
之间至少有一个元素的距离,因此可以确保 mid
永远不会与 left
或 right
重合,这减少了循环中可能的逻辑错误。
这种方法的一个显著优点是它使得二分搜索的每一步都更加明确。在传统的左闭右闭方法中,当我们找到 mid
满足条件时,我们可能需要额外的逻辑来确定是返回 mid
,还是继续在左边或右边的区间中搜索。而在 left + 1 != right
方法中,这种情况得到了简化:我们总是在确信没有遗漏任何可能的元素时才结束循环。
细节解析
1)迭代的过程
整个二分的过程是一个不断迭代区间的过程,并且 红色游标 指向的元素始终是 红色 的;绿色游标 指向的元素始终是 绿色 的。迭代的过程就是不断向 红绿边界 逼近的过程。
2)结束条件
迭代结束时,红色游标 和 绿色游标 刚好指向 红绿边界,且区间长度为 2。
3)游标初始值
为什么 红色游标 初始值为 −1,绿色游标 初始值为 n ?
能否将 红色游标 初始化为 0,绿色游标 初始化为 n−1 ? 答案是否定的,试想一下,如果数据元素都是绿色,红色游标 初始化为 0 就违背了 " 红色游标 指向的元素始终是 红色 的 " 这个条件;反之,如果元素都是红色的,也有类似问题。
4)中点位置
5)死循环
上面的程序模板是否会进入死循环?
我们可以这么来看,当区间为 2 时,循环结束。当区间为 3 时,它一定可以变成区间为 2 的情况,当区间为4时,一定可以变成区间为 2 或者 3 的情况,也就是任何一种情况下,区间一定会减小,并且当等于 2 时,循环结束。所以不会造成死循环。
实践中的应用
在实际应用中,left + 1 != right
的方法尤其适合于寻找区间的极值问题,例如寻找旋转排序数组中的最小元素,或者在一个由两种元素(如“红色”和“绿色”)组成的数组中找到颜色变化的边界。在这些情况下,这种二分搜索方法能够清晰地定位到边界点,而不需要额外的边界检查。
代码模板:
public int binarySearch(int[] nums, int target) {int left = -1, right = nums.length; // 初始化left为-1,right为数组长度while (left + 1 != right) { // 当左右指针相邻时停止int mid = left + ((right - left) >> 1); // 计算中点if (check()) { left = mid; // 更新左(或右)指针} else { right = mid; // 更新左指针}}// 循环结束时,left 为指向第一个绿色元素(或者 right 为指向最后一个红色元素)return left;
}
边界情况讨论
在数组的开始和结束位置,left + 1 != right
方法通过简化的逻辑清晰地处理了边界情况。尤其是在数组的结束位置,这种写法自然而然地避免了数组越界的风险,这是因为它始终保持 right
指针在数组范围内。
优势总结
left + 1 != right
的二分搜索法在多个方面展现出它的优势:
- 精确的边界控制:通过保持
left
和right
之间的间隔,这种方法减少了边界值的特殊处理,使得代码更加简洁。 - 逻辑一致性:更新
left
或right
变得一致和直观,不再需要额外的if
判断来避免包含或排除中点值。 - 减少错误:由于不需要处理
mid - 1
或mid + 1
,此方法减少了由于错误更新索引而导致的无限循环或漏掉正确元素的风险。 - 清晰的搜索意图:代码清晰地表达了搜索意图,特别是在区间分割的策略上,这对于阅读和维护代码的人来说是一个巨大的优势。
- 可扩展性:这种写法易于扩展到更复杂的场景,如多维数组的二分搜索,或者需要进行复杂判断逻辑的问题。
与传统二分的对比
与传统的二分搜索方法相比,left + 1 != right
的写法在概念上更为简单和直观,尤其是对于边界条件的处理。在传统的二分搜索中,如果不小心处理,很容易在边界条件上出现错误,比如忘记了 mid - 1
或 mid + 1
,或者在 while
循环的条件中使用了错误的比较运算符。而在 left + 1 != right
的方法中,这些问题都不复存在。我们可以用一种更加一致和安全的方式来处理搜索逻辑。
结论
left + 1 != right
的二分搜索方法以其简洁的边界处理和清晰的逻辑流程,提供了一种新颖而有效的解决问题的方法。它不仅加强了对二分搜索的理解,还扩展了这一算法的应用范围,使得开发者能够更容易地实现和维护相关的算法。