Convert a BST into a sorted circular doubly linked list in-place using inorder traversal.
Problem Restatement
We are given the root of a binary search tree.
We need to convert the tree into a sorted circular doubly linked list in-place.
“In-place” means we should reuse the existing tree nodes. We should not create new list nodes.
After conversion:
| Tree Pointer | List Meaning |
|---|---|
left | previous node |
right | next node |
The returned node should be the smallest node in the list.
The list must also be circular:
| Node | Required Link |
|---|---|
| smallest node | left points to the largest node |
| largest node | right points to the smallest node |
For example, if the BST contains values:
[1, 2, 3, 4, 5]then the final doubly linked list should be:
1 <-> 2 <-> 3 <-> 4 <-> 5and also circular:
5 -> 1
1 -> 5The official problem asks for a sorted circular doubly linked list and requires the conversion to be done in-place. The left pointer becomes the predecessor pointer, and the right pointer becomes the successor pointer.
Input and Output
| Item | Meaning |
|---|---|
| Input | root, the root node of a BST |
| Output | The smallest node in the converted circular doubly linked list |
| In-place | Reuse the original nodes |
| Empty tree | Return None |
| Ordering | Nodes must appear in ascending order |
The node definition is usually given as:
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = rightExamples
Consider this BST:
4
/ \
2 5
/ \
1 3The inorder order is:
1, 2, 3, 4, 5So the converted list should be:
1 <-> 2 <-> 3 <-> 4 <-> 5Because the list is circular:
1.left = 5
5.right = 1The returned node should be 1, because it is the smallest node.
For a single-node tree:
1The result is a circular list with one node:
1.right = 1
1.left = 1For an empty tree:
root = Nonewe return:
NoneFirst Thought: Collect Values or Nodes
A simple approach is:
- Traverse the BST in sorted order.
- Store all nodes in an array.
- Link adjacent nodes in the array.
- Connect the last node back to the first node.
For example, after inorder traversal:
nodes = [1, 2, 3, 4, 5]we link:
1.right = 2
2.left = 1
2.right = 3
3.left = 2
...Then make the list circular:
5.right = 1
1.left = 5This works, but it uses an extra array of size n.
We can do better.
Key Insight
A binary search tree gives sorted values through inorder traversal.
For a BST, inorder traversal visits nodes in this order:
left subtree -> current node -> right subtreeBecause of the BST property, this order is ascending.
So while doing inorder traversal, we can directly connect each visited node to the node visited immediately before it.
We only need two pointers:
| Pointer | Meaning |
|---|---|
head | The smallest node, first node in the list |
prev | The previously visited node during inorder traversal |
When we visit a node:
- If
prevexists, connectprevand the current node. - If
prevdoes not exist, the current node is the first node, so it becomeshead. - Move
prevto the current node.
After traversal finishes, prev points to the largest node.
Then we connect:
head.left = prev
prev.right = headThis makes the list circular.
Algorithm
Handle the empty tree first:
if not root:
return NoneInitialize:
head = None
prev = NoneRun inorder DFS.
For each visited node:
If this is the first visited node:
head = nodeOtherwise, connect the previous node and current node:
prev.right = node
node.left = prevThen update:
prev = nodeAfter DFS finishes, close the circular list:
head.left = prev
prev.right = headReturn:
headCorrectness
Inorder traversal of a BST visits nodes in ascending order.
During traversal, suppose the current node is node.
All nodes visited before node are smaller than or equal to node, and because we process nodes one by one in sorted order, prev is exactly the node that should come immediately before node in the final list.
When we execute:
prev.right = node
node.left = prevwe create the correct successor and predecessor links between two adjacent nodes in sorted order.
The first visited node has no smaller predecessor, so we store it as head. In a BST, the first inorder node is the smallest node, so returning head satisfies the problem requirement.
After traversal ends, prev is the last visited node. In a BST, the last inorder node is the largest node. Connecting:
head.left = prev
prev.right = headadds the required circular links between the smallest and largest nodes.
Therefore, every node is linked to its sorted predecessor and successor, and the returned node is the smallest node.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Every node is visited once |
| Space | O(h) | Recursion stack height is the tree height |
Here, n is the number of nodes.
h is the height of the tree.
For a balanced BST, h = O(log n).
For a completely skewed BST, h = O(n).
Implementation
class Solution:
def treeToDoublyList(self, root: 'Optional[Node]') -> 'Optional[Node]':
if not root:
return None
self.head = None
self.prev = None
def inorder(node: 'Optional[Node]') -> None:
if not node:
return
inorder(node.left)
if self.prev:
self.prev.right = node
node.left = self.prev
else:
self.head = node
self.prev = node
inorder(node.right)
inorder(root)
self.head.left = self.prev
self.prev.right = self.head
return self.headCode Explanation
We first handle the empty tree:
if not root:
return NoneAn empty tree has no nodes to link, so the correct answer is None.
We keep two fields:
self.head = None
self.prev = Noneself.head stores the first node visited by inorder traversal.
self.prev stores the node visited immediately before the current node.
The DFS function follows inorder order:
inorder(node.left)
...
inorder(node.right)This is the important part. Since the input is a BST, inorder traversal gives the nodes in sorted order.
When we visit a node, there are two cases.
If self.prev already exists:
self.prev.right = node
node.left = self.prevThis links the previous sorted node with the current sorted node.
If self.prev does not exist:
self.head = nodeThis means we are visiting the smallest node, so it becomes the head of the list.
Then we move self.prev forward:
self.prev = nodeAfter DFS completes, self.head is the smallest node and self.prev is the largest node.
We close the circle:
self.head.left = self.prev
self.prev.right = self.headFinally, we return the smallest node:
return self.headTesting
For local testing, we can build a small tree and walk through the circular list.
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def list_forward(head, count):
ans = []
cur = head
for _ in range(count):
ans.append(cur.val)
cur = cur.right
return ans
def list_backward(head, count):
ans = []
cur = head.left
for _ in range(count):
ans.append(cur.val)
cur = cur.left
return ans
def run_tests():
s = Solution()
root = Node(
4,
Node(2, Node(1), Node(3)),
Node(5),
)
head = s.treeToDoublyList(root)
assert list_forward(head, 5) == [1, 2, 3, 4, 5]
assert list_backward(head, 5) == [5, 4, 3, 2, 1]
assert head.val == 1
assert head.left.val == 5
assert head.left.right == head
single = Node(10)
head = Solution().treeToDoublyList(single)
assert head.val == 10
assert head.left == head
assert head.right == head
assert Solution().treeToDoublyList(None) is None
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
[4,2,5,1,3] | Checks normal BST conversion |
| Forward traversal | Confirms right links are correct |
| Backward traversal | Confirms left links are correct |
| Circular links | Confirms head and tail are connected |
| Single node | Checks self-circular case |
| Empty tree | Checks None input |