Структуры данных
Строки, динамические массивы, очереди и ассоциативные контейнеры
Строки
Строка удобна тем, что управляет хранением последовательности и предоставляет готовые операции: узнать длину, добавить символ, получить подстроку. Но каждая операция имеет свои условия. Если строка содержит три символа, её существующие элементы имеют индексы 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
#include <cstddef>
#include <iostream>
#include <string>
int main() {
std::string text = "Cat";
for (std::size_t i = 0; i < text.size(); ++i) {
char symbol = text[i];
unsigned int code = static_cast<unsigned char>(symbol);
std::cout << i << ": " << symbol << " -> " << code << '\n';
}
}#include <iostream>
#include <string>
int main() {
std::string text = "C++17";
int digits = 0;
for (char symbol : text) {
std::cout << symbol << ' ';
if (symbol >= '0' && symbol <= '9') {
++digits;
}
}
std::cout << "\ndigits: " << digits << '\n';
}#include <iostream>
#include <stdexcept>
#include <string>
int main() {
std::string text = "C++";
std::cout << text.at(0) << '\n';
try {
std::cout << text.at(10) << '\n';
} catch (const std::out_of_range&) {
std::cout << "Index outside string\n";
}
}#include <iostream>
#include <string>
int main() {
std::string text = "hello world";
for (char& symbol : text) {
if (symbol == ' ') {
symbol = '_';
}
}
std::cout << text << '\n';
}Повседневные операции
| Операция | Что делает | Особенность |
|---|---|---|
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() | Очищает / меняет длину | Не обязательно освобождает ёмкость |
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 | Символ | Значение |
|---|---|---|---|
| 0 | 0x00 | NUL · \0 | Нулевой символ; завершение C-строки |
| 1 | 0x01 | SOH | Начало заголовка |
| 2 | 0x02 | STX | Начало текста |
| 3 | 0x03 | ETX | Конец текста |
| 4 | 0x04 | EOT | Конец передачи |
| 5 | 0x05 | ENQ | Запрос |
| 6 | 0x06 | ACK | Подтверждение |
| 7 | 0x07 | BEL · \a | Звуковой сигнал |
| 8 | 0x08 | BS · \b | Возврат на позицию |
| 9 | 0x09 | HT · \t | Горизонтальная табуляция |
| 10 | 0x0A | LF · \n | Перевод строки |
| 11 | 0x0B | VT · \v | Вертикальная табуляция |
| 12 | 0x0C | FF · \f | Перевод страницы |
| 13 | 0x0D | CR · \r | Возврат каретки |
| 14 | 0x0E | SO | Переключение набора символов |
| 15 | 0x0F | SI | Возврат к основному набору символов |
| 16 | 0x10 | DLE | Экранирование данных |
| 17 | 0x11 | DC1 | Управление устройством 1 |
| 18 | 0x12 | DC2 | Управление устройством 2 |
| 19 | 0x13 | DC3 | Управление устройством 3 |
| 20 | 0x14 | DC4 | Управление устройством 4 |
| 21 | 0x15 | NAK | Отрицательное подтверждение |
| 22 | 0x16 | SYN | Синхронизация |
| 23 | 0x17 | ETB | Конец блока передачи |
| 24 | 0x18 | CAN | Отмена |
| 25 | 0x19 | EM | Конец носителя |
| 26 | 0x1A | SUB | Замена |
| 27 | 0x1B | ESC | Escape |
| 28 | 0x1C | FS | Разделитель файлов |
| 29 | 0x1D | GS | Разделитель групп |
| 30 | 0x1E | RS | Разделитель записей |
| 31 | 0x1F | US | Разделитель полей |
| 32 | 0x20 | SP | Пробел |
| 33 | 0x21 | ! | Восклицательный знак |
| 34 | 0x22 | " | Двойная кавычка |
| 35 | 0x23 | # | Решётка |
| 36 | 0x24 | $ | Знак доллара |
| 37 | 0x25 | % | Процент |
| 38 | 0x26 | & | Амперсанд |
| 39 | 0x27 | ' | Одинарная кавычка |
| 40 | 0x28 | ( | Открывающая круглая скобка |
| 41 | 0x29 | ) | Закрывающая круглая скобка |
| 42 | 0x2A | * | Звёздочка |
| 43 | 0x2B | + | Плюс |
| 44 | 0x2C | , | Запятая |
| 45 | 0x2D | - | Дефис / минус |
| 46 | 0x2E | . | Точка |
| 47 | 0x2F | / | Прямая косая черта |
| 48 | 0x30 | 0 | Цифра 0 |
| 49 | 0x31 | 1 | Цифра 1 |
| 50 | 0x32 | 2 | Цифра 2 |
| 51 | 0x33 | 3 | Цифра 3 |
| 52 | 0x34 | 4 | Цифра 4 |
| 53 | 0x35 | 5 | Цифра 5 |
| 54 | 0x36 | 6 | Цифра 6 |
| 55 | 0x37 | 7 | Цифра 7 |
| 56 | 0x38 | 8 | Цифра 8 |
| 57 | 0x39 | 9 | Цифра 9 |
| 58 | 0x3A | : | Двоеточие |
| 59 | 0x3B | ; | Точка с запятой |
| 60 | 0x3C | < | Меньше |
| 61 | 0x3D | = | Равно |
| 62 | 0x3E | > | Больше |
| 63 | 0x3F | ? | Вопросительный знак |
| 64 | 0x40 | @ | Знак @ |
| 65 | 0x41 | A | Латинская заглавная A |
| 66 | 0x42 | B | Латинская заглавная B |
| 67 | 0x43 | C | Латинская заглавная C |
| 68 | 0x44 | D | Латинская заглавная D |
| 69 | 0x45 | E | Латинская заглавная E |
| 70 | 0x46 | F | Латинская заглавная F |
| 71 | 0x47 | G | Латинская заглавная G |
| 72 | 0x48 | H | Латинская заглавная H |
| 73 | 0x49 | I | Латинская заглавная I |
| 74 | 0x4A | J | Латинская заглавная J |
| 75 | 0x4B | K | Латинская заглавная K |
| 76 | 0x4C | L | Латинская заглавная L |
| 77 | 0x4D | M | Латинская заглавная M |
| 78 | 0x4E | N | Латинская заглавная N |
| 79 | 0x4F | O | Латинская заглавная O |
| 80 | 0x50 | P | Латинская заглавная P |
| 81 | 0x51 | Q | Латинская заглавная Q |
| 82 | 0x52 | R | Латинская заглавная R |
| 83 | 0x53 | S | Латинская заглавная S |
| 84 | 0x54 | T | Латинская заглавная T |
| 85 | 0x55 | U | Латинская заглавная U |
| 86 | 0x56 | V | Латинская заглавная V |
| 87 | 0x57 | W | Латинская заглавная W |
| 88 | 0x58 | X | Латинская заглавная X |
| 89 | 0x59 | Y | Латинская заглавная Y |
| 90 | 0x5A | Z | Латинская заглавная Z |
| 91 | 0x5B | [ | Открывающая квадратная скобка |
| 92 | 0x5C | \ | Обратная косая черта |
| 93 | 0x5D | ] | Закрывающая квадратная скобка |
| 94 | 0x5E | ^ | Знак ^ |
| 95 | 0x5F | _ | Подчёркивание |
| 96 | 0x60 | ` | Обратная кавычка |
| 97 | 0x61 | a | Латинская строчная a |
| 98 | 0x62 | b | Латинская строчная b |
| 99 | 0x63 | c | Латинская строчная c |
| 100 | 0x64 | d | Латинская строчная d |
| 101 | 0x65 | e | Латинская строчная e |
| 102 | 0x66 | f | Латинская строчная f |
| 103 | 0x67 | g | Латинская строчная g |
| 104 | 0x68 | h | Латинская строчная h |
| 105 | 0x69 | i | Латинская строчная i |
| 106 | 0x6A | j | Латинская строчная j |
| 107 | 0x6B | k | Латинская строчная k |
| 108 | 0x6C | l | Латинская строчная l |
| 109 | 0x6D | m | Латинская строчная m |
| 110 | 0x6E | n | Латинская строчная n |
| 111 | 0x6F | o | Латинская строчная o |
| 112 | 0x70 | p | Латинская строчная p |
| 113 | 0x71 | q | Латинская строчная q |
| 114 | 0x72 | r | Латинская строчная r |
| 115 | 0x73 | s | Латинская строчная s |
| 116 | 0x74 | t | Латинская строчная t |
| 117 | 0x75 | u | Латинская строчная u |
| 118 | 0x76 | v | Латинская строчная v |
| 119 | 0x77 | w | Латинская строчная w |
| 120 | 0x78 | x | Латинская строчная x |
| 121 | 0x79 | y | Латинская строчная y |
| 122 | 0x7A | z | Латинская строчная z |
| 123 | 0x7B | { | Открывающая фигурная скобка |
| 124 | 0x7C | | | Вертикальная черта |
| 125 | 0x7D | } | Закрывающая фигурная скобка |
| 126 | 0x7E | ~ | Тильда |
| 127 | 0x7F | DEL | Удаление (управляющий код) |
Чтобы получить неотрицательное значение байта, сначала преобразуй 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") | Вставка происходит перед выбранной позицией. |
#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';
}Задание: замените слово cat в строке на dog. Проверьте начало, конец и отсутствие слова. Чтение строки с пробелами выполняйте через getline; после числового ввода учитывайте оставшийся перевод строки.
Динамические массивы
Вектор хранит последовательность значений. Начните с размера, допустимого индекса, добавления и обхода элементов. Проверяйте границы до обращения через квадратные скобки.
Когда значений несколько, удобно хранить их в одной последовательности. Вектор grades содержит пять оценок. У каждого существующего элемента есть индекс: первый имеет номер ноль, последний — на единицу меньше размера. Метод size сообщает число элементов. Обращение grades[i] позволяет прочитать или изменить конкретную оценку, если индекс допустим. Цикл в примере проходит именно существующие элементы и накапливает их сумму. Начальный ноль важен: до первого шага ни одна оценка ещё не учтена. Для среднего деление должно быть вещественным, поэтому размер явно преобразован к double. Пустая последовательность не имеет среднего: этот случай проверяется до деления. Добавление через push_back создаёт ещё один элемент в конце. Пока нам достаточно этих операций. Запас памяти, перенос буфера и сохранённые адреса разберём после того, как научимся уверенно обрабатывать значения и проверять границы.
#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';
}Выбирай вектор по умолчанию, когда нужен изменяемый массив с быстрым индексированием. Сейчас нужны size(), push_back() и доступ по допустимому индексу. Остальные методы разобраны ниже.
Динамический массив хранит элементы подряд в памяти, расширяя выделенный буфер при необходимости. Доступ по индексу — O(1).
Два разных числа: size() — сколько элементов действительно существует; capacity() — на сколько элементов уже выделено место. 0 ≤ size ≤ capacity.
Размер и ёмкость (size · capacity) — size() считает реально созданные элементы, capacity() — доступные слоты в буфере. reserve() может увеличить второй показатель, не изменив первый.
Представьте вектор с тремя оценками и запасом памяти на десять. Прочитать можно только три уже существующих элемента; остальные семь мест не становятся оценками сами по себе. Поэтому reserve(10) полезен перед будущими добавлениями, а resize(10) нужен, когда программа действительно должна получить десять элементов. После перераспределения памяти старые указатели, ссылки и итераторы на элементы использовать нельзя: адреса элементов могли измениться.
У vector есть размер — количество существующих элементов — и вместимость выделенной памяти. reserve влияет на вместимость, resize создаёт или удаляет элементы. При добавлении значений также важно не сохранять ссылки, которые может сделать недействительными перераспределение памяти.
Размер и запас памяти
Пунктир — только запас ёмкости, а не существующие элементы.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values;
values.reserve(8);
std::cout << values.size() << '\n';
values.resize(3, 7);
std::cout << values.size() << ' ' << values.at(0) << '\n';
}#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{2, 4, 6};
const std::size_t original_size = values.size();
for (std::size_t i = 0; i < original_size; ++i) {
const int doubled = values[i] * 2;
values.push_back(doubled);
}
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}#include <iostream>
#include <vector>
int main() {
std::vector<int> values{10, 20, 30};
if (!values.empty()) {
std::cout << values.front() << ' ' << values.back() << '\n';
values.front() = 11;
values.back() = 33;
}
std::cout << values[1] << ' ' << values.at(1) << '\n';
values.push_back(40);
std::cout << values.size() << ' ' << values.back() << '\n';
values.pop_back();
std::cout << values.size() << ' ' << values.back() << '\n';
}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() запрашивает уменьшение лишней ёмкости, но стандарт не обязывает реализацию выполнять эту просьбу.
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) в среднем. |
У этих структур различаются стоимость доступа и вставки. Двусвязный список — не просто «более быстрый вектор».
Нужно сохранение порядка — выбирай дерево. Не нужен порядок и важен средний быстрый поиск — хеширование.
Обозначения 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 проверяйте, что контейнер не пуст.
#include <iostream>
#include <queue>
#include <stack>
int main() {
std::stack<int> stack;
std::queue<int> queue;
for (int value : {1, 2, 3}) {
stack.push(value);
queue.push(value);
}
while (!stack.empty()) {
std::cout << stack.top();
stack.pop();
}
std::cout << '\n';
while (!queue.empty()) {
std::cout << queue.front();
queue.pop();
}
std::cout << '\n';
}#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> tasks;
for (int priority : {4, 9, 2}) {
tasks.push(priority);
}
while (!tasks.empty()) {
std::cout << tasks.top() << ' ';
tasks.pop();
}
std::cout << '\n';
}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().
При pop() также восстанавливается свойство кучи. Массив хранит дерево компактно без явных указателей.
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УпорядоченКлючи отсортированы. Основа — дерево поиска (обычно красно-чёрное). O(log n).
std::unordered_mapНеупорядоченОбход не гарантирует сортировки. Основа — хеш-таблица, в среднем O(1).
Итератор (iterator) — Объект, с помощью которого можно обратиться к элементу контейнера и переходить к следующему. find() возвращает итератор найденного элемента либо end().
Если нужно считать слова, выражение ++counts[word] удобно: отсутствующему слову сначала создаётся счётчик со значением ноль, а затем он увеличивается. Но при обычной проверке наличия такая вставка может оказаться неожиданной. Метод find позволяет спросить, есть ли ключ, не добавляя его. Возвращённый end() означает «совпадение не найдено» (это специальная позиция за последним элементом, а не найденная запись).
Ассоциативные контейнеры помогают искать по ключу и хранить уникальные значения. map удобен для счётчиков, set — для устранения повторений. В этих примерах порядок обхода определяется сравнением ключей.
#include <iostream>
#include <map>
#include <sstream>
#include <string>
int main() {
std::istringstream input{"red blue red"};
std::map<std::string, int> counts;
std::string word;
while (input >> word) {
++counts[word];
}
for (const auto& [key, count] : counts) {
std::cout << key << ": " << count << '\n';
}
}#include <iostream>
#include <set>
int main() {
std::set<int> values{3, 1, 3, 2};
const auto [position, inserted] = values.insert(2);
std::cout << std::boolalpha << inserted << ' ' << *position << '\n';
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}| Операция | map / set | unordered_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, если нужен доступ с исключением при отсутствии ключа.
std::map<std::string, int> m;
m["abc"] = 1; // создаёт пару
m["abc"] = 5; // обновляет значение
m.size(); // 1
m.erase("abc"); // удаляет пару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) позволяют получить диапазон одинаковых ключей.
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) — все пары с ключом 2std::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).
Не путать: амортизированное O(1) описывает последовательность операций, а O(1) в среднем у хеш-таблицы зависит от характера хеширования и входных данных.
Списки
std::list — двусвязный список, std::forward_list — односвязный. Итераторы этих списков не обеспечивают доступ по индексу за O(1); зато возможны эффективные вставка и удаление при наличии итератора на нужную позицию.
Мини-анализатор C++
Локально распознаёт только несколько типичных ошибок, не является полноценным компилятором или clang-tidy.
Локальный анализатор проверяет лишь несколько характерных ошибок. Он не является Clang, не компилирует код и не заменяет настоящий C++-линтер.
Термины
Никаких требований к поведению программы при недопустимой операции.
Примеры кода
Поиск и замена подстроки
#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';
}Сумма и среднее пяти оценок; пустой набор проверяется до деления.
#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';
}Обсудить лекцию с ИИ
Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.