Является ли рекурсия быстрее, чем зацикливание?

286

Я знаю, что рекурсия иногда намного чище, чем зацикливание, и я ничего не спрашиваю о том, когда мне следует использовать рекурсию поверх итерации, я знаю, что уже есть много вопросов по этому поводу.

Что я спрашиваю, так ли рекурсия всегда быстрее, чем цикл? Мне кажется, вы всегда сможете уточнить цикл и заставить его работать быстрее, чем рекурсивная функция, потому что цикл отсутствует, постоянно настраивая новые кадры стека.

Я специально ищу, является ли рекурсия быстрее в приложениях, где рекурсия является правильным способом обработки данных, например, в некоторых функциях сортировки, в двоичных деревьях и т. Д.

Карсон Майерс
источник
3
Иногда для итеративной процедуры или формул в замкнутой форме для некоторых повторений требуются столетия. Я думаю, что только в те времена рекурсия быстрее :) LOL
Pratik Deoghare
24
Говоря за себя, я предпочитаю итерацию. ;-)
Итератор
возможный дубликат рекурсии или итерации?
Nawfal
3
смотрите stackoverflow.com/questions/2651112/…
ctrl-alt-delor
@PratikDeoghare Нет, вопрос не в том, чтобы выбрать совершенно другой алгоритм. Вы всегда можете преобразовать рекурсивную функцию в идентично функционирующий метод, который использует цикл. Например, этот ответ имеет один и тот же алгоритм как в рекурсивном, так и в циклическом формате . В общем случае вы помещаете кортеж аргументов рекурсивной функции в стек, отправляете в стек для вызова, отбрасываете из стека для возврата из функции.
TamaMcGlinn

Ответы:

358

Это зависит от используемого языка. Вы написали «независимый от языка», поэтому я приведу несколько примеров.

В Java, C и Python рекурсия довольно дорога по сравнению с итерацией (в целом), потому что она требует выделения нового фрейма стека. В некоторых C-компиляторах можно использовать флаг компилятора, чтобы устранить эти издержки, которые преобразуют определенные типы рекурсии (фактически, определенные типы хвостовых вызовов) в переходы вместо вызовов функций.

В реализациях на языке функционального программирования иногда итерация может быть очень дорогой, а рекурсия - очень дешевой. Во многих случаях рекурсия превращается в простой переход, но для изменения переменной цикла (которая является изменяемой) иногда требуются некоторые относительно тяжелые операции, особенно в реализациях, которые поддерживают несколько потоков выполнения. В некоторых из этих сред мутация обходится дорого из-за взаимодействия между мутатором и сборщиком мусора, если они оба могут работать одновременно.

Я знаю, что в некоторых реализациях Scheme рекурсия, как правило, будет быстрее, чем зацикливание.

Короче говоря, ответ зависит от кода и реализации. Используйте любой стиль, который вы предпочитаете. Если вы используете функциональный язык, рекурсия может быть быстрее. Если вы используете императивный язык, итерация, вероятно, быстрее. В некоторых средах оба метода приводят к созданию одной и той же сборки (поместите ее в свою трубу и выкурите ее).

Приложение: В некоторых средах лучшая альтернатива - это не рекурсия и не итерация, а функции высшего порядка. К ним относятся «карта», «фильтр» и «уменьшить» (что также называется «сложить»). Они не только являются предпочтительным стилем, они не только часто более чистые, но и в некоторых средах эти функции являются первыми (или единственными), которые получают преимущество от автоматического распараллеливания - поэтому они могут быть значительно быстрее, чем итерация или рекурсия. Data Parallel Haskell является примером такой среды.

Понимание списков - это еще одна альтернатива, но обычно это просто синтаксический сахар для функций итерации, рекурсии или функций более высокого порядка.

Дитрих Эпп
источник
48
Мне +1, и я хотел бы прокомментировать, что «рекурсия» и «циклы» - это то, что люди называют своим кодом. Для производительности важно не то, как вы называете вещи, а то, как они компилируются / интерпретируются. Рекурсия, по определению, является математической концепцией и имеет мало общего с фреймами стека и сборочными материалами.
П Швед
1
Кроме того, рекурсия, как правило, более естественный подход в функциональных языках, и итерация обычно более интуитивна в императивных языках. Разница в производительности вряд ли будет заметна, поэтому просто используйте то, что кажется более естественным для данного языка. Например, вы, вероятно, не захотите использовать итерацию в Haskell, когда рекурсия намного проще.
Саша Чедыгов
4
Обычно рекурсия компилируется в циклы, причем циклы являются конструкцией более низкого уровня. Зачем? Поскольку рекурсия, как правило, хорошо обоснована в некоторой структуре данных, она создает начальную F-алгебру и позволяет вам доказать некоторые свойства завершения, а также индуктивные аргументы о структуре (рекурсивных) вычислений. Процесс, с помощью которого рекурсия компилируется в циклы, является оптимизацией хвостового вызова.
Кристофер Мичински,
Самое главное, что операции не выполнены. Чем больше у вас «IO», тем больше вы должны обрабатывать. Un-IOing-данные (также называемые индексированием) всегда являются самым большим приростом производительности для любой системы, потому что вам не нужно обрабатывать их в первую очередь.
Джефф Фишер
53

рекурсия всегда быстрее, чем цикл?

Нет, Итерация всегда будет быстрее, чем Рекурсия. (в архитектуре фон Неймана)

Объяснение:

Если вы создаете минимальное количество операций с обычного компьютера с нуля, «Итерация» стоит на первом месте в качестве строительного блока и требует меньше ресурсов, чем «рекурсия», следовательно, это быстрее.

Создание псевдо-вычислительной машины с нуля:

Задайте себе вопрос : что вам нужно, чтобы вычислить значение, то есть следовать алгоритму и достичь результата?

Мы создадим иерархию понятий, начиная с нуля и определяя в первую очередь базовые, основные понятия, затем создадим понятия второго уровня с ними и так далее.

  1. Первая концепция: ячейки памяти, память, состояние . Чтобы что-то сделать, вам нужны места для хранения конечных и промежуточных значений результатов. Давайте предположим, что у нас есть бесконечный массив «целочисленных» ячеек, называемый Memory , M [0..Infinite].

  2. Инструкции: сделать что-то - трансформировать ячейку, изменить ее значение. изменить состояние . Каждая интересная инструкция выполняет преобразование. Основные инструкции:

    а) Установить и переместить ячейки памяти

    • сохранить значение в памяти, например: сохранить 5 м [4]
    • скопируйте значение в другую позицию: например: store m [4] m [8]

    б) логика и арифметика

    • и, или, XOR, не
    • добавить, sub, mul, div. например, добавить m [7] m [8]
  3. Исполнительный агент : ядро современного процессора. «Агент» - это то, что может выполнять инструкции. Агент также может быть людьми по алгоритму на бумаге.

  4. Порядок шагов: последовательность инструкций : то есть: сделать это сначала, сделать это после и т. Д. Обязательная последовательность инструкций. Даже однострочные выражения являются «обязательной последовательностью инструкций». Если у вас есть выражение с определенным «порядком оценки», то у вас есть шаги . Это означает, что даже одно составное выражение имеет неявные «шаги», а также имеет неявную локальную переменную (назовем ее «результат»). например:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    Выражение выше подразумевает 3 шага с неявной переменной «result».

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    Поэтому даже инфиксные выражения, поскольку у вас есть определенный порядок вычисления, являются обязательной последовательностью инструкций . Выражение подразумевает последовательность операций, которые должны быть выполнены в определенном порядке, и, поскольку существуют шаги , существует также неявная промежуточная переменная «result».

  5. Указатель инструкций : если у вас есть последовательность шагов, у вас также есть неявный «указатель инструкций». Указатель инструкции отмечает следующую инструкцию и продвигается после чтения инструкции, но до ее выполнения.

    В этой псевдо-вычислительной машине указатель инструкций является частью памяти . (Примечание: обычно указатель инструкций будет «специальным регистром» в ядре ЦП, но здесь мы упростим концепции и предположим, что все данные (включая регистры) являются частью «памяти»)

  6. Переход - если у вас есть заказанное количество шагов и указатель инструкций , вы можете применить инструкцию « store », чтобы изменить значение самого указателя инструкций. Мы будем называть это конкретное использование инструкции store новым именем: Jump . Мы используем новое имя, потому что его проще воспринимать как новую концепцию. Изменяя указатель инструкции, мы инструктируем агента «перейти к шагу x».

  7. Бесконечная итерация : отскочив назад, теперь вы можете заставить агента «повторить» определенное количество шагов. На данный момент у нас есть бесконечная итерация.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. Условно - условное выполнение инструкций. С помощью условного предложения вы можете условно выполнить одну из нескольких инструкций в зависимости от текущего состояния (которое может быть установлено с помощью предыдущей инструкции).

  9. Правильная итерация : теперь с помощью условного предложения мы можем избежать бесконечного цикла инструкции возврата . Теперь у нас есть условный цикл, а затем правильная итерация

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. Именование : присвоение имен определенной ячейке памяти, содержащей данные или шаг . Это просто «удобство». Мы не добавляем никаких новых инструкций, имея возможность определять «имена» для областей памяти. «Наименование» - это не инструкция для агента, а просто удобство для нас. Присвоение имен делает код (на данный момент) более простым для чтения и изменения.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. Одноуровневая подпрограмма . Предположим, вам нужно выполнить серию шагов. Вы можете сохранить шаги в именованной позиции в памяти, а затем перейти к этой позиции, когда вам нужно выполнить их (вызвать). В конце последовательности вам нужно вернуться к точке вызова, чтобы продолжить выполнение. С помощью этого механизма вы создаете новые инструкции (подпрограммы), составляя основные инструкции.

    Реализация: (новые концепции не требуются)

    • Сохранить текущий указатель инструкций в предварительно определенной позиции памяти
    • перейти к подпрограмме
    • в конце подпрограммы вы извлекаете указатель инструкций из предопределенной области памяти, фактически возвращаясь к следующей инструкции исходного вызова

    Проблема с одноуровневой реализацией: Вы не можете вызвать другую подпрограмму из подпрограммы. Если вы это сделаете, вы перезапишете возвращающий адрес (глобальную переменную), поэтому вы не сможете вкладывать вызовы.

    Чтобы иметь лучшую реализацию для подпрограмм: вам нужен STACK

  12. Стек : Вы определяете пространство памяти для работы в качестве «стека», вы можете «выталкивать» значения в стек, а также «выталкивать» последнее «вытолкнутое» значение. Для реализации стека вам понадобится указатель стека (подобный указателю инструкций), который указывает на фактическую «верхушку» стека. Когда вы «нажимаете» значение, указатель стека уменьшается, и вы сохраняете значение. Когда вы «выталкиваете», вы получаете значение в фактическом указателе стека, а затем увеличивается указатель стека.

  13. Подпрограммы Теперь, когда у нас есть стек, мы можем реализовать надлежащие подпрограммы, разрешающие вложенные вызовы . Реализация аналогична, но вместо того, чтобы хранить указатель инструкций в предопределенной позиции памяти, мы «помещаем» значение IP в стек . В конце подпрограммы мы просто «выталкиваем» значение из стека, фактически возвращаясь к инструкции после исходного вызова . Эта реализация, имеющая «стек», позволяет вызывать подпрограмму из другой подпрограммы. С помощью этой реализации мы можем создать несколько уровней абстракции при определении новых инструкций в качестве подпрограмм, используя базовые инструкции или другие подпрограммы в качестве строительных блоков.

  14. Рекурсия : что происходит, когда подпрограмма вызывает себя? Это называется "рекурсия".

    Проблема: Перезаписывая локальные промежуточные результаты, подпрограмма может быть сохранена в памяти. Поскольку вы вызываете / повторно используете одни и те же шаги, если промежуточный результат хранится в предопределенных ячейках памяти (глобальных переменных), они будут перезаписаны при вложенных вызовах.

    Решение: чтобы разрешить рекурсию, подпрограммы должны хранить локальные промежуточные результаты в стеке , поэтому при каждом рекурсивном вызове (прямом или косвенном) промежуточные результаты хранятся в разных местах памяти.

...

Достигнув рекурсии, мы останавливаемся здесь.

Вывод:

В архитектуре фон Неймана ясно, что «Итерация» является более простым / базовым понятием, чем «Рекурсия» . У нас есть форма «Итерации» на уровне 7, в то время как «Рекурсия» находится на уровне 14 иерархии понятий.

Итерация всегда будет быстрее в машинном коде, потому что она подразумевает меньше инструкций и, следовательно, меньше циклов ЦП.

Какой лучше"?

  • Вам следует использовать «итерацию», когда вы обрабатываете простые последовательные структуры данных, и везде будет работать «простой цикл».

  • Вы должны использовать «рекурсию», когда вам нужно обработать рекурсивную структуру данных (мне нравится называть их «фрактальными структурами данных»), или когда рекурсивное решение явно более «элегантно».

Совет : используйте лучший инструмент для работы, но понимайте внутреннюю работу каждого инструмента, чтобы выбирать мудро.

Наконец, обратите внимание, что у вас есть много возможностей использовать рекурсию. Повсюду у вас есть рекурсивные структуры данных , сейчас вы смотрите на них: части DOM, поддерживающие то, что вы читаете, - это RDS, выражение JSON - это RDS, иерархическая файловая система на вашем компьютере - это RDS, то есть: у вас есть корневой каталог, содержащий файлы и каталоги, каждый каталог, содержащий файлы и каталоги, каждый из этих каталогов, содержащий файлы и каталоги ...

Лусио М. Тато
источник
2
Вы предполагаете, что ваше прогрессирование 1) необходимо и 2) что оно останавливается там, где вы его сделали. Но 1) это не нужно (например, рекурсия может быть превращена в скачок, как объяснил принятый ответ, поэтому стек не требуется), и 2) она не должна останавливаться на достигнутом (например, в конечном итоге вы Вы достигнете одновременной обработки, которая может потребовать блокировок, если у вас будет изменяемое состояние, как вы представили на втором шаге, поэтому все замедляется; в то время как неизменное решение, такое как функциональное / рекурсивное, позволит избежать блокировки, поэтому может быть быстрее / более параллельным) ,
hmijail скорбит по отставке
2
«рекурсия может быть превращена в прыжок» - ложь. Действительно полезная рекурсия не может быть превращена в прыжок. Хвостовой вызов «рекурсия» - это особый случай, когда вы кодируете «как рекурсию» что-то, что может быть упрощено компилятором до цикла. Также вы смешиваете «неизменяемость» с «рекурсией», это ортогональные понятия.
Лусио М. Тато
«Действительно полезная рекурсия не может быть превращена в скачок» -> так что оптимизация хвостового вызова как-то бесполезна? Кроме того, неизменяемость и рекурсия могут быть ортогональными, но вы делаете зацикливание ссылок с изменяемыми счетчиками - посмотрите на ваш шаг 9. Мне кажется, что вы думаете, что зацикливание и рекурсия - это принципиально разные понятия; они не. stackoverflow.com/questions/2651112/…
hmijail скорбит по отставке
@hmijail Я думаю, что слово «полезнее» лучше, чем «правда». Хвостовая рекурсия не является настоящей рекурсией, потому что она просто использует синтаксис вызова функции для маскировки безусловного ветвления, то есть итерации. Настоящая рекурсия предоставляет нам стек возврата. Однако рекурсия хвоста все еще выразительна, что делает ее полезной. Свойства рекурсии, которые облегчают или облегчают анализ кода на корректность, присваиваются итеративному коду, когда он выражается с использованием хвостовых вызовов. Хотя это иногда слегка компенсируется дополнительными усложнениями в хвостовой версии, такими как дополнительные параметры.
Каз
34

Рекурсия может быть быстрее, если альтернативой является явное управление стеком, как в алгоритмах сортировки или двоичного дерева, о которых вы упомянули.

У меня был случай, когда переписывание рекурсивного алгоритма в Java сделало его медленнее.

Таким образом, правильный подход заключается в том, чтобы сначала написать его наиболее естественным образом, оптимизировать только в том случае, если профилирование показывает его критичность, а затем измерить предполагаемое улучшение.

starblue
источник
2
+1 за « сначала напишите наиболее естественным образом » и особенно « только оптимизируйте, если профилирование показывает, что это критично »
TripeHound
2
+1 за признание того, что аппаратный стек может быть быстрее, чем программный, реализованный вручную стек в куче. Эффективно показывает, что все ответы «нет» неверны.
sh1
12

Хвостовая рекурсия выполняется так же быстро, как и петля. Во многих функциональных языках реализована хвостовая рекурсия.

mkorpela
источник
35
Хвостовая рекурсия может быть такой же быстрой, как зацикливание, когда реализуется оптимизация хвостового вызова: c2.com/cgi/wiki?TailCallOptimization
Joachim Sauer
12

Рассмотрим, что абсолютно необходимо сделать для каждой итерации и рекурсии.

  • итерация: переход к началу цикла
  • рекурсия: переход к началу вызываемой функции

Вы видите, что здесь не так много места для разногласий.

(Я предполагаю, что рекурсия - это хвостовой вызов, а компилятор знает об этой оптимизации).

Паси Саволайнен
источник
9

Большинство ответов здесь забывают очевидного виновника, почему рекурсия часто медленнее, чем итерационные решения. Это связано со сборкой и разборкой стековых фреймов, но не совсем так. Как правило, это большая разница в хранении автоматической переменной для каждой рекурсии. В итеративном алгоритме с циклом переменные часто хранятся в регистрах, и даже если они разливаются, они будут находиться в кэше уровня 1. В рекурсивном алгоритме все промежуточные состояния переменной хранятся в стеке, что означает, что они вызовут еще много разливов в память. Это означает, что даже если он выполняет одинаковое количество операций, у него будет много обращений к памяти в горячем цикле, и, что еще хуже, эти операции с памятью имеют паршивую частоту повторного использования, что делает кэши менее эффективными.

TL; DR рекурсивные алгоритмы обычно хуже кешируют, чем итеративные.

Патрик Шлютер
источник
6

Большинство ответов здесь неверны . Правильный ответ - это зависит . Например, вот две функции C, которые проходят по дереву. Сначала рекурсивный:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

А вот та же функция, реализованная с использованием итерации:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

Не важно понимать детали кода. Просто pэто узлы, и это P_FOR_EACH_CHILDделает ходьбу. В итерационной версии нам нужен явный стек, stв который узлы помещаются, а затем выталкиваются и обрабатываются.

Рекурсивная функция выполняется намного быстрее, чем итеративная. Причина в том, что в последнем случае для каждого элемента требуется CALLфункция a, st_pushа затем другая функция st_pop.

В первом случае у вас есть только рекурсив CALLдля каждого узла.

Кроме того, доступ к переменным в стеке вызовов невероятно быстр. Это означает, что вы читаете из памяти, которая, вероятно, всегда будет в самом внутреннем кэше. С другой стороны, явный стек должен поддерживаться malloc: памятью из кучи, доступ к которой намного медленнее.

С осторожной оптимизацией, такой как встраивание st_pushи st_pop, я могу достичь примерно паритета с рекурсивным подходом. Но по крайней мере на моем компьютере стоимость доступа к куче памяти больше, чем стоимость рекурсивного вызова.

Но это обсуждение в основном спорный вопрос , потому что рекурсивное дерево ходьба неправильно . Если у вас достаточно большое дерево, вам не хватит места для стека вызовов, поэтому необходимо использовать итерационный алгоритм.

Бьорн Линдквист
источник
Я могу подтвердить, что столкнулся с подобной ситуацией и что существуют ситуации, когда рекурсия может быть быстрее, чем ручной стек в куче. Особенно, когда оптимизация включена в компиляторе, чтобы избежать некоторых накладных расходов при вызове функции.
while1fork
1
Выполнял предварительный обход бинарного дерева из 7 узлов 10 ^ 8 раз. Рекурсия 25нс. Явный стек (привязанный или нет - не имеет большого значения) ~ 15 нс. Рекурсия должна делать больше (сохранение и восстановление регистров + (обычно) более строгое выравнивание кадров) в дополнение к простому нажатию и прыжку. (И это еще хуже с PLT в динамически связанных библиотеках.) Вам не нужно выделять кучу явного стека. Вы можете создать препятствие, первый кадр которого находится в обычном стеке вызовов, чтобы не жертвовать локальностью кэша для наиболее распространенного случая, когда вы не превышаете первый блок.
PSkocik
3

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

Вы ударили гвоздь по голове относительно причины; Создание и уничтожение стековых фреймов обходится дороже, чем простой переход.

Тем не менее, обратите внимание, что я сказал «имеет жизнеспособные реализации в обеих формах». Для таких вещей, как многие алгоритмы сортировки, существует не очень жизнеспособный способ их реализации, который эффективно не устанавливает собственную версию стека из-за порождения дочерних «задач», которые по своей сути являются частью процесса. Таким образом, рекурсия может быть такой же быстрой, как и попытка реализовать алгоритм с помощью цикла.

Изменить: Этот ответ предполагает, что нефункциональные языки, где большинство основных типов данных являются изменяемыми. Это не относится к функциональным языкам.

янтарный
источник
Именно поэтому несколько случаев рекурсии часто оптимизируются компиляторами в языках, где часто используется рекурсия. Например, в F #, в дополнение к полной поддержке хвостовых рекурсивных функций с кодом операции .tail, вы часто видите рекурсивную функцию, скомпилированную как цикл.
em70
Ага. Хвостовая рекурсия иногда может быть лучшим из обоих миров - функционально «подходящим» способом реализации рекурсивной задачи и производительностью использования цикла.
Январь
1
В общем, это не правильно. В некоторых средах мутация (которая взаимодействует с GC) является более дорогой, чем хвостовая рекурсия, которая преобразуется в более простой цикл в выводе, который не использует дополнительный кадр стека.
Дитрих Эпп
2

В любой реалистичной системе создание фрейма стека всегда будет обходиться дороже, чем INC и JMP. Вот почему действительно хорошие компиляторы автоматически преобразуют хвостовую рекурсию в вызов одного и того же фрейма, то есть без дополнительных затрат, поэтому вы получаете более читаемую исходную версию и более эффективную скомпилированную версию. Действительно, очень хороший компилятор должен даже быть в состоянии преобразовать нормальные рекурсии в хвост рекурсии , где это возможно.

Килиан Фот
источник
1

Функциональное программирование - это скорее « что », а не « как ».

Разработчики языка найдут способ оптимизировать работу кода под ним, если мы не попытаемся сделать его более оптимизированным, чем нужно. Рекурсию также можно оптимизировать на языках, которые поддерживают оптимизацию хвостовых вызовов.

Что важнее с точки зрения программиста, так это удобочитаемость и удобство обслуживания, а не оптимизация. Опять же, «преждевременная оптимизация - корень всего зла».

noego
источник
0

Это предположение. Обычно рекурсия, вероятно, не справляется с цикличностью часто или когда-либо при проблемах приличного размера, если оба используют действительно хорошие алгоритмы (не считая сложности реализации), она может отличаться, если используется с рекурсией языка с хвостовой рекурсией (и хвостовым рекурсивным алгоритмом) и с циклами также как часть языка) - которые, вероятно, будут очень похожи и, возможно, даже предпочтут рекурсию иногда.

Роман А. Тайчер
источник
0

Согласно теории это то же самое. Рекурсия и цикл с одинаковой сложностью O () будут работать с той же теоретической скоростью, но, конечно, реальная скорость зависит от языка, компилятора и процессора. Пример со степенью числа может быть итеративно закодирован с помощью O (ln (n)):

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }
Гидрофис Спиралис
источник
1
Большой О «пропорционален». Так и есть O(n), но один может занять xбольше времени, чем другой, для всех n.
Ctrl-Alt-Delor