按顺时针顺序对点排序?


156

给定一个x,y点数组,如何按顺时针顺序(在它们的整体平均中心点附近)对该数组的点排序?我的目标是将这些点传递给线创建函数,以得到看起来很“实心”的东西,尽可能凸,没有线相交。

对于它的价值,我正在使用Lua,但任何伪代码都将不胜感激。

更新:作为参考,这是基于Ciamej出色答案的Lua代码(忽略我的“ app”前缀):

function appSortPointsClockwise(points)
    local centerPoint = appGetCenterPointOfPoints(points)
    app.pointsCenterPoint = centerPoint
    table.sort(points, appGetIsLess)
    return points
end

function appGetIsLess(a, b)
    local center = app.pointsCenterPoint

    if a.x >= 0 and b.x < 0 then return true
    elseif a.x == 0 and b.x == 0 then return a.y > b.y
    end

    local det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)
    if det < 0 then return true
    elseif det > 0 then return false
    end

    local d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y)
    local d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y)
    return d1 > d2
end

function appGetCenterPointOfPoints(points)
    local pointsSum = {x = 0, y = 0}
    for i = 1, #points do pointsSum.x = pointsSum.x + points[i].x; pointsSum.y = pointsSum.y + points[i].y end
    return {x = pointsSum.x / #points, y = pointsSum.y / #points}
end


1
考虑计算通过该点的径向线的角度。然后按角度排序。
总统詹姆斯·波尔克(James K. Polk)

如果您不知道,lua具有一个内置函数ipairs(tbl),可将tbl 的索引和值从1 迭代到#tbl。因此,对于总和计算,您可以执行此操作,大多数人认为这看起来更干净:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle 2011年

2
@Wallacoloo这是非常有争议的。另外,在香草中,Lua ipairs比for循环的数字慢得多。
亚历山大·格拉迪什

我必须进行一些小改动才能使其适用于我的情况(只需比较相对于中心的两个点)。 gist.github.com/personalnadir/6624172代码中 所有与0的比较似乎都假设这些点围绕原点分布,而不是任意点。我还认为,第一个条件会错误地将中心点以下的点排序。不过,感谢您的代码,它确实很有帮助!
personalnadir

Answers:


192

首先,计算中心点。然后使用任何喜欢的排序算法对点进行排序,但是使用特殊的比较例程来确定一个点是否小于另一个点。

您可以通过以下简单计算来检查相对于中心的点(a)在另一个点(b)的左边还是右边:

det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)

如果结果为零,则它们位于中心的同一条线上;如果结果为正或负,则其在一侧或另一侧,因此一个点将在另一点之前。使用它,您可以构建小于关系来比较点并确定它们在已排序数组中应出现的顺序。但是您必须定义该顺序的起点在哪里,我的意思是起始角度将是哪个角度(例如,x轴的正一半)。

比较功能的代码如下所示:

bool less(point a, point b)
{
    if (a.x - center.x >= 0 && b.x - center.x < 0)
        return true;
    if (a.x - center.x < 0 && b.x - center.x >= 0)
        return false;
    if (a.x - center.x == 0 && b.x - center.x == 0) {
        if (a.y - center.y >= 0 || b.y - center.y >= 0)
            return a.y > b.y;
        return b.y > a.y;
    }

    // compute the cross product of vectors (center -> a) x (center -> b)
    int det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y);
    if (det < 0)
        return true;
    if (det > 0)
        return false;

    // points a and b are on the same line from the center
    // check which point is closer to the center
    int d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y);
    int d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y);
    return d1 > d2;
}

这将从12点开始按顺时针方向对点进行排序。同一“小时”上的点将从距中心较远的点开始排序。

如果使用整数类型(在Lua中并不真正存在),则必须确保det,d1和d2变量的类型能够保存执行的计算结果。

如果您想获得看起来像实心的东西(尽可能凸),那么我想您正在寻找的是Convex Hull。您可以使用Graham Scan进行计算。在此算法中,还必须从特殊的枢轴点开始按顺时针(或逆时针)对点进行排序。然后,每次检查是否向左或向右添加新点到凸包时都重复简单的循环步骤,此检查基于叉积,就像上面的比较功能一样。

编辑:

再添加一个if语句if (a.y - center.y >= 0 || b.y - center.y >=0),以确保具有x = 0和负y的点从与中心更远的点开始排序。如果您不在乎同一“小时”上的点顺序,则可以忽略此if语句,并始终返回a.y > b.y

更正了第一个if语句,添加了-center.x-center.y

添加了第二个if语句(a.x - center.x < 0 && b.x - center.x >= 0)。很明显,它丢失了。由于某些检查是多余的,因此现在可以重新组织if语句。例如,如果第一个if语句中的第一个条件为false,则第二个if的第一个条件必须为true。但是,为了简化起见,我决定保留原样。无论如何,编译器很有可能会优化代码并产生相同的结果。


25
+1:不atan(),没有平方根,甚至没有除法。这是计算机图形学思维的一个很好的例子。尽快排除所有容易的情况,即使在困难的情况下,也要尽可能少地计算以了解所需的答案。
RBerteig

但这需要将所有点与所有其他点进行比较。有没有插入新点的简单方法?
Iterator

2
如果点集是先验的,则只进行O(n * log n)比较。如果要在此期间添加点,则需要将它们保留在排序的集合中,例如平衡的二进制搜索树。在这种情况下,添加新点需要进行O(log n)比较,并且对于涉及极坐标的解决方案来说,它是完全相同的。
ciamej 2011年

2
是否缺少这种情况:if(ax-center.x <0 && bx-center.x> = 0)返回false;
汤姆·马丁

2
嘿。它已经很老了,但是:“这将从12点开始按顺时针顺序排列点。” 为什么是12点钟,如何将其更改为6点钟?有人可以告诉我吗?
Ismoh's

20

解决您的问题的一种有趣的替代方法是找到旅行商问题(TSP)的近似最小值。连接所有点的最短路线。如果您的点形成凸形,那应该是正确的解决方案,否则,它仍然应该看起来不错(可以将“实心”形状定义为周长/面积比低的形状,这是我们在此处进行的优化) 。

您可以使用针对TSP的优化程序的任何实现,我敢肯定,您可以使用自己选择的语言找到大量的优化程序。


kes。“有趣”是一种轻描淡写。:)
Iterator

@Iterator:我对自己的想法感到非常满意,为此感到失望:-//你认为这是有效的吗?
static_rtti 2011年

1
当然,我建议使用许多快速逼近方法之一,而不要使用NP完全原始算法。
static_rtti 2011年

6
我感谢其他角度!如果将来有人偶然发现这个问题以寻求头脑风暴的选项,那么有几个有效的答案(如果有非常不同的答案)可能会很有帮助。
菲利普·伦森

1
请注意,我的方法可能较慢,但在复杂情况下更正确:例如,设想点数为“ 8”的情况。在这种情况下,极坐标不会对您有帮助,您将获得的结果将在很大程度上取决于您选择的中心。TSP解决方案独立于任何“启发式”参数。
static_rtti 2011年

19

您要的是一个称为极坐标的系统。从笛卡尔坐标到极坐标的转换很容易以任何语言完成。公式可以在本节中找到。

转换为极坐标后,只需按角度theta排序即可。


4
这将起作用,但也将具有执行比回答订购问题所需的更多的计算量的缺陷。实际上,您实际上并不在乎实际角度或径向距离,而只是在乎它们的相对顺序。ciamej的解决方案更好,因为它避免了分割,平方根和trig。
RBerteig

1
我不确定您的“更好”标准是什么。例如,将所有点相互比较是一种计算的浪费。Trig不是吓到大人的东西,对吗?
Iterator

3
不是说触发很可怕。问题在于,trig的计算成本很高,并且不需要确定角度的相对顺序。同样,您无需取平方根即可按顺序排列半径。从笛卡尔坐标到极坐标的完整转换将同时产生反正切和平方根。因此,您的答案是正确的,但是在计算机图形或计算几何学的背景下,这可能不是最佳方法
RBerteig

得到它了。但是,OP并未发布为comp-geo,这是其他人的标签。不过,看起来其他解决方案是点数中的多项式,还是我弄错了?如果是这样,那将比触发消耗更多的周期。
Iterator

我实际上并没有注意到comp-geo标签,我只是假设问题的唯一合理应用必须是一个或另一个。毕竟,如果只有几点,性能问题将变得毫无意义,和/或操作将很少进行。到那时,完全知道如何做就变得很重要,这就是为什么我同意您的答案是正确的。它解释了如何以任何人都可以解释的术语来计算“顺时针顺序”的概念。
RBerteig

3

另一个版本(如果a在逆时针方向上位于b之前,则返回true):

    bool lessCcw(const Vector2D &center, const Vector2D &a, const Vector2D &b) const
    {
        // Computes the quadrant for a and b (0-3):
        //     ^
        //   1 | 0
        //  ---+-->
        //   2 | 3

        const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;
        const int day = ((a.y() - center.y()) > 0) ? 1 : 0;
        const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

        /* The previous computes the following:

           const int qa =
           (  (a.x() > center.x())
            ? ((a.y() > center.y())
                ? 0 : 3)
            : ((a.y() > center.y())
                ? 1 : 2)); */

        const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;
        const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;
        const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

        if (qa == qb) {
            return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());
        } else {
            return qa < qb;
       } 
    }

这更快,因为编译器(在Visual C ++ 2015上测试)不会生成跳转来计算dax,day,dbx,dby。这里是编译器的输出程序集:

; 28   :    const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;

    vmovss  xmm2, DWORD PTR [ecx]
    vmovss  xmm0, DWORD PTR [edx]

; 29   :    const int day = ((a.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm1, DWORD PTR [ecx+4]
    vsubss  xmm4, xmm0, xmm2
    vmovss  xmm0, DWORD PTR [edx+4]
    push    ebx
    xor ebx, ebx
    vxorps  xmm3, xmm3, xmm3
    vcomiss xmm4, xmm3
    vsubss  xmm5, xmm0, xmm1
    seta    bl
    xor ecx, ecx
    vcomiss xmm5, xmm3
    push    esi
    seta    cl

; 30   :    const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

    mov esi, 2
    push    edi
    mov edi, esi

; 31   : 
; 32   :    /* The previous computes the following:
; 33   : 
; 34   :    const int qa =
; 35   :        (   (a.x() > center.x())
; 36   :         ? ((a.y() > center.y()) ? 0 : 3)
; 37   :         : ((a.y() > center.y()) ? 1 : 2));
; 38   :    */
; 39   : 
; 40   :    const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;

    xor edx, edx
    lea eax, DWORD PTR [ecx+ecx]
    sub edi, eax
    lea eax, DWORD PTR [ebx+ebx]
    and edi, eax
    mov eax, DWORD PTR _b$[esp+8]
    sub edi, ecx
    sub edi, ebx
    add edi, esi
    vmovss  xmm0, DWORD PTR [eax]
    vsubss  xmm2, xmm0, xmm2

; 41   :    const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm0, DWORD PTR [eax+4]
    vcomiss xmm2, xmm3
    vsubss  xmm0, xmm0, xmm1
    seta    dl
    xor ecx, ecx
    vcomiss xmm0, xmm3
    seta    cl

; 42   :    const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

    lea eax, DWORD PTR [ecx+ecx]
    sub esi, eax
    lea eax, DWORD PTR [edx+edx]
    and esi, eax
    sub esi, ecx
    sub esi, edx
    add esi, 2

; 43   : 
; 44   :    if (qa == qb) {

    cmp edi, esi
    jne SHORT $LN37@lessCcw

; 45   :        return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());

    vmulss  xmm1, xmm2, xmm5
    vmulss  xmm0, xmm0, xmm4
    xor eax, eax
    pop edi
    vcomiss xmm0, xmm1
    pop esi
    seta    al
    pop ebx

; 46   :    } else {
; 47   :        return qa < qb;
; 48   :    }
; 49   : }

    ret 0
$LN37@lessCcw:
    pop edi
    pop esi
    setl    al
    pop ebx
    ret 0
?lessCcw@@YA_NABVVector2D@@00@Z ENDP            ; lessCcw

请享用。


1
开关中的两个return语句在数学上是等效的。是否有理由进行转换?
unagi '18

0
  • vector3 a =新的vector3(1,0,0).............. wrt X_axis
  • vector3 b = any_point-中心;
- y = |a * b|   ,   x =  a . b

- Atan2(y , x)...............................gives angle between -PI  to  + PI  in radians
- (Input % 360  +  360) % 360................to convert it from  0 to 2PI in radians
- sort by adding_points to list_of_polygon_verts by angle  we got 0  to 360

最终,您获得Anticlockwize排序的版本

list.Reverse()..................顺时针排序

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.