«Время — это единственная вещь, которую все хотят иметь, но которую нельзя купить или продлить» — Харви Маккей
Будет 5 статей по темам:
Последовательные контейнеры(ВЫ ТУТ)
Ассоциативные контейнеры
Стеки и очереди - они же адаптеры
Функциональные объекты
Основные алгоритмы - последние будут иногда проскакивать во всех статьях выше
Наполнение статьи про последовательные контейнеры:
1) VECTOR
2) LIST
3) DEQUE
1) Великий и самый простой - std::vector
Краткое описание: динамический массив с кучей различных функций для упрощения работы с ним
Памятка по функциям:
size() - возвращает текущий размер вектора.
push_back(something) - добавляет новый элемент в конец вектора.
pop_back() - удаляет последний элемент вектора.
clear() - удаляет все элементы из вектора.
Небольшая аннотация: если вектор пустой и вы вызвали clear(), то ничего не произойдет, так как внутри идет проверка на !empty(), про которую написано ниже.
empty() - проверяет, является ли вектор пустым.
at(index) - возвращает элемент на указанной позиции.
Небольшая аннотация: к сожалению она не защитит вас от ситуации с обращением к несуществующему индексу
Аналог: vec[ваш_итератор]
front() - Возвращает первый элемент вектора.
Небольшая аннотация: к сожалению она не защитит вас от ситуации с обращением к несуществующему индексу [0]
back() - Возвращает последний элемент вектора.
Небольшая аннотация: к сожалению она не защитит вас от ситуации с обращением к несуществующему индексу [vector.size() - 1]
Небольшой пример кода:
vector<int> vec = {1, 2, 3, 4, 5};//первый вариант заполнения массива
vec.push_back(1);// добавление элемента в вектор {1, 2, 3, 4, 5, 1}
vec.pop_back();//удаление последнего элемента {1, 2, 3, 4, 5}
vec.front();//первый элемент вектора - 1
vec.back();//последний элемент вектора - 2
for(auto& elem : vec){//первый вариант вывода элементов массива через foreach, а точнее одной из его вариаций
cout<<elem<<endl;
}
for(auto i = 0; i < (int)vec.size(); i++){//второй вариант вывода
cout<<vec[i]<<endl;
}
//тут давайте поговорим про объединение двух векторов, что часто нужно сделать
// объединение == конкатенация
vector<int> vec2 = {6, 7, 8, 9, 10};
vec.insert(vec.end(), vec2.begin(), vec2.end()); // имеем первый вариант заполнения массива
//ИТОГ: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
vec += vec2;//второй вариант(работает на LEETCODE. но не работает на некоторых компиляторах
//ИТОГ: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
/* Обратите внимание*/
vec = vec2; //данный вызов не произведет конкатенации, а vec будет абсолютно равен vec2
vec.clear();//очистка вектора
Список функций валидных для использования с std::vector и помогающие в решении задач:
*max_element(vec.begin(), vec.end()) - вернет наибольший элемент вектора и обратите внимание на (*) - чистый max_element() возвращает итератор и для получения значения - вы можете разыменовать его при желании.
*min_element(vec.begin(), vec.end()) - вернет наименьший элемент вектора и обратите внимание на (*) - чистый min_element() возвращает итератор и для получения значения - вы можете разыменовать его при желании.
std::sort() - используется для сортировки элементов в нужной последовательности про компараторы мы поговорим в самом конце этой статьи.
reserve() - выделит память для вашего вектора, так если вам нужно найти элемент графа с самым большим количеством узлов, то выделять память придется изначально и в принципе лучше выделять ровно столько памяти, сколько потребуется, чтобы выигрывать по времени
Также reserve очень полезен в задачах, где используются n-мерные вектора и соответственно матрицы - без него вам никуда.
2) Великий и без доступа по индексу — std::list
Честно, ни разу не использовал его при решении задач на LeetCode - я говорю именно про std::list, а так в задачах на linked list - да, в принципе сойдет, но вот при работе с какими-то проектами - я использую Qt Creator/С++ и для получения там списка элементов часто используется QList - более расширенный чем std::list, поэтому про него стоит упомянуть.
Кратко: упорядоченный контейнер состоящий из элементов выбранного типа, в котором доступ к элементам происходит через указатели. Индексы в нем отсутствуют, а соответственно и доступа по индексам там нет - главное преимущество перед вектором - это вставка и удаление элементов в начале и конце за константное время, поскольку сдвигать все элементы вектора засчет указателей не приходится.
Еще более быстрое объяснение: двусвязный список или двусторонняя очередь.
Памятка по функциям:
size() - возвращает текущий размер листа.
push_front() - добавляет новый элемент в начало листа.
push_back(something) - добавляет новый элемент в конец листа.
front() - вернет первый элемент листа.
back() - вернет последний элемент листа.
Если в листе будет находиться один элемент, то оба указателя будут указывать на один элемент.
pop_front() - удаляет первый элемент листа.
pop_back() - удаляет последний элемент листа.
clear() - удаляет все элементы из листа.
empty() - проверяет, является ли лист пустым.
merge(list& x) - объединит наши листы, где элементы будут в порядке по возрастанию
reverse() - развернет наш лист и если до этого была вызвана функция merge() или sort() - чистый, то мы получим вектор по убыванию.
remove(some_int) - удалит все элементы со значением some_int
Из минусов: чтобы получить конкретный элемент листа придется с самого начала пробегать весь лист, что в отличии от вектора, где доступ происходит за константное время по индексу - не очень хорошо.
Конечно можно рассказать, что листы придумали для того чтобы не выделять огромные последовательные куски памяти в векторе, поскольку раньше в компьютерах памяти было не так много и вроде памяти сейчас просто море, тогда зачем их используют - константное время на вставку и удаление элементов отвечу я вам. Но давайте лучше поговорим об объединении листов.
Объединение листов
Тут будет разобрано 3 варианта развития событий с вашими листами, поэтому будьте внимательны:
1 вариант - если итератор лежит в диапазоне вашего листа, куда вы хотите прикрепить другой лист, при этом другой лист останется пустым - чем-то напоминает move-конструкторыvoid splice(iterator position, list& x)
;
2 вариант - если итератор лежит в диапазоне вашего листа, а итератор j находится в диапазоне другого листа, который мы присоединяем, причем эта функция работает даже если вы используете один и тот же лист.void splice(iterator position, list& x, iterator j);
3 вариант - самый громоздкий.
position итератор - вашего листа, куда вы присоединяете.
first, last - относятся к другому листу, который присоединяем
Результат: элементы в диапазоне от [first, last] удалятся и вставятся [iterator, iterator + last]
Работает даже если лист один и тот жеvoid splice(iterator position, list& x, iterator first, iterator last);
Пример кода:
list<int> list1 = {1, 2, 3};
list<int> list2 = {4, 5, 6};
list<int>::iterator it = list1.begin() + 2;
list1.splice(it, list2); // Вставляем все элементы из list2 в позицию, указываемую итератором it
3) Лист с итераторами - std::deque
Кратко: упорядоченный контейнер состоящий из элементов выбранного типа, в котором доступ к элементам происходит через указатели. В нем присутствует доступ по индексам.
Вставка и удаление в начале и конце происходит за константое время. Если вставка и удаление происходят в середине происходит гораздо быстрее чем в векторе, но это не аксиома и нужно смотреть на размер сравниваемого вектора.
size() - возвращает текущий размер листа.
push_front() - добавляет новый элемент в начало листа.
push_back(something) - добавляет новый элемент в конец листа.
front() - вернет первый элемент листа.
back() - вернет последний элемент листа.
Если в листе будет находиться один элемент, то оба указателя будут указывать на один элемент.
pop_front() - удаляет первый элемент листа.
pop_back() - удаляет последний элемент листа.
clear() - удаляет все элементы из листа.
empty() - проверяет, является ли лист пустым.
Функции, которых нету в листе
erase(iterator pos) - удаление элемента по индексу
at(some_int) - доступ к элементу по индексу
Пример кода:
deque<int>deq;
// Добавляем элементы в конец дека
deq.push_back(4);
deq.push_back(5);
deq.push_back(6);
// Добавляем элементы в начало дека
deq.push_front(3);
deq.push_front(2);
deq.push_front(1);
// Удаляем первый элемент из дека
deq.pop_front();
//Получаем доступ по индексу к элементу дека
deq.at(0);
//Удаляем 5-ый элемент дека
deq.erase(deq.begin() + 4);
Комментарии (10)
vavp123
21.08.2023 16:48начиная с Qt6 QList == QVector.
До Qt6 QList это хитромудрёный index-based массив. Не припомню чтобы он был схож по функционалу с std::list
Deosis
21.08.2023 16:48QList - более расширенный чем std::list
Только это неправда.
QList ведёт себя как std::vector<T> для объектов небольшого размера (меньше указателя)
или как std::vector<std::unique_ptr<T>> для объектов больше указателя.
Результат: элементы в диапазоне от [first, last] удалятся и вставятся [iterator, iterator + last]
В с++ диапазоны обычно не включают правую границу, в документации указан диапазон [first, last).
А указывать тип в качестве начала диапазона вообще нельзя.
voldemar_d
21.08.2023 16:48Всё-таки есть отличия между at() и оператором [index]:
https://en.cppreference.com/w/cpp/container/vector/at
at() проверяет выход за пределы контейнера и кидает исключение, а оператор [index] этого не делает.
namedictor Автор
21.08.2023 16:48Статью я под LeetCode писал, как там обрабатывать исключение в форме:
std::cout <<
exc.what() << '\n';
. То есть я открыл рандомную задачу и проверял, как там ведут себя функции, если бы он возвращал vec.end(), то это легко обрабатывается, а std::cout - придется использовать streambuff и rdbuf - так можно делать, но неудобно и я так никогда не делал на литкоде.voldemar_d
21.08.2023 16:48Я ничего не знаю про LeetCode. Просто утверждение, что at и [] - аналоги, не совсем верное.
nishen
21.08.2023 16:48если вектор пустой и вы вызвали pop_back(), то ничего не произойдет, так как внутри идет проверка на!empty(), про которую написано ниже
Calling
pop_back
on an empty container results in undefined behavior.namedictor Автор
21.08.2023 16:48Увидел, уберу - скорее всего у меня выдавало UB, но просто ничего значимого для остановки программы не происходило, а загуглить мозгов у меня не хватило.
Deosis
21.08.2023 16:48Компилятор предполагает, что UB при выполнении не происходит.
Можно предположить, что при вызове
pop_back
на векторе, указатель на конец просто уменьшится на размер данных.Если вызвать на пустом векторе, то он окажется в настолько невалидном состоянии, что практически любая операция может сломать программу.
nobrainogain
21.08.2023 16:48Слушайте, разве эти знания действительно нужны человеку, который собирается начать решать литкод? Векторы и деки - это важные вещи, но необходимые их методы гуглятся за несколько секунд. Когда ты сдаешь задачу в систему автопроверки, она считается решенной, если она проходит все тесты. Таким образом, гораздо важнее для новичка будет учиться непосредственно решению задач, придумыванию алгоритмов.
Это как на кодфорсах: новички постоянно спрашивают гроссмейстеров какой у них сетап, каким редактором кода они пользуются, какие есть фишки, какой у них стоит шаблон и как его настроить.
Поймите, чтобы хорошо решать задачи, вам это все (поначалу) не нужно. Даже хитрые алгоритмы не нужны. Просто начните решать, все остальное придет с опытом, или подсмотрите у кого-то в сабмитах и намотаете себе на ус. Будет в тысячу раз полезнее подобных статей.
BesheniyLeshiy
В C++ не шарю, но ради любопытства прочитал.