Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1681 / 107 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Лекция 9:

Рекурсия и деревья

Аннотация: В лекции вводится понятие рекурсии, рекурсивных определений. Рассматривается известная задача "Ханойской башне". Широко используемая в программировании структура данных – дерево рассматривается как рекурсивная структура данных. Обсуждаются рекурсивные операции, выполняемые над рекурсивными структурами данных.


Рис. 8.1.

Смеющаяся корова, изображенная на фирменном жетоне "Смеющаяся Корова", носит в качестве сережек фирменные жетоны, на которых, я подозреваю, но зрение не позволяет убедиться в правильности моего предположения, изображена корова с фирменными жетонами, на которых изображена … (надеюсь, идея понятна)1У нас известен рекурсивный стишок, вошедший в поговорку: "У попа была собака, он ее любил. Она съела кусок мяса, он ее убил, и в землю закопал, и на могиле написал, что у попа была собака …" .

Эта реклама, появившаяся в 1921 году, все еще хорошо работает, являясь примером структуры, определенной рекурсивно в следующем смысле:

Рекурсивное определение
Определение понятия является рекурсивным, если оно включает один или более экземпляров самого понятия.

"Рекурсия" – использование рекурсивного определения – широко применяется в программировании: она позволяет элегантно определять синтаксические структуры; мы также познакомимся с рекурсивно определенными структурами данных и рекурсивными процедурами.

Мы будем использовать термин "рекурсивный" как сокращение "рекурсивно определенный" – рекурсивная грамматика, рекурсивная структура данных. Но это только соглашение, поскольку нельзя сказать, что понятие или структура сами по себе рекурсивны. Все, что мы знаем, – это то, что их можно рекурсивно описать в соответствии с вышеприведенным определением. Любое частичное понятие – в том числе структура, задающая Смеющуюся корову, – может быть определено как рекурсивно, так и без использования рекурсии.

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

Рекурсия является прямой, если определение А ссылается на экземпляр А, и косвенной, если для 1 <= i < n (для некоторого n >=2) определение каждого A_i ссылается на A_{i+1}, а определение A_n ссылается на A_1.

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

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

8.1. Основные примеры

В этот момент могут возникнуть вполне обоснованные сомнения – а есть ли смысл в рекурсивных определениях, как можно определить понятие через само понятие, "масло масляное"?

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

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

Рекурсивные определения

С введением универсальности мы получаем возможность определять тип как:

Т1 класс, не являющийся универсальным, такой как INTEGER или STATION;
Т2 родовое порождение в форме C[T], где Суниверсальный класс, а Т – тип.

Это определение рекурсивно, оно просто означает, что, при наличии универсальных классов ARRAY и LIST, правильными классами также будут:

  • INTEGER, STATION и им подобные в соответствии с определением Т1;
  • согласно случаю Т2, прямые родовые порождения: ARRAY[INTEGER], LIST[STATION] и так далее.
Снова рекурсивно применяя Т2: ARRAY[LIST[INTEGER]], ARRAY[ARRAY [LIST[STATION]]] и так далее – родовые порождения любого уровня вложенности.

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

Рекурсивно определенные грамматики

Рассмотрим подмножество Eiffel с двумя видами операторов.

  • Присваивание, в его обычной форме variable:= expression, рассматриваемое здесь как терминальное и далее не уточняемое.
  • Условный оператор, имеющий только часть then (без else) для простоты.

Грамматика, определяющая язык, такова:

\text{Оператор }\triangleq\text{ Присваивание | Условный}\\\text{Условный }\triangleq}\text{ if Условие then Оператор end }

Для наших непосредственных целей будем полагать, что "Условие" является терминальным понятием. Это определение грамматики очевидно рекурсивно, поскольку определение "Оператор" включает "Условный", а его определение, в свою очередь, включает "Оператор". Но так как здесь присутствует нерекурсивная часть определения – "Присваивание", то в целом грамматика четко определяет правильные конструкции языка:

  • просто Присваивание;
  • Условный, содержащий Присваивание: if c then a end;
  • то же самое с произвольной степенью вложенности: if c1 then if c2 then a end end, if c1 then if c2 then if c3 then a end end end и так далее.

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

Рекурсивно определенные структуры данных

Класс STOP представляет понятие остановки для линии метро:

class STOP create
…
feature
    next: STOP
            — Следующая остановка на той же линии.
        …Другие компоненты, здесь опущенные (см.6.5)
end
        

Наивная интерпретация будет предполагать, что каждый экземпляр STOP содержит экземпляр STOP, который, в свою очередь, содержит остановку и так до бесконечности, как в схеме со Смеющейся коровой. Это и в самом деле было бы так, если бы STOP принадлежал развернутым, а не ссылочным типам:

Вложенные поля (интерпретация не корректна)

Рис. 8.2. Вложенные поля (интерпретация не корректна)

Такое попросту невозможно. Но STOP в любом случае является ссылочным типом, подобно любому классу, определенному как class X… без всяких других квалификаций, так что реальная картина выглядит так:

Линия, связанная ссылками

Рис. 8.3. Линия, связанная ссылками

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

В той же 6-й главе, где рассматривались линии метро, шла речь и о классе PERSON c атрибутом spouse типа PERSON.

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

Рекурсивно определяемые алгоритмы и программы

Известная последовательность чисел Фибоначчи обладает многими прекрасными свойствами и появляется во многих приложениях математики и естественных наук. Она имеет следующее определение:

F_0=0\\F_1=1\\F_i=F_{i-1}+F_{i-2}\qquad \text{- Для}\;i>1
Почувствуй историю
Кролики Фибоначчи

Леонардо Фибоначчи из Пизы (1170 – 1250) сыграл ключевую роль в знакомстве Запада с трудами индийских и арабских математиков. Он известен также и собственными исследованиями, лежащими в основании современной математики. Он сформулировал задачу, приводящую к его знаменитой последовательности (которая была известна еще индийским математикам):

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

Фибоначчи

Рис. 8.4. Фибоначчи

Решение дает следующее рассуждение. Пары кроликов в месяце i включают пары кроликов, уже существующие в предыдущем месяце (кролики не умирают); обозначим их число как F_{i-1}, плюс потомство, принесенное кроликами, жившими в месяце i-2 (кролики, появившиеся в месяце i-1, потомства не приносят). Это и дает приведенную выше формулу, создающую последовательность целых чисел 0, 1, 1, 2, 3, 5, 8 и так далее. Формула приводит к рекурсивной программе, вычисляющей F_n для любого n:

fibonacci (n: INTEGER): INTEGER
        — Элемент с индексом n в последовательности Фибоначчи.
    require
        non_negative: n >= 0
    do
        if n = 0 then
            Result := 0
        elseif n = 1 then
            Result := 1
        else
            Result := fibonacci(n – 1)+ fibonacci(n – 2)
        end
    end
        
Время программирования!
Рекурсивная версия Фибоначчи

Напишите небольшую программную систему, включающую вышеприведенную рекурсивную функцию и печатающую ее результат. Протестируйте ее для небольших значений n, включая 12, как в оригинальном тексте Фибоначчи.

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

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

Время программирования!
Нерекурсивная версия Фибоначчи

Можете ли вы, не заглядывая в дальнейший текст, написать функцию, вычисляющую N-е число Фибоначчи, используя цикл, а не рекурсию?

Следующая функция дает тот же результат, что и рекурсивная версия. Проверьте это на нескольких значениях.

fibonacci1 (n: INTEGER): INTEGER
            — Элемент с индексом n в последовательности Фибоначчи.
            — (Нерекурсивная версия.)
    require
        positive: n >=1
    local
        i, previous, second_previous: INTEGER
    do
        from
            i := 1 ; Result := 1
        invariant
            Result = fibonacci(i )
            previous = fibonacci (i – 1)
        until i = n loop
            i := i + 1
            second_previous := previous
            previous := Result
            Result := previous + second_previous
        variant
            n – i
        end
    end
        
Для удобства в этой версии предполагается, что n >= 1, а не n >= 0. Благодаря правилам инициализации previous начинается с 0, что гарантирует начальное выполнение инварианта, так как F_0 = 0. Переменная second_previous обновляется на каждом шаге цикла и ей не нужно специальной инициализации.

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

Оставим в стороне вкус и эффективность. Если бы мы рассматривали только такие примеры, то необходимость рекурсивных программ была бы далеко не очевидной. Нам необходимы примеры, в которых рекурсия обеспечила бесспорные преимущества, например, за счет того, что ее нерекурсивный аналог был бы значительно сложнее и труднее в понимании. Такие примеры существуют в большом количестве. Одним из них является очаровательная головоломка – "Ханойская башня". В этом примере сконцентрировано много полезных свойств рекурсии, и практически нет не относящихся к делу деталей.