Що найбільш ефективно для GCD?


26

Я знаю, що алгоритм Евкліда є найкращим алгоритмом отримання GCD (великого загального дільника) списку натуральних чисел. Але на практиці ви можете кодувати цей алгоритм різними способами. (У моєму випадку я вирішив використовувати Java, але C / C ++ може бути іншим варіантом).

Мені потрібно використовувати найефективніший код, можливий у своїй програмі.

У рекурсивному режимі ви можете писати:

static long gcd (long a, long b){
    a = Math.abs(a); b = Math.abs(b);
    return (b==0) ? a : gcd(b, a%b);
  }

І в ітеративному режимі це виглядає приблизно так:

static long gcd (long a, long b) {
  long r, i;
  while(b!=0){
    r = a % b;
    a = b;
    b = r;
  }
  return a;
}

Існує також бінарний алгоритм для GCD, який може бути закодований просто так:

int gcd (int a, int b)
{
    while(b) b ^= a ^= b ^= a %= b;
    return a;
}

3
Я думаю, що це занадто суб'єктивно, і, можливо, навіть краще підходить для StackOverflow. "Найефективніший на практиці" залежить від багатьох (навіть непередбачуваних) факторів, таких як основна архітектура, ієрархія пам'яті, розмір і форма вводу тощо.
Juho

5
Це той самий алгоритм, що виражається рекурсивним та ітераційним способами. Я думаю, що їх різниця незначна, оскільки алгоритм Евкліда зближується досить швидко. Виберіть той, який відповідає вашим уподобанням.
площадка

6
Ви можете спробувати профайлювати ці два. Оскільки рекурсивна версія є хвостовим викликом, малоймовірно, що компілятор насправді випромінює майже той самий код.
Луї

1
це неправильно. має бути, поки b! = 0, а потім повернути a. Інакше він вимикається при поділі на нуль. також не використовуйте рекурсії, якщо у вас справді великі gcds .... ви отримуєте купу стеків та станів функцій ... чому б просто не піти ітеративно?
Cris Stringfellow

4
Зауважте, що існують асимптотично більш швидкі алгоритми GCD. Наприклад, en.wikipedia.org/wiki/Binary_GCD_algorithm
Neal Young

Відповіді:


21

Ваші два алгоритми еквівалентні (принаймні, для додатних цілих чисел, те, що відбувається з негативними цілими числами в імперативній версії, залежить від семантики Java, про %яку я не знаю напам’ять). У рекурсивній версії нехай і є аргументом го рекурсивного виклику: b i i a i + 1 = b i b i + 1 = a i m o d b iaibii

ai+1=bibi+1=aimodbi

В імперативному варіанті нехай і є значеннями змінних і на початку ї ітерації циклу. b i i a i + 1 = b i b i + 1 = a i m o d b iaibiabi

ai+1=bibi+1=aimodbi

Помітили подібність? Ваша імперативна версія та ваша рекурсивна версія обчислюють абсолютно однакові значення. Крім того, вони обидва закінчуються одночасно, коли (відповідно, ), тому вони виконують однакову кількість ітерацій. Тож алгоритмічно кажучи, різниці між ними немає. Будь-яка різниця буде справою реалізації, сильно залежати від компілятора, апаратного забезпечення, на якому він працює, і, можливо, операційної системи та інших програм, які працюють одночасно.a i = 0ai=0ai=0

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


8

Для малих чисел достатньо двійкового алгоритму GCD.

GMP, добре доглянута і перевірена в реальному світі бібліотека, перейде до спеціального половинного алгоритму GCD після проходження спеціального порогу, узагальнення алгоритму Лемера. Лемер використовує матричне множення для вдосконалення стандартних евклідових алгоритмів. Згідно з документацією, асимптотическое час роботи обох HGCD і GCD є O(M(N)*log(N)), де M(N)час для множення двох чисел N-кінцівки.

Детальну інформацію про їх алгоритм можна знайти тут .


Посилання справді не містить повних деталей і навіть не визначає, що таке "кінцівка" ...
einpoklum - відновити Моніку


2

Наскільки я знаю, Java не підтримує оптимізацію хвостової рекурсії в цілому, але ви можете протестувати свою реалізацію на Java; якщо він не підтримує, простий- forпетля повинен бути швидшим, інакше рекурсія повинна бути такою ж швидкою. З іншого боку, це бітова оптимізація, виберіть код, який, на вашу думку, простіший і читабельніший.

Слід також зазначити, що найшвидший алгоритм GCD - це не алгоритм Евкліда, алгоритм Лемера - трохи швидший.


Ви маєте на увазі , наскільки я знаю ? Ви маєте на увазі, що специфікація мови не вимагає цієї оптимізації (було б дивно, якби це було), або що більшість реалізацій не реалізують її?
PJTraill

1

По-перше, не використовуйте рекурсивність для заміни тугої петлі. Це повільно. Не покладайтеся на компілятор, щоб оптимізувати його. По-друге, у своєму коді ви телефонуєте на Math.abs () під час кожного рекурсивного дзвінка, що марно.

У своєму циклі ви можете легко уникнути тимчасових змінних і замінювати a і b весь час.

int gcd(int a, int b){
    if( a<0 ) a = -a;
    if( b<0 ) b = -b;
    while( b!=0 ){
        a %= b;
        if( a==0 ) return b;
        b %= a;
    }
    return a;
}

Заміна за допомогою a ^ = b ^ = a ^ = b робить джерело коротшим, але виконує багато інструкцій для виконання. Це буде повільніше, ніж нудний своп з тимчасовою змінною.


3
«Уникайте рекурсивності. Це повільно »- представлений як загальна порада, це хибно. Це залежить від компілятора. Зазвичай, навіть із компіляторами, які не оптимізують рекурсію, це не повільно, просто витрачає стеки.
Жил "ТАК - перестань бути злим"

3
Але для такого короткого коду різниця є істотною. Витрата на стеки означає запис і читання з пам'яті. Це повільно. Код, описаний вище, працює на 2 регістрах. Рекурсивність також означає робити дзвінки, що довше, ніж умовний стрибок. Рекурсивний виклик набагато складніше для прогнозування гілок і складніше вбудованого.
Флоріан Ф

-2

Для невеликих чисел % - це досить дорога операція, можливо, простіша рекурсивна

GCD[a,b] := Which[ 
   a==b , Return[a],
   b > a, Return[ GCD[a, b-a]],
   a > b, Return[ GCD[b, a-b]]
];

швидше? (Вибачте, код Mathematica, а не C ++)


Це не виглядає правильно. Для b == 1 він повинен повернутися 1. І GCD [2,1000000000] буде повільним.
Флоріан Ф

Ага, так, я помилився. Виправлена ​​(я думаю) та уточнена.
Пер Александерсон

Зазвичай GCD [a, 0] також повинен повернути a. Твої петлі назавжди.
Флоріан F

Я звонячи, оскільки ваша відповідь містить лише код. Нам подобається орієнтуватися на ідеї на цьому сайті. Наприклад, чому% - це дорога операція? На мій погляд, спекуляція на фрагменті коду насправді не є гарною відповіддю для цього сайту.
Juho

1
Думаю, думка про те, що модуль повільніше, ніж віднімання, можна вважати фольклором. Це справедливо як для малих цілих чисел (віднімання зазвичай займає один цикл, по модулю рідко), так і для великих цілих чисел (віднімання лінійне, я не впевнений, яка найкраща складність для модуля, але це, безумовно, гірше, ніж це). Звичайно, вам також потрібно врахувати кількість необхідних ітерацій.
Жил 'ТАК - перестань бути злим'

-2

Алгоритм Евкліда є найбільш ефективним для обчислення GCD:

Статичний довгий gcd ​​(long a, long b)
{
якщо (b == 0)
повернути a;
ще
повернути gcd (,% b);
}

приклад: -

Нехай A = 16, B = 10.
GCD (16, 10) = GCD (10, 16% 10) = GCD (10, 6)
GCD (10, 6) = GCD (6, 10% 6) = GCD (6, 4)
GCD (6, 4) = GCD (4, 6% 4) = GCD (4, 2)
GCD (4, 2) = GCD (2, 4% 2) = GCD (2, 0)


Оскільки B = 0, то GCD (2, 0) поверне 2. 

4
Це не відповідає на запитання. Автор запитує дві версії Euclid і запитує, яка швидше. Ви, здається, не помічали цього і просто оголосите рекурсивну версію єдиним алгоритмом Евкліда, і не стверджуєте, що це не швидше, ніж усе інше.
Девід Річербі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.