Есть ли известная реализация индексированного связанного списка?

Моя кишка говорит мне, что нет хорошего способа добиться этого, но, в отличие от г-на Стивена Колберта, я бы предпочел сообщество разработчиков, чем моя кишка...

Существует ли известный способ эффективного внедрения списка "лучший из обоих миров", который обеспечивает произвольный доступ по индексу и O (1) вставке/удалению, как связанный список?

Я предвижу два возможных результата: "Нет, это невозможно по следующим очевидным причинам..." или "Да, это было сделано, см. здесь, здесь и здесь".

Ответы

Ответ 1

Я не верю, что можно будет получить O(1) для вставки и поиска. В тот момент, когда вы добавляете массив (или даже фантазии, расщепляемые векторы), вставка становится O(n).

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

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

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

Решением, которое я когда-то реализовал, был 2D-список. Под этим я имею в виду:

        +-----------+    +-----------+    +-----------+
List -> | count = 7 | -> | Count = 4 | -> | Count = 6 | -> null
        +-----------+    +-----------+    +-----------+
              |                |                |
              V                V                V
        +-----------+    +-----------+    +-----------+
        |    [0]    |    |    [7]    |    |   [11]    |
        +-----------+    +-----------+    +-----------+
              |                |                |
              V                V                V
        +-----------+    +-----------+    +-----------+
        |    [1]    |    |    [8]    |    |   [12]    |
        +-----------+    +-----------+    +-----------+
              |                |                |
              :                :                :
              :                :                :
              |                |                |
              V                V                V
        +-----------+    +-----------+    +-----------+
        |    [6]    |    |   [10]    |    |   [16]    |
        +-----------+    +-----------+    +-----------+
              |                |                |
              V                V                V
             null             null             null

В то время как это сделало как вставку, так и поиск O (n), баланс был прав. В решении с чистым массивом поиск равен O(1), а вставка O(n). Для чистого связанного списка вставка O(1) (как только вы нашли точку вставки, конечно, операцию, которая сама является O(n)), а поиск - O(n).

2D-список O(n) для обоих, но с меньшим коэффициентом. Если вы хотите вставить, вы можете найти нужный столбец, просто просмотрев первую строку каждого столбца. Затем вы пересекаете столбец, который ищет правильный ряд. Затем элемент вставляется и счетчик для этого столбца увеличивается. Аналогично для делеций, хотя в этом случае счетчик уменьшается, а весь столбец удаляется, когда его количество достигает нуля.

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

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

Ответ 3

Когда я реализовал связанный список в классе, я подумал об оптимизации времени доступа, сохранив 3 дополнительных поля: node в середине списка, индекс последнего доступного node и самого последнего доступного node.

Чтобы получить индекс node по индексу, я сначала посмотрел бы на все доступные пути, чтобы достигнуть node по данному индексу, а затем выбрал самый дешевый способ сделать это. Пути были бы просто:

  • Переход от начала к нужному индексу
  • Переход от последнего доступного node к желаемому индексу (вперед)
  • Переход от последнего доступного node к желаемому индексу (назад)
  • Переход от середины node к желаемому индексу (вперед)
  • Переход от середины node к желаемому индексу (назад)
  • Переход от конца node к требуемому индексу

Путь с наименьшей разницей в нашем желаемом индексе и нашем начальном индексе будет самым дешевым вариантом. Если до сих пор не было доступа к node, доступ к последним node может быть установлен как средний node. Конечно, с четным числом элементов нет фактической середины, поэтому я бы просто выбрал пол n/2.

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

Ответ 4

Ваша кишка правильна.

Связанные списки - это вставка/удаление O (1), потому что операция, которую вы выполняете для вставки или удаления, - это просто переключение пары указателей (один или два на объект, который вы вставляете, и один или два на один или два другие объекты). Очевидно, это не изменяется по размеру списка.

Список пропусков даст вам поиск O (logn), но поскольку вы поддерживаете индекс, это также означает вставку/удаление O (logn), потому что этот индекс необходимо обновлять.

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

У вас есть особая проблема в виду, которую вы пытаетесь решить?

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

Ответ 5

Как насчет хэш-таблицы? Вы получаете O (1) случайный доступ по ключу и O (1) вставку/удаление. Уловка состоит в том, что записи неупорядочены.

Для эффективной реализации упорядоченных последовательностей просмотрите деревья пальцев. Они дают вам O (1) доступ к head и last и O (log n) случайному доступу к внутренним узлам. Вставьте или удалите с обоих концов в O (1). Примечательно, что обращение к дереву пальцев занимает постоянное время.

Ответ 6

Я не знаю точной установки BigO при вставке (так как это зависит от размера выборки и роста), но Java java.util.LinkedList сразу придет в голову.

http://java.sun.com/j2se/1.5.0/docs/api/java/util/LinkedList.html

РЕДАКТИРОВАТЬ: да, видимо, под ним все еще есть истинный связанный список, и поскольку такие индексированные данные могут быть O (n/2), что, конечно, формально O (n).

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

Ответ 7

Java LinkedList имеет доступ O (n) для индексированных get. LinkedList extends AbstractSequentialList, чтобы показать, что он не предлагает индексированные значения O (1).

Я предлагаю взглянуть на Apache TreeList. Он предлагает вложения/удаление O (log n) и индексированные запросы O (1).

Ответ 8

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