问题描述:
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼100001∼10000 范围内),表示完整数组。
接下来 q行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
思路:
题目给出升序排列的数组,查找一个元素的起始位置与结束位置,直接的思路是可以用for循环遍历数组,寻找第一个不小于目标元素的元素位置和最后一个不大于目标元素的元素位置,输出元素位置 。
时间复杂度为O(n) , (这里贴一个大佬对时间复杂度的解释的帖, 有对O的解释 )
考虑对暴力算法进行优化,该题的本质问题是去查找与目标元素相同的起始元素位置和结束位置,有序、查找 ,也是二分查找的条件, 所以该题可以使用二分查找去优化,时间复杂度为O(logn)
代码:`
#include<iostream>
using namespace std;
const int N=100010;
int q[N];
int main(){
int n, m;
cin>>n>>m;
for(int i =0 ;i< n ;i++) cin>>q[i];
while(m--){
int x;
cin>>x;
int l=0,r=n-1;
while(l<r){
int mid =l+r >> 1;
//查找第一个不小于x的元素位置
if(q[mid] >= x) r=mid ;//中间点大于x 将区间缩小到mid左侧 可以锁定到第一个小于x的元素位置
else l=mid + 1; //中间值小于x 区间缩小到mid右侧
} //最终r=l; 一定能确定一个数 (二分特性 最后一定l==r 一定能查询到一个数的位置)
if(q[l] !=x) cout<<"-1 -1"<<endl; // 如果最终确定位置不为x
else {
cout<<l<<" ";
int l = 0,r=n-1;
// 查找最后一个不大于x 的元素位置
while(l<r){
int mid = l+r+1>>1;
if(q[mid] <=x) l=mid;//中间值小于x 区间缩小到mid右侧 l==r时可以锁定最后一个不大于x的元素位置
else r=mid-1; //中间值大于x 区间缩小到mid左侧
}//二分查找 从mid右侧中挑选
cout<< l <<endl;
}
}
return 0;
}
`
总结:
二分查找的模板,以及边界问题 mid =l +r >>1 or mid =l + r +1 >>1 ;
/*首先默认mid = l + r >>1
根据第一次缩小区间的情况,
若if(check()函数的功能)将区间缩小至mid左侧
则mid =l + r >> 1 , r = mid ,l =mid + 1 */
while(l<r){
int mid = l + r >> 1;
if (check()) r = mid ; //缩小到mid左侧
else l = mid + 1 ;
}
/*
首先默认mid = l + r >>1
根据第一次缩小区间的情况,
若if语句将区间缩小至mid右侧
则 mid = l + r + 1 >> 1 , l = mid , r= mid - 1 */
while(l<r){
int mid = l + r +1 >> 1;
if (check()) l = mid ; //缩小到mid右侧
else r = mid - 1 ;
}
//左不加右加(第一次缩小区间),有加必有减(mid = l + r + 1 , r = mid - 1 ) ,左不加右加(第一次缩小区间)