Тверской государственный университет
Опубликован: 13.09.2006 | Доступ: свободный | Студентов: 3490 / 369 | Оценка: 4.65 / 4.29 | Длительность: 30:37:00
Специальности: Программист, Менеджер
Лекция 10:

Процедуры и функции

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

VBA допускает создание рекурсивных процедур, т. е. процедур, при вычислении вызывающих самих себя. Вызовы рекурсивной процедуры могут непосредственно входить в ее тело, или она может вызывать себя через другие процедуры. В последнем случае в модуле есть несколько связанных рекурсивных процедур. Стандартный пример рекурсивной процедуры - функция-факториал Fact(N)= N!. Вот ее определение в VBA:

Function Fact(N As Integer) As Long
	If N <= 1 Then		' базис индукции.
		Fact = 1		' 0! =1.
	Else	' рекурсивный вызов в случае N > 0.
		Fact = Fact(N - 1) * N
	End If
End Function

Так как каждый вызов процедуры требует накладных расходов, эффективнее для факториала итеративная программа:

Function Fact1(N As Integer) As Long
Dim Fact As Long, i As Integer
Fact = 1		' 0! =1.
If N > 1 Then		' цикл вместо рекурсии.
	For i = 1 To N
		Fact = Fact * i
		Next i
	End If
	Fact1 = Fact
End Function

Приведем процедуру, оценивающую время исполнения рекурсивного и не рекурсивного варианта:

Public Sub TestRecursive()
	'Сравнение по времени рекурсивной и нерекурсивной реализации факториала.
	Dim i As Long, Res As Long
	Dim Start As Single, Finish As Single
	'Рекурсивное вычисление факториала
	Start = Timer
	For i = 1 To 100000
		Res = Fact(12)
	Next i
	Finish = Timer
	Debug.Print "Время рекурсивных вычислений:", Finish - Start
	
	'Нерекурсивное вычисление факториала
	Start = Timer
	For i = 1 To 100000
		Res = Fact1(12)
	Next i
	Finish = Timer
	Debug.Print "Время нерекурсивных вычислений:", Finish - Start
End Sub
9.3.

Вот результаты вычислений, приведенные для двух запусков тестовой процедуры:

Время рекурсивных вычислений:				6,238281 
Время нерекурсивных вычислений:			2,304688 
Время рекурсивных вычислений:				6,25 
Время нерекурсивных вычислений:			2,253906

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

Польза от рекурсивных процедур в большей мере может проявиться при обработке данных, имеющих рекурсивную структуру (скажем, иерархическую или сетевую). Основные структуры данных (объекты) Office 97 вообще-то не являются рекурсивными: один рабочий лист Excel не может быть значением ячейки другого, одна таблица Access - элементом другой и т.д. Но данные, хранящиеся на рабочих листах Excel или в БД Access, сами по себе могут задавать "рекурсивные" отношения, и для их успешной обработки следует пользоваться рекурсивными процедурами. Мы рассмотрим сейчас класс, для работы с двоичными деревьями поиска. Деревья представляют рекурсивную структуру данных, поэтому и операции над ними естественным образом определяются рекурсивными алгоритмами.

Деревья поиска

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

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

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

Класс TreeNode

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

'Class TreeNode
'Элемент дерева

Public key As String
Public info As String
Public left As New BinTree
Public right As New BinTree

Класс BinTree

Класс BinTree содержит одно свойство - объект класса TreeNode, задающий корень дерева и группу операций над элементами дерева. Одним из основных методов класса является метод SearchAndInsert, который, по существу, реализует две операции - поиска элемента в дереве по заданному ключу и вставки элемента в дерево. Заметьте, что вставка должна быть реализована так, чтобы выполнялось основное условие дерева поиска: для каждой вершины ее ключ больше всех ключей вершин левого поддерева и меньше всех ключей вершин правого поддерева. Такая структура обеспечивает эффективное выполнение операций поиска, так как в этом случае для поиска требуется просмотр вершин, лежащих только на одной ветви дерева, ведущей от корня к искомому элементу. Этот метод обеспечивает создание класса, позволяя, начав с корня, вставлять элемент за элементом. Кроме этого метода мы определим методы для удаления элементов и группу методов обхода дерева. Все методы реализованы с использованием рекурсивных алгоритмов. Приведем описание класса:

Option Explicit

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

Public root As TreeNode

Public Sub PrefixOrder()
	'Префиксный обход дерева (корень, левое поддерево, правое)

	If Not (root Is Nothing) Then
		With root
			Debug.Print "key: ",.key, "info: ",.info
			.left.PrefixOrder
			.right.PrefixOrder
		End With
	End If
				
End Sub

Public Sub InfixOrder()
	'Инфиксный обход дерева (левое поддерево, корень, правое)

	If Not (root Is Nothing) Then
		With root
			.left.InfixOrder
			Debug.Print "key: ",.key, "info: ",.info
			.right.InfixOrder
		End With
	End If
				
End Sub

Public Sub PostfixOrder()
	'Постфиксный обход дерева (левое поддерево, правое, корень)

	If Not (root Is Nothing) Then
		With root
			.left.PostfixOrder
			.right.PostfixOrder
			Debug.Print "key: ",.key, "info: ",.info
		End With
	End If
				
End Sub

Public Sub SearchAndInsert(key As String, info As String)
	'Если в дереве есть узел с ключом key,
	'то возвращается информация в этом узле - работает поиск
	'Если такого узла нет, то создается новый узел и его поля
	'заполняются информацией, - работает вставка.
	'Вначале поиск
	If root Is Nothing Then ' элемент не найден и происходит вставка
		Set root = New TreeNode
		root.key = key: root.info = info
	ElseIf key < root.key Then
		'Поиск в левом поддереве
		root.left.SearchAndInsert key, info
	ElseIf key > root.key Then
		'Поиск в правом поддереве
		root.right.SearchAndInsert key, info
	Else 'Элемент найден - возвращается результат поиска
		info = root.info
	End If
		
End Sub

Public Sub DelInTree(key As String)
	'Эта процедура позволяет удалить элемент дерева с заданным ключом
	'Удаление с сохранением структуры дерева более сложная операция,
	'чем вставка или поиск. Причина сложности в том, что при удалении
	'элемента остаются два его потомка, которые необходимо корректно
	'связать с оставшимися элементами, чтобы не нарушить структуру дерева поиска.
	'В программе анализируются три случая:
	'Удаляется лист дерева (нет потомков - нет проблем),
	'Удаляется узел с одним потомком (потомок замещает удаленный узел),
	'Есть два потомка. В этом случае узел может быть заменен одним из двух
	'возможных кандидатов, не имеющих двух потомков.
	'Кандидатами являются самый левый узел правого подддерева и
	'самый правый узел левого поддерева.
	'Мы производим удаление в левом поддереве.
	
	Dim q As TreeNode
	If root Is Nothing Then
		Debug.Print "Key is not found"
	ElseIf key < root.key Then
		'Удаляем из левого поддерева
		root.left.DelInTree key
	ElseIf key > root.key Then
		'Удаляем из правого поддерева
		root.right.DelInTree key
	Else
		'Удаление узла
		Set q = root
		If q.right.root Is Nothing Then
			Set root = q.left.root
		ElseIf q.left.root Is Nothing Then
			Set root = q.right.root
		Else 'есть два потомка
			q.left.ReplaceAndDelete q
		End If
		Set q = Nothing
	End If
	
End Sub

Public Sub ReplaceAndDelete(q As TreeNode)
	'Заменяет узел на самый правый
	If Not (root.right.root Is Nothing) Then
		root.right.ReplaceAndDelete q
	Else	'Найден самый правый
		q.key = root.key: q.info = root.info
		Set root = root.left.root
	End If
	
End Sub
9.4.

Все методы класса довольно подробно прокомментированы, однако хотелось бы подчеркнуть некоторые моменты:

  1. Начнем с общего замечания, связанного с реализацией рекурсивных алгоритмов. Рекурсия это мощный инструмент, полезный при решении многих задач по обработке данных. Для тех, кто не привык писать рекурсивные программы, мы рекомендуем внимательно разобрать реализацию приведенных методов класса. Каждое рекурсивное определение содержит некоторый базис, позволяющий найти решение в простейшем случае без использования рекурсии, а затем вся задача сводится к нескольким подобным задачам, но меньшей размерности. Если число задач, к которым сводится исходная задача, не меньше двух, то можно заведомо говорить, что рекурсивное решение намного проще не рекурсивного алгоритма и использование рекурсии оправданно. Для пояснения этих общих утверждений обратимся к примеру. В нашем классе приведены три метода обхода бинарного дерева: PrefixOrder, InfixOrder, PostfixOrder. Написать не рекурсивный алгоритм, который обходил бы все узлы дерева некоторым заданным образом не так то просто. Другое дело рекурсивное определение. Действительно базисное решение очевидно, - когда дерево пусто, то ничего и делать не надо. Если же оно не пусто, то у нас есть корень дерева, а у него два потомка, которые в свою очередь являются деревьями. Поэтому для обхода всего дерева достаточно посетить корень, а затем обойти (рекурсивно) оба поддерева. Меняя порядок посещения корня и поддеревьев, получаем три различных способа обхода дерева. Заметим, именно благодаря тому, что сама структура данных рекурсивна, рекурсивные алгоритмы естественным образом описывают решения задач по обработке таких данных. Рекурсивные определения просты и понятны, но напоминают некоторый фокус. Наиболее сложно воспроизвести вычисления, выполняемые рекурсивным алгоритмом.
  2. Для простоты в методе SearchAndInsert мы совместили две операции поиска элемента по заданному ключу и вставки нового элемента. Если в дереве найден элемент с заданным ключом, то предполагается, что речь идет о поиске и возвращается информация из информационного поля этого элемента. Если в дереве нет элемента с таким ключом, то создается новый узел дерева. Заметьте, что наше решение не позволяет производить замену элемента, а в процессе поиска не уведомляет об отсутствии элемента с заданным ключом
  3. Удаление элемента из дерева поиска осложняется тем, что нужно поддерживать структуру дерева поиска. В тех случаях, когда нужно удалить элемент, у которого есть два потомка, вызывается специальная процедура ReplaceAndDelete. Эта процедура ищет кандидата, который мог бы заменить удаляемый элемент, сохраняя структуру дерева.
  4. Недостатком деревьев поиска является то, что они могут быть плохо сбалансированы и могут иметь относительно длинные ветви. Так, если при создании дерева поиска, ключи будут поступать в отсортированном порядке, то дерево будет представлено одной ветвью. Работа с этой структурой данных предполагает, что при создании и добавлении элементов в дерево ключи поступают в случайном порядке, хорошо перемешанные. Эта структура особенно применима в тех случаях, когда в процессе работы над данными широко используются все операции - поиск, вставка и удаление.
  5. Мы не стали писать реализацию этого класса, оперирующего с данными, хранящимися в списках Excel или базе данных Access, поскольку это выходит за рамки этой лекции.

Работа со словарем

Используем класс BinTree для работы со словарем. В нашем примере работы с классом будет создаваться словарь, в нем будет осуществляться поиск и удаление элементов. Вот текст процедуры, выполняющей эти операции:

Public Sub WorkwithBinTree()
 Dim MyDict As New BinTree
 Dim englword As String, rusword As String
 'Создание словаря

 MyDict.SearchAndInsert key:="dictionary", info:="словарь"
 MyDict.SearchAndInsert key:="hardware", info:="аппаратура, аппаратные средства"
 MyDict.SearchAndInsert key:="processor", info:="процессор"
 MyDict.SearchAndInsert key:="backup", info:="резервная копия"
 MyDict.SearchAndInsert key:="token", info:="лексема"
 MyDict.SearchAndInsert key:="file", info:="файл"
 MyDict.SearchAndInsert key:="compiler", info:="компилятор"
 MyDict.SearchAndInsert key:="account", info:="учетная запись"

 'Обход словаря
 MyDict.PrefixOrder
 
 'Поиск в словаре
 englword = "account": rusword = ""
 MyDict.SearchAndInsert key:=englword, info:=rusword
 Debug.Print englword, rusword
 
 'Удаление из словаря
 MyDict.DelInTree englword
 englword = "hardware"
 MyDict.DelInTree englword

 'Обход словаря
 MyDict.PrefixOrder
 
End Sub
9.5.

Приведем результаты ее работы:

key:			dictionary	info:		 словарь
key:			backup		info:		 резервная копия
key:			account		info:		 учетная запись
key:			compiler	info:		 компилятор
key:			hardware	info:		 аппаратура, аппаратные средства
key:			file		info:		 файл
key:			processor	info:		 процессор
key:			token		info:		 лексема
account		учетная запись
key:			dictionary	info:		 словарь
key:			backup		info:		 резервная копия
key:			compiler	info:		 компилятор
key:			file		info:		 файл
key:			processor	info:		 процессор
key:			token		info:		 лексема

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

Лексикографическое дерево, задающее словарь

увеличить изображение
Рис. 9.3. Лексикографическое дерево, задающее словарь
полина есенкова
полина есенкова
Дмитрий Вологжин
Дмитрий Вологжин
Добрый день, прошел тесты с 1 по 9, 10 не сдал, стал читать лекцию и всё пройденные тесты с 1 по 9 сбросились, когда захотел пересдать 10 тест.