凸包算法Jarvis's march步进法和Graham扫描法的原理及实现

凸包概念

在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。
        用自己的话说就是在一个点集中,能够包含所有点的凸多边形(所有的点都能落入多边形的内部)。专业的描述可以通过百度百科了解。在作者Kyle Loudon的《Mastering Algorithms with C》一书的中文版中描述到一个点集的凸包是指包含该点集中的所有点的最小凸多边形。如果一个多边形内任意两点之间的连线完全包含在该多边形内,则称这个多边形是凸多边形;否则多边形就是凹的。要想画一个点集的凸包,可把它假想成一块板子上的钉子。如果用细线将最外层的钉子逐个连接起来,那么细线所围成的形状就是凸包。如下图所示a为凸包,b为凹多边形。     

如图c所示所有的黑色点表示一个点集,P1~P8表示生成生成凸包的点集。
                                                   

                                          
         在这里介绍两种求有限点集的凸包,一种Jarvis's march的步进法,另一种是Grahamd的扫描法。本文档代码实现在Qt5.7.0环境下,仅供作为参考,不保证直接拿去使用没有问题。

通用函数

1)共线情况找出距离远的点

#define SEGMENTLEN(x0,y0,x1,y1) (sqrt(pow(((x1)-(x0)), 2.0) + pow(((y1)-(y0)), 2.0)))

2)判断点的位置(上边/下边)

qreal Convex::comparePointClock(const QPointF &point_0, const QPointF &point_c, const QPointF &point_i)
{
	return ((point_i.x() - point_0.x())*(point_c.y() - point_0.y()) - (point_i.y() - point_0.y())*(point_c.x() - point_0.x()));
}

3)删除重复坐标

quint32 Convex::removeRepeatPoints(QVector<QPointF> &vecPoints)
{
	if (vecPoints.isEmpty())
		return 0;

	QVector<QPointF> tempVecPorint;
	tempVecPorint = vecPoints;
	vecPoints.clear();
	QPointF tempPoint;
	while (tempVecPorint.size())
	{
		tempPoint = tempVecPorint.at(0);
		tempVecPorint.removeAll(tempPoint);
		vecPoints.push_back(tempPoint);
	}

	return vecPoints.size();
}

4)获取最小坐标

QPointF Convex::getMinimumPoint(const QVector<QPointF> &vecPoints)
{
	if (vecPoints.isEmpty())
		return QPointF();

	QPointF minPoint = vecPoints.at(0);
	quint16 point_x = vecPoints.at(0).x(), point_y = vecPoints.at(0).y();
	for (QVector<QPointF>::const_iterator it = vecPoints.constBegin(); it != vecPoints.constEnd(); it++)
	{
		//比较Y坐标,找Y坐标最小的
		if (it->y() < minPoint.y())
		{
			minPoint = (*it);
		}
		else
		{
			//Y坐标相同,找X坐标小的
			if (it->y() == minPoint.y() && it->x() < minPoint.x())
			{
				minPoint = (*it);
			}
		}
	}

	return minPoint;
}

Jarvis's march 步进算法,复杂度O(nH),H为点的个数

步骤:

1)找到坐标最下的点,此点必定在凸包点集中,(如果出现纵坐标最小的点有多个,那么在这些点中找到横坐标最小的点,即点集中最左下角的点)起始点作为P_0,并把其入栈。

2)遍历点集利用向量叉积的方法判断点是在线的上边(左边)还是下边(右边),设第二个点为P_c,遍历的点为P_i。如果向量叉积结果>0说明P_i在P_0P_c连线的下边(右边),<0说明P_i在P_0P_c连线的上边(左边),==0说明P_i在P_0P_c连线上。如果点在直线的下方则更新P_c为P_i;如果在线上的话,找到距离P_0较远的点作为P_c,然后把P_c作为P_0入栈,依次类推直到遍历一周再次到达第一个入栈的点。

具体实现源码如下:

//Jarvis's march 算法,O(nH),H为点的个数。
qint8 Convex::getConvexHullJarvis(const QVector<QPointF> &vecSourPoints, QVector<QPointF> &vecTarPoints)
{
	if (vecSourPoints.isEmpty())
		return -1;

	QPointF minPoint;
	QPointF lowPoint, point_0, point_i, point_c;
	qreal count = 0,z = 0;
	qreal length_1, length_2;
	QVector<QPointF> tempVecPoint(vecSourPoints);

	vecTarPoints.clear();
	//删除重复坐标
	if (removeRepeatPoints(tempVecPoint) <= 0)
		return -1;

	//查找最小坐标
	minPoint = getMinimumPoint(tempVecPoint);
	lowPoint = minPoint;
	
	point_0 = lowPoint;

	do {
		//起始点point_0压入凸包点集中
		vecTarPoints.push_back(point_0);
		
		count = 0;
		for (QVector<QPointF>::iterator it = tempVecPoint.begin(); it != tempVecPoint.end(); it++)
		{
			//跳过起始坐标
			if ((*it) == point_0)
				continue;

			count++;
			if (count == 1) //把第一个遍历的点作为point_c
			{
				point_c = (*it);
				continue;
			}
			//如果z>0则point在point_i和point_c连线的下方,z<0则point_i在连线的上方,z=0则point_i共线
			z = comparePointClock(point_0,point_c,(*it));//((it->x() - point_0.x())*(point_c.y() - point_0.y()) - (it->y() - point_0.y())*(point_c.x() - point_0.x()));
			if (z > 0)
			{
				point_c = (*it);
			}
			else if (z == 0)
			{
				//共线情况找出距离point_0较远的那个点作为point_c
				length_1 = SEGMENTLEN(point_0.x(),point_0.y(),it->x(),it->y());
				length_2 = SEGMENTLEN(point_0.x(), point_0.y(), point_c.x(), point_c.y());
				if (length_1 > length_2)
				{
					point_c = (*it);
				}
			}
		}
		point_0 = point_c;

	} while (point_0 != lowPoint);
	vecTarPoints.push_back(lowPoint);
	if (vecTarPoints.isEmpty())
		return -1;
	return 0;
}

Graham 扫描算法,复杂度O(nlgn)

步骤:

1)与Jarvis's march算法一样找到坐标最下的点作为P_0。

2)对一批无序的点集中的点按照极角从小到大进行排序,如果极角相同则按由近及远进行排序(以P_0为起始点)。

按极角从小到大进行排序:

QPointF m_point0;
bool comPolarAngle(const QPointF &point_1, const QPointF &point_2)
{
	qreal z = ((point_2.x() - m_point0.x())*(point_1.y() - m_point0.y()) - (point_2.y() - m_point0.y())*(point_1.x() - m_point0.x()));
	if (fabs(z) < 1e-6)
	{
		qreal length_1 = SEGMENTLEN(m_point0.x(), m_point0.y(), point_1.x(), point_1.y());
		qreal length_2 = SEGMENTLEN(m_point0.x(), m_point0.y(), point_2.x(), point_2.y());

		return length_1 > length_2;
	}
	else
	{
		return z < 0;
	}
}
bool Convex::sortByPolarAngle(QVector<QPointF> &vecPoints)
{
	if (vecPoints.isEmpty())
		return false;

	QVector<QPointF> tempVecPoint(vecPoints);
	tempVecPoint.removeOne(m_point0);

	qreal z = 0;
	qSort(tempVecPoint.begin(), tempVecPoint.end(), comPolarAngle);
	tempVecPoint.push_front(m_point0);

	vecPoints = tempVecPoint;

	return true;
}

3)让排序后的点集中的前三个点依次入栈,然后开始遍历其后点,如果其后点与栈顶两个点不构成向左旋转的关系,则弹出栈顶元素,直到没有点需要出栈,那么就将当前点入栈,依次循环直到算有点都遍历结束。

具体实现源码:

//Graham 扫描算法,O(nlgn)。
qint8 Convex::getConvecHullGraham(const QVector<QPointF> &vecSourPoints, QVector<QPointF> &vecTarPoints)
{
	if (vecSourPoints.isEmpty())
		return -1;
	
	QVector<QPointF> tempVecPoint(vecSourPoints);

	//删除重复坐标
	if (removeRepeatPoints(tempVecPoint) <= 0)
		return -1;

	//查找最小坐标
	QPointF minPoint;
	minPoint = getMinimumPoint(tempVecPoint);
	m_point0 = minPoint;

	//按极角进行排序
	if(!sortByPolarAngle(tempVecPoint))
		return -1;

	vecTarPoints.clear();
	vecTarPoints.push_back(tempVecPoint.at(0));
	vecTarPoints.push_back(tempVecPoint.at(1));
	vecTarPoints.push_back(tempVecPoint.at(2));
	qint32 vecTop = 2;
	for (int i = 3; i < tempVecPoint.size(); i++)
	{
		while (vecTop > 0
			&& (comparePointClock(vecTarPoints.at(vecTop - 1), vecTarPoints.at(vecTop), tempVecPoint.at(i)) >= 0))
		{
			vecTop--;
			vecTarPoints.pop_back();
		}
		vecTarPoints.push_back(tempVecPoint.at(i));
		vecTop++;
	}
	vecTarPoints.push_back(minPoint);

	if (vecTarPoints.isEmpty())
		return -1;
	return 0;
}

注:源码.h和.cpp文件请在本人GitHub中浏览,望与参考的人一起学习进步!

地址:https://github.com/CMwshuai/ConvexHull.git

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
凸包算法是一种计算给定点集的凸包的方。其中,Graham扫描算法Jarvis步进算法是两种常见且经典的凸包算法。 1. Graham扫描算法: - 首先,选择一个点作为起始点(通常选择最下方的点,如果存在多个最下方的点,则选择最左边的点)。 - 将其余点按照相对于起始点的极角进行排序(逆时针排序)。 - 依次遍历排序后的点集,对每个点进行如下判断: - 如果当前点与栈顶的两个点构成的向量形成逆时针转向,将该点入栈。 - 否则,将栈顶的点出栈,直到当前点与栈顶的两个点构成的向量形成逆时针转向,然后将当前点入栈。 - 遍历结束后,栈中剩余的点即为凸包上的点。 2. Jarvis步进算法(也称为Gift Wrapping算法): - 首先,选择一个起始点(通常选择最左边的点)作为凸包上的一个点。 - 从起始点开始,依次选择能够使得当前点与下一个点构成的向量形成逆时针转向的下一个点,将其加入凸包。 - 重复上述过程,直到再次回到起始点为止。 这两种算法都是基于极角的思想,通过不断地寻找相邻点之间的逆时针转向来构建凸包。它们的时间复杂度都是O(nh),其中n是点的个数,h是凸包上的点的个数。相较而言,Graham扫描算法在一般情况下更快一些,但在特殊情况下可能会出现退化,而Jarvis步进算法则相对稳定但效率较低。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值