想象一下这样的场景:幼儿园老师带着二十几个小朋友到操场做游戏,老师想让大家手拉手围成一个最紧凑的大圆圈,把所有人都圈在里面,而且这个圈的周长要最短、绳子要最省。你会怎么做?聪明的老师会说:“先找到一个最靠边的小朋友当旗手,其他人按从旗手看出去的方向排好队,然后一个接一个拉进圈里,发现有人拐弯方向不对(本该右转却左转)就请他出列。”这就是 Graham 扫描的思路。另一位老师更直接:“大家先按从左到右、矮个在前排好队,我先把上半圈的队伍拉出来,再把下半圈的队伍拉出来,拼在一起就是一个完整的大圈。”这就是 Andrew 单调链的思路。两条路殊途同归,都能用最快的方式找到那个“最外圈”,只是排队的规则、检查的细节不一样。本文带你用这个故事看懂两大凸包算法的原理、差异与选型,从此面对“一堆点怎么求外围边界”的问题时,心里永远有谱。
凸包是计算几何的基石操作,在自动驾驶障碍物圈定、图像轮廓提取、点云聚类等 AI 场景中无处不在。掌握 Graham 与 Andrew 两种经典算法及其取舍,是构建扎实工程能力的必经之路。
可视化演示
凸包算法中Graham Scan基于极角排序,Andrew单调链基于坐标排序,两者都是O(nlogn)解法。本... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
一、凸包问题:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
凸包概念与意义
老师带着孩子们做围圈游戏,第一步要弄明白:什么才算“一个合格的圈”?规矩很简单:所有小朋友都必须站在圈内(或圈边上),圈必须是凸的——不能有凹进去的“口袋”,而且这个圈的周长要尽可能短。把小朋友抽象成平面上的点,这个“最短凸圈”就是凸包。
在数学上,凸包是包含点集的最小凸集。想象你在桌上钉了一排图钉,用一根橡皮筋把它们全部套住,松手后橡皮筋绷紧形成的多边形,就是这些图钉的凸包。橡皮筋只会碰到最外面的图钉,内部的图钉完全不影响它的形状。这正是凸包名字的由来——它是“包住所有点的最外层凸壳”。
凸包在 AI 与工程中无处不在:自动驾驶用凸包圈定障碍物群的安全范围,机器视觉用凸包描述手部轮廓,点云聚类后用凸包生成包围盒。理解凸包,就理解了“如何给一堆散点画最紧凑的边界”。
回到围圈游戏:凸包就是老师最终让小朋友手拉手形成的那条最短的凸线圈。接下来所有算法,都是在回答同一个问题——如何高效地找出“哪些小朋友该站在圈上”。
- 凸组合条件每个点 pᵢ 配一个权重 λᵢ,权重必须非负,且所有权重之和恰好为 1,保证结果点落在点集的“内部范围”内。
- 所有加权平均的集合遍历所有可能的权重分配,得到的全部点构成的集合就是凸包,它恰好是最小的凸多边形
- 几何直觉两个点的凸包是线段,三个点组成的凸包是三角形,多个点的凸包是最外圈围成的凸多边形
计算几何基础
围圈时老师需要一条规则来判断“新加入的小朋友应该站在哪儿”。这条规则就是“左转还是右转”:假设你已经确定了两个小朋友 A、B 的位置,现在来了一位新朋友 C,老师要看从 A 到 B 再到 C 的路线是向左拐还是向右拐。向左拐说明 C 在圈的外侧,可以保留;向右拐说明 C 缩进了圈内,得请他出列。这个“左转右转”的数学工具,就是向量叉积。
叉积是计算几何的“红绿灯”。两个平面向量 a、b 的叉积为一个标量(实际上叉积向量垂直于平面,其模长为 |a||b|sinθ)。它的符号直接告诉我们旋转方向:正数表示 b 在 a 的逆时针方向(左转),负数表示顺时针方向(右转),零表示两者共线。更妙的是,叉积可以通过坐标直接计算,不需要任何三角函数,整数坐标下甚至完全精确。
有了叉积这把尺子,Graham 和 Andrew 就有了共同的“检查口令”:每次准备把新点纳入凸包时,都看它相对于当前最后两个点是不是左转。如果是右转或共线,说明当前凸包顶点“突出去”了,需要回退。这个“左转保留、右转弹出”的规则,贯穿两大算法的全部扫描过程。
老师手里的“左转右转”规则,本质上就是叉积。下一个问题:有了规则,该按什么顺序把小朋友拉进圈里?这引出了两种截然不同的排队策略。
- 坐标代入将向量 a 的 x 分量乘 b 的 y 分量,减去 a 的 y 分量乘 b 的 x 分量,得到一个标量结果。
- 符号判决结果为正表示 b 相对 a 逆时针旋转(左转),为负表示顺时针旋转(右转),为零表示两向量方向相同或相反(共线)。
- 三点转向判断 A→B→C 的转弯方向时,令 a = B−A、b = C−B,代入叉积公式即可,无需计算任何角度。
Graham 算法
Graham 老师的做法很有仪式感:先请队伍最左下角的小朋友(y 坐标最小,若相同取 x 最小)当旗手,站到圈的中心参考点。然后其余小朋友按“从旗手看出去的方向角”从小到大排队——方向角最小的先来,像钟表指针从 0 度开始顺时针扫过一圈。排好队后,老师挨个把小朋友拉进圈,每拉进一个,就检查“最后三个人的转弯方向”:若是左转就留下,若是右转(或共线)就把中间那个小朋友请出列,继续回退检查,直到满足左转为止。
这个“拉进—检查—回退”的过程,在计算机里用栈完美模拟。栈里存的是当前认定的凸包顶点。新点到来时压入栈顶,然后 while 循环检查栈顶三个元素:若 cross(倒数第三,倒数第二,倒数第一) ≤ 0,说明倒数第二点不是凸包顶点,弹出它,继续检查,直到新的三个点形成左转。扫描完所有点后,栈中剩下的就是凸包顶点。
Graham 的关键在于“按角度排序后一趟扫描”。极角排序本身是 O(n log n),扫描阶段每个点至多入栈一次、出栈一次,是 O(n)。它的思想之美在于把二维的“围圈”问题,通过角度排序降维成一维的“排队检查”问题。
Graham 老师的方向角排队法思路清晰、教学意义极强,但它有一个小隐患:计算方向角需要调用 atan2 反三角函数,这是浮点运算,在角度非常接近时可能产生精度误差。接下来出场的 Andrew 老师,换了一个更“憨厚”但更稳的排队方式。
- 栈顶三点设栈中最后三个点按顺序为 P_{k-2}(倒数第三)、P_{k-1}(倒数第二)、P_k(当前新点)。
- 叉积判定计算从 P_{k-2} 到 P_{k-1} 再到 P_k 的转向叉积,若 ≤ 0 表示右转或直行,此时中间点 P_{k-1} 凹进圈内。
- 弹栈回退将 P_{k-1} 弹出栈,因为它在凸包内侧不可能是边界点,然后继续用新的倒数第二、倒数第三与新点比较。
Andrew 算法
Andrew 老师的策略完全不同:不找旗手、不算角度,直接让所有小朋友按“从左到右,x 相同则矮个在前”排成一列。然后他从左端出发,先圈“上半圈”(下凸链):一个接一个把小朋友拉进圈,规则和 Graham 一样——最后三个点必须左转,否则弹出中间点。走到最右端后,再调头从右往左走,圈出“下半圈”(上凸链)。两条链拼在一起,就是完整的凸包。
为什么不用角度排序?因为按 x 坐标排序是纯整数比较(或浮点比较),完全不涉及三角函数,数值上更干净。Andrew 的精妙之处在于:凸包天然由“上边界”和“下边界”两部分组成。从左往右扫,遇到的是不断“左转上升”的下边界;从右往左扫,遇到的是不断“左转下降”的上边界。两次扫描各维护一个单调栈,合起来恰好覆盖凸包的全部顶点。
这个思路被称为“单调链”(Monotone Chain),因为每条链在 x 方向上都是单调递增(或递减)的。对比 Graham 的“一维角度排序 + 一圈扫描”,Andrew 是“一维坐标排序 + 两段扫描”。两者排序都是 O(n log n),扫描都是 O(n),但 Andrew 的排序比较更简单、更稳,常数因子也更小——这也是它在工程上更受欢迎的原因。
Andrew 老师的“排两趟队”法既稳又省事,和 Graham 老师“按角度排一趟队”殊途同归。那么,两位老师的方案到底谁快谁稳?下一节我们掰开揉碎对比一番。
- 坐标排序将所有点按 x 升序排列(x 相同按 y 升序),得到从左到右的一列队伍。
- 下凸链构建从左到右扫描每个点压入栈,若最后三个点叉积 ≤ 0(未左转)则弹出中间点,扫描完得到下边界。
- 上凸链构建从右到左再次扫描每个点,同样的左转检查,得到上边界,两链首尾相连构成凸包。
算法对比
两位老师都声称自己的围圈方法“最快”,那到底怎么比?先看总工作量:Graham 的排序基于 atan2 角度,每个点要算一次反三角函数,再按浮点角度排序;Andrew 的排序基于坐标大小,直接比较 x、y 数值。从时间复杂度的“大 O”视角看,排序都是 O(n log n)、扫描都是 O(n),两者在最坏情况下渐近复杂度完全一致,都是 O(n log n)。
但实际工程中,常数因子同样重要。Graham 每排一个点就要算一次 atan2——这是一个昂贵的数学函数,可能比一次整数比较慢几十倍;而 Andrew 的比较只是几次加减法和乘法。在 10 万个点的数据上,这种常数差距可能是几百毫秒对几毫秒的差别。此外,Graham 排序后对共线点的处理依赖极角相等的浮点判断,而 Andrew 的共线判断用的是整数叉积,更可控。
数值稳定性是另一个关键维度。atan2 在角度相差极小(比如几个点几乎在同一方向但略偏)时,浮点舍入可能导致排序错位,进而让凸包“掉点”或“多出尖刺”;而 Andrew 的坐标比较在输入为整数时完全精确,即使输入是浮点数,比较的相对误差也小得多。因此在要求稳健的工业代码中,Andrew 几乎是默认答案。
从“复杂度相同”这个结论看,两位老师打了个平手。但赛场上的真实差别在细节——排序的单价、浮点的误差、以及面对“歪瓜裂枣”的退化输入时的表现。
- 排序主导排序阶段是瓶颈,两种算法都需要对 n 个点排序,因此总时间至少为 O(n log n)。
- 扫描线性排序完成后,每个点至多入栈和出栈各一次,扫描阶段是线性的。
- 总量相同两部分相加,大 O 意义下两者均为 O(n log n),复杂度不分伯仲。
退化与技巧
真实世界里,小朋友不会整整齐齐站成理想位置:可能有两人站同一位置(重复点),可能三个以上的人恰好站在同一条直线上(共线点),也可能位置坐标带有小数点。这些“退化情况”正是算法翻车的高发区。
先处理重复点。如果点集里有完全相同的坐标,排序后它们会相邻,扫描时叉积为 0 会触发弹栈逻辑,虽然通常不影响最终凸包,但最好在预处理阶段用哈希表去重,既省时间又避免意外。共线点则更微妙:假设一排小朋友站成直线,凸包只需要两个端点,中间的人都是“冗余”的。两种算法用叉积 ≤ 0 弹出共线点,天然去掉了中间点,但如果你希望凸包边上保留所有共线点(比如需要描边),就要把条件改成 < 0。
浮点误差是另一只“隐形老虎”。Graham 的 atan2 在角度极接近时可能因舍入排错序,而 Andrew 的整数叉积在坐标是整数时零误差。若坐标本身是浮点数,建议设置一个极小阈值 eps(如 1e-9)来判定叉积是否为 0,避免把“几乎共线”误判成“左转或右转”。这些小技巧,都是工程师在实践中踩坑踩出来的。
退化情况就像操场上的突发状况,考验的是老师的临场应变。两位老师的应对策略大同小异,但 Andrew 因为少了三角函数,在“精度防线”上天然更坚固。那么,到了真正带队出发时,该选哪套方案?
- 共线判定当叉积结果恰好为 0 时,三个点共线(或两点重合),这是退化情况的核心判据。
- 去重预处理在排序前用集合或哈希表去除坐标完全相同的点,让后续扫描更干净。
- 阈值容错浮点坐标下引入 eps 阈值,把 |cross| < eps 视为共线,抵御舍入误差。
选型总结
现在回到最初的问题:作为工程师(或者那位老师),到底该用 Graham 还是 Andrew?答案很直接:绝大多数场景默认选 Andrew。原因我们已经反复看到——坐标排序更简单、常数因子更小、整数输入下零误差、共线点处理更直观。它就像一个“不会出错的稳重老师”,适合批量生产环境。
那 Graham 就一无是处吗?当然不。Graham 的“极角排序”思想是计算几何中最优雅的抽象之一:把二维问题通过角度降维成一维问题。理解 Graham,你对“如何设计排序让扫描更简单”会有更深的体会。在教学、面试讲解、或者需要按角度访问点时,Graham 仍然不可替代。此外,Graham 的变体在动态凸包(增量加点)场景中也更易扩展。
最后给出工程建议:默认用 Andrew,但两者结果在理想情况下完全一致。不妨在你的代码库里同时实现两个版本,用随机点集做对拍测试——你会发现它们输出完全相同的凸包。这种“双实现互验”的做法,本身就是工程上检验算法正确性的利器。
两位老师最终都带着孩子们围出了那个最紧凑的圈。Graham 用角度排序画出一条优美的弧线,Andrew 用坐标排序踏出两条踏实的小路。你手上的场景,决定你请哪位老师出马。
- 算法等价性两种算法求出的凸包顶点集合相同,差异仅在顶点排列的起点和方向(可能不同)。
- 选择偏好工程默认取 Andrew,因为其在常数因子、数值稳定性、代码简洁度上全面占优。
- 学习路径先掌握 Graham 理解极角排序思想,再切换到 Andrew 写出生产级代码,是推荐的学习曲线。
AI 应用场景
自动驾驶中的障碍物包围圈
阶段:感知与路径规划
解决的问题:激光雷达扫描到障碍物的点云是散乱点,传统方法用矩形包围盒(AABB)框住障碍物会留下大量无效区域,尤其当障碍物群呈斜向分布时浪费严重。用凸包圈出点云的最外边界,可以得到贴合障碍物真实轮廓的最小凸多边形,为路径规划提供精准的安全边界。
具体案例:自动驾驶车辆在十字路口检测到前方 15 米处有 3 个行人,LiDAR 产生约 200 个点。系统用 Andrew 单调链对这些点求凸包,生成贴合行人群轮廓的蓝色包围圈。规划模块以此为障碍物区域计算绕行轨迹,凸包计算耗时不到 0.5 毫秒,远快于障碍物跟踪的帧间隔。
手势识别中的凸包与凸缺陷检测
阶段:特征提取
解决的问题:手部轮廓有很多凹陷(手指间的缝隙),直接用轮廓匹配难以区分手势。先对指尖点集求凸包,再计算凸包与手部轮廓之间的“凸缺陷”区域,凸缺陷的个数和深度能精确反映伸出了几根手指,这是识别剪刀、石头、布等手势的经典特征。
具体案例:智能电视的隔空手势控制中,摄像头捕获手部深度图,算法提取手部轮廓关键点后求凸包,检测出 4 个凸缺陷对应 4 根手指伸展。手势“握拳”对应 0 个凸缺陷,“张开手掌”对应 4 个凸缺陷,系统据此切换电视页面。
点云聚类后的簇边界生成
阶段:数据后处理与结果可视化
解决的问题:无人配送车的 3D 点云经过 DBSCAN 聚类后得到若干簇,每个簇需要生成紧凑边界用于后续障碍物匹配。凸包比 AABB 包围盒更贴合簇的实际形状,又比直接绘制所有点更轻量,是簇描述与可视化的标准选择。
具体案例:仓储机器人对仓库环境的实时点云聚类后,对每个簇用 Andrew 算法计算凸包并渲染为绿色半透明多边形,地图上清晰显示货架与行人的外轮廓,同时将凸包顶点作为特征送入分类器,区分“静态障碍物”与“移动行人”。
Python 演示
import mathimport random class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"({self.x}, {self.y})" def cross(o, a, b): """向量 oa 与 ob 的叉积正负判定方向""" return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) def graham_scan(points): """Graham 扫描:极角排序 + 栈式扫描""" pts = sorted(points, key=lambda p: (p.y, p.x)) base = pts[0] rest = sorted(pts[1:], key=lambda p: math.atan2( p.y - base.y, p.x - base.x)) stack = [base] for p in rest: while len(stack) >= 2 and cross( stack[-2], stack[-1], p) <= 0: stack.pop() stack.append(p) return stack def andrew_monotone_chain(points): """Andrew 单调链:坐标排序 + 上下链构建""" pts = sorted(points, key=lambda p: (p.x, p.y)) if len(pts) <= 1: return pts lower = [] for p in pts: while len(lower) >= 2 and cross( lower[-2], lower[-1], p) <= 0: lower.pop() lower.append(p) upper = [] for p in reversed(pts): while len(upper) >= 2 and cross( upper[-2], upper[-1], p) <= 0: upper.pop() upper.append(p) return lower[:-1] + upper[:-1] def hull_key(hull): return sorted((p.x, p.y) for p in hull) if __name__ == "__main__": random.seed(42) pts = [Point(random.randint(0, 50), random.randint(0, 50)) for _ in range(20)] hull_g = graham_scan(pts) hull_a = andrew_monotone_chain(pts) assert hull_key(hull_g) == hull_key(hull_a), "两算法结果不一致" print("随机点:", pts) print("Graham 凸包:", hull_g) print("Andrew 凸包:", hull_a) print("凸包顶点数:", len(hull_a))
key=lambda p: (p.y, p.x)先按 y 找最左下点作为旗手,这是 Graham 的第一步。math.atan2(p.y - base.y, p.x - base.x)计算每个点相对旗手的极角,按角度排队。cross(stack[-2], stack[-1], p) <= 0核心守卫:不是左转就弹出栈顶点,保证凸包严格凸。key=lambda p: (p.x, p.y)Andrew 直接按坐标排序,不需要任何三角函数。for p in reversed(pts)从右到左再扫一遍,构建上凸链。lower[:-1] + upper[:-1]拼接上下链时去掉首尾重复的端点,得到闭合凸包。
常见误区
误区我一直以为凸包就是用一个多边形把所有点围起来,所以多边形内部的点也应该算作凸包的“一部分”,求凸包就是保留所有点的坐标。
正解凸包只关心最外圈的边界顶点,内部点对凸包形状毫无贡献,就像围圈游戏里只有站在圈上的小朋友是“边界”,站在圈里的小朋友不会影响圈的形状。Graham 和 Andrew 的栈操作会把内部点全部弹出,最终栈里只留下凸包顶点。如果你需要保留所有点,那是在做“点集划分”,与凸包是两回事。
为何常见:生活直觉里“围住等于包含”,而数学上凸包是边界多边形而非集合本身,初学者容易把“被围住的对象”误当成凸包的元素。
误区Graham 扫描是教科书里第一个教的凸包算法,肯定比后出现的 Andrew 单调链更高效,生产环境应该优先选 Graham。
正解两者的渐近复杂度完全一样,都是 O(n log n)。真正的差别在常数因子:Graham 每个点都要算一次 atan2 反三角函数,这个函数可能比一次整数比较慢几十倍;Andrew 只需比较坐标。在十万级点集上,Andrew 通常比 Graham 快 2-3 倍,且代码更简洁。工程上默认选 Andrew 并非因为它“更高级”,而是因为它更省、更稳。
为何常见:教学顺序容易让人产生“先学到的就是最好的”的错觉,而忽略了算法分析中常数因子和工程实现成本的重要性。
误区既然 Graham 要按极角排序,那我用 math.atan2 算出每个点的角度再排序,结果一定是精确的,不需要担心任何问题。
正解atan2 返回的是浮点数,当两个点相对旗手的角度非常接近(例如 0.000001 弧度之差)时,浮点舍入可能让排序结果发生交换,导致扫描时叉积判断错乱,凸包出现“掉点”或“多余尖刺”。Andrew 用坐标排序完全没有这个隐患;若一定要用 Graham,可将 atan2 换成比较叉积的 comparator(即按象限 + 叉积排序),用整数运算避免浮点误差。
为何常见:现代 CPU 的浮点运算看起来“够精确”,但排序对顺序极其敏感,微小误差会被放大成结构性的错误,初学者很难意识到这一点。
误区即使所有点都在同一条直线上,凸包也应该是一个扁扁的多边形,顶点数应该接近点的个数。
正解当所有点共线时,凸包退化为一条线段,只有两个端点。Graham 和 Andrew 的弹栈条件(cross ≤ 0)会把中间的共线点全部剔除。如果你希望保留所有共线点,需要把弹栈条件改成 cross < 0,并且预先处理好重复点。理解退化的关键在于:凸包是“最小凸多边形”,而“退化多边形”允许退化为线段甚至点。
为何常见:头脑中的“凸多边形”模板默认至少有三条边,忽视了几何对象在退化情况下可以降维这一事实。
误区几何算法肯定要处理浮点数,整数坐标没法做叉积和角度计算,所以坐标类型必须是 float。
正解恰恰相反,整数坐标在凸包问题中是“最幸福”的输入。Andrew 算法全程只用加减乘法和比较,整数叉积零误差;Graham 虽然需要 atan2(浮点),但可以改成基于叉积的极角比较器来保持整数精度。浮点输入反而要引入 eps 阈值处理“几乎共线”的情况。很多竞赛题和高性能计算场景刻意用整数坐标来规避精度陷阱。
为何常见:受中学解析几何“坐标是实数”的惯性影响,忽略了计算机中整数运算的精确性和浮点运算的误差代价。
面试题
Q1. 点集凸包描述正确的是哪一项?
- A.包含所有点的最小凸多边形← 正确答案
- B.包含所有点的最小圆
- C.所有点的外围矩形
- D.连接最远两点间的线段
答案:A。凸包是包含点集的最小凸多边形,边界由最外层点构成,内部点不参与边界。最小圆(外接圆)不是多边形,AABB 矩形是轴对齐包围盒,连接最远两点的线段只覆盖两个点,都偏离凸包定义。可以用“最紧凑围圈”的比喻来记忆:圈只沿最外面的点绷紧。
Q2. Graham 扫描与 Andrew 单调链的时间复杂度均为多少?
- A.O(n)
- B.O(n log n)← 正确答案
- C.O(n²)
- D.O(log n)
答案:B。两种算法都包含一个排序步骤(Graham 极角排序、Andrew 坐标排序),排序是 O(n log n);排序后的扫描阶段每个点至多入栈出栈一次,是 O(n)。因此总复杂度由排序主导,为 O(n log n)。这就像老师先花时间排队,再快速挨个检查,排队的开销决定了整体效率。
Q3. 叉积 cross(a,b) = a.x×b.y − a.y×b.x 的值为负,表示向量 b 相对向量 a 的方向是?
- A.逆时针(左转)
- B.顺时针(右转)← 正确答案
- C.同向
- D.反向
答案:B。叉积为正表示 b 在 a 的逆时针方向(左转),为负表示顺时针方向(右转),为零表示共线。想象方向盘:叉积为负就像向右打方向盘,车辆右转。这个方向判断是凸包扫描中“弹栈还是保留”的核心依据。
Q4. Graham 扫描中,选取的基准点(旗手)满足什么条件?
- A.x 坐标最小的点
- B.y 坐标最小的点,相同则取 x 最小← 正确答案
- C.离原点最近的点
- D.x 坐标最大的点
答案:B。Graham 选择 y 坐标最小的点作为基准,保证该点一定是凸包顶点;若 y 相同取 x 最小,是为了在“最底部一横排”中固定一个唯一的起点。选择基准后,其余点按相对它的极角排序,完成一圈扫描。最左下角的选择让基准天然位于凸包底边,不会被后续扫描弹出。
Q5. Andrew 单调链算法对点集进行排序的规则是?
- A.按极角从小到大
- B.按 x 坐标升序,相同则按 y 升序← 正确答案
- C.按到原点距离从近到远
- D.随机打乱
答案:B。Andrew 用坐标排序而非角度排序:先按 x 升序,x 相同按 y 升序。这样排序后的点天然形成从左到右的序列,之后从左到右构建下凸链、从右到左构建上凸链。坐标排序只涉及数值比较,避免了 atan2 的浮点误差,是 Andrew 数值稳健的根源。
Q6. 关于 Andrew 单调链的构建过程,下列说法正确的有哪些?
- A.从左到右扫描构建下凸链← 正确答案
- B.从右到左扫描构建上凸链← 正确答案
- C.两条链拼接得到完整凸包← 正确答案
- D.只需要扫描一次即可得到凸包
答案:ABC。Andrew 的凸包由下凸链和上凸链拼接而成:从左到右一路“左转上升”得到下凸链,从右到左一路“左转下降”得到上凸链,首尾相连闭合为凸包。选项 D 错误——只扫一次只能得到一条链,无法构成闭合多边形。这就像围圈分两趟:先围上半圈,再回头围下半圈。
Q7. 在大规模工程应用中,Andrew 通常比 Graham 更受推荐的根本原因是什么?
- A.时间复杂度更低
- B.不需要排序
- C.坐标比较避免 atan2 浮点误差且常数因子更小← 正确答案
- D.能得到不同结果的凸包
答案:C。两者渐近复杂度相同,都是 O(n log n),因此 A 不对;两者都需要排序,B 不对;结果在理想条件下完全一致,D 不对。Andrew 的优势在于排序阶段只用坐标比较,而 Graham 需要算 atan2——一个昂贵的三角函举。在十万级点集上,Andrew 的常数因子通常只有 Graham 的一半甚至更低,且整数坐标下零误差。
Q8. 以下哪些场景会用到凸包算法?
- A.自动驾驶中点云障碍物安全边界圈定← 正确答案
- B.图像手势识别中的凸缺陷检测← 正确答案
- C.点云聚类后的簇轮廓生成← 正确答案
- D.数组快速排序
答案:ABC。凸包的典型应用包括:1)点云障碍物圈定边界,2)手部轮廓凸缺陷分析,3)聚类簇的可视化与特征描述。数组快速排序与凸包无关。记住一句话:凡是需要给“一堆散点画最紧凑外圈”的地方,都可能有凸包的身影。
Q9. Graham 扫描的 while 循环中,若叉积 cross(stack[-2], stack[-1], p) ≤ 0,应执行什么操作?
- A.将 p 压入栈
- B.弹出 stack[-1] 并继续检查← 正确答案
- C.弹出 stack[-2] 并继续检查
- D.结束扫描
答案:B。叉积 ≤ 0 表示从 stack[-2] 到 stack[-1] 再到 p 的转向为右转或共线,此时 stack[-1] 位于凸包内侧,不可能是边界顶点,因此弹出它,然后用新的栈顶两个点与新点 p 继续检查,直到满足左转再压入 p。这就像围圈时发现中间的小朋友“凹进圈里”了,老师请他出列,再检查前一位。
Q10. 当所有点都共线时,凸包的顶点个数是多少?
- A.等于点数 n
- B.2 个(两个端点)← 正确答案
- C.1 个
- D.0 个
答案:B。所有点共线时凸包退化为一条线段,只有两个端点。Graham 和 Andrew 的弹栈条件 cross ≤ 0 会剔除中间的共线点,仅保留首尾两个最远点。若想保留全部共线点,需将条件改为 cross < 0。这提醒我们:凸包是“最小凸多边形”,在极端情况下允许退化为线段。