Counting Element Occurrences in a Linked List without Recursion

Rate this post

πŸ’‘ Problem Formulation: In many scenarios, we need to count the number of times a particular element appears in a data structure. Specifically, we want to determine the frequency of an element within a linked list while avoiding recursion due to stack limitations or preference for iterative solutions. For example, given a linked list 10 -> 20 -> 20 -> 10 -> 30 and the element 20, the desired output is 2, since 20 occurs twice in the list.

Method 1: Iterative Traversal

This approach involves iterating through each node in the linked list sequentially and maintaining a count of how many times the target element is encountered. It’s straightforward and requires no auxiliary data structures. The function will typically accept the head of the linked list and the target element as arguments.

Here’s an example:

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

def count_occurrences(head, target):
    current = head
    count = 0
    while current:
        if current.data == target:
            count += 1
        current = current.next
    return count

# Example Usage
head = Node(10)
head.next = Node(20)
head.next.next = Node(20)
head.next.next.next = Node(10)

print(count_occurrences(head, 20))

Output:

2

This code snippet defines a Node class to create the linked list and a count_occurrences function to iterate over each node, incrementing a counter when the target element is found. The example usage builds a linked list with the elements 10, 20, 20, 10 and then counts the occurrences of the element 20.

Method 2: Iterative Traversal with a Sentinel Node

In this method, we add a sentinel or dummy node at the starting of the linked list to simplify boundary conditions. With a sentinel node, we can uniformly handle the list, even if it’s empty or has a single node. As before, we traverse the list and tally occurrences of the target element.

Here’s an example:

class Node:
    ...

def count_occurrences_with_sentinel(head, target):
    sentinel = Node(None)  # Sentinel node
    sentinel.next = head
    current = sentinel.next
    count = 0
    while current:
        if current.data == target:
            count += 1
        current = current.next
    return count

# Example Usage
...

print(count_occurrences_with_sentinel(head, 20))

Output:

2

This example refactors the original count_occurrences function by introducing a sentinel node. The process of counting the element occurrences remains unchanged, but with the sentinel node, we enhance the function’s robustness against special cases.

Method 3: Using a Hash Table to Aggregate Counts

A hash table can be used to maintain a running total of element occurrences. We traverse the linked list, storing the counts in the hash table. Finally, we retrieve the count for the target element. This method is more useful if we need to count occurrences for multiple elements without traversing the list multiple times.

Here’s an example:

class Node:
    ...

def count_occurrences_with_hash(head, target):
    current = head
    occurrences = {}
    while current:
        occurrences[current.data] = occurrences.get(current.data, 0) + 1
        current = current.next
    return occurrences.get(target, 0)

# Example Usage
...

print(count_occurrences_with_hash(head, 20))

Output:

2

In the code snippet above, we create a hash table occurrences to store the counts of each element as we iterate through the linked list. The method .get() of the dictionary ensures a default count of 0 for new elements. At the end, we get the count for the target element, which is 20 in this example.

Method 4: Iterative Traversal Using a Generator

This modern Python approach leverages a generator to iterate over the linked list nodes. It abstracts the traversal logic, making the counting code cleaner and potentially easier to read. We use a generator to yield nodes and a simple loop to count the occurrences.

Here’s an example:

class Node:
    ...

def nodes_generator(head):
    current = head
    while current:
        yield current
        current = current.next

def count_occurrences_with_generator(head, target):
    return sum(1 for node in nodes_generator(head) if node.data == target)

# Example Usage
...

print(count_occurrences_with_generator(head, 20))

Output:

2

In this example, nodes_generator is a Python generator yielding nodes of the list sequentially. The count_occurrences_with_generator leverages a generator expression combined with the built-in sum() function to count the target element occurrences concisely.

Bonus One-Liner Method 5: Counting with Generator Expression Directly

If we want the absolute shortest solution and are comfortable with Python’s more advanced one-liners, we can combine the traversal and counting steps into a single generator expression.

Here’s an example:

class Node:
    ...

# Example Usage
...

print(sum(1 for node in iter(lambda: head, None) if node and node.data == 20))

Output:

2

This one-liner uses iter() with a lambda function and None as a sentinel value to create an iterator that traverses the linked list nodes. Then, a generator expression within sum() is used to count the occurrences of 20.

Summary/Discussion

  • Method 1: Iterative Traversal. Simple and direct. Suitable for single-use counts. Not ideal for multiple counts in large lists, as it requires separate traversals for each count.
  • Method 2: Iterative Traversal with Sentinel Node. Slightly more complex due to the addition of a sentinel node. Enables uniform handling and robustness for edge cases.
  • Method 3: Using a Hash Table to Aggregate Counts. Efficient for multiple queries. Adds space complexity due to the hash table. Ideal when the list needs to be traversed once for several counts.
  • Method 4: Iterative Traversal Using a Generator. Offers clean and modular code. Abstracts away the traversal logic, which is great for readability and reusability.
  • Bonus One-Liner Method 5: Offers the most concise solution. It’s clever and concise but may sacrifice readability for brevity, thus not always recommended for complex or production code.