Сортувати точки в порядку годинникової стрілки?


156

З огляду на масив x, y точок, як я сортую точки цього масиву за порядком годинникової стрілки (навколо їх загальної середньої центральної точки)? Моя мета - передати точки функції створення ліній, щоб в кінцевому підсумку виглядати досить «суцільно», як можна випукше, не перетинаючи лінії.

Для чого це варто, я використовую Lua, але будь-який псевдокоп буде вдячний.

Оновлення: Для довідки, це код 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
Подумайте про обчислення кута радіальної лінії через цю точку. Потім сортуйте за кутом.
президент Джеймс К. Полк

Якщо ви цього не знаєте, у 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

2
@Wallacoloo Це дуже сперечально. Крім того, у ванілі Lua ipairsзначно повільніше, ніж числовий для циклу.
Олександр Гладиш

Мені довелося внести невеликі зміни, щоб він працював у моєму випадку (просто порівнюючи дві точки відносно центру). gist.github.com/personalnadir/6624172 Усі ці порівняння з 0 у коді, здається, припускають, що бали розподіляються навколо початку, на відміну від довільної точки. Я також думаю, що перша умова буде неправильно сортувати точки нижче центральної точки. Дякуємо за код, але це було дуже корисно!
personalnadir

Відповіді:


192

Спочатку обчисліть центральну точку. Потім сортуйте точки за допомогою будь-якого алгоритму сортування, який вам подобається, але використовуйте спеціальну процедуру порівняння, щоб визначити, чи одна точка менша за іншу.

Ви можете перевірити, чи одна точка (а) ліворуч або праворуч від другої (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 мають такий тип, який зможе утримувати результат виконаних обчислень.

Якщо ви хочете домогтися чогось суцільного, максимально опуклого, то, мабуть, ви шукаєте опуклого корпусу . Ви можете обчислити його за допомогою програми Graham Scan . У цьому алгоритмі вам також доведеться сортувати точки за годинниковою стрілкою (або проти годинникової стрілки), починаючи зі спеціальної точки повороту. Потім ви повторюєте прості кроки циклу кожного разу, перевіряючи, чи повертаєте ви вліво або вправо, додаючи нові точки до опуклого корпусу, ця перевірка заснована на поперечному творі так само, як у наведеній вище функції порівняння.

Редагувати:

Додано ще одне, якщо вислів, if (a.y - center.y >= 0 || b.y - center.y >=0)щоб переконатися, що точки, що мають x = 0 та мінус y, відсортовані, починаючи з тих, які знаходяться далі від центру. Якщо вам не байдуже порядок балів на ту саму «годину», ви можете опустити це, якщо заяву, і завжди повертатись a.y > b.y.

Виправлено перший, якщо заяви з додаванням -center.xі -center.y.

Додано друге, якщо твердження (a.x - center.x < 0 && b.x - center.x >= 0). Це був очевидний нагляд, що він відсутній. Виписки if можуть бути реорганізовані зараз, оскільки деякі перевірки зайві. Наприклад, якщо перша умова в першому, якщо твердження є помилковим, то перша умова другої, якщо має бути істинною. Однак я вирішив залишити код таким, який він є для простоти. Цілком можливо, що компілятор оптимізує код і все одно дасть такий самий результат.


25
+1: Ні atan(), немає квадратного кореня і навіть немає поділів. Це хороший приклад мислення комп'ютерної графіки. Скасуйте всі найпростіші випадки якнайшвидше, і навіть у важких випадках обчисліть якомога менше, щоб знати потрібну відповідь.
RBerteig

Але це вимагає порівняння всіх балів з усіма іншими. Чи існує простий метод вставки нових точок?
Ітератор

2
якщо множина точок відома апріорі, вона бере лише порівняння O (n * log n). Якщо ви хочете тим часом додати бали, вам потрібно зберегти їх у відсортованому наборі, наприклад, урівноваженому дереві двійкових пошуків. У такому випадку додавання нової точки вимагає порівняння O (log n), і це саме те саме для рішення, що включає полярні координати.
ciamej

2
Чи відсутній цей випадок: якщо (ax - center.x <0 && bx - center.x> = 0) повернути false;
Том Мартін

2
Гей там. Він досить старий, але: "Це впорядкує точки за годинниковою стрілкою, починаючи з 12 години". Чому 12 годин і як я можу змінити її на 6? Хтось може мені сказати?
Ісмох

20

Цікавим альтернативним підходом до вашої проблеми було б знайти приблизний мінімум до проблеми мандрівного продавця (TSP), тобто. найкоротший маршрут, що з'єднує всі ваші пункти. Якщо ваші точки утворюють опуклу форму, це має бути правильним рішенням, інакше вона все-таки повинна виглядати добре («тверду» форму можна визначити як ту, яка має низьке співвідношення периметр / площа, для чого ми тут оптимізуємо) .

Ви можете використовувати будь-яку реалізацію оптимізатора для TSP, з якого я впевнений, що ви зможете знайти тон на своїй обраній мові.


Yikes. "Цікаво" - заниження. :)
Ітератор

@Iterator: Я був цілком задоволений своєю ідеєю, я був дуже розчарований, коли прийняв до неї відповідальність: - / Ви вважаєте, що це справедливо?
static_rtti

1
Я пропонував використовувати одне з багатьох швидких наближень, а не NP-повний оригінальний алгоритм, звичайно.
static_rtti

6
Я ціную додатковий кут! Мати декілька достовірних, якщо дуже різних відповідей, може бути корисною, якщо хтось у майбутньому трапиться натрапити на цю нитку, шукаючи варіанти мозкового штурму.
Філіп Ленсен

1
Зауважте, що мій підхід, мабуть, повільніший, але більш правильний у складних випадках: уявіть випадок, коли, наприклад, бали за "8". Полярні координати не допоможуть вам у цьому випадку, і результат, який ви отримаєте, буде сильно залежати від обраного вами центру. Рішення TSP не залежить від будь-яких "евристичних" параметрів.
static_rtti

19

Що ви просите, це система, відома як полярні координати . Перетворення від декартової до полярної координат легко проводиться будь-якою мовою. Формули можна знайти в цьому розділі .

Після перетворення на полярні координати просто сортуйте за кутом, тета.


4
Це буде спрацьовувати, але це також матиме дефект робити більше обчислень, ніж потрібно для відповіді на питання замовлення. На практиці вас насправді не цікавлять ні фактичні кути, ні радіальні відстані, а лише їх відносний порядок. Рішення ciamej краще, оскільки воно дозволяє уникнути поділів, квадратних коренів і тригерів.
RBerteig

1
Я не впевнений, який ваш критерій "кращого". Наприклад, порівняння всіх точок один з одним є свого роду марною витратою обчислень. Триг - це не те, що лякає дорослих, чи не так?
Ітератор

3
Це не триге страшно. Проблема полягає в тому, що тригмент обчислюється досить дорого, і він не був необхідний для визначення відносного порядку кутів. Так само вам не потрібно брати квадратні корені, щоб привести радіуси в порядок. Повна конверсія від декартової до полярної координат буде виконувати як дугову дотичну, так і квадратну корень. Отже, ваша відповідь правильна, але в контексті комп’ютерної графіки чи обчислювальної геометрії це, мабуть, не найкращий спосіб зробити це.
RBerteig

Зрозумів. Однак ОП не розміщувалась як comp-geo, це було тегом когось іншого. І все-таки, схоже, що інше рішення є многочленом у # балах, чи я помиляюся? Якщо так, то спалюється більше циклів, ніж триггер.
Ітератор

Я фактично не помічав тег comp-geo, я просто припускав, що єдиними раціональними додатками для цього питання повинні бути те чи інше. Зрештою, питання про продуктивність стає суперечливим, якщо є лише кілька пунктів, та / або операція буде виконана досить рідко. Тоді знання про те, як це зробити, стає важливим, і тому я погоджуюся, що ваша відповідь правильна. Він пояснює, як обчислити поняття "порядок за годинниковою стрілкою" в термінах, які можна пояснити майже нікому.
RBerteig

3

Інша версія (повернення true, якщо a надходить до b у напрямку проти годинникової стрілки):

    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
Два твердження повернення в комутаторі математично еквівалентні. Чи є причина у тому, щоб мати вимикач?
Унагі

0
  • vector3 a = новий вектор3 (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 () .................. Clockwise_order

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.