凸包算法Java代码的一种实现

2,122 阅读3分钟

1,(What)什么是凸包?

凸包(Convex Hull)是一个计算几何(图形学)中的概念。 在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,...Xn)的凸组合来构造. 在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。 用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点。(摘自百度百科)

2,(Why)为什么要用凸包?

在做一些范围匹配,范围包含,电子围栏项目的时候可以通过构造凸包来初步筛选,较少范围匹配次数,提升程序运行效率。

3,(How)Java代码的一种实现

凸包工具类:

package com.example.demo.convex;

import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import static java.lang.Math.abs;

/**
 * @author lxw
 * @date 2020-08-17
 */
public class MyConvexHull {

    private static int MAX_ANGLE = 4;
    private double currentMinAngle = 0;
    /**
     * 最终生成的凸包的结果集
     */
    private List<Point2D.Double> hullPointList;
    private List<Integer> indexList;
    private List<Point2D.Double> pointDoubles;

    /**
     * 凸包里面的起始点的在原pointDoubles里的下标
     */
    private int firstIndex;

    /**
     * 获取一串点的凸包
     *
     * @param pointDoubleList
     * @return
     */
    public static List<Point2D.Double> getConvexPoint(List<Point2D.Double> pointDoubleList) {
        // 去重处理,过滤重复点
        pointDoubleList = pointDoubleList.stream().distinct().collect(Collectors.toList());
        // 初始化参数值
        MyConvexHull convexUtil = new MyConvexHull(pointDoubleList);
        return convexUtil.calculateHull();
    }

    public MyConvexHull() {

    }

    /**
     * 构造方法
     *
     * @param pointDoubleList
     */
    public MyConvexHull(List<Point2D.Double> pointDoubleList) {
        this.hullPointList = new LinkedList<>();
        this.indexList = new LinkedList<>();
        this.pointDoubles = pointDoubleList;
        // 获取凸包的起始点的在pointDoubles里面的下标
        firstIndex = getFirstPoint(pointDoubleList);
        // 设置凸包的起始点
        addToHull(firstIndex);
    }

    /**
     * 计算返回凸包
     *
     * @return
     */
    public List<Point2D.Double> calculateHull() {
        for (int i = getNextIndex(firstIndex); i != firstIndex; i = getNextIndex(i)) {
            addToHull(i);
        }
        return showHullPoints();
    }

    /**
     * 把pointDoubles里面下标为index的点添加进凸包
     *
     * @param index
     */
    private void addToHull(int index) {
        indexList.add(index);
        hullPointList.add(pointDoubles.get(index));
    }

    /**
     * 展示凸包
     *
     * @return
     */
    private List<Point2D.Double> showHullPoints() {
        Iterator<Point2D.Double> itPoint = hullPointList.iterator();
        List<Point2D.Double> resultList = new ArrayList<>();
        while (itPoint.hasNext()) {
            Point2D.Double p = itPoint.next();
            resultList.add(new Point2D.Double(p.getX(), p.getY()));
        }
        return resultList;
    }

    /**
     * 从下标currentIndex开始获取下一个在pointDoubles符合要求的凸包的点的下标
     *
     * @param currentIndex
     * @return
     */
    private int getNextIndex(int currentIndex) {
        double minAngle = MAX_ANGLE;
        double pseudoAngle;
        int minIndex = 0;
        for (int i = 0; i < pointDoubles.size(); i++) {
            if (i != currentIndex) {
                double dx = pointDoubles.get(i).getX() - pointDoubles.get(currentIndex).getX();
                double dy = pointDoubles.get(i).getY() - pointDoubles.get(currentIndex).getY();
                pseudoAngle = getPseudoAngle(dx, dy);
                if (pseudoAngle >= currentMinAngle && pseudoAngle < minAngle) {
                    minAngle = pseudoAngle;
                    minIndex = i;
                } else if (pseudoAngle == minAngle) {
                    if ((abs(pointDoubles.get(i).getX() - pointDoubles.get(currentIndex).getX()) >
                            abs(pointDoubles.get(minIndex).getX() - pointDoubles.get(currentIndex).getX()))
                            || (abs(pointDoubles.get(i).getY() - pointDoubles.get(currentIndex).getY()) >
                            abs(pointDoubles.get(minIndex).getY() - pointDoubles.get(currentIndex).getY()))) {
                        minIndex = i;
                    }
                }
            }

        }
        currentMinAngle = minAngle;
        return minIndex;
    }

    /**
     * 获取最下(y坐标最小)的一个点,如果有多个最下的点,取其中最左(x坐标最小)的一个
     *
     * @param pointDoubleList
     * @return
     */
    private int getFirstPoint(List<Point2D.Double> pointDoubleList) {
        int minIndex = 0;
        for (int i = 1; i < pointDoubleList.size(); i++) {
            if (pointDoubleList.get(i).getY() < pointDoubleList.get(minIndex).getY()) {
                minIndex = i;
            } else if ((pointDoubleList.get(i).getY() == pointDoubleList.get(minIndex).getY())
                    && (pointDoubleList.get(i).getX() < pointDoubleList.get(minIndex).getX())) {
                minIndex = i;
            }
        }
        return minIndex;
    }

    /**
     * 计算角度的偏离程度
     *
     * @param dx
     * @param dy
     * @return
     */
    private double getPseudoAngle(double dx, double dy) {
        if (dx > 0 && dy >= 0) {
            // 平面直角坐标系的第一象限,返回值值域[0,1)
            return dy / (dx + dy);
        }
        if (dx <= 0 && dy > 0) {
            // 平面直角坐标系的第二象限,返回值值域[1,2)
            return 1 + (abs(dx) / (abs(dx) + dy));
        }
        if (dx < 0 && dy <= 0) {
            // 平面直角坐标系的第三象限,返回值值域[2,3)
            return 2 + (dy / (dx + dy));
        }
        if (dx >= 0 && dy < 0) {
            // 平面直角坐标系的第四象限,返回值值域[3,4)
            return 3 + (dx / (dx + abs(dy)));
        }
        throw new Error("坐标有重复" + dx + dy);
    }

    /**
     * 构造Path2D.Double对象,形成封闭多边形
     *
     * @param polygon
     * @return
     */
    public static Path2D.Double create(List<Point2D.Double> polygon) {
        Path2D.Double generalPath = new Path2D.Double();
        Point2D.Double first = polygon.get(0);
        // 通过移动到指定坐标(以双精度指定),将一个点添加到路径中
        generalPath.moveTo(first.getX(), first.getY());
        for (int i = 1; i < polygon.size(); i++) {
            // 通过绘制一条从当前坐标到新指定坐标(以双精度指定)的直线,将一个点添加到路径中。
            generalPath.lineTo(polygon.get(i).getX(), polygon.get(i).getY());
        }
        // 将几何多边形封闭
        generalPath.lineTo(first.getX(), first.getY());
        generalPath.closePath();
        return generalPath;
    }

凸包测试类:

package com.example.demo.convex;

import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lxw
 * @date 2020-08-17
 */
public class ConvexHullTest {
    public static void main(String[] args) {
        Point2D point2D0 = new Point2D.Double(7.0, 5.0);
        Point2D point2D1 = new Point2D.Double(3.0, 5.0);

        // 构造原始多边形
        List<Point2D.Double> pointDoubleList = buildPointDoubleList();
        Path2D.Double polygon = MyConvexHull.create(pointDoubleList);
        System.out.println("原始的凹多边形:");
        System.out.println(polygon.contains(point2D0));
        System.out.println(polygon.contains(point2D1));

        // 构造凸包
        List<Point2D.Double> convexHullList = MyConvexHull.getConvexPoint(pointDoubleList);
        Path2D.Double convexHull = MyConvexHull.create(convexHullList);
        System.out.println("原始的凹多边形对应的凸包:");
        System.out.println(convexHull.contains(point2D0));
        System.out.println(convexHull.contains(point2D1));
    }

    public static List<Point2D.Double> buildPointDoubleList() {
        List<Point2D.Double> doubleList = new ArrayList<>();
        Point2D.Double point2D0 = new Point2D.Double(0.0, 0.0);
        Point2D.Double point2D1 = new Point2D.Double(10.0, 0.0);
        Point2D.Double point2D2 = new Point2D.Double(5.0, 5.0);
        Point2D.Double point2D3 = new Point2D.Double(10.0, 10.0);
        Point2D.Double point2D4 = new Point2D.Double(0.0, 10.0);
        doubleList.add(point2D0);
        doubleList.add(point2D1);
        doubleList.add(point2D2);
        doubleList.add(point2D3);
        doubleList.add(point2D4);
        return doubleList;

    }
}

4,几点思考

4.1 把实际问题抽象成理论问题,把理论问题具象到平面几何问题,再把平面几何问题抽象成为编程模型问题,然后解决,整个问题迎刃而解之。

比如把平面多边形抽象为编程里拥有一系列点的List,利用坐标的末减初来表达一个向量。

4.2 涉及到数学平面向量和矩阵的基本知识。

4.3 思考下各种算法情况下的时间复杂度问题。

5, 凸包算法其他博客链接推荐

blog.csdn.net/bone_ace/ar…