········fragrances ← fragrances + new_fragrances
····return fragrances
Добавление каждого нового цветка приводит к удвоению количества ароматов в множестве fragrances, что говорит об экспоненциальном росте (2k+1 = 2 × 2k). Алгоритмы, которые удваивают число операций, если объем входных данных увеличивается на один элемент, — экспоненциальные, их временная сложность —
Генерирование степенных множеств эквивалентно генерированию таблиц истинности (см. раздел «Логика» в главе 1). Если обозначить каждый цветок логической переменной, то любой аромат легко представить в виде значений True/False этих переменных. В таблице истинности каждая строка будет возможной формулой аромата.
Рис. 3.2. Итеративное перечисление всех ароматов с использованием четырех цветков
3.2. Рекурсия
Мы говорим о
Рис. 3.3. Рекурсивное вычисление шестого числа Фибоначчи
function fib(n)
····if n ≤ 2
········return 1
····return fib(n — 1) + fib(n — 2)
При использовании рекурсии требуется творческий подход, чтобы понять, каким образом задача может быть поставлена с точки зрения самой себя. Чтобы проверить, является ли слово палиндромом[30], нужно посмотреть, изменится ли оно, если его перевернуть. Это можно сделать, проверив, одинаковы ли первая и последняя буквы слова и не является ли палиндромом заключенная между ними часть слова (рис. 3.4).
Рис. 3.4. Рекурсивная проверка, является ли слово racecar палиндромом
function palindrome(word)
····if word.length ≤ 1
········return True
····if word.first_char ≠ word.last_char
········return False
····w ← word.remove_first_and_last_chars
····return palindrome(w)
Рекурсивные алгоритмы имеют
Рекурсия против итераций
Рекурсивные алгоритмы обычно проще и короче итеративных. Сравните эту рекурсивную функцию с power_set из предыдущего раздела, которая не использует рекурсию:
function recursive_power_set(items)
····ps ← copy(items)
····for each e in items
·······ps ← ps.remove(e)
·······ps ← ps + recursive_power_set(ps)
·······ps ← ps.add(e)
····return ps
Эта простота имеет свою цену. Рекурсивные алгоритмы при выполнении порождают многочисленные копии самих себя, создавая дополнительные вычислительные издержки. Компьютер должен отслеживать незаконченные рекурсивные вызовы и их частичные вычисления, что требует большего объема памяти. При этом дополнительные такты центрального процессора расходуются на переключение с одного рекурсивного вызова на следующий и назад.
Эту проблему можно наглядно увидеть на
Если требуется максимальная производительность, то можно избежать этих дополнительных издержек, переписав рекурсивный алгоритм в чисто итеративной форме. Такая возможность есть всегда. Это компромисс: итеративный программный код обычно выполняется быстрее, но вместе с тем он более громоздкий и его труднее понять.
3.3. Полный перебор
Полный перебор, он же метод «грубой силы», предполагает перебор
Рис. 3.5. Простое объяснение: полный перебор[31]
Давайте посмотрим, как ее можно использовать, чтобы решить следующую задачу.