В процессе разработки часто возникает задача получения набора случайных чисел, которые не должны повторяться. Стандартный объект платформы 1С ГенераторСлучайныхЧисел сам по себе выдает последовательность, в которой значения могут дублироваться, так как каждое новое число генерируется независимо от предыдущих. В этой статье мы разберем несколько способов решения этой задачи: от простой проверки на вхождение до профессиональных алгоритмов перемешивания последовательностей.
Рассмотрим типичную ошибку начинающего разработчика. Если мы создаем объект ГенераторСлучайныхЧисел внутри цикла и пытаемся получить значения, мы можем столкнуться не только с логическими дублями, но и с повторяющимися сериями чисел. Посмотрим на некорректный пример:
СЗ = Новый СписокЗначений;
Для нум = 0 По 8 Цикл
рандом = Новый ГенераторСлучайныхЧисел; // ОШИБКА: пересоздание в цикле
данные = рандом.СлучайноеЧисло(0, 8);
СЗ.Добавить(данные);
КонецЦикла;
Важное правило: объект ГенераторСлучайныхЧисел следует инициализировать один раз вне цикла. Если создавать его слишком часто (в одну и ту же миллисекунду), системный таймер, по которому инициализируется генератор, может выдать одинаковое начальное значение, и вы получите идентичные числа.
Проанализируем самый простой метод. Мы генерируем число и проверяем, нет ли его уже в нашем списке. Если число найдено, мы просто пробуем еще раз. Этот метод хорош, если диапазон доступных чисел значительно больше, чем количество чисел, которые нам нужно получить.
ВыбранныеЧисла = Новый Массив;
ГСЧ = Новый ГенераторСлучайныхЧисел;
Пока ВыбранныеЧисла.Количество() < 9 Цикл
Случайное = ГСЧ.СлучайноеЧисло(0, 100);
Если ВыбранныеЧисла.Найти(Случайное) = Неопределено Тогда
ВыбранныеЧисла.Добавить(Случайное);
КонецЕсли;
КонецЦикла;
Однако, разберем ситуацию, когда нам нужно получить все числа из диапазона от 0 до 8 в случайном порядке. Если мы будем использовать этот метод, то поиск последнего, девятого числа может затянуться: генератор будет выдавать уже существующие числа 1, 2, 3... и "попадание" в единственное оставшееся число может занять много итераций. Поэтому для получения случайных перестановок лучше использовать другой подход.
Выясним причину популярности этого метода. Мы заранее создаем массив со всеми возможными значениями, а затем "вытаскиваем" из него элементы случайным образом. Это гарантирует отсутствие дублей и стопроцентное заполнение результата за фиксированное количество шагов.
Рассмотрим пошагово реализацию:
МассивЧисел = Новый Массив;
Для инд = 0 по 8 Цикл
МассивЧисел.Добавить(инд);
КонецЦикла;
ПеремешанныйМассив = Новый Массив;
ГСЧ = Новый ГенераторСлучайныхЧисел;
Для инд = 0 по 8 Цикл
// Верхняя граница уменьшается с каждой итерацией
СлучайныйИндекс = ГСЧ.СлучайноеЧисло(0, МассивЧисел.Количество() - 1);
ПеремешанныйМассив.Добавить(МассивЧисел[СлучайныйИндекс]);
МассивЧисел.Удалить(СлучайныйИндекс);
КонецЦикла;
Проанализируем производительность предыдущего метода. Метод Удалить() для массива в 1С заставляет систему сдвигать все элементы, стоящие после удаленного. На маленьких массивах это незаметно, но на тысячах элементов это станет проблемой. Профессиональный подход — использование алгоритма Тасования Фишера — Йетса.
Суть алгоритма заключается в том, что мы не удаляем элементы, а меняем их местами. Мы идем с конца массива к началу и меняем текущий элемент со случайным элементом, стоящим перед ним. Это работает максимально быстро (сложность O(n)).
МассивДанных = Новый Массив;
Для и = 0 По 100 Цикл
МассивДанных.Добавить(и);
КонецЦикла;
ГСЧ = Новый ГенераторСлучайныхЧисел;
н = МассивДанных.Количество();
Пока н > 1 Цикл
н = н - 1;
к = ГСЧ.СлучайноеЧисло(0, н);
// Меняем местами элементы
ВременнаяПеременная = МассивДанных[к];
МассивДанных[к] = МассивДанных[н];
МассивДанных[н] = ВременнаяПеременная;
КонецЦикла;
Посмотрим на альтернативные варианты, предложенные экспертами. Если нам нужно просто быстро проверить уникальность при генерации большого объема данных, объект Соответствие работает быстрее, чем поиск в массиве через Найти(), так как использует хеш-таблицы.
УникальныеДанные = Новый Соответствие;
ГСЧ = Новый ГенераторСлучайныхЧисел;
Пока УникальныеДанные.Количество() < 10 Цикл
Число = ГСЧ.СлучайноеЧисло(1, 1000);
УникальныеДанные.Вставить(Число, Истина); // Дубли просто перезапишут ключ
КонецЦикла;
Также можно использовать ТаблицаЗначений. Мы можем добавить все сгенерированные числа в колонку таблицы, а затем вызвать метод Свернуть("ИмяКолонки"). Платформа сама удалит все повторяющиеся строки, оставив только уникальные значения.
Разберем важные нюансы, которые помогут избежать ошибок в крупных проектах:
Новый ГенераторСлучайныхЧисел(НачальноеЧисло) позволяет задать конкретное число для старта. Если вы укажете, например, 123, то при каждом запуске программы последовательность "случайных" чисел будет абсолютно одинаковой. Это критически важно для отладки алгоритмов или создания повторяемых тестов.RAND(). Если вам нужно выбрать случайные записи из базы данных, рекомендуется сначала выгрузить идентификаторы нужных строк в таблицу значений, добавить колонку со случайным числом, сгенерированным в коде 1С, и затем загрузить это обратно во временную таблицу для сортировки в запросе.Новый УникальныйИдентификатор(). Это гораздо надежнее, чем пытаться сгенерировать уникальное числовое ID через ГСЧ.СлучайноеЧисло() работает с целыми числами. Если вам нужно случайное дробное число от 0 до 1, вы можете сгенерировать большое целое число и разделить его на его максимально возможный предел.Подведем итог: для простой генерации небольшого списка уникальных чисел подойдет проверка через Найти() или использование Соответствия. Если же ваша задача — перемешать существующий набор данных (например, порядок вопросов в тесте или очередность обработки документов), используйте алгоритм перемешивания массива с обменом элементов.