Sắp xếp điểm theo chiều kim đồng hồ?


156

Cho một mảng gồm các điểm x, y, làm cách nào để sắp xếp các điểm của mảng này theo thứ tự theo chiều kim đồng hồ (xung quanh điểm trung tâm tổng thể của chúng)? Mục tiêu của tôi là chuyển các điểm đến một chức năng tạo dòng để kết thúc với một cái gì đó trông khá "vững chắc", càng lồi càng tốt mà không có đường nào giao nhau.

Đối với những gì nó có giá trị, tôi đang sử dụng Lua, nhưng bất kỳ mã giả nào cũng sẽ được đánh giá cao.

Cập nhật: Để tham khảo, đây là mã Lua dựa trên câu trả lời xuất sắc của Ciamej (bỏ qua tiền tố "ứng dụng" của tôi):

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
Hãy suy nghĩ về việc tính góc của đường xuyên tâm qua điểm đó. Sau đó sắp xếp theo góc.
Tổng thống James K. Polk

Trong trường hợp bạn không biết, lua có hàm dựng sẵn ipairs(tbl)lặp lại qua các chỉ số và giá trị của tbl từ 1 đến #tbl. Vì vậy, để tính tổng, bạn có thể làm điều này, điều mà hầu hết mọi người thấy có vẻ sạch sẽ hơn:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle

2
@Wallacoloo Điều đó rất đáng tranh cãi. Ngoài ra, trong vanilla Lua ipairschậm hơn đáng kể so với số vòng lặp.
Alexander Gladysh

Tôi đã phải thực hiện một số thay đổi nhỏ để làm cho nó hoạt động cho trường hợp của tôi (chỉ cần so sánh hai điểm so với một trung tâm). gist.github.com/personalnadir/6624172 Tất cả những so sánh đó với 0 trong mã dường như cho rằng các điểm được phân phối xung quanh gốc, trái ngược với điểm tùy ý. Tôi cũng nghĩ rằng điều kiện đầu tiên sẽ sắp xếp các điểm dưới điểm trung tâm không chính xác. Cảm ơn về mã mặc dù, nó thực sự hữu ích!
Personalnadir

Câu trả lời:


192

Đầu tiên, tính điểm trung tâm. Sau đó sắp xếp các điểm bằng bất kỳ thuật toán sắp xếp nào bạn thích, nhưng sử dụng thói quen so sánh đặc biệt để xác định xem một điểm có nhỏ hơn điểm kia hay không.

Bạn có thể kiểm tra xem một điểm (a) ở bên trái hay bên phải của điểm (b) khác liên quan đến trung tâm bằng phép tính đơn giản này:

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

nếu kết quả bằng 0, thì chúng nằm trên cùng một đường thẳng từ tâm, nếu nó dương hoặc âm, thì nó ở bên này hay bên kia, vì vậy một điểm sẽ đi trước điểm kia. Sử dụng nó, bạn có thể xây dựng một mối quan hệ ít hơn để so sánh các điểm và xác định thứ tự chúng sẽ xuất hiện trong mảng được sắp xếp. Nhưng bạn phải xác định đâu là điểm bắt đầu của thứ tự đó, ý tôi là góc nào sẽ là góc bắt đầu (ví dụ: nửa dương của trục x).

Mã cho chức năng so sánh có thể trông như thế này:

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;
}

Điều này sẽ sắp xếp các điểm theo chiều kim đồng hồ bắt đầu từ 12 giờ. Các điểm trên cùng một "giờ" sẽ được sắp xếp bắt đầu từ những điểm xa hơn từ trung tâm.

Nếu sử dụng các loại số nguyên (không thực sự có trong Lua), bạn phải đảm bảo rằng các biến det, d1 và d2 thuộc loại có thể giữ kết quả tính toán được thực hiện.

Nếu bạn muốn đạt được thứ gì đó trông rắn chắc, càng lồi càng tốt, thì tôi đoán bạn đang tìm kiếm một Convex Hull . Bạn có thể tính toán nó bằng Graham Scan . Trong thuật toán này, bạn cũng phải sắp xếp các điểm theo chiều kim đồng hồ (hoặc ngược chiều kim đồng hồ) bắt đầu từ một điểm trục đặc biệt. Sau đó, bạn lặp lại các bước lặp đơn giản mỗi lần kiểm tra xem bạn rẽ trái hay phải thêm điểm mới vào thân lồi, kiểm tra này dựa trên một sản phẩm chéo giống như trong chức năng so sánh ở trên.

Biên tập:

Đã thêm một câu lệnh if if (a.y - center.y >= 0 || b.y - center.y >=0) để đảm bảo rằng các điểm có x = 0 và âm y được sắp xếp bắt đầu từ các điểm nằm xa trung tâm. Nếu bạn không quan tâm đến thứ tự các điểm trong cùng một 'giờ', bạn có thể bỏ qua điều này nếu câu lệnh và luôn trả về a.y > b.y.

Đã sửa lỗi đầu tiên nếu thêm -center.x-center.y.

Đã thêm câu lệnh if thứ hai (a.x - center.x < 0 && b.x - center.x >= 0). Đó là một sự giám sát rõ ràng rằng nó đã bị mất. Các câu lệnh if có thể được tổ chức lại ngay bây giờ vì một số kiểm tra là dư thừa. Ví dụ, nếu điều kiện đầu tiên trong câu lệnh if đầu tiên là sai, thì điều kiện đầu tiên của câu lệnh thứ hai nếu phải là true. Tôi đã quyết định, tuy nhiên, để lại mã vì nó là vì đơn giản. Hoàn toàn có khả năng trình biên dịch sẽ tối ưu hóa mã và tạo ra kết quả tương tự.


25
+1: Không atan(), không có căn bậc hai và thậm chí không có phân chia. Đây là một ví dụ tốt về tư duy đồ họa máy tính. Bỏ tất cả các trường hợp dễ dàng càng sớm càng tốt, và ngay cả trong các trường hợp khó, hãy tính toán càng ít càng tốt để biết câu trả lời cần thiết.
RBerteig

Nhưng nó đòi hỏi so sánh tất cả các điểm với tất cả những người khác. Có một phương pháp đơn giản để chèn điểm mới?
Lặp lại

2
nếu tập hợp các điểm được biết là một tiên nghiệm thì chỉ cần so sánh O (n * log n). Nếu bạn muốn thêm điểm trong thời gian này thì bạn cần giữ chúng trong một tập hợp được sắp xếp, chẳng hạn như cây tìm kiếm nhị phân cân bằng. Trong trường hợp như vậy, việc thêm một điểm mới yêu cầu so sánh O (log n) và nó hoàn toàn giống nhau đối với giải pháp liên quan đến tọa độ cực.
ciamej

2
Đây có phải là trường hợp thiếu: if (ax - centre.x <0 && bx - centre.x> = 0) return false;
Tom Martin

2
Này đó. Nó khá cũ, nhưng: "Điều này sẽ sắp xếp các điểm theo chiều kim đồng hồ bắt đầu từ 12 giờ." Tại sao 12 giờ và làm thế nào tôi có thể thay đổi thành 6? Ai có thể cho tôi biết?
Ismoh

20

Một cách tiếp cận thú vị khác cho vấn đề của bạn sẽ là tìm mức tối thiểu gần đúng cho Vấn đề nhân viên bán hàng du lịch (TSP), tức là. con đường ngắn nhất liên kết tất cả các điểm của bạn. Nếu các điểm của bạn tạo thành hình lồi, thì đó phải là giải pháp phù hợp, nếu không, nó vẫn trông đẹp (hình dạng "rắn" có thể được định nghĩa là hình có tỷ lệ chu vi / diện tích thấp, đó là những gì chúng tôi đang tối ưu hóa ở đây) .

Bạn có thể sử dụng bất kỳ triển khai trình tối ưu hóa nào cho TSP, trong đó tôi khá chắc chắn rằng bạn có thể tìm thấy một tấn trong ngôn ngữ bạn chọn.


Rất tiếc. "Thú vị" là một cách đánh giá thấp. :)
Lặp lại

@Iterator: Tôi khá hài lòng với ý tưởng của mình, tôi đã khá thất vọng khi bị hạ bệ vì nó: - / Bạn có nghĩ nó hợp lệ không?
static_rtti

1
Tôi đã đề nghị sử dụng một trong nhiều phép tính gần đúng nhanh, chứ không phải thuật toán gốc hoàn chỉnh NP, tất nhiên.
static_rtti

6
Tôi đánh giá cao các góc bổ sung! Để có một số câu trả lời hợp lệ, nếu rất khác nhau, có thể giúp ích rất nhiều nếu ai đó trong tương lai tình cờ vấp phải chủ đề này để tìm kiếm các lựa chọn động não.
Philipp Lenssen

1
Lưu ý rằng cách tiếp cận của tôi có thể chậm hơn, nhưng chính xác hơn trong các trường hợp phức tạp: ví dụ, hãy tưởng tượng trường hợp các điểm cho "8". Các tọa độ cực sẽ không giúp bạn trong trường hợp đó và kết quả bạn sẽ nhận được sẽ phụ thuộc rất nhiều vào trung tâm bạn đã chọn. Giải pháp TSP độc lập với bất kỳ tham số "heuristic" nào.
static_rtti

19

Những gì bạn đang yêu cầu là một hệ thống được gọi là tọa độ cực . Chuyển đổi từ Cartesian sang tọa độ cực được thực hiện dễ dàng trong bất kỳ ngôn ngữ nào. Các công thức có thể được tìm thấy trong phần này .

Sau khi chuyển đổi sang tọa độ cực, chỉ cần sắp xếp theo góc, theta.


4
Điều này sẽ hoạt động, nhưng nó cũng sẽ có khiếm khuyết khi thực hiện nhiều tính toán hơn mức cần thiết để trả lời câu hỏi đặt hàng. Trong thực tế, bạn không thực sự quan tâm đến các góc thực tế hoặc khoảng cách xuyên tâm, chỉ là thứ tự tương đối của chúng. Giải pháp của ciamej là tốt hơn vì nó tránh được sự phân chia, căn bậc hai và trig.
RBerteig

1
Tôi không chắc tiêu chí của bạn là "tốt hơn". Ví dụ, so sánh tất cả các điểm với nhau là một sự lãng phí tính toán. Trig không phải là thứ khiến người lớn sợ hãi, phải không?
Lặp lại

3
Nó không phải là trig là đáng sợ. Vấn đề là trig rất tốn kém để tính toán và không cần thiết để xác định thứ tự tương đối của các góc. Tương tự, bạn không cần lấy căn bậc hai để đặt bán kính theo thứ tự. Một chuyển đổi đầy đủ từ tọa độ Descartes sang tọa độ cực sẽ thực hiện cả tiếp tuyến cung và căn bậc hai. Do đó câu trả lời của bạn là chính xác, nhưng trong bối cảnh đồ họa máy tính hoặc hình học tính toán, nó có thể không phải là cách tốt nhất để làm điều đó.
RBerteig

Hiểu rồi. Tuy nhiên, OP không đăng bài dưới dạng comp-Geo, đó là thẻ của người khác. Tuy nhiên, có vẻ như giải pháp khác là đa thức trong số điểm, hoặc tôi nhầm? Nếu vậy, điều đó đốt cháy nhiều chu kỳ hơn trig.
Lặp lại

Tôi thực sự không nhận thấy thẻ comp-Geo, tôi chỉ cho rằng các ứng dụng hợp lý duy nhất cho câu hỏi phải là cái này hay cái kia. Rốt cuộc, câu hỏi về hiệu suất sẽ trở thành tranh luận nếu chỉ có một vài điểm và / hoặc thao tác sẽ được thực hiện hiếm khi đủ. Tại thời điểm đó, biết làm thế nào để làm điều đó trở nên quan trọng và đó là lý do tại sao tôi đồng ý câu trả lời của bạn là chính xác. Nó giải thích làm thế nào để tính toán khái niệm "thứ tự theo chiều kim đồng hồ" theo thuật ngữ có thể được giải thích cho bất kỳ ai.
RBerteig

3

Một phiên bản khác (trả về true nếu a đến trước b theo hướng ngược chiều kim đồng hồ):

    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;
       } 
    }

Việc này nhanh hơn, vì trình biên dịch (đã thử nghiệm trên Visual C ++ 2015) không tạo ra bước nhảy để tính toán dax, day, dbx, dby. Ở đây tập hợp đầu ra từ trình biên dịch:

; 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

Thưởng thức.


1
Hai câu lệnh return trong switch là tương đương về mặt toán học. Có một lý do để có công tắc?
unagi

0
  • vector3 a = vector3 mới (1, 0, 0) .............. wrt X_axis
  • vector3 b = any_point - Trung tâm;
- 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

Cuối cùng, bạn nhận được các câu đã được sắp xếp Anticlockwize

list.Reverse () .................. Clockwise_order

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.