二分查找,又称为折半查找(Binary Search),是一种效率较高的搜索算法。采用 折半查找判定树 更能直观看出其时间复杂度为 O(log N)。
该算法采用分而治之的思想,要求线性表中的元素按关键字有序排列,其基本流程如下:
- 确定搜索范围:确定要在哪个区间内进行搜索。设置两个指针,一个指向数组的起始位置,另一个指向数组的结束位置。
- 计算中间元素的索引:计算起始指针 start 与结束指针 end 所指位置的中间元素的索引 mid = (start + end) / 2。
- 比较中间元素:将目标元素与中间元素进行比较。
· 如果目标元素 == 中间元素,搜索完成,找到了目标元素的位置。
· 如果目标元素 < 中间元素,说明目标元素可能在左半部分,将搜索范围缩小为左半部分。
· 如果目标元素 > 中间元素,说明目标元素可能在右半部分,将搜索范围缩小为右半部分。- 更新搜索范围: 根据比较的结果,更新指针的位置,缩小搜索范围。
· 如果目标元素在左半部分,更新 end = mid - 1。
· 如果目标元素在右半部分,更新 start = mid + 1。- 重复步骤:重复步骤 2-4,直到找到目标元素或确定目标元素不在数组中。
下面通过几道题目理解一下二分查找的思想。注意 : 数组不一定都要有序哦!
问题1
给定一个有序的整形数组,查找某个数字是否存在于该数组中。
private static boolean exit(int[] arr, int num) {
if (arr == null || arr.length == 0) {
return false;
}
int l = 0;
int r = arr.length - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (arr[mid] < num) {
l = mid + 1;
} else if (arr[mid] > num) {
r = mid - 1;
} else {
return true;
}
}
return false;
}
这是最经典的二分查找代码,每次将查找区间划分成两部分,通过判断 arr[mid]
与 mid
的大小关系确定下一次查找操作所在的区域。
该方法的成立需要数组有序。
问题2
给定一个升序的整形数组,查找 小于等于 某个数字最 右 侧的位置。
例如:给定数组 arr=[2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 6] 找出小于等于 3 最右侧的位置,即返回 5 。表示前 5 个数都符合 ≤3 的要求。
public static int smallOrEqualRightIndex(int[] arr, int num) {
int index = -1;
int l = 0;
int r = arr.length - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (arr[mid] <= num) {
index = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return index + 1;
}
与问题 1 的代码很类似,该问题是要确定 ≤ ,因此当 arr[mid] == num
时,依然需要做二分,继续向右侧寻找,直到找到最右侧 ( 即下一位置值大于 num )为止。
该方法的成立需要数组有序。
问题3
给定一个升序的整形数组,查找 大于等于 某个数字最 左 侧的位置。
例如:给定数组 arr=[2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 6] 找出大于等于 3 最左侧的位置,即返回 4 。表示从第 4 个数开始之后的数都符合 ≥3 的要求。
public static int bigOrEqualLeftIndex(int[] arr, int num) {
int index = -1;
int l = 0;
int r = arr.length - 1;
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (arr[mid] >= num) {
index = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
return index + 1;
}
会做问题 2 后,该问题的解答也就轻而易举了。当 arr[mid] == num
时,依然需要做二分,继续向左侧寻找,直到找到最左侧 ( 即上一位置值小于 num )为止。
该方法的成立需要数组有序。
问题4
给定一个任意两相邻的数一定不相等的无序数组,找到该数组中任意一个局部最小值并返回其下标。
局部最小值:
· 若 i 非数组的首尾下标,则满足arr[i-1] < arr[i] < arr[i+1]
· 若 i == 0,则满足arr[0] < arr[1]
· 若 i == n - 1,则满足arr[n-1] < arr[n-2]
public static int getLessIndex(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int n = arr.length - 1;
if (arr[0] < arr[1] || n == 0) {
return 0;
}
if (arr[n] < arr[n - 1]) {
return n;
}
int l = 1;
int r = n - 1;
while (l < r) {
int mid = l + ((r - l) >> 1);
if (arr[mid] > arr[mid + 1]) {
l = mid + 1;
} else if (arr[mid] > arr[mid - 1]) {
r = mid - 1;
} else {
return mid;
}
}
return l;
}
该问题是较为特殊的使用二分查找的例子,题目并未给出有序的数组。对于边界条件的判断较为简单,当符合边界判断时直接返回。
当进入到 while 条件中时,局部最小值一定存在于数组非两边边界的内部。题目只要求找出任意一个局部最小值,因此,若 arr[mid] > arr[mid + 1]
或 arr[mid] > arr[mid - 1]
时,可以大胆抛弃掉一半,在另外一半中进行查找。只有符合条件时才返回下标 mid 值。
细心的小伙伴会发现,问题 4 的循环条件是 l < r
,当 l == r
时已经跳出了循环,此时 mid 所指正符合arr[mid-1] < arr[mid] < arr[mid+1]
,因此跳出循环后返回 l
即为所求。
因此,回答文章题目的问题。二分查找一定要有序么?答案是:不一定。
在具体问题的分析中,只要能够把问题合理的划分为左、右两侧如何取舍的正确逻辑,那该问题就可以使用“二分”!