При каких обстоятельствах полезны связанные списки? - PullRequest
105 голосов
/ 12 марта 2010

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

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

Редактировать: Должен сказать, я впечатлен не только количеством, но и качеством ответов. Я могу принять только один, но есть еще два или три, которые, я бы сказал, стоило бы принять, если бы чего-то лучшего не было. Только пара (особенно та, которую я принимал) указали на ситуации, когда связанный список давал реальное преимущество. Я думаю, что Стив Джессоп заслуживает какого-то почетного упоминания за то, что он придумал не один, а три разных ответа, и я нашел их весьма впечатляющими. Конечно, несмотря на то, что он был опубликован только как комментарий, а не как ответ, я думаю, что запись в блоге Нила также стоит прочитать - не только информативную, но и довольно интересную.

Ответы [ 15 ]

2 голосов
/ 04 августа 2010

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

1 голос
/ 09 января 2018

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

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

В результате давайте возьмем случай, когда мы хотим сделать аналогичный эквивалент хранения последовательности переменной длины, которая содержит миллион вложенных подпоследовательностей переменной длины. Конкретным примером является индексированная сетка, хранящая миллион полигонов (некоторые треугольники, некоторые четырехугольники, некоторые пятиугольники, некоторые шестиугольники и т. Д.), И иногда многоугольники удаляются из любой точки сетки, а иногда многоугольники перестраиваются для вставки вершины в существующий многоугольник или удалить один. В этом случае, если мы сохраним миллион крошечных std::vectors, мы столкнемся с выделением кучи для каждого отдельного вектора, а также с потенциально взрывоопасным использованием памяти. Миллион крошечных SmallVectors может не так сильно страдать от этой проблемы в обычных случаях, но тогда их предварительно выделенный буфер, который не выделяется отдельно в куче, может все еще вызывать взрывное использование памяти.

Проблема здесь в том, что миллион std::vector экземпляров будет пытаться хранить миллион вещей переменной длины. Вещи переменной длины, как правило, нуждаются в распределении кучи, поскольку они не могут быть очень эффективно сохранены непрерывно и удалены в постоянном времени (по крайней мере, простым способом без очень сложного распределителя), если они не сохранили свое содержимое в другом месте в куче.

Если вместо этого мы сделаем это:

struct FaceVertex
{
    // Points to next vertex in polygon or -1
    // if we're at the end of the polygon.
    int next;
    ...
};

struct Polygon
{
     // Points to first vertex in polygon.
    int first_vertex;
    ...
};

struct Mesh
{
    // Stores all the face vertices for all polygons.
    std::vector<FaceVertex> fvs;

    // Stores all the polygons.
    std::vector<Polygon> polys;
};

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

Вот еще один пример:

enter image description here

... где ячейки сетки используются для ускорения столкновения частица-частица, скажем, для 16 миллионов частиц, движущихся в каждом отдельном кадре. В этом примере сетки частиц, используя связанные списки, мы можем переместить частицу из одной ячейки сетки в другую, просто изменив 3 индекса. Стирание из одного вектора и возвращение к другому может быть значительно более дорогостоящим и приводить к большему выделению кучи. Связанные списки также уменьшают объем памяти ячейки до 32 бит. Вектор, в зависимости от реализации, может предварительно выделить свой динамический массив так, что он может занять 32 байта для пустого вектора. Если у нас около миллиона ячеек сетки, это большая разница.

... и именно здесь я нахожу связанные списки наиболее полезными в наши дни, и я специально нахожу разнообразие «индексированных связанных списков» полезным, поскольку 32-разрядные индексы вдвое уменьшают требования к памяти для ссылок на 64-разрядных компьютерах, и они подразумевает, что узлы хранятся в массиве непрерывно.

Часто я также комбинирую их с индексированными свободными списками, чтобы разрешить постоянное удаление и вставку в любом месте:

enter image description here

В этом случае индекс next указывает либо на следующий свободный индекс, если узел был удален, либо на следующий использованный индекс, если узел не был удален.

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

1 голос
/ 20 сентября 2016

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

Пример, который приходит на ум, может быть, если вы будете моделировать подвесную цепь. Если вы хотите узнать, какое напряжение имеет какая-либо конкретная ссылка, ваш интерфейс может включать в себя метод получения «кажущегося» веса. Реализация которого будет включать ссылку, запрашивающую следующую ссылку для ее кажущегося веса, а затем добавляющую собственный вес к результату. Таким образом, вся длина до дна будет оцениваться с помощью одного вызова клиента цепочки.

Будучи сторонником кода, который читается как естественный язык, мне нравится, как это позволяет программисту спрашивать звено цепи, какой вес он несет. Кроме того, сохраняется необходимость вычисления этих дочерних свойств в границах реализации ссылки, что устраняет необходимость в услуге вычисления веса цепи ".

0 голосов
/ 11 ноября 2014

Есть две дополняющие операции, которые тривиально O (1) в списках и которые очень сложно реализовать в O (1) в других структурах данных - удаление и вставка элемента из произвольной позиции, при условии, что вам нужно поддерживать порядок элементов ,

Хэш-карты, очевидно, могут выполнять вставку и удаление в O (1), но тогда вы не можете перебирать элементы по порядку.

Учитывая вышеизложенный факт, хеш-карту можно объединить со связанным списком для создания изящного кэша LRU: карта, в которой хранится фиксированное количество пар ключ-значение и отбрасывается наименее недавно полученный ключ, чтобы освободить место для новых.

Записи в хэш-карте должны иметь указатели на узлы связанного списка. При доступе к хэш-карте узел связанного списка отсоединяется от своей текущей позиции и перемещается в начало списка (O (1), ура для связанных списков!). Когда необходимо удалить наименее недавно использованный элемент, элемент из хвоста списка должен быть удален (снова O (1) при условии, что вы сохраняете указатель на хвостовой узел) вместе со связанной записью хэш-карты (поэтому обратные ссылки из необходим список к хеш-карте.)

0 голосов
/ 12 марта 2010

Я использовал связанные списки (даже дважды связанные списки) в прошлом в приложении C / C ++. Это было до .NET и даже STL.

Возможно, я бы сейчас не использовал связанный список на языке .NET, потому что весь необходимый вам код обхода предоставляется для вас с помощью методов расширения Linq.

...