Почему удаление в одном связанном списке O(1)?

Я не понимаю, почему удаление в конце одного связанного списка происходит в O (1) раз, как статья в Википедии говорит.

один связанный список состоит из узлов. Узел содержит какие-то данные, и ссылку на следующий узел. Ссылка на последний узел в связанном списке имеет значение null.

--------------    --------------           --------------
| data | ref | -> | data | ref | -> ... -> | data | ref |
--------------    --------------           --------------

Я действительно могу удалить последний узел в O (1). Но в этом случае вы не устанавливаете ссылку на новый последний узел, предыдущий один, к null, так как он все еще содержит ссылку на удаленный последний узел. Поэтому мне было интересно, пренебрегают ли они этим при анализе времени выполнения? Или считается, что вам не нужно менять это, так как ссылка, ну, просто указывает на ничто, и такое рассматривается как null?

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

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

8 ответов


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

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

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

надеюсь, что это помогает!


добавление/удаление любой узел любой расположение O (1). Код просто играет с фиксированной стоимостью (несколько вычислений указателей и malloc/frees), чтобы добавить/удалить узел. Эта арифметическая стоимость фиксируется для любого конкретного случая.

однако стоимость достижения(индексирования) нужного узла составляет O (n).

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


например, вы можете иметь указатель на элемент до последнего ("второй от конца") и при удалении: 1. Исключить * следующий элемент" второй от конца". 2. Установите этот "второй от конца" *рядом с NULL


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

ваш "пустой" список начинается с одного sentinel

Head -> [Sentinel]

добавить

Head -> 1 -> 2 -> 3 -> [Sentinel] 

Теперь удалите хвост (3), пометив узел, который был 3 как недопустимый, а затем удалив ссылку на старый sentinel и освободив память для него:

Head -> 1 -> 2 -> 3 -> [Sentinel] 
Head -> 1 -> 2 -> [Sentinel] -> [Sentinel] 
Head -> 1 -> 2 -> [Sentinel]

O (1) просто означает "постоянная стоимость". Это не означает 1 операцию. Это означает, что операции "не более C" фиксируются независимо от изменения других параметров (например, размера списка). На самом деле, в Иногда запутанном мире big-Oh: O(1) == O(22).

напротив, удаление всего списка имеет стоимость O(n), потому что стоимость изменяется с размером (n) списка.


на будущее, я должен сказать, после некоторых исследований я обнаружил, что ни один из аргументов, в ответ на этот вопрос является актуальным. Ответ заключается в том, что мы просто решаем, чтобы верхняя часть стека была главой связанного списка (а не хвостом). Это внесет небольшое изменение в процедуру push, но затем pop и push останутся o(1).


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

M pointer to node to delete
M.data=M.next.data
M.next=M.next.next

в противном случае, если вам нужно искать узел, то вы не можете сделать лучше, чем O(n)


Да, вы можете сделать это за O(1) время, даже если вы не поддерживаете указатель на "предыдущий элемент".

допустим, у вас есть этот список, где "head" и "tail" - статические указатели, а "End" - узел, отмеченный как end. Вы можете использовать" next == NULL " как обычно, но в этом случае вы должны игнорировать значение:

head -> 1 -> 2 -> 3 -> 4 -> 5 -> End <- tail

теперь вам дается указатель на узел 3, но не на его предыдущий узел. у вас тоже есть указатели головы и хвоста. Вот некоторые код python-ish, предполагающий класс SingleLinkedList.

class SingleLinkedList:

    # ... other methods

    def del_node(self, node):  # We "trust" that we're only ever given nodes that are in this list.
        if node is self.head:
            # Simple case of deleting the start
            self.head = node.next
            # Free up 'node', which is automatic in python
        elif node.next is self.tail
            # Simple case of deleting the end. This node becomes the sentinel
            node.value = None
            node.next = None
            # Free up 'self.tail'
            self.tail = node
        else:
            # Other cases, we move the head's value here, and then act
            # as if we're deleting the head.
            node.value = self.head.value
            self.head = self.head.next
            new_head = self.head.next
            # Free up 'self.head'
            self.head = new_head

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