LeetCode 3294 - Convert Doubly Linked List to Array II
This problem gives us a pointer to an arbitrary node inside a doubly linked list. Unlike many linked list problems, we are not guaranteed to receive the head of the list. Instead, we may receive any node somewhere in the middle or even the tail.
Difficulty: 🟡 Medium
Topics: Array, Linked List, Doubly-Linked List
Solution
Problem Understanding
This problem gives us a pointer to an arbitrary node inside a doubly linked list. Unlike many linked list problems, we are not guaranteed to receive the head of the list. Instead, we may receive any node somewhere in the middle or even the tail.
A doubly linked list node contains three fields:
val, the integer stored in the nodenext, a pointer to the next nodeprev, a pointer to the previous node
Our task is to return all node values in the correct list order as an integer array.
For example, suppose the list is:
1 <-> 2 <-> 3 <-> 4 <-> 5
If we are given the node containing 5, we still need to return:
[1, 2, 3, 4, 5]
This means we cannot simply traverse forward from the given node. We must first locate the true head of the linked list by following prev pointers backward.
The constraints are small, with at most 500 nodes, which means even less optimized approaches would work comfortably within limits. However, the goal is still to design the cleanest and most efficient solution possible.
The problem also guarantees:
- The list contains at least one node
- All node values are unique
- The structure is a valid doubly linked list
- The input node always belongs to the list
Several edge cases are important:
- The given node may already be the head
- The given node may be the tail
- The list may contain only one node
- Traversing backward incorrectly could miss the head
- Traversing forward before locating the head would produce an incomplete array
Because the list is doubly linked, we can move in both directions, which is the key observation that makes the solution straightforward.
Approaches
Brute Force Approach
A brute force style solution would first collect all nodes reachable from the given node in both directions separately.
One possible implementation is:
- Traverse backward using
prevpointers and store values in a temporary list. - Reverse that temporary list because values were collected backward.
- Traverse forward from the original node using
nextpointers. - Combine the results carefully while avoiding duplication of the starting node.
This approach works because every node in the list is reachable from the given node through either prev or next links.
However, the implementation becomes unnecessarily complicated. We must manage two traversals, handle reversing, and avoid duplicate inclusion of the starting node. The extra bookkeeping makes the solution harder to reason about.
Optimal Approach
The better solution uses the structure of a doubly linked list more naturally.
The key insight is that every node has access to its predecessor through the prev pointer. Therefore, starting from the given node, we can repeatedly move backward until we reach the true head, which is the node whose prev pointer is None or nil.
Once we have the head, the problem becomes a standard linked list traversal:
- Start from the head
- Traverse using
next - Append each value to the answer array
This approach is clean, simple, and optimal.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Requires backward collection, reversal, and duplicate handling |
| Optimal | O(n) | O(1), excluding output | First locate head, then perform normal forward traversal |
Algorithm Walkthrough
- Start with the given node.
The input node may be anywhere in the list, so we cannot immediately begin building the output array. 2. Move backward until the head is reached.
While the current node has a non-null prev pointer, move to current.prev.
After this loop finishes, the current node is guaranteed to be the head because no further previous node exists. 3. Initialize an empty result array.
This array will store the linked list values in order. 4. Traverse forward from the head.
While the current node is not null:
- Append
current.valto the result array - Move to
current.next
- Return the result array.
Since we started at the true head and traversed forward exactly once, the values are collected in correct order.
Why it works
The correctness comes from two properties of doubly linked lists.
First, every node can reach the head by repeatedly following prev pointers. Since the list is finite and properly connected, this process eventually reaches the unique node whose prev pointer is null.
Second, traversing forward from the head using next pointers visits every node exactly once in correct order. Therefore, the produced array exactly matches the linked list sequence.
Python Solution
from typing import List, Optional
"""
# Definition for a Node.
class Node:
def __init__(self, val, prev=None, next=None):
self.val = val
self.prev = prev
self.next = next
"""
class Solution:
def toArray(self, node: 'Optional[Node]') -> List[int]:
# Move backward to find the head
while node.prev:
node = node.prev
result = []
# Traverse forward and collect values
while node:
result.append(node.val)
node = node.next
return result
The implementation begins by locating the true head of the doubly linked list. This is done with a simple loop that repeatedly follows the prev pointer until no previous node exists.
Once the head is found, we initialize an empty result list. We then perform a standard linked list traversal using the next pointer.
At each node, we append the node's value to the output array. Because traversal starts from the head and proceeds forward, values are collected in the exact order required by the problem.
The implementation is concise because the doubly linked structure already provides all navigation capability we need.
Go Solution
/**
* Definition for a Node.
* type Node struct {
* Val int
* Next *Node
* Prev *Node
* }
*/
func toArray(head *Node) []int {
// Move backward to find the real head
for head.Prev != nil {
head = head.Prev
}
result := []int{}
// Traverse forward and collect values
for head != nil {
result = append(result, head.Val)
head = head.Next
}
return result
}
The Go implementation follows exactly the same logic as the Python solution.
One important Go-specific detail is the use of nil instead of Python's None. The traversal loops terminate when pointers become nil.
The result is stored in a slice, which dynamically grows as values are appended. Since the maximum list size is only 500 nodes, repeated appends are perfectly efficient for this problem.
Also note that the original problem statement contains a typo in the function signature:
func toArray(head *node) []int
The correct type name should be *Node, matching the struct definition.
Worked Examples
Example 1
Input:
head = [1,2,3,4,5]
node = 5
The given node is the tail.
Initial structure:
1 <-> 2 <-> 3 <-> 4 <-> 5
^
node
Step 1, Move backward to find head
| Current Node | prev exists? | Action |
|---|---|---|
| 5 | Yes | Move to 4 |
| 4 | Yes | Move to 3 |
| 3 | Yes | Move to 2 |
| 2 | Yes | Move to 1 |
| 1 | No | Stop |
Now we are at the head node.
Step 2, Traverse forward
| Current Node | Result Array |
|---|---|
| 1 | [1] |
| 2 | [1, 2] |
| 3 | [1, 2, 3] |
| 4 | [1, 2, 3, 4] |
| 5 | [1, 2, 3, 4, 5] |
Final output:
[1,2,3,4,5]
Example 2
Input:
head = [4,5,6,7,8]
node = 8
Initial structure:
4 <-> 5 <-> 6 <-> 7 <-> 8
^
node
Step 1, Move backward
| Current Node | prev exists? | Action |
|---|---|---|
| 8 | Yes | Move to 7 |
| 7 | Yes | Move to 6 |
| 6 | Yes | Move to 5 |
| 5 | Yes | Move to 4 |
| 4 | No | Stop |
Step 2, Traverse forward
| Current Node | Result Array |
|---|---|
| 4 | [4] |
| 5 | [4, 5] |
| 6 | [4, 5, 6] |
| 7 | [4, 5, 6, 7] |
| 8 | [4, 5, 6, 7, 8] |
Final output:
[4,5,6,7,8]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited at most once while moving backward and once while moving forward |
| Space | O(1), excluding output | Only a few pointer variables are used |
The algorithm is linear because every node participates in at most two traversals. One traversal moves backward to locate the head, and the second traversal moves forward to build the answer.
The auxiliary space usage is constant because no additional data structures proportional to input size are allocated. The returned array itself is not counted as extra space since it is required output.
Test Cases
from typing import Optional, List
class Node:
def __init__(self, val, prev=None, next=None):
self.val = val
self.prev = prev
self.next = next
class Solution:
def toArray(self, node: 'Optional[Node]') -> List[int]:
while node.prev:
node = node.prev
result = []
while node:
result.append(node.val)
node = node.next
return result
def build_dll(values):
nodes = [Node(v) for v in values]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i + 1]
nodes[i + 1].prev = nodes[i]
return nodes
sol = Solution()
# Example 1, given node is tail
nodes = build_dll([1, 2, 3, 4, 5])
assert sol.toArray(nodes[-1]) == [1, 2, 3, 4, 5]
# Example 2, another tail input
nodes = build_dll([4, 5, 6, 7, 8])
assert sol.toArray(nodes[-1]) == [4, 5, 6, 7, 8]
# Single node list
nodes = build_dll([10])
assert sol.toArray(nodes[0]) == [10]
# Given node is already head
nodes = build_dll([1, 2, 3])
assert sol.toArray(nodes[0]) == [1, 2, 3]
# Given node is middle node
nodes = build_dll([5, 10, 15, 20, 25])
assert sol.toArray(nodes[2]) == [5, 10, 15, 20, 25]
# Two node list, start from second node
nodes = build_dll([7, 8])
assert sol.toArray(nodes[1]) == [7, 8]
# Larger list
values = list(range(1, 101))
nodes = build_dll(values)
assert sol.toArray(nodes[50]) == values
| Test | Why |
|---|---|
| Tail node input | Verifies backward traversal works correctly |
| Second example | Confirms general correctness on another list |
| Single node list | Validates smallest possible input |
| Node already at head | Ensures unnecessary backward traversal is avoided correctly |
| Middle node input | Tests arbitrary node access |
| Two node list | Validates minimal multi-node structure |
| Large list | Confirms scalability and traversal correctness |
Edge Cases
Single Node List
The smallest valid input contains only one node. In this case, both prev and next are null.
A buggy implementation might assume at least one backward or forward move is possible. Our solution handles this naturally because both loops terminate immediately, and the single value is returned correctly.
Given Node Is Already the Head
If the provided node is already the first node, the backward traversal should stop immediately.
An incorrect implementation could accidentally move past the head or mishandle null pointers. Our condition:
while node.prev:
ensures movement only happens when a previous node exists.
Given Node Is the Tail
This is one of the most important scenarios because traversing only forward would produce an incomplete result containing just the tail node.
Our algorithm explicitly solves this by first locating the true head before collecting values. Even if the input node is the final node in the list, backward traversal reconstructs access to the entire structure.
Given Node Is in the Middle
The input node may be anywhere inside the linked list.
This case tests whether the implementation correctly combines backward discovery with forward traversal. By first locating the head and then restarting traversal from there, the solution guarantees the full list is returned regardless of the starting position.