Конкатенация строк – базовая операция над строковыми данными, заключающаяся в сцеплении (приписывании) одной строки к другой с образованием новой последовательности символов. Несмотря на кажущуюся простоту, корректная и эффективная конкатенация требует понимания: (1) математических свойств операции, (2) моделей памяти и сложности, (3) особенностей представления строк (байты, кодовые точки Unicode, графемные кластеры), (4) различий реализации в языках программирования. Для подготовки к ЕГЭ по информатике эта тема важна как с точки зрения аккуратной работы с индексами и циклами, так и в плане оценки ресурсоёмкости алгоритмов.
Пусть Σ – алфавит символов, Σ* – множество всех конечных строк над Σ. Для строк A, B ∈ Σ* операция конкатенации определяется как последовательное следование символов:
Определение: A ∘ B – строка, полученная приписыванием B к A.
Длина: len(A ∘ B) = len(A) + len(B).
Нейтральный элемент: пустая строка ε такова, что ε ∘ A = A ∘ ε = A.
Ассоциативность: (A ∘ B) ∘ C = A ∘ (B ∘ C).
Не коммутативна: обычно A ∘ B ≠ B ∘ A.
Вывод: множество Σ* с операцией конкатенации образует моноид (Σ*, ∘, ε).
Практическое следствие для кода: благодаря ассоциативности можно изменять порядок группировки при сцеплении без изменения результата (что полезно для оптимизации), но нельзя менять порядок самих операндов.
Неизменяемые строки
В языках с «иммутабельными» строками (Python, Java, большинство функциональных языков) конкатенация создаёт новую строку:
Время: O(len(A) + len(B)) – нужно скопировать оба фрагмента.
Память: создаётся новый буфер длины len(A)+len(B).
Важное следствие: многократная конкатенация в цикле через + может приводить к квадратичной трудоёмкости из-за повторных копирований:
Если сцеплять k частей средней длины m по одной, то:
Общее время ≈ O(m * (1 + 2 + ... + k)) = O(m * k^2).
Для избежания этого используют накапливание в коллекции и последующее «массовое» соединение (например, ''.join(parts) в Python).
Изменяемые буферы
В языках со строками-буферами (C++ std::string с резервированием, Java StringBuilder) можно заранее выделить ёмкость и дописывать хвост без повторного копирования:
Предвыделение: reserve(total_length) (C++), начальная ёмкость конструктора (Java new StringBuilder(capacity)).
Амортизированная сложность серии дописаний тогда близка к O(total_length).
Формулы оценки
Общее время сцепления списка строк S = [s1, s2, ..., sk] при оптимальном методе:
T ≈ c * (len(s1) + ... + len(sk)), где c – постоянная копирования.
Пиковая память:
При иммутабельных + в цикле: может временно достигать суммы нескольких промежуточных копий.
При буфере с резервированием: ≈ sum(len(si)) + служебные накладные.

Единицы измерения длины
Байты (UTF-8 переменной длины), кодовые точки Unicode (U+XXXX), графемные кластеры (то, что пользователь видит как один символ, напр. «й» = «и» + «◌̆», флаги-эмодзи – пары суррогатов).
Формулы длины применимы к любой метрике, но в реальном коде len() часто возвращает коды или байты в зависимости от языка/типа строки.
Правило 1. Если вы оперируете позициями и подстроками, согласуйте метрику: «длина в символах» ≠ «длина в байтах».
Нормализация Unicode
При конкатенации «канонически эквивалентных» представлений (например, й как один символ и как комбинация) итог может отличаться побайтно. Если важен устойчивый вид (сравнения, хэширование), применяют нормализацию (NFC/NFD) до сцепления.
Правило 2. Для задач сравнения/индексации нормализуйте строки до конкатенации.
Python
Быстрое массовое сцепление: «.join(list_of_parts).
В циклах не делать s += part для больших k; лучше накапливать parts.append(part) и затем join.
Преобразование типов: s + str(x); не смешивать несовместимые типы.
Шаблон:
parts = []
for chunk in source:
parts.append(chunk)
result = «.join(parts)
C++
std::string поддерживает reserve(total), operator+=, append.
Чтобы избежать многократного перераспределения, вызывайте reserve.
Для конвейерной сборки: std::ostringstream (но он медленнее, чем reserve+append в больших объёмах).
Шаблон:
std::string result;
result.reserve(total_len);
for (auto&& s : parts) result += s;
Java
Никогда не конкатенируйте в больших циклах через +; используйте StringBuilder.
Инициализируйте ёмкость: new StringBuilder(totalLen).
Pascal / PascalABC.NET
Оператор + корректен, но в больших циклах используйте буферизацию (массив строк + «склейка» специализированной функцией).
C (массивы char)
Функции strcat/strncat требуют гарантии размера целевого буфера.
Правило безопасности: всегда контролируйте длину: суммарная длина + 1 для '\0'.
В реальных задачах предпочтительнее расчёт длины и memcpy в заранее выделенный буфер.
Правило 3 (безопасность буфера). При сцеплении в массиве char необходимо:
required = len(A) + len(B) + 1; allocate(required); copy(A); append(B); set('\0');.
Предварительный подсчёт длины:
total = sum(len(si) for si in parts)
Затем единственное выделение и копирование.
«Собирай, потом склей»: накапливайте части в структуре, которая не копирует строку при добавлении, затем выполняйте одно «join/append».
Вставка разделителей: вместо ручного res += part + delim используйте функции соединения с разделителем (Python sep.join, Java String.join, C++20 std::format/алгоритмы вывода).
Стриминг: при выводе больших данных сцепляйте прямо в поток вывода (записывайте последовательно без промежуточной огромной строки).
Структуры канатов (rope): для чрезвычайно больших текстов используют деревья кусочков, позволяющие O(log n) вставки/склейки без копирования всего массива (подходит для редакторов/диффов).
Ошибка 1: «квадратичная» конкатенация в цикле через +.
Решение: join/StringBuilder/reserve.
Ошибка 2: смешение типов (строка + число) без приведения.
Решение: явный str(x)/форматирование.
Ошибка 3: переполнение C-буфера при strcat.
Решение: расчёт размера + memcpy / «безопасные» альтернативы.
Ошибка 4: нарушение инвариантов Unicode (разные формы записи одной буквы).
Решение: единая нормализация (NFC) на входе.
Ошибка 5: неверный расчёт разделителей (лишний или отсутствующий).
Решение: «соединяй» списки готовых частей – функции join сами правильно вставят разделитель.
Линейное сканирование и построение строки: аккуратные циклы, индексы, проверка границ.
Оценка сложности: умение показать O(n)/O(n^2) в зависимости от метода сцепления.
Преобразование формата: сборка строки из токенов/чисел с нужным разделителем, без лишнего пробела в конце.
Проверка корректности: длина результата, количество символов, наличие шаблонов (подстроки), сравнение строк после сборки.
Акуратность с Unicode (в теории): различать «длина в символах» и «в байтах».
Правило 1: len(A ∘ B) = len(A) + len(B).
Правило 2: группируйте сцепление так, чтобы минимизировать копирования.
Правило 3: для серии частей используйте «копи-один-раз» (join/StringBuilder/reserve).
Правило 4: перед конкатенацией неоднородных типов – приведение/форматирование.
Правило 5: при работе с C-строками учитывайте завершающий '\0'.
Правило 6: для устойчивых сравнений нормализуйте Unicode (NFC).
Упражнение 1. Ассоциативность и сложность
Дан список строк S = [s1, s2, ..., sk], где len(si) = i для i = 1..k.
a) Оцените суммарную длину L = sum(len(si)).
b) Покажите, почему конкатенация слева направо (((s1 ∘ s2) ∘ s3) ... ∘ sk) при иммутабельных строках даёт трудоёмкость порядка O(k^2).
c) Предложите порядок действий с одной аллокацией и оцените сложность.
Упражнение 2. Разделители и «лишний пробел»
Нужно собрать строку из n чисел a1, a2, ..., an, разделив их одним пробелом, без
пробела в конце.
a) Запишите общий алгоритм без ветвления внутри цикла, используя «массовую склейку».
b) Докажите, что количество пробелов в результате равно n-1.
c) Проверьте на краевых случаях n=0 и n=1.
Упражнение 3. Unicode и нормализация
Строки X и Y визуально одинаковы, но len_bytes(X) ≠ len_bytes(Y) в UTF-8.
a) Объясните, как такое возможно в терминах кодовых точек и комбинирующих знаков.
b) Предложите процедуру normalize_NFC перед конкатенацией, которая гарантирует побайтовую стабильность результата.
c) Покажите, как изменится длина в байтах после сцепления X ∘ Y в зависимости от выбранной формы.
Упражнение 4. Безопасное сцепление в C
Есть два char* с корректной '\0'-терминацией: A и B.
a) Выведите формулу требуемого размера целевого буфера: req = ....
b) Опишите пошаговый алгоритм копирования без использования strcat, только через memcpy.
c) Объясните, почему использование strncat с неверной «остаточной ёмкостью» приводит к обрезанию строки.
Упражнение 5. Оценка памяти и выбор структуры
Имеется k = 10^5 фрагментов средней длины m = 20. Требуется собрать итоговую строку.
a) Оцените общий объём копируемых данных.
b) Сравните по времени и памяти: (1) накапливать s += part, (2) использовать список + join, (3) предвыделить буфер известной длины и копировать блоками.
c) Обоснуйте, какой подход адекватен при ограничении памяти 64 МБ.
Конкатенация строк – не только операция «склеивания», но и дисциплина проектирования строковых вычислений: выбор корректной метрики длины, нормализация, безопасная работа с буферами, оценка сложности и грамотные паттерны сборки. Для задач ЕГЭ это означает умение аккуратно конструировать строки, строго считать длину/число разделителей, выбирать линейные по времени решения и избегать скрытой квадратичности. Освоив приведённые правила и отработав упражнения, вы получите уверенность как в теоретических вопросах, так и в практической реализации строковых алгоритмов.