Как реализовать алгоритм нумерации строк таблицы значений с заполнением пропущенных номеров?

Программист
← На главную

В процессе разработки на платформе 1С часто возникает задача управления порядковыми номерами строк в таблице значений (ТЗ). Типовая ситуация: пользователь добавляет и удаляет строки, при этом важно сохранять привязку конкретного номера к существующей строке. Если из последовательности 1, 2, 3, 4, 5 была удалена строка №3, то при добавлении новой записи система должна присвоить ей именно номер 3, а не 6. В этой статье мы подробно разберем несколько алгоритмов реализации такого «умного» нумератора — от простых до высокопроизводительных.

Метод 1: Последовательный перебор (простой цикл)

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

Проанализируем пример кода, реализующий этот подход:


Функция ПолучитьНовыйНомерГруппы(ТЗГруппы)
    // Выгружаем колонку, чтобы ускорить поиск, или ищем прямо в ТЗ
    Для Счетчик = 1 По ТЗГруппы.Количество() + 1 Цикл
        НайденнаяСтрока = ТЗГруппы.Найти(Счетчик, "НомерГруппы");
        Если НайденнаяСтрока = Неопределено Тогда
            Возврат Счетчик;
        КонецЕсли;
    КонецЦикла;
    
    Возврат 1; // Если таблица пуста
КонецФункции

Важный нюанс: Обратите внимание, что цикл идет до ТЗГруппы.Количество() + 1. Это необходимо для случая, когда в последовательности нет пропусков — тогда новый номер будет следующим по порядку после максимального существующего.

Несмотря на простоту, у этого метода есть существенный недостаток — производительность. На каждой итерации цикла вызывается метод Найти(), который сам по себе выполняет перебор таблицы. В худшем случае сложность алгоритма составит O(n²), что станет заметно на таблицах с тысячами строк.

Метод 2: Программный стек удаленных номеров (LIFO)

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

Разберем алгоритм по шагам:

  1. Создаем переменную модуля или реквизит формы СтекУдаленныхНомеров (тип Массив).
  2. При удалении строки из основной таблицы значений, мы берем ее номер и добавляем в этот массив: СтекУдаленныхНомеров.Добавить(УдаляемыйНомер).
  3. При добавлении новой строки сначала проверяем наш стек:
    • Если стек не пуст, берем последний добавленный номер (метод Поп), удаляем его из массива и присваиваем новой строке.
    • Если стек пуст, значит «дырок» нет, и мы просто берем МаксимальныйНомер + 1.

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

Метод 3: Сортировка и поиск разрыва (оптимизированный перебор)

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

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


Функция НайтиПервыйСвободныйНомерОптимизировано(ТЗ)
    КопияТЗ = ТЗ.Скопировать();
    КопияТЗ.Сортировать("НомерГруппы Возр");
    
    ОжидаемыйНомер = 1;
    Для Каждого СтрокаТЗ Из КопияТЗ Цикл
        Если СтрокаТЗ.НомерГруппы > ОжидаемыйНомер Тогда
            // Мы нашли дырку!
            Прервать;
        ИначеЕсли СтрокаТЗ.НомерГруппы = ОжидаемыйНомер Тогда
            ОжидаемыйНомер = ОжидаемыйНомер + 1;
        КонецЕсли;
    КонецЦикла;
    
    Возврат ОжидаемыйНомер;
КонецФункции

В данном случае сложность определяется скоростью сортировки (O(n log n)), что значительно быстрее, чем поиск в цикле для каждой строки.

Метод 4: Использование объекта «Соответствие» (Map)

Для тех, кто хочет добиться максимальной скорости поиска без сортировки, отлично подойдет объект Соответствие. Поиск в нем работает на основе хеш-таблиц и практически не зависит от объема данных.

Рассмотрим реализацию:

  1. Создаем объект Соответствие.
  2. Одним циклом обходим ТЗ и добавляем все существующие номера в Ключи соответствия. Значение может быть любым (например, Истина).
  3. Запускаем второй цикл от 1 до бесконечности, проверяя наличие ключа методом Получить().

ХешНомеров = Новый Соответствие;
Для Каждого Стр Из ТЗ Цикл
    ХешНомеров.Вставить(Стр.НомерГруппы, Истина);
КонецЦикла;

СвободныйНомер = 1;
Пока ХешНомеров.Получить(СвободныйНомер) <> Неопределено Цикл
    СвободныйНомер = СвободныйНомер + 1;
КонецЦикла;

Этот метод сочетает в себе простоту кода и высокую скорость работы на больших массивах данных.

Метод 5: Запрос к временной таблице

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

Проанализируем логику запроса:

  1. Помещаем существующие номера во временную таблицу ВТ_СуществующиеНомера.
  2. Нам нужна «эталонная» последовательность чисел от 1 до N. В 1С ее можно получить из любой достаточно большой системной таблицы (например, справочника Номенклатура или ФизическиеЛица), используя функцию АВТОНОМЕРЗАПИСИ() или выбрав поле ССЫЛКА и ограничив количество.
  3. Выполняем ЛЕВОЕ СОЕДИНЕНИЕ таблицы-последовательности с ВТ_СуществующиеНомера.
  4. Где значение из ВТ_СуществующиеНомера равно NULL — там и находится пропущенный номер. Выбираем МИНИМУМ из таких номеров.

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

Рекомендации по выбору решения

Подводя итог, выберем оптимальный вариант в зависимости от условий:

Важное предостережение: Если ваша обработка работает в многопользовательском режиме и записывает данные в общую базу, помните о транзакционных блокировках. Два пользователя могут одновременно вычислить, что номер «7» свободен, и попытаться занять его. В таких случаях лучше доверить нумерацию стандартным механизмам автопрефиксации 1С или использовать объект БлокировкаДанных.

← На главную