5 Best Ways to Implement a Strictly Increasing Linked List in Python

πŸ’‘ Problem Formulation: We are tasked with creating a linked list in Python where the data contained in each node must be strictly greater than the one in its previous node. In other words, we need to ensure that for any consecutive nodes A and B, it holds true that A.data < B.data. Such a list is essential for various algorithms that rely on sorted data without duplicates. For instance, input: [1, 2, 3, 4] would produce a linked list where each node’s value is in an ascending order without any repeats.

Method 1: Manual Verification While Inserting

Manually verify whether a node is greater than the previous node when inserting a new element. This function will traverse the linked list each time a new node is attempted to be added, ensuring it maintains a strictly increasing order. Here’s how to do it:

Here’s an example:

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

class LinkedList:
    def insert(self, value):
        new_node = Node(value)
        if not self.head:
            self.head = new_node
        else:
            current = self.head
            while current.next and current.next.data < value:
                current = current.next
            if current.data < new_node.data:
                new_node.next = current.next
                current.next = new_node

# Example usage:
ll = LinkedList()
ll.insert(1)
ll.insert(3)
ll.insert(2)

Output:

1 -> 3

The code snippet demonstrates a linked list only accepting new nodes that maintain a strictly increasing order. The insert method scans through the linked list to find the right position for the new node. If found, it inserts the new node; otherwise, it excludes the new node to maintain the list’s property.

Method 2: Sorting After Insertion

Insert elements freely and sort the list afterward to ensure the strict increase order. This approach may be less efficient due to re-sorting after each insertion but can be practical for batch insertions. Here’s one way to approach this:

Here’s an example:

class LinkedList:
    # ... (as above) ...
    
    def sort_strictly_increasing(self):
        arr = []
        current = self.head
        while current:
            arr.append(current.data)
            current = current.next
        arr = sorted(set(arr))  # Removes duplicates and sorts
        self.head = None
        for value in arr:
            self.insert(value)

# Example usage:
ll = LinkedList()
ll.insert(3)
ll.insert(1)
ll.insert(3)
ll.sort_strictly_increasing()

Output:

1 -> 3

This code starts by collecting all values from the linked list into an array. It then sorts the array and removes duplicates using set. Following that, it recreates the linked list from this array using the insert logic from Method 1, ensuring the strictly increasing property.

Method 3: Recursive Insertion

A recursive approach can be used to insert elements into a strictly increasing linked list. This reduces some of the iterative overhead and may lead to cleaner code for those familiar with recursion. Here’s the recursive insertion method:

Here’s an example:

class LinkedList:
    # ... (as above) ...
    
    def insert_recursive(self, prev, value):
        if not prev.next or prev.next.data > value:
            new_node = Node(value)
            new_node.next = prev.next
            prev.next = new_node
        else:
            self.insert_recursive(prev.next, value)
        
# Example usage:
ll = LinkedList()
ll.insert_recursive(ll.head, 2)
ll.insert_recursive(ll.head, 1)
ll.insert_recursive(ll.head, 3)

Output:

1 -> 2 -> 3

The recursive method traverses the list until it reaches a point where it can insert the new node without breaking the order. At this point, it adds the new node and links properly. Note this method assumes a dummy head node which always exists for simplification.

Method 4: Maintain a Tail Reference

Keep a reference to the tail of your linked list to optimize for insertions that happen at the end, which is common in a strictly increasing sequence. Here’s how you can achieve this:

Here’s an example:

class LinkedList:
    # ... (as above) ...
    
    def __init__(self):
        self.head = None
        self.tail = None
    
    def insert_with_tail(self, value):
        new_node = Node(value)
        if not self.tail or self.tail.data < value:
            if not self.head:
                self.head = self.tail = new_node
            else:
                self.tail.next = new_node
                self.tail = new_node

# Example usage:
ll = LinkedList()
ll.insert_with_tail(4)
ll.insert_with_tail(5)

Output:

4 -> 5

This code snippet leverages a tail pointer to optimize for the common case of appending elements to the list. When inserting, it checks if the new value is greater than the tail’s value and then performs the insertion in constant time.

Bonus One-Liner Method 5: Inserting with List Comprehension

If we convert our linked list to a Python list, we can use list comprehension along with the sort and unique functionalities of sets. It’s a less traditional approach but can be succinct for certain applications. Here’s a clever trick:

Here’s an example:

class LinkedList:
    # ... (as above) ...

    def to_list(self):
        return [node.data for node in self]

    def insert_all(self, values):
        self.head = None
        unique_sorted_values = sorted(set(self.to_list() + values))
        for value in unique_sorted_values:
            self.insert(value)

# Example usage:
ll = LinkedList()
ll.insert_all([5, 2, 3, 2])

Output:

2 -> 3 -> 5

This unique method first converts the linked list into a Python list, then adds the new values, removes duplicates, and sorts them. Finally, it reconstructs the linked list. It’s a quick way to concatenate and sort in just a few lines of code but is not ideal for large data sets due to the potential inefficiency.

Summary/Discussion

  • Method 1: Manual Verification. Strengths: Insertion maintains order instantaneously. Weaknesses: Insertion time increases with the size of the list.
  • Method 2: Sorting After Insertion. Strengths: Simple implementation. Weaknesses: Inefficient for continuous insertions as it sorts after each insertion.
  • Method 3: Recursive Insertion. Strengths: Elegant recursive approach; cleaner code. Weaknesses: May run into stack overflow with large lists.
  • Method 4: Maintain a Tail Reference. Strengths: Optimized for continuously increasing sequences; constant time append at the tail. Weaknesses: Requires maintenance of an additional tail pointer.
  • Method 5: Inserting with List Comprehension. Strengths: One-liner, cool for small data sets. Weaknesses: Conversion overhead; not practical for large or real-time data.