二分法题需要分析问题的二段性,最简单的是已排序递增数组中找到某值位置,那么这个问题二段性为:
该值左边的所有数据都小于该值,该数组右边的所有数据都大于该值; 整个数组被查找值分为连续两段,给定一个条件,该条件必须在其一段中满足,在另一段中不能满足,选取index通过不断在两个条件中反复横跳,数据规模会以2的次方程度下降,直到两个条件的边界,最终会落在满足条件的那一方。
二分法依据满足二段性条件区域分为两套模板
右区间模板
在这里设定的条件为arr[i] >= target
; 写出二分法模板代码如下:
#include <iostream>
#include <vector>
using namespace std;
int bsearch(vector<int> &arr, int target)
{
int l = 0;
int r = arr.size()-1;
while (l < r) {
int mid = l + (r - l) / 2;
if (arr[mid] >= target) { // 条件定义处
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
int main()
{
vector<int> arr = {1, 1, 3, 5, 5, 8, 9};
int index = bsearch(arr, 10);
cout << "index " << index << endl;
return 0;
}
依据 arr[i] >= target 条件总共分四种情况
1 target在arr数组最大和最小的范围内,且存在在数组中
因为
如以上数组,target为5,,那么整个数组在第二个元素的左边全部都小于target;第二个元素的右边全部都都大于等于target;通过这个条件将整个数组分割成两个状态;这就是二段性。
2 target在arr数组最大和最小的范围内,但是不存在在数组中
当我们明白二分法二段性的本质之后,那么下面这些性质就跟随而来; 还是上面这个例子,如果target是7,我们要搜索7,但是7在整个数组不存在,我们预期的是找不到直接返回-1, 那这里该返回什么?
这里返回的是5,也就是8对应的数组下标。因为8是满足大于等于7的第一个数。也就是说,二分法不一定能搜索到你想查找的那个值,而只是满足你定义的那个二断性条件的边界值。
3 target小于arr数组最小值
接着来看边界情况,如果target是0, 所有的数组元素都满足条件,因此这里返回零,但是要注意的是,这个时候它不是两个条件中横跳,因为没有符合另一个条件的数据,只能不断被消减规模,直到数组边界零。当然,arr[0] >= target, 符合给定的条件,可以使用。
4 target大于 arr数组最大值
接着来看上边界,当target = 10 时,也是同样没有符合另一个条件的数据,因此index被压缩到上边界,也就是数组最后一个元素9,但是最后一个元素也是不符合预期的,arr[arr.size()-1] < target; 这个index是错误的。
总结:
模板二分法查找,只有当目标在数组内才能找到准确索引索引下标。当不存在于数组内时,我们依然可以借助二断性找到边界并进行处理。
但是需要对于3和4这种超出边界的情况需要特殊处理。
左区间模板
arr[i] >= target的属性是该判断条件满足数组右边集合,另一个二分法的模板是条件满足二分法左边集合。
#include <iostream>
#include <vector>
using namespace std;
int bsearch(vector<int> &arr, int target)
{
int l = 0;
int r = arr.size() - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (arr[mid] <= target) { // 条件定义处
l = mid;
} else {
r = mid - 1;
}
}
return l;
}
int main()
{
vector<int> arr = {1, 1, 3, 5, 5, 8, 9};
int index = bsearch(arr, 5);
cout << "index " << index << endl;
return 0;
}
当然,依据 arr[i] <= target 条件同样分四种情况,与右属性的分析一样,这里不一一赘述。
由于这里是arr[i] <= 5的边界值,那么这里的index不再是3,而是4
查找的目标值有多个的情况分析
针对同一个target, 右区域模板满足找到target的索引最小值,左区域模板是满足target的索引最大值。
通过同时调用左区间模板和右区间模板,我们可以查找到一个重复元素的左边界和右边界。
对于二段性的界定分析
简单的二分法,一般是排序数组;而难度较高的题则不一定是有序数组,可能是满足某个规律的一组数据。因此难点更多在于二段性的划分。
更可能的是,由于给定数据太过无规律,根本没有考虑到考题是一道二分题。
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n)
时间复杂度和 O(1)
空间复杂度。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
这题的难点在于二段性的识别,在target左边,奇数下标与偶数下标相等,且相同的数偶数下标在前。
在target右边,奇数下标与偶数下标相等,且相同的数奇数下标在前。通过这种二段性,可以构造check函数。
class Solution {
public:
bool check(vector<int>& nums, int mid) {
int x = nums[mid];
if (mid % 2 == 0) {
if (nums[mid+1] == x) {
return false;
} else {
return true;
}
} else {
if (nums[mid-1] == x) {
return false;
} else {
return true;
}
}
return false;
}
int search(vector<int>& nums, int l, int r) {
while (l < r) {
int mid = (l + r) / 2;
if (check(nums, mid) == true) {
r = mid;
} else {
l = mid + 1;
}
}
return nums[l];
}
int singleNonDuplicate(vector<int>& nums) {
if (nums.size() == 1)
return nums[0];
return search(nums, 0, nums.size()-1);
}
};
二维矩阵二段性分析
二维有序矩阵一般满足从左到右递增,从上到下递增。由于这个性质,【左下角】是解决二维矩阵的关键。
锚定一个左下角值x,在x下方的矩阵所有数必定大于等于这个数;如果target < x, 说明target在x的上半区域。
当target > x时,由于每次指定的x都是【左下角】,是这一列的最大值,因此target在x的右边区域。
通过这个二段性分析,不断降低行数,增加列数,直到要搜索的target变成真正的【左下角】。
编写一个高效的算法来搜索 m x n
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false