«Время — это единственная вещь, которую все хотят иметь, но которую нельзя купить или продлить» — Харви Маккей

Будет 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)


  1. BesheniyLeshiy
    21.08.2023 16:48

    В C++ не шарю, но ради любопытства прочитал.


  1. vavp123
    21.08.2023 16:48

    начиная с Qt6 QList == QVector.

    До Qt6 QList это хитромудрёный index-based массив. Не припомню чтобы он был схож по функционалу с std::list


    1. Deosis
      21.08.2023 16:48

      QList - более расширенный чем std::list

      Только это неправда.

      QList ведёт себя как std::vector<T> для объектов небольшого размера (меньше указателя)

      или как std::vector<std::unique_ptr<T>> для объектов больше указателя.

      Результат: элементы в диапазоне от [first, last] удалятся и вставятся [iterator, iterator + last]

      В с++ диапазоны обычно не включают правую границу, в документации указан диапазон [first, last).

      А указывать тип в качестве начала диапазона вообще нельзя.


  1. voldemar_d
    21.08.2023 16:48

    Всё-таки есть отличия между at() и оператором [index]:

    https://en.cppreference.com/w/cpp/container/vector/at

    at() проверяет выход за пределы контейнера и кидает исключение, а оператор [index] этого не делает.


    1. namedictor Автор
      21.08.2023 16:48

      Статью я под LeetCode писал, как там обрабатывать исключение в форме: std::cout << exc.what() << '\n';. То есть я открыл рандомную задачу и проверял, как там ведут себя функции, если бы он возвращал vec.end(), то это легко обрабатывается, а std::cout - придется использовать streambuff и rdbuf - так можно делать, но неудобно и я так никогда не делал на литкоде.


      1. voldemar_d
        21.08.2023 16:48

        Я ничего не знаю про LeetCode. Просто утверждение, что at и [] - аналоги, не совсем верное.


  1. nishen
    21.08.2023 16:48

    если вектор пустой и вы вызвали pop_back(), то ничего не произойдет, так как внутри идет проверка на!empty(), про которую написано ниже

    Calling pop_back on an empty container results in undefined behavior.


    1. namedictor Автор
      21.08.2023 16:48

      Увидел, уберу - скорее всего у меня выдавало UB, но просто ничего значимого для остановки программы не происходило, а загуглить мозгов у меня не хватило.


      1. Deosis
        21.08.2023 16:48

        Компилятор предполагает, что UB при выполнении не происходит.

        Можно предположить, что при вызове pop_back на векторе, указатель на конец просто уменьшится на размер данных.

        Если вызвать на пустом векторе, то он окажется в настолько невалидном состоянии, что практически любая операция может сломать программу.


  1. nobrainogain
    21.08.2023 16:48

    Слушайте, разве эти знания действительно нужны человеку, который собирается начать решать литкод? Векторы и деки - это важные вещи, но необходимые их методы гуглятся за несколько секунд. Когда ты сдаешь задачу в систему автопроверки, она считается решенной, если она проходит все тесты. Таким образом, гораздо важнее для новичка будет учиться непосредственно решению задач, придумыванию алгоритмов.

    Это как на кодфорсах: новички постоянно спрашивают гроссмейстеров какой у них сетап, каким редактором кода они пользуются, какие есть фишки, какой у них стоит шаблон и как его настроить.

    Поймите, чтобы хорошо решать задачи, вам это все (поначалу) не нужно. Даже хитрые алгоритмы не нужны. Просто начните решать, все остальное придет с опытом, или подсмотрите у кого-то в сабмитах и намотаете себе на ус. Будет в тысячу раз полезнее подобных статей.