做了力扣704的二分发现有acwing更全面的模板之后,趁热打铁,把这个学了。
目录
看到这道题,本想用力扣704那道题的解法,即,首先通过二分找到目标元素,即mid值所对的元素,确定了起始位置,然后再写一个函数计算终止位置,具体实现是,累计目标元素出现的次数,再加上begin的初值也就得到了end的值。
根据上面的想法,然后我就兴致冲冲的写了一堆bug,,,
在不断的调试代码中,我终于纠正到了最后一个bug,也终于发现这种方法是不可行的,最重要的是终于发现了704那道题的二分和我们的acwing模板例题的不同。唉。。。。
704思路的误用(可跳)
自然是想着看用704那道题的二分思路,照葫芦画瓢的写出找起始位置的函数 。
先展示find错误代码!!!
void find(int a[],int k,int n)
{
int left=0;
int right=n-1;
int begin=-1,end=-1;//初始化为-1 -1 当while循环结束,值没有发生改变就表明数组中不存在目标元素(是不是还挺合理[doge 苦笑])
while(left<=right)
{
int mid=left+(right-left)/2;
if(a[mid]==k)
{
begin=mid;
end=begin+find2(begin,right,a,k);//根据目标元素的个数 得到end的值
break;
}
else if(a[mid]<k)
{
left=mid+1;
}
else
{
right=mid-1;
}
}
cout<<begin<<" "<<end<<endl;
}
接着不就是end值也就是数组中目标元素个数的求解嘛。又是好长时间的修改调试之后。
写出了如下这个我怎么看也没问题的find2代码
int find2(int begin,int right,int a[],int k)
{
int i=0;
while(begin<=right)
{
if(a[begin++]==k)//begin先使用后++ 说明i所得到的是k的总个数
{
i++;
}
}
return i-1;//但我们计算终止位置的时候,是用begin后有几个k决定的
}
结果执行,还是不对。。。
于是我明白了,问题在于,find函数所找到的元素,并不一定是起始元素的位置!!!!
ok,正片开始
由于704题目要求,当然如果你没看过我的那篇题目详解或者没做过那道题,可能不知道或者忘记了,没关系,再回顾一下那道题的题干。
仔细看看清楚,这道题所求解的,是在已知升序数组中是否存在目标元素。
写出的函数所实现的功能是,通过二分查找,判断数组中是否存在目标元素。
并不具有普适性(毕竟哪有那么多像这道题一样的二分基础题对吧?),对于更复杂的问题就需要更加高级的二分算法。
789暴力解法
嗯,为了像之前一样完全展示我的做题过程,我还是先展示 发现704思路不适用时,我是如何思考的。
发现上述查找第一个目标元素的功能无法用类似704的方法实现,于是很自然想到,既然数组是升序的,那就遍历吧。
这就不再多说了吧,自然是暴力实现的。我将必要的解释写在注释里,方便理解。
暴力解法代码如下
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n, q;
int find2(int begin, int right, int a[], int k) // 找end
{
int j = 0; // 表示目标元素出现的次数
while (begin <= right)
{
if (a[++begin] == k) // begin前置++是因为我们想得到的是begin之后有多少个目标元素,不包含本来就已经有的这一个
{
j++;
}
}
return j;
}
void find(int a[], int k, int n) // 查找数组中遇到的第一个目标元素
{
int left = 0;
int right = n - 1;
int begin = -1, end = -1; // 令初始值为-1 -1 当数组中没有目标元素时直接输出
while (left <= right)
{
if (a[left] == k) // 只要找到了数组中第一个目标元素,就得到了begin
{
begin = left;
end = begin + find2(begin, right, a, k); // 得到begin之后找end
break; // 那么这个循环得功能就已经实现了,break跳出循环即可,避免给begin多次赋值而导致错误
}
left++; // 必须在这里++而不能在前面if语句里,是因为如果在if语句里++,会导致begin赋值错误
}
cout << begin << " " << end << endl;
}
int main()
{
cin >> n >> q;
int i = 0; // 表示数组下标从零开始
int j = n;
while (j--) // 用j来代替n做遍历,避免下面find函数的参数n值得改变出现错误
{
cin >> a[i++];
}
int k = 0; // 目标元素
while (q--)
{
cin >> k;
find(a, k, n);
}
return 0;
}
嗯,整整50行代码。
观察发现这样写时间复杂度是O(nq)
这就不再赘述了。我们快开始看二分解法吧。
789二分解法
我们首先要明确,不管是704那种简单的二分查找,还是像这道题一样复杂的查找范围的二分,我们的目的都是:
通过二分不断更新mid的值,直到得到mid=k ,当k存在于数组中时,mid最终的值就是我们想要的 二分的结果或者说是答案。
但是,即使k并不存在于数组中,二分算法仍然会计算搜索区间的中点mid,然后根据a[mid]与目标元素的大小关系来更新搜索区间的边界,也会得到一个mid值。
所以无论k是否存在,二分一定是有结果的。
如果使用像704那道题的说法:通过二分查找,判断数组中是否存在目标元素。
那么这道题就应该是:一组元素中存在一个分界线,分界线以前满足这个性质,分界线以后不满足这个性质。寻找 左边界(即 数组中从左向右看 第一个>=k的元素) ,即模板一;寻找右边界(即 数组中从左向右看 最后一个<=k的元素 ),即模板二。由此得到数的范围。
关于这两个模板解释如下图
好啦,二分算法的思想就想明白了。
另外这道题还需要注意的:
1.while()循环出口的判断条件left<right不能取等:
我们如何会想到取等?因为在704里,是可以取等的对吧?并且我们给出了清晰的可以取等的原因,为什么这里不行了呢?
那道题我们是把二分函数写在了主函数之外,我们找到了mid值,直接return,就跳出了while循环,不存在死循环的问题。
但是这里我们采用把二分过程写在主函数里,倘若条件为left<=right,当left+1=right时,数组中有两个元素(a[left]和a[right]),mid=left,且恰好满足a[mid]>=k,更新了right=mid也就=left,那么这时,left=right,倘若循环条件真的是left<=right,此时仍然符合,就会继续进入循环,但是之后的循环根本不会再改变范围,所以会陷入死循环。
2.模板二中mid=(left+right+1)/2:
为什么需要加1呢?
这是为了更精确的找到最后一个符合条件的元素,我们尽量把左区间向后找。
假设:我们要在数组【1,2,2,3】中查找2的最后一个出现位置。初始时,搜索区间为【0,3】,此时mid=left+right>>1 =1,所以我们将搜索区间更新为【1,3】此时,搜索区间缩小了。
在下一次迭代中,mid=2,搜索区间更新为【2,3】。此时,搜索区间再次缩小。
然而,在下一次迭代中,mid仍然等于2,搜索区间为【2,3】,并未缩小,之后不管循环多少次,虽然数组中还有两个元素,但mid总是等于2,left总是等于2,陷入了死循环,得不到我们想要的值。
所以我们把mid设置为left+right+1>>1,使其更靠近后半部分,与我们想找的“最后一个”形成一种促进效果。
而模板一中不需要,是因为mid的值不+1会更靠近搜索区间的左边界。这样,在更新搜索区间的边界时,搜索区间就会更多地向前移动,从而更容易找到目标元素的第一个出现位置。
嗯,关于模板二到底是按照 避免死循环的原因 还是 更容易正确的找到目标这个原因来解释mid偏移1,我还不能准确的判断,希望指点,非常感谢。
好啦,注意点也说完了,下面就是代码实现了。
#include <iostream>
using namespace std;
const int N = 100000;
int a[N];
int n, q;
int main()
{
cin >> n >> q;
// 输入数组
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
// 开始执行询问
while (q--)
{
// 输入目标元素
int k;
cin >> k;
// 定义左右边界
int left = 0, right = n - 1; // 位置从零开始
// 开始二分边界
while (left < right) // 注意不可取等
{
int mid = left + (right - left) / 2;
// 根据题目需要,首先要找到数组中 出现第一个目标元素 的位置---查找不小于目标值的第一个位置
if (a[mid] >= k)
right = mid;
else
left = mid + 1;
}
// 循环出口是right=left=mid
// 通过上述二分:不论数组中k是否存在,都会得到一个mid值
// 所以我们要先判断该值是否等于k
if (a[left] != k) // 说明数组中不存在k
cout << "-1 -1" << endl;
else // a[mid]=k时,说明起始位置已经找到,接下来要找终止位置
{
cout << left << " "; // 记得输出起始位置 注意空格
left = 0;
right = n - 1;
// 查找终止位置 ---查找不大于目标值的最后一个位置
while (left < right)
{
int mid = left + right + 1 >> 1; // 为了避免死循环需要+1再除二
if (a[mid] <= k)
left = mid;
else
right = mid - 1;
}
cout << left << endl;
}
}
return 0;
}
ok,好长【笑哭】
整数二分就说到这里了。
有问题欢迎指出,非常感谢!!
也欢迎交流建议奥。