关于二分法,推荐去看这篇文章,解释的非常清楚了。
这里只是用自己的方法做一个总结,记录下我个人的理解。
1. 二分法求值
这个是最简单的情况了,在数组source中寻找是否存在目标值target。如果存在返回索引,如果不存在返回-1。
解决思路就是,在source数组的索引 [0, source.length() - 1] 中,使用中间索引mid对应的值和target进行比较;
- 如果区间的长度为0,则返回-1;
- 如果mid对应的值等于target,则直接返回索引mid;
- 如果mid对应的值比target大,则在左边的区间 [0, mid - 1] 中继续使用二分法;
- 如果mid对应的值比target小,则在右边的区间 [mid + 1, source.length() - 1] 中继续使用二分法;
事实上,可以采用递归和迭代两种方式来实现二分法,但一般采用迭代的方式来实现,因为其效率更高。
/**
* 二分法求值
*/
public int findTarget(int[] source, int target) {
// 如果区间的长度为0,则返回-1;
if (source.length == 0) {
return -1;
}
// 搜索区间为左闭右闭区间[left, right]
int left = 0;
int right = source.length - 1;
// 这里循环终止的条件是left > right,即区间的长度为0
while (left <= right) {
int mid = (right - left) / 2 + left;
if (source[mid] > target) {
// 搜索区间变为[left, mid - 1]
right = mid - 1;
} else if (source[mid] < target) {
// 搜索区间变为[mid + 1, right]
left = mid + 1;
} else {
// 找到了,返回索引
return mid;
}
}
// 没找到,返回-1
return -1;
}
2. 二分法求边界
不同的场景中,对边界的定义可能不同。为了方便记忆,我们先定义一下本文中边界的概念:
- 左边界:数组source中小于target的最大索引的后一位,即第一个大于等于target的索引
- 右边界:数组source中大于target的最小索引的前一位,即最后一个小于等于target的索引
例如,当source = {1, 2, 2, 3, 4, 6, 7, 9}; target = 2的时候:
左边界left = 1,即第一个大于等于2的索引;
右边界right = 2,即最后一个小于等于2的索引;
当source = {1, 2, 2, 3, 4, 6, 7, 9}; target = 5的时候:
左边界left = 5,即第一个大于等于5的索引;
右边界right = 4,即最后一个小于等于5的索引;
(当然left > right说明target不存在)
这样定义边界有两个好处:
一是方便记忆代码,这点在后边会有总结;
二是索引在 [left, right] 中的值都是target,区间的长度就是source中target的个数。
(1) 求左边界
第一种方法,用左闭右闭区间求解:
/**
* 左闭右闭区间求左边界
*/
int left_bound1(int[] source, int target) {
// 如果区间的长度为0,则返回-1;
if (source.length == 0) {
return -1;
}
// 搜索区间为左闭右闭区间[left, right]
int left = 0;
int right = source.length - 1;
// 这里循环终止的条件是left = right + 1,即区间的长度为0
while (left <= right) {
int mid = left + (right - left) / 2;
if (source[mid] == target) {
// 如果索引mid处的值等于target,那么mid要么是左边界,要么在左边界的右边
// 如果mid是在左边界的右边,可以在左区间[left, mid - 1]中寻找左边界
// 如果mid刚好就是左边界,这样不是把区间移过了?
// 注意到其实循环终止的条件是left = right + 1,最后只要返回left的值即可保证正确性,如果返回right就错了
right = mid - 1;
} else if (source[mid] < target) {
// 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right]中寻找左边界
left = mid + 1;
} else if (source[mid] > target) {
// 如果索引mid处的值比target大,说明需要在左区间[left, mid - 1]中寻找左边界
right = mid - 1;
}
}
return left;
}
第二种方法,用左闭右开区间求解:
/**
* 左闭右开区间求左边界
*/
int left_bound(int[] source, int target) {
// 如果区间的长度为0,则返回-1;
if (source.length == 0) {
return -1;
}
// 搜索区间为左闭右开区间[left, right)
int left = 0;
int right = source.length;
// 这里循环终止的条件是left == right,即区间的长度为0
while (left < right) {
int mid = left + (right - left) / 2;
if (source[mid] == target) {
// 如果索引mid处的值等于target,那么mid要么是左边界,要么在左边界的右边
// 如果mid是在左边界的右边,可以在左区间[left, mid)中寻找左边界
// 如果mid刚好就是左边界,这样不是把区间移过了?
// 事实上,由于区间右侧为开,所以看似移过了,其实right的值还是在左边界上
// 注意到其实循环终止的条件是left = right,最后无论返回left还是right都行
right = mid;
} else if (source[mid] < target) {
// 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right)中寻找左边界
left = mid + 1;
} else if (source[mid] > target) {
// 如果索引mid处的值比target大,说明需要在左区间[left, mid)中寻找左边界
right = mid;
}
}
return left;
}
(2) 求右边界
第一种方法,用左闭右闭区间求解:
/**
* 左闭右闭区间求右边界
*/
int right_bound(int[] source, int target) {
// 如果区间的长度为0,则返回-1;
if (source.length == 0) {
return -1;
}
// 搜索区间为左闭右闭区间[left, right]
int left = 0;
int right = source.length - 1;
// 这里循环终止的条件是left = right + 1,即区间的长度为0
while (left <= right) {
int mid = left + (right - left) / 2;
if (source[mid] == target) {
// 如果索引mid处的值等于target,那么mid要么是右边界,要么在右边界的左边
// 如果mid是在右边界的左边,可以在右区间[mid + 1, right]中寻找右边界
// 如果mid刚好就是右边界,这样不是把区间移过了?
// 注意到其实循环终止的条件是right = left - 1,最后只要返回right的值即可保证正确性,如果返回left就错了
left = mid + 1;
} else if (source[mid] < target) {
// 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right]中寻找右边界
left = mid + 1;
} else if (source[mid] > target) {
// 如果索引mid处的值比target大,说明需要在左区间[left, mid - 1]中寻找右边界
right = mid - 1;
}
}
return right;
}
第二种方法,用左闭右开区间求解:
/**
* 左闭右开区间求右边界
*/
int right_bound2(int[] source, int target) {
// 如果区间的长度为0,则返回-1;
if (source.length == 0) {
return -1;
}
// 搜索区间为左闭右开区间[left, right]
int left = 0;
int right = source.length;
// 这里循环终止的条件是left = right,即区间的长度为0
while (left < right) {
int mid = left + (right - left) / 2;
if (source[mid] == target) {
// 如果索引mid处的值等于target,那么mid要么是右边界,要么在右边界的左边
// 如果mid是在右边界的左边,可以在右区间[mid + 1, right)中寻找右边界
// 如果mid刚好就是右边界,这样不是把区间移过了?
// 是的,所以最后的结果应该-1
// 注意到其实循环终止的条件是left = right,最后无论返回left-1还是right-1都行
left = mid + 1;
} else if (source[mid] < target) {
// 如果索引mid处的值比target小,说明需要在右区间[mid + 1, right)中寻找右边界
left = mid + 1;
} else if (source[mid] > target) {
// 如果索引mid处的值比target大,说明需要在左区间[left, mid)中寻找右边界
right = mid;
}
}
return left - 1;
}
总结一下:
- 左闭右闭区间法中二分的结果是:[left, mid - 1], [mid + 1, right]
左闭右开区间法中二分的结果是:[left, mid), [mid + 1, right) - 左闭右闭区间法中求左边界返回left,求右边界返回right
左闭右开区间法中求左边界返回left或right都行,求右边界时返回left-1或right-1;
个人更倾向于左闭右闭区间法,这样可以和二分求值统一起来,移动左边界就mid+1,移动右边界就mid-1,求左区间就返回left,求右区间就返回right。