Загружаем лабораторные…
// C++ · Лекция 04

Структуры данных

Строки, динамические массивы, очереди и ассоциативные контейнеры

Строки

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

Для практики важно различать логическую длину строки, индексы существующих символов и нулевой завершающий символ для совместимости с C. Операции с s[i] и s.at(i) выглядят похоже, но по-разному ведут себя при ошибке.

Строка — изменяемая последовательность символов. Индексация начинается с нуля, а size() возвращает длину без завершающего нуля.

Неопределённое поведение (undefined behavior · UB) — Стандарт не устанавливает, что должна сделать программа в этой ситуации. Например, индексирование s[100] при длине строки 3 — не «гарантированная ошибка», а UB.

Для строки "cat" значение size() равно трём, а последний обычный символ находится по индексу size() - 1. Такое вычисление последнего индекса имеет смысл только для непустой строки. Ещё одна полезная оговорка: std::string хранит элементы типа char. В UTF-8 одна русская буква может занимать несколько таких элементов, поэтому длина в байтах не всегда равна числу видимых букв.

Как пройти по строке через for

Если нужен номер символа, используй обычный for (std::size_t i = 0; i < text.size(); ++i). На каждом шаге text[i] обращается к очередному элементу. Условие именно i < text.size(): завершающий ноль в такой обход не входит. Пустая строка просто даст ноль итераций.

Вернитесь после темы ссылок. Для первого решения используйте text[i].

Когда индексы не нужны, запись for (char symbol : text) короче: цикл по диапазону (range-based for) сам перебирает все элементы. Здесь symbol — копия очередного char. Для изменения исходной строки нужна ссылка: for (char& symbol : text). Во вкладках ниже есть оба вида обхода и пример изменения символов.

Во вкладках можно сравнить обход строки по индексу и цикл по диапазону, увидеть числовые коды char и изменить символы через ссылку. Все примеры обходят только существующие элементы; код ASCII показан для строки с латинскими буквами.

Что происходит при обращении по индексу?Нажимай ячейки

std::string s = "abc"; · size() = 3

s[1] → 'b' (char)
Жёлтая ячейка — завершающий нулевой символ. Пятая ячейка нарисована только для демонстрации выхода за границы.

Повседневные операции

ОперацияЧто делаетОсобенность
s[i]
Обращается к символу
Без проверки границ
s.at(i)
Обращается к символу
Проверяет индекс, бросает исключение
s.size()
Возвращает длину
Тип результата — size_t
s.front(), s.back()
Первый / последний символ
Не вызывать на пустой строке
s += t, s + t
Приписывание / создание конкатенации
+ создаёт результат
push_back(c)
Добавляет символ в конец
Меняет длину
pop_back()
Удаляет последний символ
На пустой строке — UB
clear(), resize()
Очищает / меняет длину
Не обязательно освобождает ёмкость
string.cpp
std::string s = "abc";
s[1] = 'x';          // "axc"
s.push_back('!');    // "axc!"
s.pop_back();        // "axc"
char first = s.front();
char last  = s.back();
std::size_t n = s.size();

Зачем нужен завершающий '\0'? Он позволяет взаимодействовать с C-строками. Для чтения s[s.size()] стандарт разрешает получить '\0', но s.at(s.size()) бросает std::out_of_range, а s[s.size()+1] — выход за границы.

char и таблица ASCII

char хранит одну единицу кодировки размером в один байт C++ (на обычных современных системах это 8 бит). Символ и число — разные способы представить это значение: в ASCII-совместимой кодировке, например UTF-8, 'A' имеет код 65, а '0' — 48, а не ноль. Поэтому std::cout << symbol печатает символ, а преобразование к целому позволяет увидеть его числовой код.

ASCII задаёт 128 кодов: от 0 до 127. Коды 32–126 печатные (включая пробел), остальные — управляющие. Например, '\n' означает перевод строки, а '\0' — нулевой символ. Значения выше 127 зависят от кодировки и не входят в ASCII; для русской буквы в UTF-8 обычно нужны два байта, поэтому один char не всегда равен одной видимой букве.

Таблица ASCII · все 128 кодов
Dec — десятичный код, Hex — шестнадцатеричный. Управляющие символы показаны именами и escape-последовательностями.
DecHexСимволЗначение
00x00NUL · \0Нулевой символ; завершение C-строки
10x01SOHНачало заголовка
20x02STXНачало текста
30x03ETXКонец текста
40x04EOTКонец передачи
50x05ENQЗапрос
60x06ACKПодтверждение
70x07BEL · \aЗвуковой сигнал
80x08BS · \bВозврат на позицию
90x09HT · \tГоризонтальная табуляция
100x0ALF · \nПеревод строки
110x0BVT · \vВертикальная табуляция
120x0CFF · \fПеревод страницы
130x0DCR · \rВозврат каретки
140x0ESOПереключение набора символов
150x0FSIВозврат к основному набору символов
160x10DLEЭкранирование данных
170x11DC1Управление устройством 1
180x12DC2Управление устройством 2
190x13DC3Управление устройством 3
200x14DC4Управление устройством 4
210x15NAKОтрицательное подтверждение
220x16SYNСинхронизация
230x17ETBКонец блока передачи
240x18CANОтмена
250x19EMКонец носителя
260x1ASUBЗамена
270x1BESCEscape
280x1CFSРазделитель файлов
290x1DGSРазделитель групп
300x1ERSРазделитель записей
310x1FUSРазделитель полей
320x20SPПробел
330x21!Восклицательный знак
340x22"Двойная кавычка
350x23#Решётка
360x24$Знак доллара
370x25%Процент
380x26&Амперсанд
390x27'Одинарная кавычка
400x28(Открывающая круглая скобка
410x29)Закрывающая круглая скобка
420x2A*Звёздочка
430x2B+Плюс
440x2C,Запятая
450x2D-Дефис / минус
460x2E.Точка
470x2F/Прямая косая черта
480x300Цифра 0
490x311Цифра 1
500x322Цифра 2
510x333Цифра 3
520x344Цифра 4
530x355Цифра 5
540x366Цифра 6
550x377Цифра 7
560x388Цифра 8
570x399Цифра 9
580x3A:Двоеточие
590x3B;Точка с запятой
600x3C<Меньше
610x3D=Равно
620x3E>Больше
630x3F?Вопросительный знак
640x40@Знак @
650x41AЛатинская заглавная A
660x42BЛатинская заглавная B
670x43CЛатинская заглавная C
680x44DЛатинская заглавная D
690x45EЛатинская заглавная E
700x46FЛатинская заглавная F
710x47GЛатинская заглавная G
720x48HЛатинская заглавная H
730x49IЛатинская заглавная I
740x4AJЛатинская заглавная J
750x4BKЛатинская заглавная K
760x4CLЛатинская заглавная L
770x4DMЛатинская заглавная M
780x4ENЛатинская заглавная N
790x4FOЛатинская заглавная O
800x50PЛатинская заглавная P
810x51QЛатинская заглавная Q
820x52RЛатинская заглавная R
830x53SЛатинская заглавная S
840x54TЛатинская заглавная T
850x55UЛатинская заглавная U
860x56VЛатинская заглавная V
870x57WЛатинская заглавная W
880x58XЛатинская заглавная X
890x59YЛатинская заглавная Y
900x5AZЛатинская заглавная Z
910x5B[Открывающая квадратная скобка
920x5C\Обратная косая черта
930x5D]Закрывающая квадратная скобка
940x5E^Знак ^
950x5F_Подчёркивание
960x60`Обратная кавычка
970x61aЛатинская строчная a
980x62bЛатинская строчная b
990x63cЛатинская строчная c
1000x64dЛатинская строчная d
1010x65eЛатинская строчная e
1020x66fЛатинская строчная f
1030x67gЛатинская строчная g
1040x68hЛатинская строчная h
1050x69iЛатинская строчная i
1060x6AjЛатинская строчная j
1070x6BkЛатинская строчная k
1080x6ClЛатинская строчная l
1090x6DmЛатинская строчная m
1100x6EnЛатинская строчная n
1110x6FoЛатинская строчная o
1120x70pЛатинская строчная p
1130x71qЛатинская строчная q
1140x72rЛатинская строчная r
1150x73sЛатинская строчная s
1160x74tЛатинская строчная t
1170x75uЛатинская строчная u
1180x76vЛатинская строчная v
1190x77wЛатинская строчная w
1200x78xЛатинская строчная x
1210x79yЛатинская строчная y
1220x7AzЛатинская строчная z
1230x7B{Открывающая фигурная скобка
1240x7C|Вертикальная черта
1250x7D}Закрывающая фигурная скобка
1260x7E~Тильда
1270x7FDELУдаление (управляющий код)

Чтобы получить неотрицательное значение байта, сначала преобразуй char к unsigned char, затем к целому типу: unsigned int code = static_cast<unsigned char>(symbol);. Это показано во вкладке «for по индексу» выше. Такое преобразование показывает код байта; декодирование букв Unicode — отдельная задача.

Поиск и преобразование подстроки

ОперацияПримерЧто проверить
find
text.find("two")
Результат не равен std::string::npos перед использованием позиции.
substr
text.substr(position, 3)
Второй аргумент — длина, а не конечный индекс.
erase
text.erase(position, 3)
Удаление сдвигает следующие символы; сохранённые позиции могут измениться.
insert
text.insert(position, "three")
Вставка происходит перед выбранной позицией.
string_operations.cpp
#include <cstddef>
#include <iostream>
#include <string>
int main() {
    std::string text = "one two";
    const std::size_t position = text.find("two");
    if (position != std::string::npos) {
        std::cout << text.substr(position, 3) << '\n';
        text.erase(position, 3);
        text.insert(position, "three");
    }
    std::cout << text << '\n';
}
Сначала проверяем npos; position — позиция, а 3 — длина фрагмента.
Результатtwo one three

Задание: замените слово cat в строке на dog. Проверьте начало, конец и отсутствие слова. Чтение строки с пробелами выполняйте через getline; после числового ввода учитывайте оставшийся перевод строки.

Динамические массивы

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

Когда значений несколько, удобно хранить их в одной последовательности. Вектор grades содержит пять оценок. У каждого существующего элемента есть индекс: первый имеет номер ноль, последний — на единицу меньше размера. Метод size сообщает число элементов. Обращение grades[i] позволяет прочитать или изменить конкретную оценку, если индекс допустим. Цикл в примере проходит именно существующие элементы и накапливает их сумму. Начальный ноль важен: до первого шага ни одна оценка ещё не учтена. Для среднего деление должно быть вещественным, поэтому размер явно преобразован к double. Пустая последовательность не имеет среднего: этот случай проверяется до деления. Добавление через push_back создаёт ещё один элемент в конце. Пока нам достаточно этих операций. Запас памяти, перенос буфера и сохранённые адреса разберём после того, как научимся уверенно обрабатывать значения и проверять границы.

code-guided-grades.cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> grades{5, 4, 3, 5, 4};
    if (grades.empty()) {
        std::cout << "No grades\n";
        return 0;
    }
    int sum = 0;
    for (int grade : grades) {
        sum += grade;
    }
    const double average = sum / static_cast<double>(grades.size());
    std::cout << sum << ' ' << average << '\n';
}
Сумма и среднее пяти оценок; пустой набор проверяется до деления.
Результат21 4.2

Выбирай вектор по умолчанию, когда нужен изменяемый массив с быстрым индексированием. Сейчас нужны size(), push_back() и доступ по допустимому индексу. Остальные методы разобраны ниже.

Динамический массив хранит элементы подряд в памяти, расширяя выделенный буфер при необходимости. Доступ по индексу — O(1).

Два разных числа: size() — сколько элементов действительно существует; capacity() — на сколько элементов уже выделено место. 0 ≤ size ≤ capacity.

Размер и ёмкость (size · capacity) — size() считает реально созданные элементы, capacity() — доступные слоты в буфере. reserve() может увеличить второй показатель, не изменив первый.

Представьте вектор с тремя оценками и запасом памяти на десять. Прочитать можно только три уже существующих элемента; остальные семь мест не становятся оценками сами по себе. Поэтому reserve(10) полезен перед будущими добавлениями, а resize(10) нужен, когда программа действительно должна получить десять элементов. После перераспределения памяти старые указатели, ссылки и итераторы на элементы использовать нельзя: адреса элементов могли измениться.

У vector есть размер — количество существующих элементов — и вместимость выделенной памяти. reserve влияет на вместимость, resize создаёт или удаляет элементы. При добавлении значений также важно не сохранять ссылки, которые может сделать недействительными перераспределение памяти.

Элементы std::vector
3size()
4capacity()
1мест в запасе
20
41
62
·3

Размер и запас памяти

Пунктир — только запас ёмкости, а не существующие элементы.

Три элемента с индексами 0, 1 и 2.
vector.cpp
std::vector<int> v = {1, 2, 3};
v.reserve(10);       // capacity() >= 10, size() == 3
v.push_back(4);      // size() == 4
v.resize(6, 7);     // {1, 2, 3, 4, 7, 7}
v.clear();          // size() == 0
// capacity() может остаться прежней

Методы и обращение к элементам std::vector

В примерах ниже v — объект std::vector<int>. front() возвращает первый элемент, back() — последний, а v[i] позволяет выбрать элемент по индексу. Это обращения к самим элементам (полученное значение можно читать или менять), а не удаление из контейнера.

ОперацияЧто делаетПример и условие
v[i]
Обращается к элементу по индексу без проверки границ.
v[1] = 20; Допустим только индекс i < v.size().
v.at(i)
Обращается к элементу и проверяет индекс.
v.at(1). При i >= v.size() бросает std::out_of_range.
v.front()
Возвращает первый элемент.
v.front() = 10; Сначала проверь !v.empty().
v.back()
Возвращает последний элемент.
v.back() = 30; Сначала проверь !v.empty().
v.empty()
Проверяет, нет ли элементов.
if (!v.empty()) — условие перед чтением первого или последнего элемента.
v.size()
Возвращает количество существующих элементов.
i < v.size() — граница обхода. Тип результата — беззнаковый size_type.
v.capacity()
Возвращает число элементов, которое помещается в текущем буфере без перераспределения.
Может быть больше size(). Запас памяти не даёт права читать несуществующие элементы.
v.push_back(value)
Добавляет элемент в конец; размер увеличивается на один.
v.push_back(42); Может перераспределить память.
v.emplace_back(args...)
Создаёт элемент в конце из переданных аргументов.
v.emplace_back(42); Для простого int результат здесь такой же, как у push_back(42).
v.pop_back()
Удаляет последний элемент, но не возвращает его значение.
if (!v.empty()) v.pop_back(); На пустом векторе вызывать нельзя.
v.insert(pos, value)
Вставляет элемент перед позицией-итератором; следующие элементы сдвигаются.
v.insert(v.begin(), 7); — вставка в начало, в том числе пустого вектора.
v.erase(pos)
Удаляет элемент в позиции-итераторе; следующие элементы сдвигаются.
if (!v.empty()) v.erase(v.begin()); Передавать v.end() как удаляемую позицию нельзя.
v.clear()
Удаляет все элементы: размер становится равен нулю.
Выделенный буфер сохраняется; capacity() не уменьшается.
v.resize(n, value)
Меняет число элементов на n; при росте добавляет копии value.
v.resize(5, 0); При уменьшении лишние элементы уничтожаются. v.resize(5) добавит нулевые int.
v.reserve(n)
При необходимости увеличивает ёмкость, не меняя размер.
v.reserve(100); — запас под будущие добавления, без создания ста элементов.
v.shrink_to_fit()
Запрашивает уменьшение ёмкости до размера.
Реализация может проигнорировать запрос; это не гарантия освобождения памяти.
v.begin(), v.end()
Задают диапазон для обхода: начало и позицию за последним элементом.
for (auto it = v.begin(); it != v.end(); ++it). Разыменовывать end() нельзя.
v.data()
Возвращает указатель на непрерывное хранилище элементов.
Передавай вместе с v.size(). Не освобождай через delete[]: памятью владеет вектор.

Итератор — это позиция в контейнере (его можно передвигать и разыменовывать, как указатель). После перераспределения памяти все прежние указатели, ссылки и итераторы на элементы недействительны. Даже без перераспределения вставка или удаление может сделать недействительными позиции от места изменения и дальше; после push_back прежний end() тоже меняется.

reserve(n) reserve · Если n больше текущей capacity, выделяет достаточно места, но не создаёт новых элементов. Если n меньше либо равно capacity, ничего не делает.

Обеспечивает capacity ≥ n. Размер и существующие элементы не меняются.

resize(n, value) resize · Изменяет количество существующих элементов. При расширении новые элементы создаются; старые сохраняются. При уменьшении лишние уничтожаются.

Обеспечивает size = n. Создаёт новые элементы или удаляет лишние.

✕ Не работает

v.reserve(10);
v[9] = 42; // UB, если size < 10

✓ Правильно

v.resize(10);
v[9] = 42; // элемент существует

Освобождение памяти: clear() удаляет элементы, но обычно оставляет capacity. shrink_to_fit() запрашивает уменьшение лишней ёмкости, но стандарт не обязывает реализацию выполнять эту просьбу.

bounds.cpp
std::vector<int> v = {2, 4, 6};
int x = v[1];       // 4
int y = v.at(1);    // 4
// v.at(3);         // std::out_of_range
// v[3];            // UB: за пределами size()

Как выбирать контейнер

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

Для первых задач достаточно: текст — std::string; изменяемая последовательность — std::vector. Остальные строки таблицы пригодятся после практики обхода и поиска. Не требуется запоминать устройство всех контейнеров на этом занятии.

Вопрос выбора

Сначала определите, какую операцию программа делает чаще всего: обращение по индексу, добавление с конца, поиск по ключу, извлечение максимума или обход в сортированном порядке.

ЗадачаОбычно подойдётПочему
Хранить последовательность и читать по индексу
vector
Непрерывная память, доступ O(1).
Добавлять и удалять элементы с двух концов
deque
Эффективные операции на обоих концах.
Последний пришёл — первый вышел
stack
Интерфейс LIFO.
Первый пришёл — первый вышел
queue
Интерфейс FIFO.
Всегда получать максимальный элемент
priority_queue
Куча с быстрым доступом к вершине.
Хранить уникальные ключи в отсортированном виде
set
Обход по возрастанию.
Связать ключ со значением в отсортированном виде
map
Пары ключ → значение.
Быстро искать по ключу, порядок не нужен
unordered_map
Хеш-таблица, обычно O(1) в среднем.
Последовательные контейнеры
vector↔deque↔list

У этих структур различаются стоимость доступа и вставки. Двусвязный список — не просто «более быстрый вектор».

Ассоциативные контейнеры
дерево↔хеш-таблица

Нужно сохранение порядка — выбирай дерево. Не нужен порядок и важен средний быстрый поиск — хеширование.

Обозначения O(1), O(log n) и O(n) описывают, как растёт объём работы при увеличении числа элементов, а не точное время в секундах. Например, линейный поиск может проверить все записи, тогда как индексирование вектора не требует обходить предыдущие элементы. Для небольшой учебной задачи понятный vector часто удобнее сложной комбинации структур; усложнять хранение стоит под конкретную потребность.

Очереди и другие контейнеры

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

ТипИдеяГлавное отличие
std::deque
Двусторонняя очередь
Быстро добавляет/убирает с обоих концов; индекс O(1)
std::list
Двусвязный список
Нет обычного быстрого индексирования
std::forward_list
Односвязный список
Более узкий интерфейс
std::stack
Стек LIFO
push / pop / top; адаптер над deque по умолчанию
std::queue
Очередь FIFO
push / pop / front / back; адаптер над deque
std::priority_queue
Куча
top — максимальный элемент; по умолчанию использует vector

LIFO / FIFO (stack · queue) — Стек забирает последним добавленный элемент (LIFO), очередь — первым добавленный (FIFO). Эти правила определяют, где удобно применять структуры.

Положите числа 1, 2 и 3 в стек и в очередь. У стека первым доступно 3 через top(), у очереди — 1 через front(). Метод pop() только удаляет этот элемент: если значение понадобится дальше, его следует сохранить до удаления. Перед чтением и удалением проверьте empty(), потому что у пустой структуры нет элемента для такой операции.

Адаптер задаёт порядок доступа к элементам: стек возвращает последний добавленный, очередь — первый, а очередь с приоритетом — наибольший по выбранному сравнению. Перед чтением top или front проверяйте, что контейнер не пуст.

Стек, очередь и приоритетная очередьpush / pop
LIFO: последний вошёл — первый вышел
3
7
4
Выбери тип адаптера и попробуй операции.
adapters.cpp
std::stack<int> s;
s.push(5); s.push(8);
int top = s.top();       // 8
s.pop();

std::queue<int> q;
q.push(5); q.push(8);
int first = q.front();   // 5
q.pop();

Очередь с приоритетом

В обычной очереди порядок определяется временем добавления, а в priority_queue — приоритетом. Стандартная реализация по умолчанию выдаёт наибольшее значение через top().

Куча / heap
push(9)вставить элемент
⟶
Восстановить кучуO(log n)
⟶
top() = 9O(1)

При pop() также восстанавливается свойство кучи. Массив хранит дерево компактно без явных указателей.

priority.cpp
std::priority_queue<int> pq;
pq.push(4); pq.push(9); pq.push(2);
int mx = pq.top();      // 9
pq.pop();               // удалит 9

Словари и множества

При поиске по индексу мы спрашиваем: «Что лежит на третьем месте?». При поиске по ключу вопрос другой: «Какое значение связано с этим именем?». Например, в словаре можно хранить название товара и количество на складе. Ключ определяет, какую запись искать, а значение содержит связанные с ним данные. Множество решает более узкую задачу — хранит сами уникальные ключи без отдельного значения.

Ассоциативный контейнер позволяет искать данные по ключу. map хранит пары ключ → значение, а set — уникальные значения.

std::mapУпорядочен
alice1
bob4
eva7

Ключи отсортированы. Основа — дерево поиска (обычно красно-чёрное). O(log n).

std::unordered_mapНеупорядочен
eva7
alice1
bob4

Обход не гарантирует сортировки. Основа — хеш-таблица, в среднем O(1).

Итератор (iterator) — Объект, с помощью которого можно обратиться к элементу контейнера и переходить к следующему. find() возвращает итератор найденного элемента либо end().

Если нужно считать слова, выражение ++counts[word] удобно: отсутствующему слову сначала создаётся счётчик со значением ноль, а затем он увеличивается. Но при обычной проверке наличия такая вставка может оказаться неожиданной. Метод find позволяет спросить, есть ли ключ, не добавляя его. Возвращённый end() означает «совпадение не найдено» (это специальная позиция за последним элементом, а не найденная запись).

Ассоциативные контейнеры помогают искать по ключу и хранить уникальные значения. map удобен для счётчиков, set — для устранения повторений. В этих примерах порядок обхода определяется сравнением ключей.

Ассоциативные контейнеры в действииКлючи и значения
alice
7
bob
3
eva
2
Каждому уникальному ключу соответствует одно значение.
Операцияmap / setunordered_map / unordered_set
find(k)
O(log n)
O(1) в среднем; O(n) худший
insert(...)
O(log n)
O(1) в среднем; O(n) худший
erase(k)
O(log n)
O(1) в среднем; O(n) худший
operator[]
Только map (не set)
Только unordered_map (не set)
Порядок обхода
По возрастанию ключей
Не гарантирован

Тонкость map: m[key] создаёт элемент с value-initialized значением, если ключа не было. Для поиска без вставки используй find, contains (C++20) или at, если нужен доступ с исключением при отсутствии ключа.

map.cpp
std::map<std::string, int> m;
m["abc"] = 1;       // создаёт пару
m["abc"] = 5;       // обновляет значение
m.size();            // 1
m.erase("abc");      // удаляет пару
set.cpp
std::set<int> s;
s.insert(3);
s.insert(3);     // дубликата не будет
s.size();        // 1
bool found = s.count(3) != 0;
s.erase(3);

А если ключи повторяются?

multimap и multiset разрешают дубликаты. У multimap нет operator[]; lower_bound(k) и upper_bound(k) позволяют получить диапазон одинаковых ключей.

lower_bound / upper_boundИнтервал [first,last)
10
21
22
23
44
lower_bound(2) → индекс 1upper_bound(2) → индекс 4
multimap.cpp
std::multimap<int, char> m;
m.insert({2, 'a'});
m.insert({2, 'b'});
auto first = m.lower_bound(2); // первый >= 2
auto last = m.upper_bound(2);  // первый > 2
// [first, last) — все пары с ключом 2
unordered.cpp
std::unordered_map<int, int> m;
m[10] = 1;
m[3] = 8;
// Обход НЕ гарантирует сортировку по ключам
// find / insert / erase: O(1) в среднем

O(1) в среднем — не гарантия: у хеш-таблиц можно получить много коллизий; тогда операции будут занимать существенно больше времени. Для задач с жёсткими ограничениями времени это иногда важно.

+Дополнительно

Амортизированная сложность

Отдельный vector::push_back иногда переносит все элементы и стоит O(n), но при геометрическом росте буфера серия из n добавлений требует суммарно O(n). Поэтому стоимость одного добавления амортизированно O(1).

Стоимость последовательных push_back()Упрощённая модель
12
🟩 обычная вставка: O(1)🟨 перевыделение: O(n)
27всего условных операций
2.25в среднем на push
Даже с редкими дорогими вставками общая работа за n добавлений растёт линейно: O(n).

Не путать: амортизированное O(1) описывает последовательность операций, а O(1) в среднем у хеш-таблицы зависит от характера хеширования и входных данных.

Списки

Дополнительные последовательные структуры

std::list — двусвязный список, std::forward_list — односвязный. Итераторы этих списков не обеспечивают доступ по индексу за O(1); зато возможны эффективные вставка и удаление при наличии итератора на нужную позицию.

Мини-анализатор C++

Локально распознаёт только несколько типичных ошибок, не является полноценным компилятором или clang-tidy.

Попробуй найти опасные конструкцииЭкспериментальный анализатор

Локальный анализатор проверяет лишь несколько характерных ошибок. Он не является Clang, не компилирует код и не заменяет настоящий C++-линтер.

Термины

Undefined behavior · неопределённое поведение

Никаких требований к поведению программы при недопустимой операции.

v[100] при v.size()==2

Примеры кода

Поиск и замена подстроки

string-cases.cpp
#include <iostream>
#include <string>
int main() {
    std::string text;
    if (!std::getline(std::cin, text)) {
        std::cerr << "Cannot read line\n";
        return 1;
    }
    const auto position = text.find("two");
    if (position == std::string::npos) {
        std::cout << "Not found\n";
        return 0;
    }
    text.replace(position, 3, "three");
    std::cout << text << '\n';
}

Сумма и среднее пяти оценок; пустой набор проверяется до деления.

code-guided-grades.cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> grades{5, 4, 3, 5, 4};
    if (grades.empty()) {
        std::cout << "No grades\n";
        return 0;
    }
    int sum = 0;
    for (int grade : grades) {
        sum += grade;
    }
    const double average = sum / static_cast<double>(grades.size());
    std::cout << sum << ' ' << average << '\n';
}

Обсудить лекцию с ИИ

Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.

Посмотреть промпт