不同查找情况下的二分查找
对于有序序列,二分查找是一种非常高效的查找算法。但是二分查找一般刚接触的时候,很难处理好边界条件,下面我们来看看不同情况下面的二分查找。
注意:
- 除特别说明外,均返回索引值。
- 本文中关于“目标值”这个词,认为需要返回下标所在的值被称为目标值。因此找到某个小于或大于某个值num的值ret,我们认为ret是目标值。
- 本文中对于“中值”这个词没有明确定义,有时候是指下标,有时候是指值。但是不影响理解。
- 举例均为递增序列。
1. 查找指定值,没有返回-1
int binary_search(int *a, int l, int r, int n)
{
while (l < r)
{
int mid = (l + r) >> 1;
if(a[mid] == n) return mid;
if(a[mid] < n) l = mid + 1;
else r = mid;
}
return -1;
}
2. 查找指定值,如果有多个,返回第一个
对于多个相同值的情况,主要要考虑当中值等于目标值时,应该向左还是向右查找,以及如何向左或向右查找。比如下面的例子。
+-------+---+---+---+---+---+---+---+---+---+---+---+
| number| 1 | 2 | 3 | 3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+-------+---+---+---+---+---+---+---+---+---+---+---+
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10|
+-------+---+---+---+---+---+---+---+---+---+---+---+
^
mid
当中值等于目标值时,向左查找,那么当中值(index = 3
)找到了目标值 3
后,此时满足了条件 a[mid] == n
,但是我们还要继续向左查找,所以此时应该将右边界 r
设置为 mid
。
int binary_search(int *a, int l, int r, int n)
{
while (l < r)
{
int mid = (l + r) >> 1;
if(a[mid] >= n) r = mid;
else l = mid + 1;
}
return l;
}
3. 查找指定值,如果有多个,返回最后一个
按照上一种情况的思路,当中值等于目标值时,向右查找,那么当中值(index = 3
)找到了目标值 3
后,此时满足了条件 a[mid] == n
,但是我们还要继续向右查找,所以此时应该将左边界 l
设置为 mid
,而不是 mid + 1
,因为 mid
也有可能是最后一个目标值。
另外需要注意的是,l = mid
时,下一次循环的中值 mid
有可能等于 l
,(读者可以思考一下这是为什么吗,为什么r
不用担心这个问题?)这样会导致死循环,所以需要将中值 mid
设置为 (l + r + 1) >> 1
。
int binary_search(int *a, int l, int r, int n)
{
while (l < r)
{
int mid = (l + r + 1) >> 1;
if(a[mid] <= n) l = mid;
else r = mid - 1;
}
return l;
}
4. 查找第一个大于指定值的元素
对于这种情况,需要考虑的是当中值等于目标值相邻元素区间的最后一个元素时,如何向右查找。比如下面的例子。
+-------+---+---+---+---+---+---+---+---+---+---+---+
| number| 1 | 2 | 3 | 3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+-------+---+---+---+---+---+---+---+---+---+---+---+
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10|
+-------+---+---+---+---+---+---+---+---+---+---+---+
^
mid
此时中值 (index = 4
)等于 3
,目标值为 4
,需要向右查找,既当a[mid] == n
时,应该将左边界 l
设置为 mid + 1
。
int binary_search(int *a, int l, int r, int n)
{
while (l < r)
{
int mid = (l + r) >> 1;
if(a[mid] <= n) l = mid + 1;
else r = mid;
}
return l;
}
5. 查找最后一个小于指定值的元素
对于这种情况,需要考虑的是当中值等于目标值相邻元素区间的第一个元素时,如何向左查找。比如下面的例子。
+-------+---+---+---+---+---+---+---+---+---+---+---+
| number| 1 | 2 | 3 | 3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+-------+---+---+---+---+---+---+---+---+---+---+---+
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10|
+-------+---+---+---+---+---+---+---+---+---+---+---+
^
mid
此时中值 (index = 2
)等于 3
,目标值为 2
,需要向左查找,既当a[mid] == n
时,应该将右边界 r
设置为 mid - 1
。
int binary_search(int *a, int l, int r, int n)
{
while (l < r)
{
int mid = (l + r + 1) >> 1;
if(a[mid] >= n) r = mid - 1;
else l = mid;
}
return l;
}
分析
分析完上述五种情况,我们可以发现,问题关键在于:
- 避免死循环
- 当中值等于目标值区间时,需要向左还是向右查找
- 当中值处于相邻元素的区间内的边界时,向左、向右还是保持不变
避免死循环
由于循环条件是 l < r
,若 l
和 r
相邻时,中值 mid
会等于 l
,此时若让 l = mid
,并且(l + r) >> 1
,则会导致死循环。所以当 l
和 r
相邻时,若要让 l = mid
,需要保证 r = mid - 1
,既保证总会存在一种可能让区间缩小,同时设置中值 mid
为 (l + r + 1) >> 1
。
总结来说,避免死循环需要保证区间会缩小。
或者说,避免死循环需要保证,当 l
和 r
相邻时,mid
更新值不能和之前它赋值的值相同。
当中值等于目标值区间时,需要向左还是向右查找
这种情况就是上面的第二种和第三种情况,当中值等于目标值时,需要向左还是向右查找,取决于我们要找的是第一个还是最后一个目标值。
这时我们需要保证,下一次更新的中值 mid
需要向边界靠近,但是不能越过边界。
比如我们要找目标值区间的第一个值,那么当中值等于目标值时,需要向左查找。因此我们考虑当 mid
等于目标值下标时,对于 r
进行一些操作。因为若想让 mid
向左靠近,那么就需要让 r
向左靠近。
但是我们要考虑到,mid
有可能已经是最左边的边界了,因此每次更新 r
时,需要保证 r
不能越过边界,既需要让 r = mid
。
找目标值区间的最后一个值时,同理。
当中值处于相邻元素的区间内的边界时,向左、向右还是保持不变
这种情况就是上面的第四种和第五种情况,当中值处于相邻元素的区间内的边界时,向左、向右还是保持不变,取决于我们要找的是第一个还是最后一个大于等于或小于等于目标值的元素。
比如我们要找小于目标值的最后一个元素,那么当中值处于相邻元素的区间内的边界时,需要向左查找。因此我们考虑当 mid
等于目标值右边一个数的第一个下标时,对于 r
进行一些操作。因为若想让 mid
向左,那么就需要让 r
向左靠近。所以需要让 r = mid - 1
。
找大于目标值的第一个元素时,同理。
总结
总结上述内容,当我们需要二分查找的时候,需要考虑以下几个问题:
- 避免死循环
- 在某一区间内考虑需不需要越过边界。
设中值已经到了二分对比值所在区间的中间,要找谁就让中值等于二分对比值时,与之相反的边界更新,其更新的值按如下规则:
- 找二分对比值所在区间值
- 让与之相反的边界等于中值
- 找二分对比值所在区间相邻值
- 让与之相反的边界等于中值加上或减去1
问题 | 二分对比值所在区间 | 需要查找的目标值 | 与之相反的边界更新情况 | 更新中值 |
---|---|---|---|---|
1 | 严格递增序列,无重复 | 二分对比值 | if(a[mid] == n) return mid; |
mid = (l + r) >> 1; |
2 | 递增序列,有重复 | 二分对比值的第一个 | if(a[mid] >= n) r = mid; |
mid = (l + r) >> 1; |
3 | 递增序列,有重复 | 二分对比值的最后一个 | if(a[mid] <= n) l = mid; |
mid = (l + r + 1) >> 1; |
4 | 递增序列,有重复 | 大于二分对比值的第一个 | if(a[mid] <= n) l = mid + 1; |
mid = (l + r + 1) >> 1; |
5 | 递增序列,有重复 | 小于二分对比值的最后一个 | if(a[mid] >= n) r = mid - 1; |
mid = (l + r) >> 1; |