vlambda博客
学习文章列表

【二分查找】边界问题

【练习】http://111.229.45.229/

P1004 数字查找

P1005 等式配对


1004: 【二分】数字查找2

时间限制:  1 Sec   内存限制: 128 MB

题目描述

输入n个数字,进行T次查询,每次输入关键字,查询第一个大于关键字的数字,若不存在则输出"NO"。

输入

第一行n和T,代表n个数字,T次查询
接下来一行n个数字
接下来T行表示每次查询的数字

输出

对于每次查询,输出结果

样例输入

5 3
2 3 1 4 7
6
1
7

样例输出

7
2
NO


【算法分析】

  传统的对分查找其实并没有太多应用场合,许多题目背景中,我们无法直接判断出当前元素是不是答案

  不同于高考要求的传统对分,上题中我们无法直接得出查找的元素是否是我们想要的答案。以查找值5为例,我们只能判断当前的查找元素是否大于5,却不能知道是否前面还有大于5的,所以无法直接确定当前是不是第一个大于5的元素了。

  当然,我们可以选择先查找5,查找结束后,在结束时的位置继续向右循环查找,这种方法可以很大程度上提高效率,但是对于某一些特殊数据仍然会耗费大量时间,这里需要用到对分查找边界了。


再举一个例子

  • 序列1、3、4、6、7、8、10,查找第一个大于5的元素。

观察这个序列,要查找第一个大于5的元素位置,这里用颜色标记出来:

13467810

红色的是符合(大于5)的,蓝色是不符合(<=5)的。由于我们希望找到第一个大于5的元素,此时,根据是否大于5,将区间分成了两部分我们需要查找的是一个临界点,也就是两个区间的交接处,这是对分查找变体应用中最为常见的形式。

 

我们还是设置变量lrmidkey

如果a[mid]小于key(不符合的),那么说明答案在右边,于是l=mid

如果a[mid]大于key(符合的),那么说明答案在左边或者当前就是答案,于是r=mid

最后一定会逼近答案

如下图所示,我们找的第一个大于5的数字,即紫色边界的右边第一个数字,为“6”。

                           

【基本代码】

While L + 1 < R:

         m= (L + R) >>1

         Ifkey>a[m]:  //分支1,落在右半区间

                   R= m

         Else:        //分支2,落在左半区间

                   L= m

Print(a[R])

我们分析这段二分查找的代码:

  首先和传统二分的最大区别是只有两个分支(因为只有左右区间两种情况),这样一来,也就不存在中途跳出的情况了,必定是当L>=R时跳出。

  那么,最终第一个大于5的位置是什么呢,lr?还是mid

  如下图所示,这种模板下,LR在跳出时,必定停留在这两个位置(可以多模拟几遍,会发现除非输入大于1等于10的数字,其它情况,在跳出时R的位置都刚好是答案)。

  这是因为LR的移动方式所导致的,由于LR都只能移动到m,所以LR都不可能越过紫色的“边界”,最后“隔海相望”,于是将循环边界设置为L+1<R(小细节:如果输入的数字是一个大于等于10的数字怎么办?也就是说整个序列都不存在大于key的数字。具体解决办法可以写if语句特殊判定,或者将R的初始值改为n+1,在输出时特判)。

【二分查找】边界问题

其实打法并不唯一,对于新手来说最后逼近答案时的临界情况是不好把握的,需要自己去推敲,这里共整理了4种模板。注意模板之间没有本质区别,一种模板可以无差别地替换成其它模板,其功能完全一样。

模板2

While L <= R:

         m= (L + R) >>1

         Ifkey>a[m]:  //分支1,落在右半区间

                   R= m-1

         Else:        //分支2,落在左半区间

                   L= m+1

Print(a[L])

此种模板下,LR在跳出时的位置如下图所示,则对应的,答案变为a[L]

                           

【二分查找】边界问题

模板3

While L < R:

         m= (L + R) >>1

         Ifkey>a[m]:  //分支1,落在右半区间

                   R= m-1

         Else:        //分支2,落在左半区间

                   L= m

Print(a[L])

此种模板下,LR在跳出时的位置如下图所示,则对应的,答案变为a[L+1]a[R+1]

模板4

While L < R:

         m= (L + R + 1) >>1

         Ifkey>a[m]:  //分支1,落在右半区间

                   R= m

         Else:        //分支2,落在左半区间

                   L= m+1

Print(a[L])

此种模板下,LR在跳出时的位置如下图所示,则对应的,答案变为a[L]a[R]


【小结】二分查找边界问题在编程中的应用更为广泛,但是由于其“模板”较多,导致初学者出现理解的混乱,事实上在抓住变化规律前,可以尝试先记住一种模板,之后渐渐理解其它模板。集中模板在本质上没有任何区别,互相之间可以完全替代。

  二分查找的难点还是在于二分的建模和查找时的验证,这回在进阶篇中进行介绍(感觉又在挖坑,,,)。