1.背景
空间点具有X,Y,Z坐标等数据,一些情况下我们需要将点作为map容器的key值,比如识别重复点或处理轮廓等情况。
2.问题
将点作为map的key值,需要自定义比较器或者重载实现点类的小于<操作运算符,判断规则是a < b 和 b < a都为false时,认为a和b相等。
一种比较器或小于运算符重载实现如下,即判断两点在一定误差范围内时,两点相等。
bool PointBorderData::operator< (const PointBorderData& other) const
{
if (abs(point.X - other.point.X) > 1e-3)
return point.X < other.point.X;
if (abs(point.Y - other.point.Y) > 1e-3)
return point.Y < other.point.Y;
if (abs(point.Z - other.point.Z) > 1e-3)
return point.Z < other.point.Z;
// 相等就 return false
return false;
}
然后这种实现可能会有问题,有些情况下“相同的点”(在误差范围内)却在map容器中都保留了,经过判断,“认为他们不同”,这是什么原因呢?
聪明的你可能现在也不会想到,直到自己遇到并处理此类问题,或继续看完本文。
3.原因
红框中选中的两个点在误差范围内,应当识别为相同点,在map容器中保留一份,但实际缺各自都被保留了。我们仔细观察其中间的点,嗯,是不是法线问题了?
C++ map容器底层是红黑树,容器元素正向是升序排列,即正序遍历时得到的元素是从最小到最大。
在这里我们姑且称上述3个点为第0、1、2点,第1个点比第0个点小,为什么反而排在第0个点后面?我们对照着前面的代码来分析,第0和1个点x坐标在误差范围内,所以比较y坐标,y值超过了误差范围,且第1点的y值小于第0点,即认为第1点小于第0点,所以它排在了第0点后面。到这里好像也没啥问题,我们继续看~
第3点与第2点比较后,第3点大于第2点,这样好像得到了结论,第0点 < 第1点 < 第2点,得到了3个“不同”的点,那为什么不让第2点和第0点直接比较呢?这样就能识别出它俩相同。
好问题,map底层是红黑树,看下图,
在某些情况下,比如“尝试插入”时的顺序为第1点,第0点,第2点时。在插入第0点时(第1点已插入),其经过一顿比较器执行后需要和第1点比较,因为比较后第0点 < 第1点,所以第0点放在了1点的左分支, 在插入第2点时,(同样经过一顿比较器执行后)到了要和第1点比较的时刻,比较后第1点 < 第2点,第2点放在了第1点的右分支上,整个过程,第2点根本没有机会和第0点比较!所以没有被识别为相同点!
好吧,终于知道问题了。改成下面这样行吗?答案时仍然可能会有问题,具体原因可以自行脑补。
bool PointBorderData::operator< (const PointBorderData& other) const
{
if (abs(point.X - other.point.X) > 1e-3)
return point.X < other.point.X;
if (abs(point.Y - other.point.Y) > 1e-3)
return point.X == other.point.X ? point.Y < other.point.Y : point.X < other.point.X;
if (abs(point.Z - other.point.Z) > 1e-3)
return point.X == other.point.X ? (point.Y == other.point.Y ? point.Z < other.point.Z : point.Y < other.point.Y) : point.X < other.point.X;
// 相等就 return false
return false;
}
4.总结
由于点在一定误差内判断为相同,比较好的处理方式是将点坐标保留一定的精度(比如乘1e3,再四舍五入取整)后进行严格数值的大小判断。
在有些情况下需要“严格彻底”的识别一定误差内的相同点,比如路径搜寻,就需要有逻辑严格的代码实现,有些情况识别不彻底也不会出错(比如点合并),最多就是多一些“相同”点。最好还是用逻辑准确的代码实现,避免一些超出我们当前认知的“不明确行为”。
欢迎交流:公众号:geometrylib