算法2026-05-10·13 分钟
算法知识库:计算几何算法实现
JavaScript/TypeScript 实现计算几何中的经典算法,如凸包、线段相交、多边形运算等。
计算几何算法实现
1. 点和向量基础
ts
class Point {
constructor(
public x: number,
public y: number,
) {}
// 向量减法
subtract(other: Point): Point {
return new Point(this.x - other.x, this.y - other.y);
}
// 向量加法
add(other: Point): Point {
return new Point(this.x + other.x, this.y + other.y);
}
// 标量乘法
multiply(scalar: number): Point {
return new Point(this.x * scalar, this.y * scalar);
}
// 叉积
cross(other: Point): number {
return this.x * other.y - this.y * other.x;
}
// 点积
dot(other: Point): number {
return this.x * other.x + this.y * other.y;
}
// 距离
distance(other: Point): number {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
// 距离平方(避免开方运算)
distanceSquared(other: Point): number {
const dx = this.x - other.x;
const dy = this.y - other.y;
return dx * dx + dy * dy;
}
equals(other: Point): boolean {
return this.x === other.x && this.y === other.y;
}
toString(): string {
return `(${this.x}, ${this.y})`;
}
}
class Line {
constructor(
public start: Point,
public end: Point,
) {}
// 线段长度
length(): number {
return this.start.distance(this.end);
}
// 方向向量
direction(): Point {
return this.end.subtract(this.start);
}
// 法向量
normal(): Point {
const dir = this.direction();
return new Point(-dir.y, dir.x);
}
}2. 凸包算法 (Graham Scan)
ts
// 计算极角
function polarAngle(p0: Point, p1: Point): number {
const dx = p1.x - p0.x;
const dy = p1.y - p0.y;
return Math.atan2(dy, dx);
}
// 叉积用于判断方向
function crossProduct(o: Point, a: Point, b: Point): number {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
function grahamScan(points: Point[]): Point[] {
if (points.length <= 1) return points;
// 找到最底部的点
let minY = points[0].y;
let minIndex = 0;
for (let i = 1; i < points.length; i++) {
if (points[i].y < minY || (points[i].y === minY && points[i].x < points[minIndex].x)) {
minY = points[i].y;
minIndex = i;
}
}
// 将最底部点移到数组开头
[points[0], points[minIndex]] = [points[minIndex], points[0]];
// 按极角排序
const p0 = points[0];
points.sort((a, b) => {
const angleA = polarAngle(p0, a);
const angleB = polarAngle(p0, b);
if (angleA !== angleB) {
return angleA - angleB;
}
// 如果极角相同,按距离排序
return p0.distanceSquared(a) - p0.distanceSquared(b);
});
// 移除共线的点(除了第一个和最后一个)
let m = 1;
for (let i = 1; i < points.length; i++) {
while (i < points.length - 1 && crossProduct(p0, points[i], points[i + 1]) === 0) {
i++;
}
points[m] = points[i];
m++;
}
if (m < 3) return points.slice(0, m);
// Graham扫描
const stack: Point[] = [];
stack.push(points[0], points[1], points[2]);
for (let i = 3; i < m; i++) {
while (stack.length >= 2 && crossProduct(stack[stack.length - 2], stack[stack.length - 1], points[i]) <= 0) {
stack.pop();
}
stack.push(points[i]);
}
return stack;
}
// 使用示例
function convexHullExample() {
const points = [new Point(0, 3), new Point(1, 1), new Point(2, 2), new Point(4, 4), new Point(0, 0), new Point(1, 2), new Point(3, 1), new Point(3, 3)];
const hull = grahamScan(points);
console.log('凸包顶点:');
hull.forEach((point) => console.log(point.toString()));
}3. 线段相交检测
ts
// 判断点是否在线段上
function onSegment(p: Point, q: Point, r: Point): boolean {
return Math.min(p.x, r.x) <= q.x && q.x <= Math.max(p.x, r.x) && Math.min(p.y, r.y) <= q.y && q.y <= Math.max(p.y, r.y);
}
// 计算方向
function orientation(p: Point, q: Point, r: Point): number {
const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
if (Math.abs(val) < 1e-9) return 0; // 共线
return val > 0 ? 1 : 2; // 顺时针或逆时针
}
// 检查两条线段是否相交
function doIntersect(p1: Point, q1: Point, p2: Point, q2: Point): boolean {
const o1 = orientation(p1, q1, p2);
const o2 = orientation(p1, q1, q2);
const o3 = orientation(p2, q2, p1);
const o4 = orientation(p2, q2, q1);
// 一般情况
if (o1 !== o2 && o3 !== o4) {
return true;
}
// 特殊情况:共线且有重叠
if (o1 === 0 && onSegment(p1, p2, q1)) return true;
if (o2 === 0 && onSegment(p1, q2, q1)) return true;
if (o3 === 0 && onSegment(p2, p1, q2)) return true;
if (o4 === 0 && onSegment(p2, q1, q2)) return true;
return false;
}
// 计算线段交点
function lineIntersection(p1: Point, q1: Point, p2: Point, q2: Point): Point | null {
const denom = (p1.x - q1.x) * (p2.y - q2.y) - (p1.y - q1.y) * (p2.x - q2.x);
if (Math.abs(denom) < 1e-9) {
return null; // 平行
}
const t = ((p1.x - p2.x) * (p2.y - q2.y) - (p1.y - p2.y) * (p2.x - q2.x)) / denom;
const u = -((p1.x - q1.x) * (p1.y - p2.y) - (p1.y - q1.y) * (p1.x - p2.x)) / denom;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
return new Point(p1.x + t * (q1.x - p1.x), p1.y + t * (q1.y - p1.y));
}
return null;
}4. 多边形面积计算
ts
// 使用鞋带公式计算多边形面积
function polygonArea(points: Point[]): number {
if (points.length < 3) return 0;
let area = 0;
const n = points.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
area += points[i].x * points[j].y;
area -= points[j].x * points[i].y;
}
return Math.abs(area) / 2;
}
// 计算带符号的面积(用于判断多边形方向)
function signedPolygonArea(points: Point[]): number {
if (points.length < 3) return 0;
let area = 0;
const n = points.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
area += points[i].x * points[j].y;
area -= points[j].x * points[i].y;
}
return area / 2;
}5. 点在多边形内检测
ts
// 射线法判断点是否在多边形内
function pointInPolygon(point: Point, polygon: Point[]): boolean {
let inside = false;
const n = polygon.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = polygon[i].x,
yi = polygon[i].y;
const xj = polygon[j].x,
yj = polygon[j].y;
if (yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi) {
inside = !inside;
}
}
return inside;
}
// 转角法判断点是否在多边形内
function pointInPolygonWinding(point: Point, polygon: Point[]): boolean {
let winding = 0;
const n = polygon.length;
for (let i = 0; i < n; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % n];
if (a.y <= point.y) {
if (b.y > point.y && crossProduct(a, b, point) > 0) {
winding++;
}
} else {
if (b.y <= point.y && crossProduct(a, b, point) < 0) {
winding--;
}
}
}
return winding !== 0;
}6. 多边形凸包检测
ts
// 检查多边形是否为凸多边形
function isConvex(polygon: Point[]): boolean {
if (polygon.length < 3) return false;
const n = polygon.length;
let sign = 0;
for (let i = 0; i < n; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % n];
const c = polygon[(i + 2) % n];
const cross = crossProduct(a, b, c);
if (cross !== 0) {
if (sign === 0) {
sign = cross > 0 ? 1 : -1;
} else if ((cross > 0 && sign < 0) || (cross < 0 && sign > 0)) {
return false;
}
}
}
return true;
}7. 最近点对问题
ts
// 分治法求最近点对
function closestPair(points: Point[]): { distance: number; pair: [Point, Point] } {
// 按x坐标排序
const sortedX = points.slice().sort((a, b) => a.x - b.x);
// 按y坐标排序
const sortedY = points.slice().sort((a, b) => a.y - b.y);
return closestPairUtil(sortedX, sortedY);
}
function closestPairUtil(px: Point[], py: Point[]): { distance: number; pair: [Point, Point] } {
const n = px.length;
if (n <= 3) {
return bruteForce(px);
}
const mid = Math.floor(n / 2);
const midPoint = px[mid];
// 分成左右两半
const leftX = px.slice(0, mid);
const rightX = px.slice(mid);
const leftY = py.filter((p) => p.x <= midPoint.x);
const rightY = py.filter((p) => p.x > midPoint.x);
// 递归求左右两半的最小距离
const leftResult = closestPairUtil(leftX, leftY);
const rightResult = closestPairUtil(rightX, rightY);
let minResult = leftResult.distance < rightResult.distance ? leftResult : rightResult;
// 检查跨越中线的点
const strip: Point[] = [];
for (const point of py) {
if (Math.abs(point.x - midPoint.x) < minResult.distance) {
strip.push(point);
}
}
// 在strip中寻找更小的距离
for (let i = 0; i < strip.length; i++) {
for (let j = i + 1; j < strip.length && strip[j].y - strip[i].y < minResult.distance; j++) {
const dist = strip[i].distance(strip[j]);
if (dist < minResult.distance) {
minResult = { distance: dist, pair: [strip[i], strip[j]] };
}
}
}
return minResult;
}
function bruteForce(points: Point[]): { distance: number; pair: [Point, Point] } {
let minDist = Infinity;
let closestPair: [Point, Point] = [points[0], points[1]];
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
const dist = points[i].distance(points[j]);
if (dist < minDist) {
minDist = dist;
closestPair = [points[i], points[j]];
}
}
}
return { distance: minDist, pair: closestPair };
}8. 实现要点
- 凸包算法用于求点集的最小凸边界。
- 线段相交检测是计算几何的基础操作。
- 多边形面积计算使用鞋带公式。
- 点在多边形内检测有多种算法,各有优劣。
- 最近点对问题使用分治法可达到 O(n log n) 时间复杂度。
算法计算几何JavaScript