1. Оценка сложности (Big O), массивы и строки: базовые техники манипуляции данными
Оценка сложности (Big O), массивы и строки: базовые техники манипуляции данными
Алгоритмическое собеседование в Яндекс — это не просто проверка знания синтаксиса языка программирования. Это тест на способность писать эффективный код, который будет работать с огромными объемами данных. Когда вы работаете с миллионами запросов в секунду, разница между эффективным и неэффективным алгоритмом означает разницу между работающим сервисом и упавшим сервером.
Фундаментом для написания такого кода является умение оценивать сложность алгоритмов и глубокое понимание базовых структур данных: массивов и строк.
Асимптотическая сложность (Big O Notation)
Big O (О-большое) — это математическая нотация, которая описывает, как возрастает время выполнения алгоритма или потребление памяти при увеличении размера входных данных. Мы не измеряем время в секундах, так как оно зависит от железа. Мы измеряем количество операций.
!Сравнение скорости роста различных алгоритмических сложностей
Основные классы сложности
Как считать сложность?
Рассмотрим пример кода. Допустим, у нас есть функция, которая суммирует элементы матрицы:
Здесь мы видим два вложенных цикла. Внешний цикл выполняется раз (количество строк), внутренний — раз (количество столбцов). Общее количество операций можно выразить формулой:
Где — общее время выполнения, — константа времени выполнения одной операции сложения, — количество строк, — количество столбцов.
В нотации Big O мы отбрасываем константы и младшие порядки, поэтому сложность будет . Если матрица квадратная (), то сложность составляет .
Пространственная сложность (Space Complexity)
Помимо времени (), важно оценивать память. Если ваш алгоритм создает копию массива длины , его пространственная сложность — . Если вы используете только несколько переменных для хранения промежуточных итогов, сложность — (дополнительная память).
Массивы (Arrays)
Массив — это структура данных, хранящая элементы одного типа в непрерывном блоке памяти. Это ключевое определение: непрерывный блок.
Доступ к памяти
Благодаря непрерывности, доступ к любому элементу массива происходит за . Компьютер вычисляет адрес ячейки памяти по формуле:
Где — искомый адрес в памяти, — начальный адрес массива, — порядковый номер элемента, — размер одного элемента в байтах.
Операции с массивами
* Чтение/Запись по индексу: . Мы просто переходим по адресу. * Поиск элемента (без сортировки): . В худшем случае нужно проверить каждый элемент. * Вставка/Удаление в конец: (амортизированная сложность для динамических массивов). * Вставка/Удаление в середину или начало: . Это дорогая операция, так как требуется сдвинуть все последующие элементы, чтобы освободить или заполнить место.
!Сдвиг элементов массива при удалении элемента из середины
Динамические массивы
В Python (list), Java (ArrayList) и C++ (std::vector) массивы могут менять размер. Как это работает, если память должна быть непрерывной?
Когда место в выделенном блоке заканчивается, создается новый блок памяти (обычно в 1.5 или 2 раза больше), и все старые элементы копируются туда. Копирование занимает , но так как оно происходит редко, средняя (амортизированная) сложность добавления в конец остается .
Строки (Strings)
Строки во многих отношениях похожи на массивы символов, но имеют свои особенности в зависимости от языка программирования.
Неизменяемость (Immutability)
В Python, Java, Go и JavaScript строки неизменяемы. Это значит, что вы не можете просто заменить символ в строке. Любая операция модификации создает новую строку.
Рассмотрим опасный паттерн:
На каждой итерации создается новая строка, копирующая содержимое предыдущей. Сумма длин копируемых строк образует арифметическую прогрессию:
Где — общее количество скопированных символов, — количество итераций. Это приводит к сложности .
Решение: Используйте массив символов или специальные строители строк (StringBuilder в Java, list c последующим .join() в Python, strings.Builder в Go). Это снижает сложность до .
Техника двух указателей (Two Pointers)
Это один из самых популярных паттернов на собеседованиях в Яндекс при работе с массивами и строками. Суть метода заключается в использовании двух индексов (указателей), которые движутся по структуре данных по определенным правилам.
Сценарий 1: Встречное движение
Используется, когда нужно найти пару элементов в отсортированном массиве или проверить симметрию.
Пример: Проверка на палиндром. Нужно проверить, читается ли строка одинаково слева направо и справа налево.
left на начало (0).right на конец (len - 1).while left < right:left вправо, right влево.
* Если не равны — это не палиндром.Сложность: , так как мы проходим каждый элемент один раз. Пространство: .
!Работа метода двух указателей при проверке палиндрома
Сценарий 2: Быстрый и медленный указатели (Черепаха и Заяц)
Используется для поиска циклов или середины списка, а также для операций "in-place" (на месте) в массивах.
Пример: Удаление дубликатов из отсортированного массива. Задача: удалить дубликаты, не используя дополнительную память, вернуть новую длину.
slow указывает на место, куда мы запишем следующий уникальный элемент.fast бежит вперед и ищет уникальные элементы.arr[fast] не равен arr[slow], мы увеличиваем slow и записываем туда arr[fast].Этот алгоритм работает за по времени и по памяти, что является идеальным решением.
Итоги
* Big O позволяет оценить масштабируемость алгоритма. Стремитесь к , или . Избегайте на больших данных. * Массивы обеспечивают мгновенный доступ по индексу, но вставка и удаление из середины требуют . * Строки в большинстве языков неизменяемы. Конкатенация в цикле — частая ошибка, ведущая к . Используйте буферы. * Два указателя — мощная техника для оптимизации задач с до , особенно на отсортированных данных.