在计算机科学的查找算法中,二分查找以其高效性占据着重要地位。它利用数据的有序性,通过不断缩小查找范围,将原本需要线性时间的查找过程优化为对数时间,成为处理大规模有序数据查找问题的首选算法。
二分查找的基本概念
二分查找(Binary Search),又称折半查找,是一种在有序数据集合中查找特定元素的高效算法。其核心原理是:通过不断将查找范围减半,快速定位目标元素。
与线性查找逐个遍历元素不同,二分查找依赖数据的有序性(通常为升序或降序),每次都与查找范围的中间元素比较,根据比较结果缩小查找范围:
- 若中间元素等于目标值,则查找成功。
- 若中间元素大于目标值,则目标值只可能在左半部分(针对升序数据)。
- 若中间元素小于目标值,则目标值只可能在右半部分(针对升序数据)。
例如,在有序数组[1, 3, 5, 7, 9, 11, 13]中查找目标值7,二分查找的过程如下:
- 初始范围为整个数组(索引0到6),中间元素为索引3的7,与目标值相等,查找成功。
再如查找目标值5:
- 初始范围0-6,中间元素7(索引3),7 > 5,缩小范围至0-2。
- 范围0-2,中间元素3(索引1),3 < 5,缩小范围至2-2。
- 范围2-2,中间元素5(索引2),与目标值相等,查找成功。
二分查找的算法思想
二分查找的核心思想是分治与缩小范围,基于有序数据的特性,通过每次与中间元素比较,将查找范围缩小一半,从而高效定位目标元素。其基本思路可概括为:
- 确定查找范围:初始化左边界left为0,右边界right为数据长度n-1(闭区间[left, right])。
- 计算中间位置:在当前范围中,计算中间索引mid = left + (right - left) / 2(避免left + right溢出)。
- 比较与缩小范围:
-
- 若nums[mid] == target:找到目标值,返回mid。
-
- 若nums[mid] > target:目标值在左半部分,更新右边界right = mid - 1。
-
- 若nums[mid] < target:目标值在右半部分,更新左边界left = mid + 1。
- 终止条件:
-
- 若left > right:范围无效,说明目标值不存在,返回-1。
-
- 若找到目标值,提前返回索引。
二分查找的关键在于维护正确的查找范围和避免边界错误。根据范围定义的不同(如左闭右开[left, right)),具体实现会略有差异,但核心思想一致。
二分查找的解题思路
使用二分查找解决实际问题时,需遵循以下步骤:
- 确认数据有序性:二分查找仅适用于有序数据(升序或降序,需统一处理),若数据无序,需先排序(但排序成本可能高于查找收益,需权衡)。
- 定义查找范围:明确left和right的初始值(如left=0,right=n-1),并始终保持范围的一致性(如闭区间或左闭右开)。
- 循环查找:在left <= right(闭区间)条件下循环,计算mid并比较,根据结果调整left或right。
- 处理边界与特殊情况:
-
- 数据为空时直接返回-1。
-
- 目标值小于最小值或大于最大值时,可提前判断。
-
- 存在重复元素时,需明确查找 “第一个出现”“最后一个出现” 还是 “任意一个”(需调整边界更新逻辑)。
LeetCode 例题及 Java 代码实现
例题 1:二分查找(LeetCode 704)
给定一个n个元素有序的(升序)整型数组nums和一个目标值target,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。
解题思路
标准二分查找问题,直接应用上述解题思路,使用闭区间[left, right]实现。
代码实现
public class BinarySearch {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
// 计算mid,避免left + right溢出
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
// 目标在左半部分,缩小右边界
right = mid - 1;
} else {
// 目标在右半部分,缩小左边界
left = mid + 1;
}
}
// 未找到目标值
return -1;
}
public static void main(String[] args) {
BinarySearch solution = new BinarySearch();
int[] nums = {-1, 0, 3, 5, 9, 12};
System.out.println(solution.search(nums, 9)); // 输出:4
System.out.println(solution.search(nums, 2)); // 输出:-1
}
}
例题 2:在排序数组中查找元素的第一个和最后一个位置(LeetCode 34)
给定一个按照升序排列的整数数组nums,和一个目标值target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回[-1, -1]。
解题思路
本题需查找重复元素的边界,可通过两次二分查找实现:
- 查找左边界:找到第一个>= target的位置,若该位置元素等于target,则为左边界;否则不存在。
- 查找右边界:找到最后一个<= target的位置,若该位置元素等于target,则为右边界。
代码实现
import java.util.Arrays;
public class SearchRange {
public int[] searchRange(int[] nums, int target) {
int leftBound = findLeftBound(nums, target);
// 若左边界不存在,直接返回[-1, -1]
if (leftBound == -1) {
return new int[]{-1, -1};
}
int rightBound = findRightBound(nums, target);
return new int[]{leftBound, rightBound};
}
// 查找左边界:第一个等于target的位置
private int findLeftBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 找到目标,继续向左查找更左的位置
result = mid;
right = mid - 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return result;
}
// 查找右边界:最后一个等于target的位置
private int findRightBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 找到目标,继续向右查找更右的位置
result = mid;
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return result;
}
public static void main(String[] args) {
SearchRange solution = new SearchRange();
int[] nums = {5, 7, 7, 8, 8, 10};
System.out.println(Arrays.toString(solution.searchRange(nums, 8))); // 输出:[3, 4]
System.out.println(Arrays.toString(solution.searchRange(nums, 6))); // 输出:[-1, -1]
}
}
例题 3:搜索旋转排序数组(LeetCode 33)
整数数组nums按升序排列,数组中的值互不相同。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了旋转,使数组变为[nums[k], nums[k+1],..., nums[n-1], nums[0], nums[1],..., nums[k-1]]。例如,[0,1,2,4,5,6,7]旋转后可能变为[4,5,6,7,0,1,2]。给定旋转后的数组nums和一个整数target,如果nums中存在这个目标值,则返回它的索引,否则返回-1。
解题思路
旋转数组可分为两个有序子数组,仍可使用二分查找:
- 计算mid后,判断[left, mid]或[mid, right]是否为有序区间。
- 在有序区间内判断target是否存在,缩小查找范围。
代码实现
public class SearchRotatedSortedArray {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 判断左半部分是否有序
if (nums[left] <= nums[mid]) {
// 目标在左半有序区间内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
public static void main(String[] args) {
SearchRotatedSortedArray solution = new SearchRotatedSortedArray();
int[] nums = {4, 5, 6, 7, 0, 1, 2};
System.out.println(solution.search(nums, 0)); // 输出:4
System.out.println(solution.search(nums, 3)); // 输出:-1
}
}
二分查找与考研 408
在计算机考研 408 中,二分查找是数据结构与算法部分的核心考点,主要涉及以下内容:
1. 算法原理与实现
考研 408 重点考查二分查找的基本原理和不同场景下的实现,包括:
- 标准二分查找(查找任意位置)。
- 查找边界(第一个 / 最后一个等于目标值的位置)。
- 变体场景(如旋转数组、山脉数组等)。
要求考生能手动模拟查找过程,并写出正确代码,尤其注意边界条件(如left与right的更新逻辑)。
2. 时间复杂度与空间复杂度
- 时间复杂度:每次查找范围减半,最坏情况下需要log2(n) + 1次比较,时间复杂度为O(log n),远优于线性查找的O(n)。
- 空间复杂度:迭代实现的二分查找空间复杂度为O(1)(仅需常数变量);递归实现为O(log n)(递归栈深度)。考研中多考查迭代实现。
3. 适用条件与局限性
- 适用条件:
-
- 数据必须有序(升序或降序)。
-
- 数据支持随机访问(如数组),链表等无法随机访问的结构不适用(需O(n)时间定位mid,失去优势)。
- 局限性:
-
- 数据无序时需先排序(排序时间O(n log n),若仅查找一次,成本高于线性查找)。
-
- 不适用于频繁插入 / 删除的场景(维护有序性成本高)。
-
- 处理重复元素时,需额外逻辑确定边界。
4. 与其他查找算法的对比
考研常对比二分查找与线性查找、哈希查找的差异:
算法 |
数据要求 |
时间复杂度(平均) |
空间复杂度 |
适用场景 |
二分查找 |
有序、随机访问 |
O(log n) |
O(1) |
大数据量、静态有序数组 |
线性查找 |
无要求 |
O(n) |
O(1) |
小数据量、无序数据、链表 |
哈希查找 |
无要求 |
O(1)(理想) |
O(n) |
频繁查找、内存充足 |
5. 递归实现与迭代实现
考研可能考查二分查找的递归实现,需注意递归终止条件和参数传递:
// 二分查找的递归实现
public int binarySearchRecursive(int[] nums, int left, int right, int target) {
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
return binarySearchRecursive(nums, left, mid - 1, target);
} else {
return binarySearchRecursive(nums, mid + 1, right, target);
}
}
6. 错误分析与常见问题
考研中常考二分查找的边界错误,例如:
- mid计算溢出(应使用left + (right - left)/2而非(left + right)/2)。
- 循环条件错误(如left < right导致漏查)。
- 边界更新错误(如right = mid而非right = mid - 1导致死循环)。
总结
二分查找凭借O(log n)的高效时间复杂度,成为处理有序数据查找的首选算法。本文通过详细讲解其思想、解题思路、LeetCode 实战及考研 408 考点,帮助你全面掌握这一算法。
学习时,需重点关注边界条件处理和不同场景的变体实现,通过手动模拟和代码练习加深理解。对于考研 408 考生,还需掌握时间 / 空间复杂度分析、适用条件及与其他算法的对比,确保在考试中准确应对各类相关题目。
二分查找的核心是 “缩小范围”,理解这一思想后,即使面对旋转数组等复杂场景,也能灵活调整逻辑,高效解决问题。
希望本文能够帮助读者更深入地理解二分查找,并在实际项目中发挥其优势。谢谢阅读!
希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。