3217. Delete Nodes From Linked List Present in Array

WHAT TO KNOW - Sep 7 - - Dev Community

Deleting Nodes from a Linked List Present in an Array

Introduction

In the realm of data structures, linked lists and arrays are two fundamental building blocks that play pivotal roles in various algorithms and applications. Linked lists offer dynamic memory allocation and efficient insertion/deletion operations, while arrays provide direct access to elements through their indices. This article delves into a specific problem involving these two data structures: deleting nodes from a linked list whose elements are represented by an array.

Imagine you have a linked list where each node stores a reference to its next node and an integer value. This linked list is represented by an array, where each element of the array holds the value of a node. The challenge lies in efficiently deleting specific nodes from this linked list based on their values. This problem has applications in various scenarios, such as:

  • Database Management : When deleting records from a database, you might need to remove corresponding nodes from a linked list representation.
  • Graph Algorithms : Removing nodes from a graph's adjacency list (often represented as linked lists) is essential in algorithms like depth-first search (DFS) or breadth-first search (BFS).
  • Memory Management : Linked lists are used in garbage collection algorithms to track objects and remove them when they are no longer needed.

This article explores different approaches to solve this problem, ranging from basic traversal techniques to more optimized solutions. We will analyze the complexities, strengths, and weaknesses of each approach, providing a comprehensive guide to effectively deleting nodes from a linked list represented by an array.

Understanding Linked Lists and Arrays

Linked Lists

A linked list is a linear data structure where elements are linked together through pointers or references. Each node in a linked list typically contains two parts:

  • Data : The actual value stored in the node.
  • Next Pointer : A reference to the next node in the list.

The first node in the list is known as the head, and the last node points to NULL (indicating the end of the list). Linked lists are dynamic in nature, meaning they can grow or shrink as needed. They are particularly useful for handling insertion and deletion operations, which can be performed in constant time at any position in the list.

Linked List Diagram

Arrays

An array is a contiguous block of memory that stores elements of the same data type. Each element in an array can be accessed directly using its index, which is a numerical representation of its position within the array. Arrays provide constant-time access to elements, but their size is fixed at the time of creation. This means that adding or removing elements can be less efficient than in a linked list, especially if you need to insert or delete in the middle of the array.

Array Diagram

The Problem: Deleting Nodes from a Linked List in an Array

In this context, we have a linked list represented by an array. Each element in the array represents a node in the linked list. Each node is defined as a struct or class that contains two members:

  • Data : The value stored in the node (an integer in this case).
  • Next : The index of the next node in the list (or -1 if it's the last node).

The challenge is to remove nodes from this linked list based on their data values. We need to maintain the integrity of the linked list, ensuring that the remaining nodes are properly connected after the deletion.

Approaches to Deletion

We'll explore several approaches to solve this problem, ranging from simple traversal to more optimized solutions. The choice of approach will depend on factors such as the size of the linked list, the frequency of deletions, and the desired efficiency.

1. Simple Traversal

The simplest approach involves iterating through the linked list, comparing the data of each node with the target value. If a match is found, we update the `next` pointer of the preceding node to skip the target node.

Algorithm:

  1. Create a variable `previous` to keep track of the node before the current node. Initialize it to -1 (indicating the beginning of the list).
  2. Iterate through the linked list using a loop.
  3. For each node, check if its data value matches the target value:
    • If it matches:
      • If `previous` is -1, it means the target node is the head of the list. Update the head pointer to the next node.
      • Otherwise, set the `next` pointer of the `previous` node to the `next` pointer of the current node.
    • If it doesn't match:
      • Update `previous` to the index of the current node.

Example:

Consider a linked list represented by the following array:

linked_list = [
  {"data": 1, "next": 2}, 
  {"data": 2, "next": 3}, 
  {"data": 3, "next": 4}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

Let's say we want to delete the node with data value 3. After applying the simple traversal approach, the array would be updated as follows:

linked_list = [
  {"data": 1, "next": 2}, 
  {"data": 2, "next": 4}, 
  {"data": 3, "next": 4}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

Notice that the node with data value 3 is still present in the array, but its `next` pointer is now pointing to the same node as the `next` pointer of the node with data value 2. This effectively removes the node from the linked list.

Time Complexity: O(n), where n is the length of the linked list. We need to iterate through all nodes in the worst case.

Space Complexity: O(1). We use a constant amount of extra space.

2. Hash Table Approach

For more efficient deletion, we can use a hash table to store the indices of nodes we want to delete. We iterate through the linked list and check if the current node's index is present in the hash table. If it is, we mark the node as deleted by setting its `next` pointer to -1. This avoids unnecessary iteration and comparisons.

Algorithm:

  1. Create a hash table (e.g., a dictionary in Python).
  2. Iterate through the linked list. For each node:
    • If the node's index is present in the hash table, set its `next` pointer to -1.
    • Otherwise, continue to the next node.

Example:

Consider the same linked list as before:

linked_list = [
  {"data": 1, "next": 2}, 
  {"data": 2, "next": 3}, 
  {"data": 3, "next": 4}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

If we want to delete nodes with data values 2 and 4, we would first create a hash table:

delete_indices = {2: True, 4: True}
Enter fullscreen mode Exit fullscreen mode

Iterating through the linked list and using the hash table, we would update the array as follows:

linked_list = [
  {"data": 1, "next": -1}, 
  {"data": 2, "next": -1}, 
  {"data": 3, "next": -1}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(n), where n is the length of the linked list. We still need to iterate through all nodes, but we avoid unnecessary comparisons by using the hash table. The time complexity can be considered O(n + k), where k is the number of elements in the hash table.

Space Complexity: O(k), where k is the number of nodes to be deleted. We store the indices of these nodes in the hash table.

3. In-Place Deletion (Iterative)

This approach involves iterating through the linked list and deleting nodes in-place without creating any additional data structures. We find the node to be deleted and update the `next` pointer of the previous node to point to the node after the one being deleted.

Algorithm:

  1. Create a variable `current` to keep track of the current node and initialize it to the head of the list.
  2. Iterate through the linked list using a loop.
  3. For each node, check if its data value matches the target value:
    • If it matches:
      • If `current` is the head of the list, update the head pointer to the next node.
      • Otherwise, find the node before the `current` node (by iterating backwards) and set its `next` pointer to the `next` pointer of the `current` node.
    • If it doesn't match:
      • Update `current` to the next node.

Example:

Consider the linked list:

linked_list = [
  {"data": 1, "next": 2}, 
  {"data": 2, "next": 3}, 
  {"data": 3, "next": 4}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

Let's delete the node with data value 2. The array would be updated as follows:

linked_list = [
  {"data": 1, "next": 3}, 
  {"data": 2, "next": 3}, 
  {"data": 3, "next": 4}, 
  {"data": 4, "next": -1}
]
Enter fullscreen mode Exit fullscreen mode

The node with data value 2 is still present in the array, but its `next` pointer now points to the same node as the `next` pointer of the node with data value 1. Effectively, the node with data value 2 is removed from the linked list.

Time Complexity: O(n), where n is the length of the linked list. We still need to iterate through all nodes in the worst case. Finding the preceding node can take O(n) time in the worst case.

Space Complexity: O(1). We use a constant amount of extra space.

4. In-Place Deletion (Recursive)

This approach involves a recursive function to traverse the linked list and delete nodes. It's similar to the iterative approach but leverages recursion for a more compact and potentially more elegant implementation.

Algorithm:

  1. Define a recursive function that takes the head index, the target value, and the previous node index as arguments.
  2. In the function, check if the `current` node's data value matches the target value:
    • If it matches:
      • If the `current` node is the head, update the head pointer to the next node.
      • Otherwise, set the `next` pointer of the `previous` node to the `next` pointer of the `current` node.
    • If it doesn't match:
      • Recursively call the function for the `next` node, passing the current node index as the previous node index.

Example:

Let's assume the `deleteNode()` function is our recursive function:

def deleteNode(head, target, previous):
  if head == -1:
    return head

  if linked_list[head]["data"] == target:
    if previous == -1:
      head = linked_list[head]["next"]
    else:
      linked_list[previous]["next"] = linked_list[head]["next"]
  else:
    head = deleteNode(linked_list[head]["next"], target, head)

  return head
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(n), where n is the length of the linked list. We still need to iterate through all nodes in the worst case.

Space Complexity: O(n) in the worst case, due to the recursive call stack. The space complexity can be considered O(h), where h is the maximum depth of the recursion tree, which is equal to the length of the list in the worst case.

Best Practices

Here are some best practices for deleting nodes from a linked list represented by an array:

  • Choose the Right Approach: Select the approach that best suits your needs based on factors like the size of the linked list, the frequency of deletions, and the desired efficiency.
  • Handle Special Cases: Consider edge cases, such as deleting the head node, deleting the last node, and deleting a node in the middle of the list. Ensure your code correctly handles these scenarios.
  • Update the Head Pointer: If the head node is being deleted, remember to update the head pointer to the next node.
  • Avoid Memory Leaks: After deleting a node, ensure that you are not leaving any dangling pointers that could lead to memory leaks.
  • Validate Inputs: Always validate user inputs to prevent unexpected behavior or errors. For example, check if the target value exists in the linked list.
  • Test Thoroughly: Write thorough unit tests to ensure your deletion logic works correctly in all scenarios.

Conclusion

Deleting nodes from a linked list represented by an array is a common problem in data structures and algorithms. We've explored several approaches, from simple traversal to more optimized hash table-based and in-place deletion methods. The choice of approach depends on the specific requirements and constraints of your application. By understanding the complexities, strengths, and weaknesses of each approach, you can select the most appropriate solution for your needs and implement efficient and robust deletion logic.

Remember to prioritize best practices, such as handling edge cases, updating the head pointer, avoiding memory leaks, validating inputs, and thorough testing to ensure the correctness and reliability of your code.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player