Вычисление факториала во встроенном языке 1С
Содержание
Факториал числа Н — это произведение всех целых чисел от 1 до Н, обозначается Н!. Во встроенном языке 1С штатной функции для него нет, поэтому факториал пишут руками: либо циклом, либо через рекурсию. Для значений до 33! результат ещё помещается в обычный тип Число, дальше нужна длинная арифметика через строки.
Ниже — готовые функции для обоих способов, разбор точности при больших значениях и короткий приём, как не считать огромные факториалы там, где нужны всего лишь сочетания.
Что произведение чисел от 1 до N даёт на выходе
Факториал растёт очень быстро. Несколько опорных значений, которые полезно держать в голове при отладке:
| Н | Н! | Разрядов |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 1 | 1 |
| 5 | 120 | 3 |
| 10 | 3 628 800 | 7 |
| 13 | 6 227 020 800 | 10 |
| 20 | 2 432 902 008 176 640 000 | 19 |
Два правила, которые задают всю логику функции:
- 🔹
0! = 1по определению. Это не ошибка и не частный случай — пустое произведение равно единице. - 🔹 Факториал определён только для целых неотрицательных чисел. Для дробного или отрицательного аргумента результата нет, функция должна такой вызов отклонять.
Начальное значение накопителя в функции — именно 1, а не 0. Если стартовать с нуля, любое умножение обнулит результат, и функция всегда вернёт 0.
Функция факториала через цикл
Самый надёжный вариант — обычный цикл с накоплением произведения. Такую функцию логично вынести в общий модуль (например, ОбщегоНазначенияСервер в своей обработке или собственный общий модуль): тогда её можно вызывать из любого места конфигурации.
Функция Факториал(Н) Экспорт
Результат = 1;
Для Сч = 2 По Н Цикл
Результат = Результат * Сч;
КонецЦикла;
Возврат Результат;
КонецФункции
Цикл начинается с 2, потому что умножение на 1 ничего не меняет. При Н = 0 и Н = 1 цикл просто не выполнится, и функция корректно вернёт единицу — отдельная ветка для нуля не нужна.
Проверить результат можно прямо в консоли запросов или во внешней обработке:
Сообщить("5! = " + Строка(Факториал(5))); // 5! = 120
Сообщить("0! = " + Строка(Факториал(0))); // 0! = 1
Обратите внимание: число склеивается со строкой только через Строка(). Прямая конкатенация "5! = " + Факториал(5) вызовет ошибку преобразования типов.
Рекурсивный вариант и когда он уместен
Рекурсия выражает определение факториала буквально: Н! = Н × (Н−1)!. Код выходит короче, но у него есть цена.
Функция ФакториалРекурсивно(Н) Экспорт
Если Н <= 1 Тогда
Возврат 1;
КонецЕсли;
Возврат Н * ФакториалРекурсивно(Н - 1);
КонецФункции
Ключевая строка — базовый случай Если Н <= 1. Без него функция уйдёт в бесконечный вызов самой себя и упадёт с переполнением стека. Условие <= 1 (а не = 1) заодно защищает от нуля: при Н = 0 функция тоже вернёт единицу.
Когда что выбирать:
- ➕ Цикл — рабочий выбор по умолчанию. Нет накладных расходов на вызовы, предсказуемое потребление памяти.
- ➖ Рекурсия — нагляднее в учебных примерах, но каждый вложенный вызов занимает место в стеке. Для факториала это обычно не проблема (аргументы маленькие), однако на очень больших Н глубокая рекурсия рискует упереться в лимит вложенности.
Если переписываете чужой рекурсивный код и он падает на больших значениях — почти всегда дело именно в глубине рекурсии, а не в самой формуле. Замена на цикл снимает проблему.
Большие числа: до какого N результат точный
Тип Число в 1С хранит ограниченное количество значащих разрядов — порядка 38 цифр. Пока результат в этот лимит укладывается, он точный:
- 33! — это 37 цифр, ещё помещается полностью.
- 34! — уже 39 цифр, в обычное число не влезает: получите либо погрешность в младших разрядах, либо ошибку.
То есть для Н больше 33 обычная функция даёт неверный результат без видимого предупреждения. Это самая коварная ошибка: код не падает, а молча возвращает «почти правильное» число.
Если по задаче нужны значения больше 33!, число приходится хранить как строку и умножать «в столбик» — поразрядно, как на бумаге. Накапливаем цифры в массиве от младшего разряда к старшему:
Функция ФакториалБольшой(Н) Экспорт
Цифры = Новый Массив;
Цифры.Добавить(1);
Для Множитель = 2 По Н Цикл
Перенос = 0;
Для Индекс = 0 По Цифры.ВГраница() Цикл
Произведение = Цифры[Индекс] * Множитель + Перенос;
Цифры[Индекс] = Произведение % 10;
Перенос = Цел(Произведение / 10);
КонецЦикла;
Пока Перенос > 0 Цикл
Цифры.Добавить(Перенос % 10);
Перенос = Цел(Перенос / 10);
КонецЦикла;
КонецЦикла;
// Собираем строку от старшего разряда к младшему
Строка = "";
Индекс = Цифры.ВГраница();
Пока Индекс >= 0 Цикл
Строка = Строка + Строка(Цифры[Индекс]);
Индекс = Индекс - 1;
КонецЦикла;
Возврат Строка;
КонецФункции
Здесь % — остаток от деления, Цел() — целая часть, ВГраница() — индекс последнего элемента массива. Результат — строка произвольной длины, арифметических ограничений у неё нет. Проверка:
Сообщить(ФакториалБольшой(25)); // 15511210043330985984000000
Платить за это приходится скоростью: длинное умножение заметно медленнее обычного, поэтому включайте его только когда значения реально выходят за предел точного числа.
Ещё одна ловушка — вывод. Даже если результат посчитан строковой функцией, попытка сначала превратить его обратно в Число (например, для вывода через Сообщить) снова упрётся в тот же предел разрядности. Большой факториал нужно и хранить, и показывать как строку — не конвертируя в число ни на одном шаге. То же касается записи такого значения в реквизит: строковый реквизит подойдёт, числовой обрежет разряды.
Как не считать лишнего: оптимизация и кэш
Сам по себе факториал считается за один проход — алгоритм линейный, ускорять в нём особо нечего. Оптимизация нужна в двух ситуациях.
Повторные вызовы с одними и теми же аргументами. Если функцию дёргают в цикле по тысяче раз подряд, результаты имеет смысл кэшировать. Простой кэш на соответствии:
Перем КэшФакториалов;
Функция ФакториалСКэшем(Н) Экспорт
Если КэшФакториалов = Неопределено Тогда
КэшФакториалов = Новый Соответствие;
КонецЕсли;
Значение = КэшФакториалов.Получить(Н);
Если Значение <> Неопределено Тогда
Возврат Значение;
КонецЕсли;
Значение = Факториал(Н);
КэшФакториалов.Вставить(Н, Значение);
Возврат Значение;
КонецФункции
Главный приём — часто факториал вообще не нужен. В прикладных расчётах он почти всегда появляется внутри формулы числа сочетаний или размещений. И там считать два отдельных факториала — ошибка: промежуточные значения разрастаются до огромных чисел, хотя итог помещается в обычное. Правильно сокращать дробь сразу (об этом в разделе про комбинаторику).
Переменную модуля КэшФакториалов нельзя делить между сеансами — на сервере у каждого вызова своё окружение. Кэш живёт в пределах одного исполнения, этого для циклов достаточно.
Проверка аргумента и типичные ошибки
Боевая функция должна отклонять бессмысленный ввод, иначе получите либо вечный цикл, либо тихо неверный результат. Минимальная защита:
Функция ФакториалБезопасный(Н) Экспорт
Если ТипЗнч(Н) <> Тип("Число") Тогда
ВызватьИсключение "Аргумент факториала должен быть числом";
КонецЕсли;
Если Н < 0 Тогда
ВызватьИсключение "Факториал определён только для неотрицательных чисел";
КонецЕсли;
Если Н <> Цел(Н) Тогда
ВызватьИсключение "Факториал определён только для целых чисел";
КонецЕсли;
Результат = 1;
Для Сч = 2 По Н Цикл
Результат = Результат * Сч;
КонецЦикла;
Возврат Результат;
КонецФункции
Что чаще всего ломает факториал на практике:
- 🔸 Накопитель стартует с нуля. Результат всегда 0. Лечится заменой
Результат = 0наРезультат = 1. - 🔸 Нет базового случая в рекурсии. Бесконечные вызовы и переполнение стека.
- 🔸 Передают дробь.
Цел(Н)отсекает дробную часть, и5,7!молча считается как5!. Поэтому дробный аргумент лучше отклонять явно. - 🔸 Считают Н > 33 обычным числом. Результат выглядит правдоподобно, но неверен в младших разрядах. Если значения большие — только строковая функция.
- 🔸 Отрицательный аргумент. Цикл
Для Сч = 2 По -3просто не выполнится и вернёт 1 — внешне «работает», по сути неправильно.
Где факториал реально нужен в задачах
В чистом учёте факториал почти не встречается — он всплывает в комбинаторике и вероятностных расчётах: сколько способов распределить, сколько уникальных комбинаций возможно, какова вероятность совпадения. Типовой случай — число сочетаний C(n, k): сколькими способами выбрать k элементов из n без учёта порядка.
Через формулу n! / (k! · (n−k)!) считать её напролом нельзя — факториалы в числителе разрастаются до чисел за пределом точности, хотя сам результат маленький. Правильный способ — умножать и делить по шагам, сокращая дробь на ходу:
Функция ЧислоСочетаний(Н, К) Экспорт
Если К < 0 ИЛИ К > Н Тогда
Возврат 0;
КонецЕсли;
Если К > Н - К Тогда
К = Н - К; // симметрия: C(n,k) = C(n,n-k)
КонецЕсли;
Результат = 1;
Для Сч = 1 По К Цикл
Результат = Результат * (Н - К + Сч) / Сч;
КонецЦикла;
Возврат Результат;
КонецФункции
Проверка: ЧислоСочетаний(5, 2) даёт 10, ЧислоСочетаний(49, 6) — 13 983 816 (столько комбинаций в лотерее «6 из 49»). Числа здесь остаются небольшими, предел точности не задевается — именно потому что факториалы напрямую не вычисляются.
Рядом с сочетаниями в задачах всплывают ещё два родственника факториала:
- Перестановки — сколькими способами упорядочить все Н элементов. Это и есть чистый факториал:
P(Н) = Н!. Пример: 5 товаров в витрине можно расставитьФакториал(5)= 120 способами. - Размещения — сколькими способами выбрать K элементов из Н с учётом порядка:
A(Н, К) = Н! / (Н−К)!. Как и сочетания, её безопаснее считать произведением последних K множителей, не вычисляя полные факториалы:
Функция ЧислоРазмещений(Н, К) Экспорт
Если К < 0 ИЛИ К > Н Тогда
Возврат 0;
КонецЕсли;
Результат = 1;
Для Сч = 0 По К - 1 Цикл
Результат = Результат * (Н - Сч);
КонецЦикла;
Возврат Результат;
КонецФункции
Для ЧислоРазмещений(5, 2) это 5 × 4 = 20. Разница с сочетаниями ровно в К!: A(n,k) = C(n,k) · k! — порядок внутри выборки учитывается.
В языке запросов факториал и эти формулы недоступны — умножений в произвольном диапазоне там нет. Считайте значения в коде (функцией общего модуля), а в запрос передавайте уже готовый результат параметром.
Частые вопросы
Есть ли в 1С встроенная функция факториала?
Можно ли посчитать факториал дробного числа?
Почему факториал большого числа считается неправильно?
Число хранит ограниченное число разрядов (порядка 38 цифр). Начиная примерно с 34! результат перестаёт в них помещаться, и младшие разряды теряются. Для таких значений нужна строковая функция с умножением «в столбик».Где разместить функцию, чтобы вызывать её отовсюду?
Сервер и ключевым словом Экспорт у функции. Тогда она доступна из документов, обработок, отчётов и форм.