聊聊左闭右开区间

《编程珠玑》里说过:大约10%的专业程序员,才能够正确地写出二分查找。尽管第一个二分查找程序于1946年就公布了,但是第一个没有bug的程序在1962年才出现。

二分查找易错的原因之一,是比较范围写的不对,对于一些情况是错误的。记住比较范围是左闭右开区间,就很难写错了。

左闭右开区间在STL数据结构的构造参数,一些api的返回值范围等场景,也都广泛应用。仔细思考下,还是有些规律在里面的。

先来看三个使用的场景。

一、javascript随机数返回值

在js中,产生随机数函数 Math.random() 返回值是[0,1)间的实数。

通过偏移和缩放返回值,能映射到任意实数区间。再通过取下界,可以随机产生任意整数,从而满足各种随机数的场景。

如果返回范围是全封闭,全开,或者左开右闭可以吗?
答案是不可以。

全封闭:包含了上界的1,会造成产生整数的随机数概率不均匀。

例如要随机产生0-9十个整数,做法是Math.floor(Math.random()*10),产生的数是均匀随机,满足需求。如果包含了上界的1,那么产生10是超出范围的。只有随机出1时才产生10,概率要比其他整数小很多,随机的概率就不均匀了。

全开区间:没有了0点,映射生成整数的时候,产生0的概率要比其他整数的概率要低,不满足随机均匀。

左开右闭:左开右闭和左闭右开是对称的,理论上是可以的,但是和左闭右开相比,有些不习惯。

二、迭代器遍历

for(int i = 0; i < count; ++i)
{
    ...
}

在C语言中,数组的个数是count,遍历的循环下标的区间也是左闭右开[0,count)。因为数组是从0开始计数,所以不能包含上界。

A、要写为左右都是闭区间,[0,count-1]也是可以的。但是有个减一比较别扭。

B、有些迭代器,例如链表或数的子节点指针,就要设计个哨兵节点表示结束,判断结束时直接判断迭代器是否不等于end,不用右开区间,要判断next是否是null,会多出一些额外的判断,对一些边界要用特殊逻辑。

for(iterator it=a.start();it!=a.end();++it)
{
    ...
}

三、在算法和api中的左闭右开区间

在C语言标准库的二分查找bsearch,快速排序qsort的函数里,无论是函数内部实现,还是参数,都传的左闭右开区间;

在STL容器的构造函数,算法操作,也都是左闭右开区间。

一方面是为了容易迭代,更通用。

另一方面是像二分、快排,都是分治算法,一个左闭右开区间[x,y),子区间可以分解为[x,y0),[y0,y1),[y1,y2)...[yn,y),父子同构,天然适合分治实现。

在整数范围内,如果非要写为左闭右闭区间,也是可以的。[x,y]分解的子区间为[x,y0-1],[y0,y1-1],[y1,y2-1]...[yn,y]。但是无论怎么分,总有一个区间和其他不同,划分偏左或偏右一个元素,划分是不整齐的。要打各种边界处理补丁来弥补。

在实数范围来进行计算,就用左毕右闭区间就不能实现了。

例如利用二分在单调递增的函数上逼近计算,每次都是分为[x,(x+y)/2),和[(x+y)/2,y)两个区间,如果左右封闭,要么进行一次重复判断计算,要么表示不出来。在实属范围内,给定一个实数x0,是给不出和他距离最小的x1的确切值的,更别说用计算机表示了。

总结

左闭右开区间划分的子区间,也符合左闭右开的性质,同构的;

按比例划分子区间后,映射到边界节点上的概率也是成比例的;

全闭区间要处理边界情况,有特殊点,对程序设计和算法理解造成障碍。

综上,在应用数字范围的场景中,抽象成左闭右开区间,是一个好方法。

你可能感兴趣的:(技术学习,架构师修炼之路)