Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MakeevGA-Haskell-a4-shortcode_2014_05_31.doc
Скачиваний:
15
Добавлен:
19.01.2023
Размер:
1.79 Mб
Скачать

Еще раз о рекурсии

В заключение, в этой главе я хотел бы еще раз вернуться к тому, о чем мы говорили чуть раньше:

Решение задачи над списком с помощью рекурсии всегда заключается в следующем вопросе самому себе. Предположим, нужно решить какую-то задачу для списка xs. Предположим, что кто-то нам дал ответ на нашу задачу, но для списка tail xs. Сможем ли мы тогда сразу легко дать ответ и для всего списка xs тоже?

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

length :: [a] -> Int

length [] = 0

length (x:xs) = 1 + length xs

Ну кто, скажите, в здравом уме будет при написании этой функции рассуждать в ключе "а что если кто-то уже вычислил длину хвоста списка"? Этот пример ни разу не поучительный по той причине, что нашему разуму с самого начала понятно, как посчитать длину списка: что там считать-то, тыкай пальцем в каждый элемент и называй следующее натуральное число! И такая рекурсивная запись ничего разуму не дает – просто еще один странный способ выдирания гланд через желудок вычисления того, что всем ясно, как вычислять.

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

divide :: [a] -> [([a],[a])]

Она берет список, например, наших символов, и возвращает – вглядитесь в эту мешанину круглых и квадратных скобок – список кортежей вида ([a],[a]), где на первом месте стоит список всех тех, кто попал в первую команду, а на втором месте – кто попал во вторую.

На всякий случай. Я в курсе, что можно перебрать все бинарные представления чисел от 0 до 2N. Вы ведь понимаете, что речь совсем не об этом, правда? Мы учимся искать рекурсивные решения. Когда вы этому научитесь, то сможете в каждом случае решать, нужно ли рекурсивное решение, или удобнее нерекурсивное.

Простой случай для функции: если есть только я, то я могу образовать собой единственным или одну команду, или другую:

divide [me] = [ ([me],[]) , ([],[me]) ]

А что дальше? Ну как, мозг, есть варианты? Вот здесь, где пасует наше интуитивное алгоритмическое мышление, и выходит на передний план рекурсивный способ во всей его красе. Что если есть "я" и "все остальные", как нам разделиться всеми возможными способами на две команды?

Так вот – предположим, что "все остальные" уже умеют делиться на команды, и уже поделились всеми возможными способами! А как же я? Меня-то забыли! Надо модифицировать все возможные способы с учетом того, что еще есть я!

divide (me:others) = join me (divide others) where

Вот оно, мы свели задачу к другой: пусть есть набор всевозможных разделений мальчишек на команды, – как этому набору добавить еще и меня? Это и будет делать локальная функция join:

join :: a -> [([a],[a])] -> [([a],[a])]

Она берет меня и всевозможные деления на команды, и должна создавать новые всевозможные деления, – но уже с учетом меня.

join me [] = []

join me ((side1,side2):divisions) =

[(me:side1,side2),(side1,me:side2)] ++

join me divisions

Итак, если есть список возможных разделений, то нужно его рекурсивно обработать. Берем каждое разделение без учета меня (side1,side2), и в результат складываем два возможных разделения уже с учетом меня. Одно, где я присоединяюсь к первой команде, а второе – где я иду во вторую команду: [(me:side1,side2),(side1,me:side2)].

Вот, собственно, и все. Если выкинуть определения типов, то задача решается в несколько строчек:

divide [me] = [ ([me] , []) , ([] , [me]) ]

divide (me:others) = join me (divide others) where

join me [] = []

join me ((side1,side2):divisions) =

[(me:side1,side2),(side1,me:side2)] ++

join me divisions

Проверим?

SomeModule> divide "abc"

[("abc",""), ("bc","a"), ("ac","b"), ("c","ab"),

("ab","c"), ("b","ac"), ("a","bc"), ("","abc")]

Чем этот пример поучителен? Тем, что наше интуитивное умение строить в голове алгоритмы здесь откровенно пасует, пытаясь сразу представить себе алгоритм построения решения. Все, что интуиция может – это охватить один шаг: если есть 10 вариантов разбиения на команды для какой-то группы людей, то в случае, если появляется еще один кто-то, у нас становится 20 вариантов разбиения, потому что в каждом варианте новый человек может пойти либо в одну команду, либо в другую.

Интуиция может охватить только один шаг алгоритма – но большего то и не надо! Все остальное сделает правильно запущенная рекурсия!