go语言:实现graham scan葛立恒扫描法算法(附带源码)

项目背景详细介绍

在计算几何(Computational Geometry)领域中,**凸包(Convex Hull)**是一个极其基础、同时又非常核心的问题。

简单来说,给定平面上的一组点,凸包就是:

能够包住所有点的、最小的凸多边形

直观理解:

  • 想象在桌面上撒一把钉子
  • 用一根橡皮筋把所有钉子圈起来
  • 橡皮筋形成的形状,就是这些点的凸包

凸包在工程与科研中有大量实际应用,例如:

  • 计算机图形学(碰撞检测、可视区域)
  • GIS / 地图系统(区域边界计算)
  • 图像处理(目标轮廓提取)
  • 机器人路径规划
  • 模式识别与机器学习
  • 游戏引擎中的物理系统

在众多凸包算法中,**Graham Scan(葛立恒扫描法)**是:

  • 最经典
  • 最适合教学
  • 数学与工程结合度极高

的一种算法。

它由 Ronald Graham 在 1972 年提出,是第一个时间复杂度达到 O(n log n) 的凸包算法,至今仍是计算几何入门必学内容。


项目需求详细介绍

本项目的目标是:

使用 Go 语言完整实现 Graham Scan(葛立恒扫描法)算法,计算二维平面点集的凸包。

具体需求如下:

  1. 功能需求
    • 输入:二维平面上的一组点 (x, y)
    • 输出:按逆时针顺序排列的凸包顶点集合
    • 支持任意数量的点(≥ 3)
  2. 算法要求
    • 严格遵循 Graham Scan 算法流程
    • 使用叉积判断转向(左转 / 右转)
    • 正确处理共线点情况
  3. 工程要求
    • 使用 Go 标准库
    • 结构清晰、注释完整
    • 算法步骤清晰可追踪
  4. 教学要求
    • 明确解释几何意义
    • 每一步都有直觉说明
    • 便于板书推导与代码对照

相关技术详细介绍

1. 二维点的表示

在 Go 中,二维点通常用结构体表示:

type Point struct { X int Y int }

使用整数而非浮点数的好处是:

  • 避免精度误差
  • 叉积计算更安全
  • 教学更直观

2. 向量叉积(Cross Product)

Graham Scan 的核心判断依据是向量叉积的符号。

给定三个点 O, A, B,定义向量:

OA = A - O OB = B - O

叉积(二维)结果:

cross = OA.x * OB.y - OA.y * OB.x

含义:

  • cross > 0 :左转(逆时针)
  • cross < 0 :右转(顺时针)
  • cross = 0 :共线

3. 极角排序(Polar Angle Sort)

Graham Scan 的第一步是:

选一个基准点,然后按极角排序所有其他点

排序规则:

  1. 极角从小到大(逆时针)
  2. 极角相同,距离基准点近的在前

实现思路详细介绍

Graham Scan 的整体流程可以概括为四个步骤:


步骤一:选择基准点(Pivot)

从所有点中选出:

  • Y 最小
  • 若 Y 相同,取 X 最小

该点必然是凸包上的点,用作极角计算的参考。


步骤二:按极角排序

将除基准点外的所有点,按照它们与基准点形成的极角排序。

目的:

  • 保证我们是逆时针扫描点集

步骤三:扫描并维护栈

维护一个“栈”(slice):

  1. 先压入前两个点
  2. 从第三个点开始:
    • 若出现 右转或共线
    • 弹出栈顶
    • 直到形成左转
  3. 当前点入栈

步骤四:栈中剩余点即为凸包

扫描结束后,栈中点:

  • 按逆时针顺序
  • 正好构成凸包边界

完整实现代码

// ========================================== // 文件名:main.go // 功能:使用 Graham Scan 算法计算二维点集的凸包 // ========================================== package main import ( "fmt" "sort" ) // Point 表示二维平面中的一个点 type Point struct { X int Y int } // cross 计算向量 OA 和 OB 的叉积 // O 为公共起点 func cross(o, a, b Point) int { return (a.X-o.X)*(b.Y-o.Y) - (a.Y-o.Y)*(b.X-o.X) } // distanceSquare 计算两点间距离的平方 // 用于极角相同情况下的排序 func distanceSquare(a, b Point) int { dx := a.X - b.X dy := a.Y - b.Y return dx*dx + dy*dy } // GrahamScan 计算点集的凸包 func GrahamScan(points []Point) []Point { n := len(points) if n <= 3 { return points } // 1. 找到基准点:Y 最小,若相同取 X 最小 pivot := 0 for i := 1; i < n; i++ { if points[i].Y < points[pivot].Y || (points[i].Y == points[pivot].Y && points[i].X < points[pivot].X) { pivot = i } } // 将基准点放到第一个位置 points[0], points[pivot] = points[pivot], points[0] base := points[0] // 2. 按极角排序 sort.Slice(points[1:], func(i, j int) bool { p1 := points[i+1] p2 := points[j+1] c := cross(base, p1, p2) if c == 0 { return distanceSquare(base, p1) < distanceSquare(base, p2) } return c > 0 }) // 3. 使用栈扫描 stack := make([]Point, 0) stack = append(stack, points[0], points[1]) for i := 2; i < n; i++ { for len(stack) >= 2 { top := stack[len(stack)-1] nextToTop := stack[len(stack)-2] if cross(nextToTop, top, points[i]) > 0 { break } stack = stack[:len(stack)-1] } stack = append(stack, points[i]) } return stack } func main() { points := []Point{ {0, 3}, {2, 2}, {1, 1}, {2, 1}, {3, 0}, {0, 0}, {3, 3}, } hull := GrahamScan(points) fmt.Println("凸包点(逆时针):") for _, p := range hull { fmt.Printf("(%d, %d)\n", p.X, p.Y) } } 

代码详细解读(仅解读方法作用)

1. cross 方法

用于判断三点构成的转向关系,是整个算法的几何核心。


2. GrahamScan 方法

该方法完成了:

  • 基准点选择
  • 极角排序
  • 栈扫描与回退
  • 最终凸包构建

返回的点集已经按逆时针顺序排列。


3. 栈扫描逻辑

通过不断“回退右转点”,确保最终结果只包含凸包边界点。


项目详细总结

通过本项目,我们完整实现了 Graham Scan 算法,并理解了:

  1. 几何问题如何转化为代数判断
  2. 叉积在方向判断中的核心地位
  3. O(n log n) 算法的工程实现方式
  4. 栈结构在几何扫描中的巧妙应用

Graham Scan 是:

  • 凸包算法的入门首选
  • 计算几何的经典代表
  • 极具教学价值的算法范例

项目常见问题及解答

Q1:为什么要极角排序?

为了保证扫描是单调逆时针进行,避免回头判断。


Q2:共线点如何处理?

通过距离排序 + 回退逻辑,只保留最外层点。


Q3:时间复杂度是多少?

  • 排序:O(n log n)
  • 扫描:O(n)
  • 总体:O(n log n)

Q4:可以处理浮点坐标吗?

可以,但需要注意精度误差,教学与工程中更推荐整数。


扩展方向与性能优化

  1. 改写为泛型版本(Go 1.18+)
  2. 支持浮点坐标与 EPS 判断
  3. 与 Jarvis March 算法对比
  4. 用于多边形面积 / 周长计算
  5. 结合可视化工具展示扫描过程

Read more

继续实践OpenClaw,好不容易把web 管理面板调通,再给它配上一个大模型

继续实践OpenClaw,好不容易把web 管理面板调通,再给它配上一个大模型

OpenClaw小龙虾是github 获得星标最多的项目,OpenClaw之所以能在GitHub上获得极高的关注度,主要原因在于它提供了一个功能强大、易于扩展的AI助手开发平台。把整个操作系统,打造成AI! OpenClaw官网:OpenClaw — Personal AI Assistant 以前的安装记录:https://skywalk.blog.ZEEKLOG.net/article/details/157554991 本来感觉OpenClaw安装是挺简单的,没想到巨坑,有一台机器装好后没有web管理面板.....所以本来很简短的文档,写成了巨幅文档。 安装OpenClaw 先在192.168.1.12安装,但是它没有systemd服务,导致OpenClaw的服务无法自动启动。需要手工执行openclaw gateway命令启动。 后在192.168.1.19安装。但是装好后没有web管理面板,反复删除重装也没有,最后是安装的openclaw-cn ,才解决了问题。参见这个文档:https://skywalk.blog.ZEEKLOG.net/article/

By Ne0inhk

Flutter 三方库 jwt_io 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、全能的 JSON Web Token (JWT) 加解密与身份安全验证引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 jwt_io 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、全能的 JSON Web Token (JWT) 加解密与身份安全验证引擎 在鸿蒙(OpenHarmony)系统的端云一体化登录、政企应用的安全审计或复杂的跨端权限校验场景中,如何确保来自云端授信中心的 JWT Token 既能被正确解析(Decode),又能被严密地校验其合法性与过期时间?jwt_io 为开发者提供了一套工业级的、基于 RFC 7519 标准的 JSON Web Token 深度处理方案。本文将深入实战其在鸿蒙应用安全底座中的应用。 前言 什么是 JWT IO?它不仅是一个简单的 Base64 解码器,而是一个具备深厚 RFC

By Ne0inhk
OpenClaw dashboard命令后,无法登录web控制面板(在systemd服务无法启动的一些虚拟机里会碰到)

OpenClaw dashboard命令后,无法登录web控制面板(在systemd服务无法启动的一些虚拟机里会碰到)

先上结论 执行OpenClaw dashboard命令后,无法登录web控制面板,是因为OpenClaw的gateway服务没有起来。原来小龙虾OpenClaw 的命令没有学明白,先弄清楚命令: openclaw onboard 是配置 openclaw dashboard是显示web控制面板登录信息 openclaw gateway --verbose 是启动网关 openclaw gateway start是启动网关服务 问题就是因为这台系统的systemd没有起作用,导致openclaw的gateway服务没有起来,所以控制面板无法登录。 OpenClaw status Overview ┌─────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Item │ Value │ ├─────────────────┼────────────────────────────────────

By Ne0inhk
超容易理解+模版套路解决LeetCode 前序+中序、中序+后序、前序+后序遍历构造树问题

超容易理解+模版套路解决LeetCode 前序+中序、中序+后序、前序+后序遍历构造树问题

这三道题的解法类似 都是基于归并排序的分治思想 不断划分左右子树进行解答。下列题1和题2解法几乎完全相同 题三根据前序后序遍历的话需要加以注意 后面详细讲解 LeetCode-105. 从前序与中序遍历序列构造二叉树 原题链接 题目描述 给定两个整数数组 preorder 和 inorder: * preorder 是二叉树的 前序遍历 序列。 * inorder 是同一棵树的 中序遍历 序列。 请根据这两个数组 构造 出该二叉树,并返回其根节点。 示例展示 示例 1: 输入: preorder = [3, 9, 20, 15, 7] inorder = [9, 3, 15, 20, 7] 输出: [3, 9, 20, null, null, 15, 7]

By Ne0inhk