LeetCode 817 - Linked List Components
The problem gives us a singly linked list where every node contains a unique integer value. We are also given an array nums, and every value in nums is guaranteed to appear somewhere in the linked list.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Linked List
Solution
Problem Understanding
The problem gives us a singly linked list where every node contains a unique integer value. We are also given an array nums, and every value in nums is guaranteed to appear somewhere in the linked list.
We need to count how many connected components exist among the nodes whose values belong to nums.
Two nodes are considered connected if:
- Both node values are present in
nums - The two nodes appear consecutively in the linked list
This means we are not looking for arbitrary graph connectivity. Connectivity is determined entirely by adjacency in the linked list.
For example, suppose the linked list is:
0 -> 1 -> 2 -> 3
and:
nums = [0,1,3]
Nodes 0 and 1 are consecutive in the linked list and both belong to nums, so they form one component:
[0,1]
Node 3 is isolated because node 2 is not in nums, so 3 forms another component:
[3]
Therefore, the answer is 2.
The constraints are important:
- The linked list length can be as large as
10^4 - All node values are unique
- All values in
numsare unique
The uniqueness guarantee simplifies the problem significantly because we never need to worry about repeated values or ambiguous matches.
The main challenge is efficiently determining whether a node belongs to nums, and whether it continues an existing component or starts a new one.
Several edge cases are important:
numsmay contain only one value- All linked list nodes may belong to
nums - Every selected node may be isolated
- Components may appear at the beginning or end of the linked list
- Consecutive sequences may have different lengths
A naive implementation that repeatedly searches through nums linearly would become inefficient for large inputs.
Approaches
Brute Force Approach
A straightforward solution is to traverse the linked list and, for each node, check whether its value exists in nums using a linear scan.
Whenever we encounter a node that belongs to nums, we determine whether it starts a new component. A node starts a new component if:
-
It belongs to
nums -
The next node is either:
-
None, or -
not present in
nums
This logic works because each connected component ends exactly where adjacency breaks.
However, membership checking becomes expensive if implemented with repeated linear scans through nums.
If:
nis the linked list lengthmis the size ofnums
then each membership check costs O(m), and we perform up to O(n) checks, leading to O(n * m) time complexity.
With constraints up to 10^4, this is unnecessarily slow.
Optimal Approach
The key observation is that we only need fast membership testing.
A hash set provides O(1) average lookup time.
We first store all values from nums into a set. Then we traverse the linked list once.
For every node:
- If the node value is in the set
- And the next node is either absent or not in the set
then this node marks the end of a connected component, so we increment the answer.
This works because every component has exactly one ending node.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * m) | O(1) | Linear search in nums for every node |
| Optimal | O(n + m) | O(m) | Uses a hash set for constant time membership checks |
Algorithm Walkthrough
- Create a hash set from
nums.
We use a set because membership checks in a hash set are very fast, typically O(1) on average. This avoids repeatedly scanning the array.
2. Initialize a counter called components to 0.
This variable stores the number of connected components discovered so far.
3. Traverse the linked list from head to the end.
We inspect each node exactly once. 4. For each node, check whether its value belongs to the set.
If the value is not in the set, then this node is irrelevant to the problem and we move on. 5. If the current node belongs to the set, determine whether it is the end of a component.
A node ends a component if:
current.nextisNone, orcurrent.next.valis not in the set
This means the chain of connected nodes stops here.
6. If the current node ends a component, increment components.
Every component contributes exactly one ending node, so counting endings correctly counts components.
7. Continue until the traversal finishes.
8. Return components.
Why it works
A connected component in this problem is simply a maximal consecutive sequence of nodes whose values belong to nums.
Every such sequence has exactly one last node. By counting nodes where:
- the current value belongs to
nums, and - the next node does not continue the sequence,
we count every component exactly once.
No component is missed, and no component is counted multiple times.
Python Solution
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
from typing import List, Optional
class Solution:
def numComponents(self, head: Optional[ListNode], nums: List[int]) -> int:
nums_set = set(nums)
components = 0
current = head
while current:
if current.val in nums_set:
if current.next is None or current.next.val not in nums_set:
components += 1
current = current.next
return components
The implementation begins by converting nums into a hash set called nums_set. This gives constant time membership checks.
We then initialize components to store the number of connected components discovered during traversal.
The pointer current walks through the linked list node by node.
For every node, we first check whether its value belongs to the set. If it does not, we skip it because it cannot contribute to a component.
If the value does belong to the set, we determine whether this node marks the end of a connected sequence. That happens when either:
- there is no next node, or
- the next node's value is not in the set
Whenever this condition is true, we increment the component counter.
Finally, after traversing the entire list, we return the total number of components.
Go Solution
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func numComponents(head *ListNode, nums []int) int {
numsSet := make(map[int]bool)
for _, num := range nums {
numsSet[num] = true
}
components := 0
current := head
for current != nil {
if numsSet[current.Val] {
if current.Next == nil || !numsSet[current.Next.Val] {
components++
}
}
current = current.Next
}
return components
}
The Go implementation follows the same logic as the Python solution.
Since Go does not have a built in hash set type, we simulate one using map[int]bool.
The linked list traversal uses a pointer named current. Go uses nil instead of Python's None for missing pointers.
The rest of the logic is identical:
- identify nodes that belong to
nums - determine whether they terminate a component
- increment the counter accordingly
No integer overflow concerns exist here because the maximum number of nodes is only 10^4.
Worked Examples
Example 1
head = [0,1,2,3]
nums = [0,1,3]
The set becomes:
{0,1,3}
Linked list:
0 -> 1 -> 2 -> 3
| Current Node | In Set? | Next In Set? | Component Ends Here? | Components |
|---|---|---|---|---|
| 0 | Yes | Yes | No | 0 |
| 1 | Yes | No | Yes | 1 |
| 2 | No | Yes | No | 1 |
| 3 | Yes | No next node | Yes | 2 |
Final answer:
2
Example 2
head = [0,1,2,3,4]
nums = [0,3,1,4]
The set becomes:
{0,1,3,4}
Linked list:
0 -> 1 -> 2 -> 3 -> 4
| Current Node | In Set? | Next In Set? | Component Ends Here? | Components |
|---|---|---|---|---|
| 0 | Yes | Yes | No | 0 |
| 1 | Yes | No | Yes | 1 |
| 2 | No | Yes | No | 1 |
| 3 | Yes | Yes | No | 1 |
| 4 | Yes | No next node | Yes | 2 |
Final answer:
2
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | Building the set takes O(m), traversing the list takes O(n) |
| Space | O(m) | The hash set stores all values from nums |
The linked list is traversed exactly once, and each set lookup is constant time on average.
The additional memory usage comes entirely from storing the nums values in the hash set.
Test Cases
# Helper classes and functions for testing
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build_list(values):
dummy = ListNode()
current = dummy
for value in values:
current.next = ListNode(value)
current = current.next
return dummy.next
class Solution:
def numComponents(self, head, nums):
nums_set = set(nums)
components = 0
current = head
while current:
if current.val in nums_set:
if current.next is None or current.next.val not in nums_set:
components += 1
current = current.next
return components
solution = Solution()
# Example 1
head = build_list([0, 1, 2, 3])
assert solution.numComponents(head, [0, 1, 3]) == 2 # two separate components
# Example 2
head = build_list([0, 1, 2, 3, 4])
assert solution.numComponents(head, [0, 3, 1, 4]) == 2 # two groups
# Single node selected
head = build_list([5])
assert solution.numComponents(head, [5]) == 1 # single component
# Entire list selected
head = build_list([0, 1, 2, 3])
assert solution.numComponents(head, [0, 1, 2, 3]) == 1 # one large component
# No consecutive nodes
head = build_list([0, 1, 2, 3, 4])
assert solution.numComponents(head, [0, 2, 4]) == 3 # all isolated
# Component at beginning
head = build_list([0, 1, 2, 3])
assert solution.numComponents(head, [0, 1]) == 1 # component starts at head
# Component at end
head = build_list([0, 1, 2, 3])
assert solution.numComponents(head, [2, 3]) == 1 # component ends at tail
# Multiple mixed components
head = build_list([0, 1, 2, 3, 4, 5, 6])
assert solution.numComponents(head, [0, 1, 3, 5, 6]) == 3 # mixed structure
# Single isolated middle node
head = build_list([0, 1, 2, 3])
assert solution.numComponents(head, [2]) == 1 # isolated node
# Large consecutive block
head = build_list(list(range(10)))
assert solution.numComponents(head, [2, 3, 4, 5, 6]) == 1 # long component
Test Summary
| Test | Why |
|---|---|
[0,1,2,3], [0,1,3] |
Validates basic multiple component detection |
[0,1,2,3,4], [0,3,1,4] |
Ensures nonadjacent ordering in nums does not matter |
| Single node list | Tests smallest valid input |
| Entire list selected | Ensures one large component is counted correctly |
| Alternating selected nodes | Tests multiple isolated components |
| Component at head | Verifies handling of list beginning |
| Component at tail | Verifies handling of list ending |
| Mixed component sizes | Tests general correctness |
| Single middle node | Ensures isolated nodes work properly |
| Large consecutive block | Tests longer connected sequences |
Edge Cases
Entire Linked List Forms One Component
If every node value belongs to nums, the entire linked list becomes one connected component.
For example:
head = [0,1,2,3]
nums = [0,1,2,3]
A buggy implementation might incorrectly count every node as a separate component. Our solution avoids this by only counting nodes that terminate a component. In this case, only the final node satisfies that condition.
Every Selected Node Is Isolated
Consider:
head = [0,1,2,3,4]
nums = [0,2,4]
No two selected nodes are adjacent. Therefore, every selected node forms its own component.
This case is important because naive adjacency handling may accidentally merge nonconsecutive nodes. Our implementation checks actual linked list adjacency through current.next, so isolated nodes are counted correctly.
Component Ends at the Tail
Consider:
head = [0,1,2,3]
nums = [2,3]
The component reaches the last node of the list.
This is a common source of bugs because the final node has no next pointer. Our implementation explicitly checks:
current.next is None
which correctly identifies the end of the final component.