目录
一、问题介绍
给定升序排序的长度为n的数组,查询m次(自定义次数)
每次查询,返回一个元素的起始位置和终止位置(从0开始)
如果数组中不存在该元素,则返回-1 -1
二、代码模板
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int q[N];
int main() {
scanf_s("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf_s("%d", &q[i]);
int l = 0, r = n - 1;
while (m--) {
int x;
scanf_s("%d", &x);
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[l] != x) printf("-1 -1\n");
else {
printf("%d ", l);
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
printf("%d\n", r);
}
}
return 0;
}
三、算法解释
问题分析
解决该问题实际上是确定需要查询的元素在序列中的左右边界问题,比如1,2,2,2,3,3,4这段序列,需要查询2时就是要确定2对应的边界位置为1和3,即元素2的左边界和右边界。所以问题可以拆分为确定元素的左边界和右边界。
算法流程
算法的流程是先确定左边界,然后确定右边界。
如何确定左边界和右边界?利用2分法循环缩小l~r的范围直到l == r,来分别确定左边界和右边界。
后面确定左边界和右边界会用到的两个条件:
1.左边界左边(前面)的元素都小于x,位置在左边界或在左边界右边(后面)的元素都大于等于x
2.右边界右边(后面)的元素都大于x,位置在右边界或在右边界左边(前面)的元素都小于等于x
确定左边界:
设置初始边界l和r为0和n-1,进行二分,取mid = l + r >> 1,(后面会解释为什么不是mid = l + r + 1 >> 1),然后将去q[mid]与我们所要查询的元素进行比较,如果q[mid] >= x,则说明mid在左边界的右边(q[mid] > x)或就是右边界(q[mid] == x),接下来我们可以将l~r的范围缩小,因为mid在左边界右边或就是有边界,所以令r = mid(为什么等于mid而不是mid-1呢,因为mid可能就是左边界);如果q[mid] < x,则说明mid在左边界的左边(q[mid] < x),接下来将l~r的范围缩小,因为mid在左边界左边,所以令l = mid + 1(为什么加1呢?因为mid是在左边界左边肯定不是左边界)。然后不断循环上面的步骤来缩小确定边界的范围直到l == r时,确定了边界的位置为l(r也一样),然后再判断这个位置是不是边界(注意这里的边界只是我们通过缩小l和r的范围得到的可能作为左边界的位置,但这个位置可能不一定是元素x的左边界,所以我们还要判断这个位置对应的元素等不等于x,来最终确定是不是左边界)。如果这个位置不是左边界,则输出-1 -1,如果是左边界,则输出l(输出r也行),然后接下来要确定右边界。
确定右边界(和确定左边界比较相似了):
右边界是我们在已经确定了左边界以后进行了,所以右边界一定存在(至少也会出现在左边界的位置)。
设置初始边界l和r为0和n-1,进行二分,取mid = l + r + 1 >> 1(这里后面也会解释为什么不是mid = l + r >> 1),然后将去q[mid]与我们所要查询的元素进行比较吗,如果q[mid] <= x,则说明mid在右边界的左边(q[mid] < x)或就是右边界(q[mid] == x),接下来缩小l~r的范围,因为mid在右边界左边或就是右边界,所以令l = mid(为什么等于mid而不是mid+1呢?因为mid可能就是右边界);如果q[mid] > x,则说明mid在右边界的右边(q[mid] > x),接下来将l~r的范围缩小,因为mid在右边界右边所以令r = mid - 1(为什么要减1呢?因为mid是在右边界右边肯定不是右边界)。然后不断循环上面的步骤来缩小确定边界的范围直到l == r时,确定了边界的位置l(r也一样),然后不需要判断该位置是否就是右边界,因为既然确定了左边界就一定有右边界(就算只有一个元素,右边界的位置也可以就是左边界的位置)。然后输出l(输出r也行)
总结
实际上算法的流程就是1.用2分法缩小范围确定左边界位置 2.判断确定的位置对应元素是否等于x来确定是否是左边界 3.用2分法缩小范围确定右边界位置(该位置对应元素一定等于x)
四、边界问题
有两个前文落下未解释的边界问题:
1.为什么确定左边界时是mid = l + r >> 1而不是mid = l + r + 1 >> 1?
因为如果用mid = l + r + 1 >> 1,则当循环到最后只剩两个数时,mid选取的是第二个元素的位置即r,如果q[mid] >= x,则r = mid即r = r,范围不变,会进入死循环。e.g. x取1,最后只剩两个元素1,2,mid为1,q[mid] >= x,所以循环的范围不变,进入死循环。而使用mid = l + r >> 1,则会取前一个元素位置为mid即l,q[mid] >= x时,r = mid,因为此时l == r所以会退出循环;q[mid] < x时,l = mid + 1也会出现l == r退出循环。所以这样设置mid = l + r >> 1来确定左边界不会出现边界问题。
2.为什么确定右边界时是mid = l + r + 1 >> 1而不是mid = l + r >> 1?(和前面一个问题类似)
因为如果用mid = l + r >> 1,则当循环到最后只剩两个数时,mid选取的是第一个元素的位置即l,如果q[mid] <= x,则l = mid即l = l,范围不变,会进入死循环。而使用mid = l + r + 1 >> 1,则会取后面一个元素位置为mid即r,q[mid] <= x是,l = mid,因为此时l == r所以会退出循环;q[mid] > x时,r = mid - 1也会出现l == r退出循环。所以这样设置mid = l + r + 1 >> 1来确定右边界不会出现边界问题。