「二分查找算法(Binary Search Algorithm)」:也叫做 「折半查找算法」、「对数查找算法」。是一种在有序数组中查找某一特定元素的搜索算法。
基本算法思想:先确定待查找元素所在的区间范围,在逐步缩小范围,直到找到元素或找不到该元素为止。
二分查找算法的过程如下所示:
举个例子来说,给定一个有序数组 [0, 1, 2, 3, 4, 5, 6, 7, 8]
。如果我们希望查找 5
是否在这个数组中。
[0, 1, 2, 3, 4, 5, 6, 7, 8]
,中位数是 4
,因为 4
小于 5
,所以如果 5
存在在这个数组中,那么 5
一定在 4
右边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6, 7, 8]
。[4, 5, 6, 7, 8]
,中位数是 6
,因为 5
小于 6
,所以如果 5
存在在这个数组中,那么 5
一定在 6
左边的这一半区间中。于是我们的查找范围变成了 [4, 5, 6]
。[4, 5, 6]
,中位数是 5
,正好是我们需要查找的数字。于是我们发现,对于一个长度为 9
的有序数组,我们只进行了 3
次查找就找到了我们需要查找的数字。而如果是按顺序依次遍历数组,则最坏情况下,我们需要查找 9
次。
二分查找过程的示意图如下所示:
二分查找算法是经典的 「减而治之」 的思想。
这里的 「减」 是减少问题规模的意思,「治」 是解决问题的意思。「减」 和 「治」 结合起来的意思就是 「排除法解决问题」。即:每一次查找,排除掉一定不存在目标元素的区间,在剩下可能存在目标元素的区间中继续查找。
每一次通过一些条件判断,将待搜索的区间逐渐缩小,以达到「减少问题规模」的目的。而于问题的规模是有限的,经过有限次的查找,最终会查找到目标元素或者查找失败。
1.首先,明确区间的定义非常重要。在二分查找算法中,通常将数组的第一个元素视为区间左边界,最后一个元素视为区间右边界。因此,区间的定义有以下两种常见的方式:
[1, 2, 3, 4, 5]
的区间定义为[1, 5]
。[1, 2, 3, 4, 5]
的区间定义为[1, 5)
。 无论是哪种区间定义,都需要先明确区间的边界,以便在算法中正确地处理边界条件。
2.对于区间的不一样的定义会对我们的算法产生不一样的影响,因此我们要根据区间的定义去做一个一个理性化的分析。例如,对于左闭右闭区间,当left
和right
都等于数组的第一个元素时,我们仍然可以进行二分查找,因为此时区间[1, 1]
仍然包含数组的第一个元素。但是,当left
等于数组的第一个元素,而right
等于数组的最后一个元素时,区间[1, 5]
就不再包含数组的最后一个元素,此时我们就无法进行二分查找了。
3.坚持循环不变量。循环不变量是指在循环过程中,始终保持不变的东西。在这里我们的“不变量”是区间的定义,我们在每一个更新边界条件时候,都要保持循环“不变量”。在整个循环过程中,我们始终保证left
和right
的值满足循环不变量,这样我们才能正确地完成二分查找算法。
left
等于0,right
等于数组长度减1,即[0, n-1]
。left
等于0,right
等于数组长度,即[0, n)
。对于整数二分有两种模板,分别是 LBS,和 RBS
// 检查x是否满足某种性质
private static boolean check(int x) {
/* ... */
}
// 区间[left, right]被划分成[left, mid]和[mid + 1, right]时使用:
// 或者称之为左二分查询,查找左侧第一个满足条件的数
private static int leftBinarySearch(int[] arr, int left, int right) {
while (left < right) {
int mid = arr[left + right >> 1];
if (check(mid)) {
right = mid; // check()判断mid是否满足性质
} else {
left = mid + 1;
}
}
return left;
}
// 区间[left, right]被划分成[left, mid - 1]和[mid, right]时使用:
// 或者称之为右二分查询,查找右侧最后一个满足条件的数
private static int rightBinarySearch(int[] arr, int left, int right) {
while (left < right) {
int mid = arr[left + right + 1 >> 1];
if (check(mid)) {
left = mid; // check()判断mid是否满足性质
} else {
right = mid - 1; // 有加必有减
}
}
return left;
}
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
nums
中的所有元素是不重复的。n
将在 [1, 10000]
之间。nums
的每个元素都将在 [-9999, 9999]
之间。代码题解
//左闭右闭区间
public class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + ((right - left) >> 1);
//之所以这样定义 mid 是因为防止,数组中间值正好为 0 时候,mid 不会自动更新
//使用位运算的目的是,位运算是底层运算,防止溢出
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return nums[left] == target ? left : -1;
}
}
//左闭右闭区间
public class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
//之所以这样定义 mid 是因为防止,数组中间值正好为 0 时候,mid 不会自动更新
//使用位运算的目的是,位运算是底层运算,防止溢出
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid ;
}
}
return nums[left] == target ? left : -1;
}
}