Рекурсия для начинающих

10.02.21

Разработка - Математика и алгоритмы

Рассмотрен подход к пониманию рекурсий. Приведены примеры из реальной практики.

В теме статьи обозначено «для начинающих», чтобы Гуру сильно не «пинали» за очевидные для них понятия. Тем не менее, объяснять рекурсию будем на основе понятий, которые ранее явно не часто были обозначены (блоки кода в теле рекурсивной функции).

Мы будем говорить о рекурсии в программировании. Не будем обсуждать, что такое рекурсия, и какая она бывает. На эту тему очень много статей и книг. Например, книга «Введение в рекурсивное программирование», автор Мануэль Рубио-Санчес, посвящена только рекурсии на 436 страницах.

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

Листинг 1

Для деревьев вместо условия применяется цикл. Деревья рассмотрим ниже.

Задача - научиться понимать эти пять блоков команд.

Самая известная тема для рекурсии — это расчет факториала:

Функция Факториал(n)
   Возврат ?(Число(n) <= 1, 1, n * Факториал (n -1));                     (2)
КонецФункции

Листинг 2

 

Очень красивый код, но не очень понятный …

Напишем код расчета факториала согласно нашей модели (Листинг 1):

&НаКлиенте
Процедура ВычислитьФакториал()
  Ф=1;
  к=0;
  н=4;
  Факториал (к, н, Ф);
  Сообщить(Ф);
КонецПроцедуры

Процедура Факториал (к, н, Ф)
  к=к+1;
  Если к <= н Тогда
    Ф=Ф*к;
    Факториал (к, н, Ф);
  Иначе
    Возврат;
  КонецЕсли;
КонецПроцедуры

Не так красиво, как (2), но мы учимся.

к – счетчик;

н – значение для вычисления факториала;

Ф – факториал (для н=4 будет 24).

Код достаточно понятный. Заметим только то, что Ф рассчитывается на прямом пути. На обратном пути (Возврат) – ничего не происходит. Полезно отследить изменение параметров в отладчике:

Здесь использованы блоки 1 и 2.

Блок1 – как правило на практике, нужен для подготовки Условия.

Блок2 – нужен для подготовки вызова рекурсивной процедуры (функции);

Теперь можно изменить процедуру Факториал убрать параметр к и приблизить её к каноническому виду (1).

&НаКлиенте
Процедура ВычислитьФакториал()
	н=4;
	Ф=1;
	Сообщить Факториал (н, Ф);
КонецПроцедуры

Процедура Факториал (н, Ф)
	Если н > 0 Тогда
		Ф=Ф*н;
		Факториал (н - 1, Ф);
	КонецЕсли;
КонецПроцедуры

Блок1 – исчез.

На следующем шаге заменим процедуру на функцию и уберем вычисление Ф в Блоке2.

Для полного понимания предварительно рассмотрим следующий вариант:

Функция Факториал (н, Ф)
	Если н > 0 Тогда
		//Блок2
		Останов=Истина;
		Факториал (н -1, Ф*н);
		//Блок3;
		Останов=Истина;
	КонецЕсли;
	//Блок5
	Останов=Истина;
	Возврат Ф;
КонецФункции

Возвращаться будет 1.

Строки Останов = Истина добавлены для анализа переменных Ф и н в отладчике. Теперь нет параметра Ф в виде ссылки и возвращается локальное значение Ф, которое было зафиксировано при первом входе в рекурсию.

Исправим, добавим возврат переменной Ф. И увидим, что такое локальная переменная в рекурсивной функции – в отладчике. Это важно понимать.

Функция Факториал (н, Ф)
	Если н > 0 Тогда
		//Блок2
		Останов=Истина;
		Ф = Факториал (н -1, Ф*н);
		//Блок3;
		Останов=Истина;
	КонецЕсли;
	//Блок5
	Останов=Истина;
	Возврат Ф;
КонецФункции

На рисунке выше момент, когда вернулись в функцию, в которую входили с н = 2 и Ф = 12. Значение Ф сохранилось как для локальной переменной.

Это момент, когда еще не присвоено Ф возвращаемое значение функцией Факториал.

И вот на следующем шаге Ф переприсваивается.

В итоге функция отработает правильно.

Здесь мы не пишем функцию факториал, мы смотрим для чего нужны Блоки 1-5, как ведут себя переменные.

Рассмотрим простые примеры рекурсий в 1С.

// <Описание функции>
//Возвращает количество группировок в настройках варианта макета СКД
// Возвращаемое значение:
// Кг – Число
Функция КоличествоГруппировокВНастройке ()
	Кг=0;
	СтруктураГруппировок=Настройки.Структура;
	КоличествоГруппировок (Кг, СтруктураГруппировок);
	Возврат Кг;
КонецФункции // КоличествоГруппировокВНастройке ()

Процедура КоличествоГруппировок (Кг, СтруктураГруппировок)
	Если СтруктураГруппировок.Количество() >0 Тогда
		СтруктураГруппировок=СтруктураГруппировок [0].Структура;
		Кг=Кг + 1;
		КоличествоГруппировок (Кг, СтруктураГруппировок);
	Иначе
		Возврат;
	КонецЕсли;
КонецПроцедуры

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

ТабДок.ФиксацияСверху = Кз+Кг;

Здесь Кз - высота заголовка, Кг – высота шапки.

Функция упрощена для примера. Настройки – настройки варианта макета, объявленные в Перем:

СхемаКомпоновкиДанных = ОтчетОбъект.ПолучитьМакет(СхемаИмя);
Вариант = СхемаКомпоновкиДанных.ВариантыНастроек[0];
Настройки = Вариант.Настройки;

Рекурсивная функция написана в стиле листинга 1 и предельно понятна.

Основное применение рекурсии нашли при работе с деревьями. В 1С это в основном работа с иерархическими справочниками.

В теории деревьев различают корень, ветки и листья. Корень (ствол) это один элемент.

В 1С корня в классическом понимании у справочника нет. Корень — это сам иерархический справочник. Группы самого верхнего уровня имеют значение метода Уровень () = 0.

Рассмотрим пример определения самого старшего родителя для произвольного элемента справочника.

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

Это значит, что при движении по дереву вверх циклы не понадобятся.

&НаСервереБезКонтекста
Функция ПолучитьРодителя(ЭлементСправочника)
	Если ЭлементСправочника.Уровень() = 0 Тогда
		Возврат ЭлементСправочника;
	КонецЕсли;
	Возврат ПолучитьРодителя(ЭлементСправочника.Родитель);
КонецФункции

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

Предмет примера:

Есть объекты строительства. Объекты связаны иерархической моделью.

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

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

Вот пример из реальной БД:

Видим, что справочник «Объектов строительства» имеет до 7 уровней иерархии. В модели – есть объекты, которые будут приниматься к учету – основные средства. И есть работы, затраты которых нужно распределить:

Для решения задачи разберем алгоритм на следующей модели:

Рис. 1. Схема дерева

Рис.2. Дерево в справочнике объектов

Три уровня иерархии. Шесть объектов строительства с прямыми затратами и три с косвенными (группы).

Рассчитаем косвенные для объектов вручную.

База распределения для объекта Объект1 = 3000+2000 = 5000;

Косвенные от Объекта1:

для Объекта11 косвенные = 500 * 3000 / 5000 = 300.

для Объекта12 косвенные = 500 * 2000 / 5000 = 200.

База распределения для объекта Объект2 = 3000+2000+1000 = 6000;

Косвенные от Объекта2:

для Объекта21 косвенные = 600 * 3000 / 6000 = 300.

для Объекта22 косвенные = 600 * 2000 / 6000 = 200.

для Объекта23 косвенные = 600 * 1000 / 6000 = 100.

База распределения для объекта Объект0 – 5000+6000+1000 = 12000;

От Объекта0:

Для Объекта01 косвенные = 2400 * 1000 / 12000 = 200.

Для Объекта11 косвенные = 2400 * 3000 / 12000 = 600.

Для Объекта12 косвенные = 2400 * 2000 / 12000 = 400.

Для Объекта21 косвенные = 2400 * 3000 / 12000 = 600.

Для Объекта22 косвенные = 2400 * 2000 / 12000 = 400.

Для Объекта23 косвенные = 2400 * 1000 / 12000 = 200.

В итоге косвенные:

Для Объекта01 косвенные = 200

Для Объекта11 косвенные = 600 + 300 = 900

Для Объекта12 косвенные = 400 + 200 = 600

Для Объекта21 косвенные = 600 + 300 = 900

Для Объекта22 косвенные = 400 + 200 = 600

Для Объекта23 косвенные = 200 + 100 = 300

==================================

Итого косвенных 3500

Листинг 3

Нужно сформировать запрос с Итогами, обязательно указать тип итогов «Элементы и иерархия».

В запросе должно быть так:

ИТОГИ ПО Ссылка ИЕРАРХИЯ

Следующий момент - куда лучше выгружать данные:

  • ВыборкаИзРезультатаЗапроса

  • ДеревоЗначений

Мы выбираем дерево, потому что

  1. В отладчике можно посмотреть данные.

  2. Код более понятный – объект, строки, цикл.

  3. Есть возможность реквизиты дерева использовать для алгоритма (записывать в реквизиты данные).

В интернете много статей обхода дерева как такового. А вот обход дерева, сгенерированного запросом, рассмотрен крайне скупо. Для выборки - статей написано много.

Поэтому рассмотрим алгоритм обхода дерева вниз и вверх - подробно.

&НаСервере
Перем ОбъектыДляПриемки;
&НаСервере
Процедура КосвеныеОбъектовНаСервере()
	ОбъектыДляПриемки =  Новый ТаблицаЗначений;
	ОбъектыДляПриемки.Колонки.Добавить("Объект", Новый ОписаниеТипов("СправочникСсылка.ОбъектыСтроительства"));
	ОбъектыДляПриемки.Колонки.Добавить("Косвенные", Новый ОписаниеТипов("Число"));
	Запрос = Новый Запрос;
	Запрос.Текст = 
	"ВЫБРАТЬ
		|	ОбъектыСтроительства.Ссылка КАК Ссылка,
		|	ОбъектыСтроительства.Прямые КАК Прямые,
		|	ОбъектыСтроительства.Косвенные КАК Косвенные
		|ИЗ
		|	Справочник.ОбъектыСтроительства КАК ОбъектыСтроительства
		|ГДЕ
		|	ОбъектыСтроительства.Ссылка В ИЕРАРХИИ(&Ссылка)
		|ИТОГИ
		|	СУММА(Прямые)
		|ПО
		|	Ссылка ИЕРАРХИЯ";
	
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
Рекурсия(Дерево.Строки); 
КонецПроцедуры
&НаКлиенте
Процедура КосвеныеОбъектов(Команда)
	КосвеныеОбъектовНаСервере();
КонецПроцедуры

Объявляем переменную ОбъектыДляПриемки, которая будет видна во всех серверных функциях.

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

с типом обхода ПоГруппировкамСИерархией. Передаем в рекурсивную функцию Строки объекта строительства, который принимаем в эксплуатацию.

Принимаем в эксплуатацию в целом Объект0 (акты КС-11, КС-14), но к учету принимаем объекты самого нижнего уровня – основные средства (Дт01/Кт08.03) документом «Принятие к учету ОС».

Здесь важно – Объект0, это не корневой узел справочника! Но в дереве

он становится корневым (отбор по объекту).

Посмотрим данные дерева в отладчике:

Окно 1: Корень объекта строительства и его строки (Объект0)

Окно 2: Строка корня, нажимаем на ней F2

Окно 3: Строка корня раскрылась и видим Строки

Окно 4: После нажатия F2 на строке Строки

В четвертом окне 4 строки.

Первая строка

Проанализируем этот список (отмечен цифрой 4).

Первая строка – Объект01 – объект, который мы будем принимать к учету (это последний уровень, лист). Эта строка аналог записи в Выборке с типом – ДетальнаяЗапись.

Вторая строка – итоги по объекту Объект0. Эта строка аналог записи в Выборке с типом – ИтогПоИерархии.

Третья и четвертые строки – узлы дерева, их нужно раскрывать рекурсией.

Ниже приведены рекурсивные функции для расчета распределенных косвенных затрат.

&НаСервере
Функция Рекурсия(Строки);	
Для каждого Стр Из Строки Цикл
Если  Стр.Ссылка = Стр.Родитель.Ссылка  Тогда //Итоговая строка
Если  НЕ Стр.Родитель = Неопределено И Стр.Ссылка = Стр.Родитель.Ссылка Тогда //Итоговая строка, пропускаем её
ИначеЕсли Стр.Строки.Количество()=1 Тогда	// Лист
			СтрТЗ=ОбъектыДляПриемки.Добавить();
			СтрТЗ.Объект=Стр.Ссылка;
			СтрТЗ.Косвенные=0;
			Косвенные(СтрТЗ,Стр.Родитель);
Иначе  // Узел
		Рекурсия(Стр.Строки);
КонецЕсли; 			
КонецЦикла; 
КонецФункции 
&НаСервере
Функция Косвенные(СтрТЗ,Родитель)
	Если НЕ Родитель = Неопределено Тогда
		СтрТЗ.Косвенные = СтрТЗ.Косвенные + СтрТЗ.Объект.Прямые*Родитель.Ссылка.Косвенные/Родитель.Прямые;
		Косвенные(СтрТЗ, Родитель.Родитель);
	КонецЕсли; 
КонецФункции

В функции Рекурсия анализируем, какой тип строки дерева. Для детальной записи количество строк = 1 (своя строка). Для узла количество записей рано количеству потомков +1.

Запросом рассчитали суммарные значения прямых затрат для каждого узла дерева.

Интересная конструкция:

Если НЕ Стр.Родитель = Неопределено И Стр.Ссылка = Стр.Родитель.Ссылка

Во второй части условия проверяется ссылка Родителя, а если он не определён в первой части? Ответ: Если первая часть = Ложь то компилятор в условии И

не проверяет сведущие составляющие условия.

Поставим точку останова в функции КосвеныеОбъектовНаСервере как показано на рисунке ниже:

Увидим результаты запроса.

Так выглядит запись для корневой строки дерева после прохождения рекурсии. Сумма прямых затрат всех объектов нижних уровней равна 12 000.

На прямом пути для элемента самого нижнего уровня создаем строку в таблице значений:

Чтобы увидеть в отладчике ставим точку останове, показанную на рисунке ниже.

И сразу вызываем вторую рекурсивную функцию Косвенные.

Эта функция рассчитывает косвенные расходы для этого объекта от каждого родителя.

В итоге таблица ОбъектыДляПриемки будет выглядеть следующим образом:

Результат соответствует ручному расчету (Листинг 3).

Теперь в цикле для строк этой таблицы можно создавать документы «Принятие к учету ОС».

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

&НаСервере
Функция Рекурсия(Строки);	
	Для каждого Стр Из Строки Цикл
		Если Стр.Строки.Количество()=1 Тогда	// Лист
		 Если  НЕ Стр.Ссылка = Стр.Родитель.Ссылка  Тогда 
			СтрТЗ = ОбъектыДляПриемки.Добавить();
			СтрТЗ.Объект = Стр.Ссылка;
			СтрТЗ.Косвенные = 0;
			СтрТЗ.Прямые = Стр.Прямые;
			СтрТЗ.Уровень = Стр.Уровень()
		КонецЕсли; 				
		Иначе  // Узел
			Рекурсия(Стр.Строки);
			Для каждого СтрОС Из ОбъектыДляПриемки Цикл
				Если СтрОС.Объект.ПринадлежитЭлементу(Стр.Ссылка) Тогда
                   СтрОС.Косвенные = СтрОС.Косвенные + СтрОС.Прямые*Стр.Косвенные/Стр.Прямые;	
				КонецЕсли; 
			КонецЦикла; 
		КонецЕсли; 			
	КонецЦикла; 
КонецФункции

Добавим в таблицу ОбъектыДляПриемки колонку Прямые.

В результате расчета получим такую таблицу:

В этом варианте не используется вторая рекурсивная функция на прямом обходе дерева. Рассчитываем необходимые данные на обратном ходе. Конечно

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

Выводы являются субъективным мнением автора.

Выводы:

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

Алгоритмы в этом случае более «прозрачные».

  1. Рассматривать целесообразность и использовать обратный ход рекурсивного расчета (область кода после выхода из рекурсивной функции).

Рекурсия

См. также

Метод Дугласа-Пойкера для эффективного хранения метрик

Математика и алгоритмы Платформа 1C v8.2 Конфигурации 1cv8 Россия Абонемент ($m)

На написание данной работы меня вдохновила работа @glassman «Переход на ClickHouse для анализа метрик». Автор анализирует большой объем данных, много миллионов строк, и убедительно доказывает, что ClickHouse справляется лучше PostgreSQL. Я же покажу как можно сократить объем данных в 49.9 раз при этом: 1. Сохранить значения локальных экстремумов 2. Отклонения от реальных значений имеют наперед заданную допустимую погрешность.

1 стартмани

30.01.2024    1886    stopa85    12    

34

Алгоритм симплекс-метода для решения задачи раскроя

Математика и алгоритмы Бесплатно (free)

Разработка алгоритма, построенного на модели симплекс-метода, для нахождения оптимального раскроя.

19.10.2023    4686    user1959478    50    

34

Регулярные выражения на 1С

Математика и алгоритмы Инструментарий разработчика Платформа 1С v8.3 Мобильная платформа Россия Абонемент ($m)

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

1 стартмани

09.06.2023    7680    4    SpaceOfMyHead    17    

56

Модель распределения суммы по базе

Математика и алгоритмы Платформа 1С v8.3 Россия Абонемент ($m)

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

1 стартмани

21.03.2022    7950    7    kalyaka    11    

44

Изменения формата файлов конфигурации (CF) в 8.3.16

Математика и алгоритмы Платформа 1С v8.3 Бесплатно (free)

Дополнение по формату файлов конфигурации (*.cf) в версии 8.3.16.

16.12.2021    4559    fishca    13    

36

Интересная задача на Yandex cup 2021

Математика и алгоритмы Бесплатно (free)

Мое решение задачи на Yandex cup 2021 (frontend). Лабиринт. JavaScript.

12.10.2021    8954    John_d    73    

46

Механизм анализа данных. Кластеризация.

Математика и алгоритмы Анализ учета Платформа 1С v8.3 Анализ и прогнозирование Бесплатно (free)

Подробный разбор, с примером использования, встроенного механизма кластеризации 1С.

31.08.2021    7984    dusha0020    8    

70
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. RPGrigorev 692 10.02.21 11:35 Сейчас в теме
2. mcgoblin 3 11.02.21 12:12 Сейчас в теме
Так как статья для новичков, стоит добавить, что сильно рекурсией не стоит баловаться, она не может быть бесконечной и сколь угодно велика.

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

Источник ИТС Методрекомендации разрабам
PitonTBs; +1
4. szv 157 17.02.21 06:24 Сейчас в теме
(2) Согласен. По этому поводу в статье подробно рассматриваются прямой и обратный ходы. Прямой до точки возврата - обязательно должно быть условие прерывания дальнейшего погружения в рекурсию. Что касается рекомендаций ИТС - не могу даже придумать вариант с количеством уровней более 20 в мире 1С.
+
5. mcgoblin 3 17.02.21 06:37 Сейчас в теме
(4)Пример использования, выгрузка проводок по предприятию за год в базу аксесс. Ввиду ряда особенностей ms access (В частности объем файла) делал через рекурсию.
В итоге пришлось в цикле городить 3х этажные ЕслиТоИначе.
+
3. RustIG 1556 17.02.21 02:16 Сейчас в теме
(0) изучите мой опыт использования рекурсии для поиска номенклатуры по значению свойства https://infostart.ru/public/1043307/ - для этого только придется скачать обработку...
+
Оставьте свое сообщение