LINKED STRUCTURES: Part 2

Fred Agbo

2025-09-24

Announcements

  • Welcome back!
  • Mini-Project #1 was graded.
    • Visit me if you have any questions on it
    • My grading style nowadays is more of qualitative than quantitative
  • Week 5 assignment (Problem set 2) was due today but extended until next week Wednesday at 10pm

Inserting and Removing Items at any Position

Inserting at Any Position

  • The case of an insertion at the beginning uses the code presented earlier
  • In the case of an insertion at some other position i,
  • The operation must first find the node at position i − 1 (if i < n) or the node at position n − 1 (if i >= n)
  • There are two cases to consider:
    • That node’s next pointer is None
    • That node’s next pointer is not None

Inserting at Any Position

if head is None or index <= 0:
    head = Node(newItem, head)
else:
    # Search for node at position index - 1 or the last position
    probe = head
    while index > 1 and probe.next != None:
        probe = probe.next
        index -= 1
    # Insert new node after node at position index - 1
    # or last position
probe.next = Node(newItem, probe.next)

Inserting at Any Position

  • Inserting an item between two items in a linked structure

Removing at Any Position

  • The removal of the ith item from a linked structure has three cases:
    • i <= 0—You use the code to remove the first item
    • 0 < i < n—You search for the node at position i – 1, as in insertion, and remove the following node
    • i >= n—You remove the last node

Removing at Any Position

# Assumes that the linked structure has at least one item
if index <= 0 or head.next is None:
    removedItem = head.data
    head = head.next
    return removedItem
else:
    # Search for node at position index - 1 or
    # the next to last position
    probe = head
    while index > 1 and probe.next.next != None:
        probe = probe.next
        index -= 1
    removedItem = probe.next.data
    probe.next = probe.next.next
return removedItem

Removing at Any Position

  • Removing an item between two items in a linked structure

Complexity Trade-Off: Singly Linked Structures

  • The Running Times of Operations on Singly Linked Structures
Operation Running Time
Access (by position) O(n)
Search (by value) O(n)
Insertion at beginning O(1)
Insertion at end O(n)
Insertion at position i O(n)
Removal at beginning O(1)
Removal at end O(n)
Removal at position i O(n)
  • Access/Search: Must traverse from the head to the desired node.
  • Insert/Remove at beginning: Only requires updating the head pointer.
  • Insert/Remove at end or position i: Requires traversal to the appropriate node.

A Circular Linked Structure with a Dummy Header Node

  • The insertion and the removal of the first node are special cases of the insert ith and remove ith operations on singly linked structures:

    • These cases are special because the head pointer must be reset
  • You can simplify these operations by using a circular linked structure with a dummy header node:

  • A circular linked structure with a dummy header node is a variation of a linked list where:

    • The list is circular: the last node points back to the first node (or, more precisely, to the dummy header node).
    • There is a special node at the start called the dummy header node, which does not store user data. It simplifies edge cases (like insertions and deletions at the head or tail).

A Circular Linked Structure with a Dummy Header Node

  • A circular linked structure contains a link from the last node back to the first node in the structure

  • Key Features:

    • The dummy header node’s next pointer always points to the first real node (or to itself if the list is empty).
    • The last real node’s next pointer points back to the dummy header node, making the structure circular.

Relevance of Dommy Header Node

  • Why use it?

    • Simplifies code for insertion and deletion, as you never have to check for None or handle empty list cases separately.
    • Traversal can start at header.next and continue until you reach the header again.
      • This mean head link is always pointed to the header
  • Why keep a tail pointer?

    • The tail pointer allows constant-time (O(1)) access to the last node.
    • Without it, inserting at the end or removing the last node would require traversing the entire list (O(n)).
    • In circular lists with a dummy header, the tail pointer is especially useful for efficient end operations, even though the structure is circular.

A Circular Linked Structure with a Dummy Header Node

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

class CircularLinkedList:
    def __init__(self):
        # Create a dummy header node that points to itself
        self.header = Node()
        self.header.next = self.header
        self.tail = self.header 

    def insert(self, data):
        # Insert new node at the end (before header)
        new_node = Node(data, self.header)
        probe = self.header
        while probe.next != self.header:
            probe = probe.next
        probe.next = new_node
        self.tail = new_node

    def remove(self, data):
        # Remove first node with matching data
        probe = self.header
        while probe.next != self.header and probe.next.data != data:
            probe = probe.next
        if probe.next != self.header:
            to_remove = probe.next
            probe.next = to_remove.next
            # If removing the tail, update tail
            if to_remove == self.tail:
                if probe == self.header:
                    # List is now empty
                    self.tail = self.header
                else:
                    self.tail = probe

    def print_last(self):
        if self.tail == self.header:
            print("List is empty")
        else:
            print(self.tail.data)

            
clist = CircularLinkedList()
clist.insert(10)
clist.insert(20)
clist.insert(30)
clist.insert(50)
clist.insert(60)
clist.remove(60)
clist.print_last()

A Circular Linked Structure

  • Code for insertions at the ith position using the new representation of a linked structure
# Assuming we are using already existing header 
def insert_at(self, index, data):
    # Insert a new node with 'data' at position 'index'
    probe = self.header
    while index > 0 and probe.next != self.header:
        probe = probe.next
        index -= 1
    # Insert new node after probe
    new_node = Node(data, probe.next)
    probe.next = new_node
    # Update tail if inserted at the end
    if new_node.next == self.header:
        self.tail = new_node

Knowledge Check:

  • Suppose you have a circular linked list with a dummy header node and four data nodes containing the values 10, 20, 30, and 40 (in that order). The structure is as follows:

header → 10 → 20 → 30 → 40 → (back to header)

  • If you call the remove(30) method, draw the circular linked structure before and after the removal.

  • Clearly label the header node, all data nodes, and show the direction of all next pointers.

  • Indicate which node is the header and which is the tail after the removal.

Doubly Linked Structures

Doubly Linked Structures

  • A doubly linked structure has the advantages of a singly linked structure
  • In addition, it allows the user to do the following:
    • Move left, to the previous node, from a given node
    • Move immediately to the last node
    • The figure in the next slide shows a doubly linked structure that contains three nodes:

Doubly Linked Structures

  • Note the presence of two pointers, conventionally known as next and previous, in each node

  • Note also the presence of a second external tail pointer that allows direct access to the last node in the structure

    • Tail pointer use is similar to the circular linked structure.

Doubly Linked Structures Implementation

class Node(object):
 
    def __init__(self, data, next = None):
        """Instantiates a Node with default next of None"""
        self.data = data
        self.next = next
 
class TwoWayNode(Node):
 
    def __init__(self, data, previous = None, next = None):
        """Instantiates a TwoWayNode."""
        Node.__init__(self, data, next)
        self.previous = previous

Inserting an Item at the End of a Doubly Linked

Summary: Linked Structures

  • Singly Linked Structures
    • Nodes contain data and a reference to the next node.
    • Efficient insertion/removal at the beginning (O(1)), but O(n) for access, search, or operations at arbitrary positions.
    • Insertion/removal at the end or at position i requires traversal.
  • Circular Linked Structures with Dummy Header
    • Last node points back to the header node.
    • Dummy header simplifies edge cases for insertion/removal.
    • Useful for applications needing circular traversal.
      • Example: Implementing a round-robin scheduler in operating systems.
  • Doubly Linked Structures
    • Nodes have references to both next and previous nodes.
    • Allows bidirectional traversal and efficient insertions/removals at both ends.
    • Slightly more complex due to extra pointers.

Key Takeaways

  • Linked structures provide flexibility for dynamic data management.
  • Choice of structure (singly, circular, doubly) depends on required operations and efficiency.
  • Dummy header nodes simplify boundary cases.
  • Trade-offs exist between operation efficiency and implementation complexity.

Next week Reading:

  • FDS - Lambert
    • Chapters 5 & 6