A linked list is a linear data structure consisting of a sequence of elements, called nodes, where each node contains:
- Data: The actual value or content of the node.
- Pointer/Reference: A reference (or pointer) to the next node in the sequence.
Unlike arrays, linked lists do not store elements in contiguous memory locations. Instead, each node dynamically points to the next node, forming a chain-like structure.
Types of Linked Lists
Singly Linked List:
- Each node has:
- Data.
- A pointer to the next node.
- The last node points to
null
, indicating the end of the list.
Example:
[1] -> [2] -> [3] -> [4] -> null
- Each node has:
Doubly Linked List:
- Each node has:
- Data.
- A pointer to the next node.
- A pointer to the previous node.
- It allows traversal in both directions (forward and backward).
Example:
null <- [1] <-> [2] <-> [3] <-> [4] -> null
3. Circular Linked List:
- Similar to a singly or doubly linked list but the last node points back to the first node, creating a circular structure.
Example (Singly Circular Linked List):
[1] -> [2] -> [3] -> [4] -+ ^-----------------+
- Each node has:
4. Circular Doubly Linked List:
- Combines the features of a doubly linked list and a circular linked list. The
next
of the last node points to the first node, and theprev
of the first node points to the last node.
Key Characteristics of Linked Lists
Dynamic Size:
- The size of the linked list can grow or shrink dynamically without reallocating memory.
Efficient Insertions/Deletions:
- Adding or removing nodes is efficient because you only need to adjust pointers, unlike arrays, where shifting elements is required.
Sequential Access:
- Nodes must be accessed sequentially, starting from the head node, as there is no index-based access.
Memory Usage:
- More memory is required than arrays because each node stores a pointer/reference.
Advantages of Linked Lists
Dynamic Memory Allocation:
- Can easily grow or shrink in size without memory reallocation.
Efficient Insertions/Deletions:
- Adding or removing elements at the beginning, end, or middle of the list is more efficient than arrays.
No Contiguous Memory Requirement:
- Elements do not need to be stored in a continuous memory block.
Disadvantages of Linked Lists
Sequential Access:
- Accessing an element requires traversal, which can be slower compared to arrays (O(n) vs. O(1) for arrays).
Higher Memory Overhead:
- Each node requires extra memory for the pointer/reference.
Complex Implementation:
- Implementation and debugging can be more challenging compared to arrays.
Operations on Linked Lists
1. Traversal
- Visiting each node in the list to process or retrieve data.
- Time Complexity: O(n)
2. Insertion
- Adding a node at the beginning, end, or a specific position.
- Time Complexity:
- Beginning: O(1)
- End or Specific Position: O(n)
3. Deletion
- Removing a node from the beginning, end, or a specific position.
- Time Complexity:
- Beginning: O(1)
- End or Specific Position: O(n)
4. Search
- Finding a node with a specific value.
- Time Complexity: O(n)
5. Reverse
- Reversing the pointers of the list to reverse its order.
- Time Complexity: O(n)
Linked List vs. Arrays
Feature | Linked List | Array |
---|---|---|
Size | Dynamic | Fixed (or Resized) |
Access | Sequential (O(n)) | Random (O(1)) |
Insertion/Deletion | Efficient (O(1)) at ends | Inefficient (O(n)) |
Memory | Extra pointer per node | Contiguous block needed |
Structure | Non-contiguous memory | Contiguous memory |
Applications of Linked Lists
Dynamic Memory Allocation:
- Used to implement memory management systems.
Data Structures:
- Foundations for stacks, queues, and graphs.
Undo Functionality:
- Implementing undo/redo in editors or IDEs.
Circular Buffers:
- Used in music players or real-time applications.
Hash Tables:
- Chaining in hash table collision resolution.
1. Reverse a Linked List
Problem:
Reverse a singly linked list.
Example:
Input: 1 -> 2 -> 3 -> 4 -> 5
Output: 5 -> 4 -> 3 -> 2 -> 1
Problem Explanation:
The task is to reverse the direction of a singly linked list such that the last node becomes the first node. This is a foundational problem that helps build an understanding of pointer manipulation in linked lists.
Algorithm:
- Use three pointers:
prev
(initiallynull
) to hold the reversed part of the list.current
(initiallyhead
) to traverse the list.nextNode
to temporarily store the next node before reversing the link.
- Iterate through the list:
- Save the
current.next
innextNode
. - Point
current.next
toprev
. - Move
prev
andcurrent
one step forward.
- Save the
- When
current
becomesnull
,prev
will point to the new head of the reversed list.
Time Complexity:
- O(n): Traverses the list once.
Space Complexity:
- O(1): No extra space is used.
Solution:
class ListNode(var value: Int) { var next: ListNode? = null } fun reverseList(head: ListNode?): ListNode? { var prev: ListNode? = null var current = head while (current != null) { val nextNode = current.next current.next = prev prev = current current = nextNode } return prev } // Example Usage fun main() { val head = ListNode(1).apply { next = ListNode(2).apply { next = ListNode(3).apply { next = ListNode(4).apply { next = ListNode(5) } } } } var reversedList = reverseList(head) while (reversedList != null) { print("${reversedList.value} -> ") reversedList = reversedList.next } }
2. Merge Two Sorted Linked Lists
Problem:
Merge two sorted linked lists into one sorted list.
Example:
Input: 1 -> 2 -> 4
and 1 -> 3 -> 4
Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4
Problem Explanation:
You are given two sorted linked lists. The goal is to merge them into a single sorted linked list.
Algorithm:
- Use recursion or iteration:
- Compare the
value
of the two heads. - Append the smaller value's node to the merged list.
- Recur (or iterate) with the next node of the smaller value.
- Compare the
- When one list becomes
null
, append the other list as it is already sorted.
Time Complexity:
- O(m + n):
m
andn
are the lengths of the two lists.
Space Complexity:
- O(m + n): If recursion is used, stack space is proportional to the total length of the lists.
Solution:
fun mergeTwoLists(list1: ListNode?, list2: ListNode?): ListNode? { if (list1 == null) return list2 if (list2 == null) return list1 return if (list1.value < list2.value) { list1.next = mergeTwoLists(list1.next, list2) list1 } else { list2.next = mergeTwoLists(list1, list2.next) list2 } } // Example Usage fun main() { val list1 = ListNode(1).apply { next = ListNode(2).apply { next = ListNode(4) } } val list2 = ListNode(1).apply { next = ListNode(3).apply { next = ListNode(4) } } var mergedList = mergeTwoLists(list1, list2) while (mergedList != null) { print("${mergedList.value} -> ") mergedList = mergedList.next } }