munotes®

Insertion and Deletion With Two Links

Get access to whole semester resourcesSemester Pass

Chapter Twenty-Seven

Syllabus topic Module 1, "Linked Structures: ADT of doubly linked list, Insertion and deletion of nodes at various positions"

Pages 80 to 83 of 411

In one line

Inserting into a doubly linked list moves four pointers and deleting moves two, and both are O(1) once you hold the node, provided the order keeps every reference alive until it has been copied.

Insertion: four pointers

To insert a new node N after an existing node P, with S being P's current next:

N.prev = P

N.next = S

P.next = N

if S is not null: S.prev = N

Four assignments, and the last is conditional because P may have been the last node.

The rule for the order is the same as chapter 20's, generalised: set the new node's links first, while P and S are both still reachable, then redirect P and S. Redirect first and you lose the address you still need.

Deletion: two pointers, and no search

To delete a node X that you are holding, with P its previous and S its next:

if P is not null: P.next = S else: head = S

if S is not null: S.prev = P else: tail = P

Two assignments, both conditional on the ends. No walk at all. This is the operation the whole structure exists for, and it is the difference from chapter 19, where holding the node was not enough.

Both, run, with the invariant checked after every step

class DNode:
    def __init__(self, data):
        self.prev = None
        self.data = data
        self.next = None


class DList:
    def __init__(self, values=()):
        self.head = self.tail = None
        self.n = 0
        for value in values:
            self.append(value)

    def append(self, value):
        node = DNode(value)
        if self.tail is None:
            self.head = self.tail = node
        else:
            node.prev = self.tail
            self.tail.next = node
            self.tail = node
        self.n += 1
        return node

    def insert_after(self, p, value):
        """Insert a new node after p. Four assignments, in a safe order."""
        n = DNode(value)
        s = p.next
        n.prev = p                      # 1. new node's links first
        n.next = s
        p.next = n                      # 3. then redirect the neighbours
        if s is not None:
            s.prev = n
        else:
            self.tail = n
        self.n += 1
        return n

    def insert_before(self, s, value):
        if s.prev is None:
            n = DNode(value)
            n.next = s
            s.prev = n
            self.head = n
            self.n += 1
            return n
        return self.insert_after(s.prev, value)

    def delete(self, x):
        """Delete a node we are holding. Two assignments, no walk."""
        p, s = x.prev, x.next
        if p is not None:
            p.next = s
        else:
            self.head = s
        if s is not None:
            s.prev = p
        else:
            self.tail = p
        x.prev = x.next = None          # so a stale node cannot be walked
        self.n -= 1

    def forward(self):
        out, walk = [], self.head
        while walk is not None:
            out.append(walk.data)
            walk = walk.next
        return out

    def backward(self):
        out, walk = [], self.tail
        while walk is not None:
            out.append(walk.data)
            walk = walk.prev
        return out

    def check(self):
        if self.head is not None and self.head.prev is not None:
            return "BROKEN: head.prev"
        if self.tail is not None and self.tail.next is not None:
            return "BROKEN: tail.next"
        walk, seen = self.head, 0
        while walk is not None:
            seen += 1
            if walk.next is not None and walk.next.prev is not walk:
                return "BROKEN: chains disagree at %r" % (walk.data,)
            walk = walk.next
        if seen != self.n:
            return "BROKEN: counter %d, chain %d" % (self.n, seen)
        return "ok"


lst = DList(["A", "B", "D"])
print("start          :", lst.forward(), lst.backward(), lst.check())

b = lst.head.next
lst.insert_after(b, "C")
print("insert C after B:", lst.forward(), lst.backward(), lst.check())

lst.insert_after(lst.tail, "E")
print("insert E at end :", lst.forward(), lst.backward(), lst.check())

lst.insert_before(lst.head, "Z")
print("insert Z at head:", lst.forward(), lst.backward(), lst.check())

lst.delete(lst.head)
print("delete the head :", lst.forward(), lst.backward(), lst.check())

lst.delete(lst.tail)
print("delete the tail :", lst.forward(), lst.backward(), lst.check())

middle = lst.head.next
lst.delete(middle)
print("delete a middle :", lst.forward(), lst.backward(), lst.check())

only = DList(["solo"])
only.delete(only.head)
print("delete the only :", only.forward(), only.backward(), only.check(),
      "| head", only.head, "tail", only.tail)
munotes.in80

Insertion and Deletion With Two Links

start          : ['A', 'B', 'D'] ['D', 'B', 'A'] ok
insert C after B: ['A', 'B', 'C', 'D'] ['D', 'C', 'B', 'A'] ok
insert E at end : ['A', 'B', 'C', 'D', 'E'] ['E', 'D', 'C', 'B', 'A'] ok
insert Z at head: ['Z', 'A', 'B', 'C', 'D', 'E'] ['E', 'D', 'C', 'B', 'A', 'Z'] ok
delete the head : ['A', 'B', 'C', 'D', 'E'] ['E', 'D', 'C', 'B', 'A'] ok
delete the tail : ['A', 'B', 'C', 'D'] ['D', 'C', 'B', 'A'] ok
delete a middle : ['A', 'C', 'D'] ['D', 'C', 'A'] ok
delete the only : [] [] ok | head None tail None

Eight operations, and after every one both chains were walked and compared. The forward and backward readings are reverses of each other at every step, which is the invariant holding.

The order that corrupts it, run

class DNode:
    def __init__(self, data):
        self.prev = None
        self.data = data
        self.next = None


def chain(head, cap=8):
    out, walk, n = [], head, 0
    while walk is not None and n < cap:
        out.append(walk.data)
        walk = walk.next
        n += 1
    if walk is not None:
        out.append("... still going")
    return out


a, b = DNode("A"), DNode("B")
a.next, b.prev = b, a
head = a

n = DNode("NEW")
a.next = n                # WRONG: A redirected before NEW recorded what followed
n.next = a.next           # so NEW now points at ITSELF
n.prev = a

print("forward from the head:", chain(head))
print("did B survive? B is still an object:", b.data,
      "| reachable from the head:", "B" in chain(head))
forward from the head: ['A', 'NEW', 'NEW', 'NEW', 'NEW', 'NEW', 'NEW', 'NEW', '... still going']
did B survive? B is still an object: B | reachable from the head: False
munotes.in81

Insertion and Deletion With Two Links

The same failure as chapter 20, and for the same reason: a.next was overwritten while it was still the only record of where B was. B is intact and unreachable, and the list has a cycle.

The rule is worth stating once and keeping: when rewiring pointers, write the new node's links first. Nothing that is still needed may be overwritten before it has been copied.

The costs, stated exactly

OperationSingly linkedDoubly linked
Insert after a held nodeO(1), 2 assignmentsO(1), 4 assignments
Insert before a held nodeO(n): the predecessor must be foundO(1), 4 assignments
Delete a held nodeO(n): the predecessor must be foundO(1), 2 assignments
Delete the last nodeO(n) even with a tailO(1)
Delete by valueO(n) to searchO(n) to search

The last row is the honest one and examiners like it. Deleting by value is O(n) on both, because the search dominates. The doubly linked list wins only when the node is already in hand, which is exactly what happens while traversing, and what a hash table's bucket or a cache's entry gives you.

Quick revision

  • Insertion after p: set the new node's prev and next first, then p.next, then s.prev if s exists,

otherwise move the tail. Four assignments.

  • Deletion of a held node: p.next = s and s.prev = p, each conditional on the end, otherwise move head

or tail. Two assignments, no walk.

  • The order rule: write the new node's links first; never overwrite a reference still needed.
  • Reversing that order gives a self-pointing node, a cycle, and unreachable nodes that are still intact.
  • Deleting a held node is O(1) here and O(n) on a singly linked list.
  • Deleting by value is O(n) on both, because the search dominates.
  • Clear a deleted node's links so a stale reference cannot walk back into the list.

Test yourself

1. Write the four assignments that insert N after P, and say which is conditional. N.prev = P; N.next = S; P.next = N; and S.prev = N only if S exists, otherwise the tail moves to N.

2. Write the deletion of a held node X. If X.prev exists, X.prev.next = X.next, else head = X.next. If X.next exists, X.next.prev = X.prev, else tail = X.prev.

3. State the ordering rule for rewiring pointers, and what happens when it is broken. Set the new node's links first; never overwrite a reference that is still needed. Broken, the new node ends up pointing at itself, the list gains a cycle and the following nodes become unreachable while remaining intact.

4. Deleting a held node is O(1) here and O(n) on a singly linked list. Why? Because deletion must modify the previous node, and the doubly linked node holds its address directly, while the singly linked one can only find it by walking from the head.

munotes.in82

Insertion and Deletion With Two Links

5. Is deleting by value faster on a doubly linked list? No. The search is O(n) on both, and it dominates. The advantage applies only when the node is already in hand.

6. Why does the deletion above set the removed node's links to null? So that a stale reference to the removed node cannot be used to walk back into a list it is no longer part of.

munotes.in83

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.

Issue
Done!