Python связанный список O (1) вставить / удалить - PullRequest
7 голосов
/ 28 января 2010

Я ищу связанный список и реализацию связанных алгоритмов для Python. Все, кого я спрашиваю, просто рекомендуют использовать встроенные списки Python, но измерения производительности показывают, что вставка и удаление списков является узким местом для нашего приложения. Реализовать простой связанный список тривиально, но мне интересно, есть ли зрелая библиотека, которая включает некоторые операции, такие как сортировка, слияние, сращивание, поиск, нижняя / верхняя граница и т. Д. *

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

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

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

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

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

Ответы [ 10 ]

10 голосов
/ 28 января 2010

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


Может быть, blist было бы лучше, хотя (из здесь )?

Случаи использования, когда BList немного медленнее, чем список Python следующим образом (O (log n) против O (1)):

  1. Большой список, который никогда не меняет длины.
  2. Большой список, где вставки и удаления только в конце список (LIFO).

С этим отказом от ответственности, Вот некоторые из случаев, когда BLists значительно быстрее, чем встроенный список:

  1. Вставка или удаление из большого списка (O (log n) и O (n))
  2. Взятие больших кусков больших списков (O (log n) против O (n))
  3. Создание мелких копий больших списков (O (1) против O (n))
  4. Изменение больших фрагментов больших списков (O (log n + log k) против O (n + k))
  5. Умножение списка для создания большого разреженного списка (O (log k) против O (Kn)) * * тысяча тридцать-одна

Обратите внимание, что на самом деле оно реализовано в виде дерева B +, что обеспечивает высокую производительность для всех этих операций.

7 голосов
/ 28 января 2010

Списки Python O (1) для операций в конце списка .Если вы будете выполнять все операции вставки в полупоследовательном режиме - по аналогии с C, сохраняя только один указатель на середину списка как своего рода «курсор» - вы можете сэкономить много усилийпросто используя два списка Python.Один список для того, что находится перед курсором, один для того, что после;перемещение курсора включает вытягивание следующего элемента из одного списка и добавление его к другому.Это дает вам произвольную вставку O (1) в месте расположения курсора с гораздо меньшими усилиями и повторным изобретением колеса, чем создание всей новой структуры данных, что позволяет вам повторно использовать множество существующих функций списка.

Для полностью общего случаявпустив несколько ссылок в список, вы, вероятно, застряли в создании какого-либо связанного списка.

Редактировать: Вы серьезно не думаете о том, чтобы на самом деле выполнить «бинарный поиск»в связанном списке, а вы?Бинарный поиск даже не имеет смысла для встроенной структуры данных ...

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

6 голосов
/ 29 января 2010

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

Вот быстрая реализация, поддерживающая обычные вещи: вставка в любое время с постоянной ссылкой на узел, разбиение списка на два списка и вставка списка в середину другого списка (сплайс). Поддерживаются универсальные интерфейсы Python: push, pop, pushleft, popleft, extension, обычная итерация, итерация по срезу (getiter).

Я только что написал это, так что это не подтверждено, но не проверено на производстве; вероятно, есть еще ошибки.

def _ref(obj):
    """
    weakref.ref has a bit of braindamage: you can't take a weakref to None.
    This is a major hassle and a needless limitation; work around it.
    """
    from weakref import ref
    if obj is None:
        class NullRef(object):
            def __call__(self): return None
        return NullRef()
    else:
        return ref(obj)

class _node(object):
    def __init__(self, obj):
        self.obj = obj
        self._next = None
        self._prev = _ref(None)
    def __repr__(self):
        return "node(%s)" % repr(self.obj)

    def __call__(self):
        return self.obj

    @property
    def next(self):
        return self._next

    @property
    def prev(self):
        return self._prev()

# Implementation note: all "_last" and "prev" links are weakrefs, to prevent circular references.
# This is important; if we don't do this, every list will be a big circular reference.  This would
# affect collection of the actual objects in the list, not just our node objects.
#
# This means that _node objects can't exist on their own; they must be part of a list, or nodes
# in the list will be collected.  We also have to pay attention to references when we move nodes
# from one list to another.
class llist(object):
    """
    Implements a doubly-linked list.
    """
    def __init__(self, init=None):
        self._first = None
        self._last = _ref(None)
        if init is not None:
            self.extend(init)

    def insert(self, item, node=None):
        """
        Insert item before node.  If node is None, insert at the end of the list.
        Return the node created for item.

        >>> l = llist()
        >>> a = l.insert(1)
        >>> b = l.insert(2)
        >>> d = l.insert(4)
        >>> l._check()
        [1, 2, 4]
        >>> c = l.insert(3, d)
        >>> l._check()
        [1, 2, 3, 4]
        """
        item = _node(item)

        if node is None:
            if self._last() is not None:
                self._last()._next = item
                item._prev = _ref(self._last())
            self._last = _ref(item)
            if self._first is None:
                self._first = item
        else:
            assert self._first is not None, "insertion node must be None when the list is empty"
            if node._prev() is not None:
                node._prev()._next = item
            item._prev = node._prev
            item._next = node
            node._prev = _ref(item)
            if node is self._first:
                self._first = item
        return item

    def remove(self, node):
        """
        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> d = l.append(4)
        >>> e = l.append(5)
        >>> l.remove(c) # Check removing from the middle
        3
        >>> l._check()
        [1, 2, 4, 5]
        >>> l.remove(a) # Check removing from the start
        1
        >>> l._check()
        [2, 4, 5]
        >>> l.remove(e) # Check removing from the end
        5
        >>> l._check()
        [2, 4]
        """
        if self._first is node:
            self._first = node._next
        if self._last() is node:
            self._last = node._prev
        if node._next is not None:
            node._next._prev = node._prev
        if node._prev() is not None:
            node._prev()._next = node._next
        node._next = None
        node._prev = _ref(None)
        return node.obj

    def __nonzero__(self):
        """
        A list is true if it has any elements.

        >>> l = llist()
        >>> bool(l)
        False
        >>> l = llist([1])
        >>> bool(l)
        True
        """
        return self._first is not None


    def __iter__(self):
        """
        >>> l = llist([1,2,3])
        >>> [i() for i in l]
        [1, 2, 3]
        """
        return self.getiter(self._first, self._last())

    def _check(self):
        if self._last() is None:
            assert self._last() is None
            return []
        node = self._first
        ret = []
        while node is not None:
            if node._next is None:
                assert node == self._last()
            if node._prev() is None:
                assert node == self._first
            if node._next is not None:
                assert node._next._prev() == node
            if node._prev() is not None:
                assert node._prev()._next == node
            ret.append(node.obj)
            node = node._next
        return ret

    def getiter(self, first, last):
        """
        Return an iterator over [first,last].

        >>> l = llist()
        >>> l.append(1)
        node(1)
        >>> start = l.append(2)
        >>> l.extend([3,4,5,6])
        >>> end = l.append(7)
        >>> l.extend([8,9])
        >>> [i() for i in l.getiter(start, end)]
        [2, 3, 4, 5, 6, 7]
        """
        class listiter(object):
            def __init__(self, first, last):
                self.node = first
                self.final_node = last

            def __iter__(self): return self
            def next(self):
                ret = self.node
                if ret is None:
                    raise StopIteration
                if ret is self.final_node:
                    self.node = None
                else:
                    self.node = self.node._next
                return ret
        return listiter(first, last)

    def append(self, item):
        """
        Add an item to the end of the list.

        >>> l = llist()
        >>> l.append(1)
        node(1)
        >>> l.append(2)
        node(2)
        >>> l._check()
        [1, 2]
        """
        return self.insert(item, None)

    def appendleft(self, item):
        """
        Add an item to the beginning of the list.

        >>> l = llist()
        >>> l.appendleft(1)
        node(1)
        >>> l.appendleft(2)
        node(2)
        >>> l._check()
        [2, 1]
        """
        return self.insert(item, self._first)

    def pop(self):
        """
        Remove an item from the end of the list and return it.

        >>> l = llist([1,2,3])
        >>> l.pop()
        3
        >>> l.pop()
        2
        >>> l.pop()
        1
        >>> l.pop()
        Traceback (most recent call last):
        ...
        IndexError: pop from empty llist
        """
        if self._last() is None:
            raise IndexError, "pop from empty llist"
        return self.remove(self._last())

    def popleft(self):
        """
        Remove an item from the beginning of the list and return it.

        >>> l = llist([1,2,3])
        >>> l.popleft()
        1
        >>> l.popleft()
        2
        >>> l.popleft()
        3
        >>> l.popleft()
        Traceback (most recent call last):
        ...
        IndexError: popleft from empty llist
        """
        if self._first is None:
            raise IndexError, "popleft from empty llist"
        return self.remove(self._first)

    def splice(self, source, node=None):
        """
        Splice the contents of source into this list before node; if node is None, insert at
        the end.  Empty source_list.  Return the first and last nodes that were moved.

        # Test inserting at the beginning.
        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> l2 = llist([4,5,6])
        >>> l.splice(l2, a)
        (node(4), node(6))
        >>> l._check()
        [4, 5, 6, 1, 2, 3]
        >>> l2._check()
        []

        # Test inserting in the middle.
        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> l2 = llist([4,5,6])
        >>> l.splice(l2, b)
        (node(4), node(6))
        >>> l._check()
        [1, 4, 5, 6, 2, 3]
        >>> l2._check()
        []

        # Test inserting at the end.
        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> l2 = llist([4,5,6])
        >>> l.splice(l2, None)
        (node(4), node(6))
        >>> l._check()
        [1, 2, 3, 4, 5, 6]
        >>> l2._check()
        []

        # Test inserting a list with a single item.
        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> l2 = llist([4])
        >>> l.splice(l2, b)
        (node(4), node(4))
        >>> l._check()
        [1, 4, 2, 3]
        >>> l2._check()
        []
        """
        if source._first is None:
            return
        first = source._first
        last = source._last()

        if node is None:
            if self._last() is not None:
                self._last()._next = source._first
            source._first._prev = self._last
            self._last = source._last
            if self._first is None:
                self._first = source._first
        else:
            source._first._prev = node._prev
            source._last()._next = node
            if node._prev() is not None:
                node._prev()._next = source._first
            node._prev = source._last
            if node is self._first:
                self._first = source._first
        source._first = None
        source._last = _ref(None)
        return first, last

    def split(self, start, end=None):
        """
        Remove all items between [node, end] and return them in a new list.  If end is None,
        remove until the end of the list.

        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> d = l.append(4)
        >>> e = l.append(5)
        >>> l._check()
        [1, 2, 3, 4, 5]
        >>> l2 = l.split(c, e)
        >>> l._check()
        [1, 2]
        >>> l2._check()
        [3, 4, 5]

        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> d = l.append(4)
        >>> e = l.append(5)
        >>> l2 = l.split(a, c)
        >>> l._check()
        [4, 5]
        >>> l2._check()
        [1, 2, 3]

        >>> l = llist()
        >>> a = l.append(1)
        >>> b = l.append(2)
        >>> c = l.append(3)
        >>> d = l.append(4)
        >>> e = l.append(5)
        >>> l2 = l.split(b, d)
        >>> l._check()
        [1, 5]
        >>> l2._check()
        [2, 3, 4]
        """
        if end is None:
            end = self._last()

        ret = llist()

        # First, move the region into the new list.  It's important to do this first, or
        # once we remove the nodes from the old list, they'll be held only by weakrefs and
        # nodes could end up being collected before we put it into the new one.
        ret._first = start
        ret._last = _ref(end)

        # Hook our own nodes back together.
        if start is self._first:
            self._first = end._next
        if end is self._last():
            self._last = start._prev

        if start._prev() is not None:
            start._prev()._next = end._next
        if end._next is not None:
            end._next._prev = start._prev
        start._prev = _ref(None)
        end._next = None

        return ret

    def extend(self, items):
        """
        >>> l = llist()
        >>> l.extend([1,2,3,4,5])
        >>> l._check()
        [1, 2, 3, 4, 5]
        """
        for item in items:
            self.append(item)

if __name__ == "__main__":
    import doctest
    doctest.testmod()
6 голосов
/ 28 января 2010

Здесь есть один связанный список здесь (рецепт 17.14 в Python Cookbook 1-е издание), но он вряд ли "зрелый" или богатый - он просто делает очередь FIFO, поэтому он довольно минимален.

Этот рецепт представляет собой очень лаконичную C-реализацию (только для чтения) Lisp-подобных cons-блоков - просто car, cdr и cons;опять же, не богатый тип, скорее минимальный (и чтобы использовать его для изменяемых данных, в отличие от чисто функциональных подходов, вам нужно было бы по крайней мере добавить setcar и setcdr).Это может быть лучшей отправной точкой для вас просто потому, что cons-ячейки настолько общеизвестно гибки и знакомы.

Некоторые из необходимых вам операций, вероятно, лучше всего будут выполнять существующие примитивы Python.Например, для сортировки трудно понять, как сворачивание вашей собственной сортировки может побить производительность Python sorted(linkedlist) (конечно, если вы сделаете тип linkedlist Python итеративным, чтобы он хорошо работал с остальным языкоми библиотека ;-), учитывая мощь алгоритма timsort, реализованного во время выполнения Python.

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

3 голосов
/ 28 января 2010

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

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

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

редактирование:

Другая подходящая структура данных - это двоичное дерево с нитями . это похоже на обычное двоичное дерево, но каждый конечный узел указывает на следующее / предыдущее поддерево, поэтому его можно итеративно выполнять и так же, как связанный список. Реализация этого в Python оставлена ​​для читателя (или Google) в качестве упражнения.

3 голосов
/ 28 января 2010

Класс Python deque равен 0 (1) для вставки и удаления в начале и конце списка.

1 голос
/ 29 октября 2010

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

Для меня, что такое большие данные? должно быть более 1000000 наименований ...

0 голосов
/ 19 июня 2012

Как насчет использования любой из структур данных, которые обеспечивают сортированный доступ к данным? Бинарный (деревья AVL, AVL, красно-черный), например? Это гарантирует сложность вставки O (log (N)). Не O (1), но лучше, чем у вас есть.

0 голосов
/ 17 июня 2012

У меня недавно была потребность в круговом и двусвязном списке. Так как я очень знаком со связанным списком ядра Linux. Я написал список подражателей в Python. Он обеспечивает O (1) случайную вставку и удаление. Это намного быстрее, чем список Python, когда вы делаете случайную вставку и удаление в большом списке. Код здесь: https://github.com/ningke/Pylnklist. Я также написал небольшое введение здесь: http://710003.blogspot.com/2012/06/copycat-linked-list-in-python.html

0 голосов
/ 30 января 2010

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

Вы можете склеить новый список в свой список, заменив один элемент. Чтобы вставить список [6, 7, 8] в [1, 2, 3, 4, 5] с индексом 2, вы получите

[1, 2, [3, 6, 7, 8], 4, 5]

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

Таким же образом можно «удалить» элемент из списка, заменив его пустым списком.

[1, 2, [], 4, 5]

Перебрать этот смешанный список просто.

def IterateNestedList(xs):
    for x in xs:
        if isinstance(x, list):
            for y in IterateNestedList(x): yield y
        else: yield x
...