Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте, отфильтровано ли письмо в одну из следующих стандартных папок:

  • Промоакции
  • Оповещения
  • Спам

Список

21 Июн 2023

Список — это структура данных, хранящая элементы последовательно и позволяющая эффективно вставлять и удалять элементы в любом месте.

Односвязный список

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

List_ru/image1.png

У последнего узла в односвязном списке указатель на следующий узел равен нулевому указателю. Сам список — это просто указатель на первый узел.

Пример реализации односвязного списка на языке C можно посмотреть здесь, а на языке C++ — здесь.

Двусвязный список

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

List_ru/image2.png

У первого узла в двусвязном списке указатель на предыдущий узел равен нулевому указателю.

Пример реализации двусвязного списка на языке C можно посмотреть здесь, а на языке C++ — здесь.

Основные операции

Обход списка и доступ к произвольному элементу

Чтобы обойти список, достаточно хранить указатель на текущий узел. Переход на следующий узел осуществляется по хранимому в текущем узле указателю.

List_ru/image3.png

Для обращения к n-ому элементу списка нужно последовательно по указателям на следующий узел обойти первые n узлов. Как результат, доступ к произвольному элементу происходит за O(n).

Поиск

Поиск элемента в списке — это просто обход списка с проверкой каждого узла на наличие целевого элемента. В худшем случае, например, если искомый элемент отсутствует в списке, будет выполнен обход всего списка. В общем случае поиск выполняется за O(n).

Вставка

Вставка в список осуществляется за O(1). Для вставки элемента в список достаточно создать новый узел и обновить значение нескольких указателей.

Для вставки нового узла в односвязный список нужно выполнить следующие шаги:

  • Выбрать узел, после которого следует сделать вставку.
  • Сохранить значение указателя на следующий узел у выбранного.
  • Указатель на следующий узел у выбранного присвоить адресу вставляемого узла.
  • Указатель на следующий узел у вставляемого присвоить в сохранённое значение на шаге 2.

Схематически вставка выглядит так:

List_ru/image4.png

Для вставки нового узла в двусвязный список нужно выполнить следующие шаги:

  • Выбрать узел, перед которым следует сделать вставку.
  • Сохранить значение указателя на предыдущий узел у выбранного.
  • Сохранить значение указателя на следующий узел у узла перед выбранным.
  • Указатель на предыдущий узел у выбранного присвоить адресу нового узла.
  • Указатель на следующий узел у узла перед выбранным присвоить адресу нового узла.
  • Указатель на предыдущий узел у вставляемого присвоить значению, сохранённому на шаге 2.
  • Указатель на следующий узел у вставляемого присвоить значению, сохранённому на шаге 3.

Схематически вставка выглядит так:

List_ru/image5.png

Удаление

Удаление из списка выполняется за O(1). Для удаления узла из списка также достаточно обновить несколько указателей.

Для удаления узла в односвязном списке нужно выполнить следующие шаги:

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

Схематически удаление выглядит так:

List_ru/image6.png

Для удаления узла в двусвязном списке нужно выполнить следующие шаги:

  • Выбрать узел, который нужно удалить.
  • Сохранить значение указателя на предыдущий узел у удаляемого узла.
  • Сохранить значение указателя на следующий узел у удаляемого узла.
  • Указатель на предыдущий узел у узла после выбранного присвоить адресу узла, сохранённому на шаге 2.
  • Указатель на следующий узел у узла перед выбранным присвоить адресу узла, сохранённому на шаге 3.
  • Удалить узел и освободить память.

Схематически удаление выглядит так:

List_ru/image7.png

Список VS массив

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

List_ru/image8.png

Чтобы удалить элемент из середины массива, нужно сдвинуть все элементы, стоящие после удаляемого элемента, на одну позицию вперёд:

List_ru/image9.png

Чтобы удалить 100-ый элемент в массиве из 1000 элементов, нужно сделать 900 перемещений. Таким образом, удаление элемента из любой позиции, кроме конца, происходит за O(n). Удаление элемента из конца происходит за O(1).

Не лучше обстоит дело и со вставкой в середину массива. В этом случае все элементы массива после места вставки нужно переместить на одну позицию назад:

List_ru/image10.png

Чтобы вставить элемент на 100-ую позицию в массиве из 1000 элементов, нужно 900 перемещений. Таким образом, вставка элемента в любую позицию, кроме конца, происходит за O(n). Вставка элемента в конец происходит за O(1).

Как мы видим, вставка и удаление в произвольное место массива, кроме конца, неэффективны. В то же время для вставки или удаления элемента в любом месте списка достаточно обновления нескольких указателей (не более четырех). Это означает, что если вам нужно удалять и вставлять элементы в произвольные места вашей последовательности, то хранить её элементы лучше в списке.

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

  • При вставках:
    • Если произошла реаллокация для хранения большего числа элементов, то инвалидируются все итераторы и ссылки на элементы.
    • Иначе они валидны до позиции вставки. Итераторы и ссылки, начиная с позиции вставки, инвалидируются.
  • При удалениях инвалидируются все итераторы и ссылки после удаляемого элемента.

Недостаток списка заключается в медленном доступе к произвольному элементу списка. Если вам нужен 901-ый элемент списка, вам придётся обойти все 900 первых элементов списка. Кроме того, элементы списка, как правило, расположены непоследовательно в виртуальной памяти. Это означает, что каждый переход к следующему узлу будет сопровождаться произвольным обращением к памяти.

Элементы массива расположены непрерывно в виртуальной памяти. Поэтому обращение к произвольному элементу — это одно обращение по адресу, полученному с помощью адресной арифметики: к адресу нулевого элемента прибавляется смещение, равное n * sizeof(Item). Кроме этого, если выполняется последовательный доступ к элементам массива небольшого размера, современные процессоры умеют это распознавать и загружают данные заранее в кэш (cache prefetching). Например, при размере кэш-линии в 64 байта при обращении к элементу массива типа int размером 4 байта процессор загрузит в свой кэш сразу 16 элементов. В случае списков из-за кэш-промахов процессору придётся намного чаще обращаться в виртуальную память, а это значительно медленнее обращений в кэш процессора.

Поэтому, если вам нужен произвольный доступ к элементам последовательности, используйте массив вместо списка.

Краткое сравнение массива и списка выглядит так:

Обращение к произвольному элементу

Вставка нового элемента

Удаление произвольного элемента

Массив

O(1)

O(n) в общем случае

O(1) при вставке в конец

O(n)

O(1) при удалении из конца

Список

O(n)

O(1)

O(1)

Дополнительные ссылки

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form