Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@ def is_valid_alien_order(self, words: List[str], order: str) -> bool:
return False
return True


if __name__ == "__main__":
unittest.main()
77 changes: 77 additions & 0 deletions datastructures/trees/binary/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,80 @@ entire height of the tree, including `h` steps.
The space complexity of this solution is `O(1)` as there is no additional space being used. Only two pointers are being
maintained requiring constant space.

---

## Connect All Siblings of a Binary Tree

Given the root of a perfect binary tree, where each node is equipped with an additional pointer, next, connect all nodes
from left to right. Do so in such a way that the next pointer of each node points to its immediate right sibling except
for the rightmost node, which points to the first node of the next level.

The next pointer of the last node of the binary tree (i.e., the rightmost node of the last level) should be set to NULL.

> A binary tree in which all the levels are completely filled with nodes, and all leaf nodes (nodes with no children)
> are at the same level.

### Constraints

- The number of nodes in the tree is in the range [0,500].
- -1000 <= `node.data` <= 1000

### Examples

![Example 1](./images/examples/connect_all_siblings_of_binary_tree_example_1.png)
![Example 2](./images/examples/connect_all_siblings_of_binary_tree_example_2.png)

### Solution

The algorithm connects all nodes by utilizing the structure of the perfect binary tree, where each non-leaf node has
exactly two children. Using this property, the algorithm connects nodes without needing extra space. It uses two pointers:
one to traverse the current level and another to connect nodes at the next level. Starting from the root, the algorithm
links each node’s left child to its right child and then connects the right child to the next node’s left child on the
same level, continuing this process across all nodes at that level. If no adjacent node is on the same level, the right
child’s next pointer is connected to the first node on the next lower level. This process continues until all nodes are
connected in a manner that reflects level-order traversal.

The steps of the algorithm are given below:

1. If the root is None, the tree is empty. In this case, return immediately.
2. Initialize two pointers current and last to the root node. The current pointer traverses the nodes level by level,
while the last keeps track of the last node connected via the next pointer.
3. The loop continues as long as current.left exists. In a perfect binary tree, all non-leaf nodes have a left child,
so this condition ensures that the loop continues until all levels are processed.
- First connection: last.next = current.left connects the last node (initially the root) to the current.left child.
After this, last is updated to current.left.
- Second connection: last.next = current.right connects current.left (now pointed to by last) to current.right. last
is then updated to current.right.
- Move to the next node: current = current.next moves the current pointer to the next node.
4. Finally, return the modified root of the tree, where all nodes are connected to their next sibling in the level-order
traversal.

Let’s look at the following illustration(s) to get a better understanding of the solution:

![Solution 1](./images/solutions/connect_all_siblings_of_binary_tree_solution_1.png)
![Solution 2](./images/solutions/connect_all_siblings_of_binary_tree_solution_2.png)
![Solution 3](./images/solutions/connect_all_siblings_of_binary_tree_solution_3.png)
![Solution 4](./images/solutions/connect_all_siblings_of_binary_tree_solution_4.png)
![Solution 5](./images/solutions/connect_all_siblings_of_binary_tree_solution_5.png)
![Solution 6](./images/solutions/connect_all_siblings_of_binary_tree_solution_6.png)
![Solution 7](./images/solutions/connect_all_siblings_of_binary_tree_solution_7.png)
![Solution 8](./images/solutions/connect_all_siblings_of_binary_tree_solution_8.png)
![Solution 9](./images/solutions/connect_all_siblings_of_binary_tree_solution_9.png)
![Solution 10](./images/solutions/connect_all_siblings_of_binary_tree_solution_10.png)
![Solution 11](./images/solutions/connect_all_siblings_of_binary_tree_solution_11.png)
![Solution 12](./images/solutions/connect_all_siblings_of_binary_tree_solution_12.png)
![Solution 13](./images/solutions/connect_all_siblings_of_binary_tree_solution_13.png)
![Solution 14](./images/solutions/connect_all_siblings_of_binary_tree_solution_14.png)
![Solution 15](./images/solutions/connect_all_siblings_of_binary_tree_solution_15.png)
![Solution 16](./images/solutions/connect_all_siblings_of_binary_tree_solution_16.png)
![Solution 17](./images/solutions/connect_all_siblings_of_binary_tree_solution_17.png)

#### Time Complexity

The time complexity of the solution is O(N), where N is the number of nodes in the binary tree. The algorithm traverses
each node exactly once, connecting the left and right children as it moves through the tree.

#### Space Complexity

The space complexity of the solution is O(1) because the algorithm uses only a constant amount of extra space,
specifically the pointers current and last, regardless of the size of the tree.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 18 additions & 2 deletions datastructures/trees/binary/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(
right: Optional["BinaryTreeNode"] = None,
key: Optional[Any] = None,
parent: Optional["BinaryTreeNode"] = None,
next: Optional["BinaryTreeNode"] = None,
) -> None:
"""
Constructor for BinaryTreeNode class. This will create a new node with the provided data and optional
Expand All @@ -29,10 +30,19 @@ def __init__(
right (Optional[BinaryTreeNode]): Right child of the node
key (Optional[Any]): Key for the node, if not provided a hash of the data is used
parent (Optional[BinaryTreeNode]): Parent of the node
next (Optional[BinaryTreeNode]): Next child of the node which is the sibling of the node. The sibling is the
node on the same level as this node. If this is the rightmost node in the tree that is not on the last level
of the tree, then this is the next node on the next level starting from the left. If this is the last node
in the tree, then this is None.
"""
super().__init__(data, key, parent)
self.left: Optional[BinaryTreeNode] = left
self.right: Optional[BinaryTreeNode] = right
# Next is a pointer that connects this node to it's right sibling in the tree. If this node is the right most
# node on a given level, then it is connected to the first node on the next level. If this node is the last node
# in the tree on the last node, then it is pointed to None. By default, it is set to None.
# Note that if this is the root node, it is connected to the left most node on the next level.
self.next: Optional[BinaryTreeNode] = next

def insert_node(self, data: T) -> None:
"""
Expand Down Expand Up @@ -181,7 +191,9 @@ def height(self) -> int:
pass

def __repr__(self):
return f"BinaryTreeNode(data={self.data}, key={self.key}, left={self.left}, right={self.right})"
parent_data = self.parent.data if self.parent else None
next_data = self.next.data if self.next else None
return f"BinaryTreeNode(data={self.data}, key={self.key}, left={self.left}, right={self.right}, parent={parent_data}, next={next_data})"

def __eq__(self, other: "BinaryTreeNode") -> bool:
"""Checks if this node is equal to another node based on the data they contain
Expand All @@ -193,7 +205,11 @@ def __eq__(self, other: "BinaryTreeNode") -> bool:
if other is None:
return False

if other.data == self.data:
if (
other.data == self.data
and self.left == other.left
and self.right == other.right
):
return True

return False
Expand Down
40 changes: 40 additions & 0 deletions datastructures/trees/binary/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import unittest
from typing import List
from parameterized import parameterized
from datastructures.trees.binary.utils import (
lowest_common_ancestor,
lowest_common_ancestor_ptr,
connect_all_siblings,
connect_all_siblings_ptr,
)
from datastructures.trees.binary.node import BinaryTreeNode

Expand Down Expand Up @@ -182,5 +186,41 @@ def test_4(self):
self.assertEqual(expected, actual)


CONNECT_ALL_SIBLINGS_TEST_CASES = [
(
BinaryTreeNode(
data=100,
left=BinaryTreeNode(
data=50, left=BinaryTreeNode(data=25), right=BinaryTreeNode(data=75)
),
right=BinaryTreeNode(
data=200, left=BinaryTreeNode(data=300), right=BinaryTreeNode(data=10)
),
),
[100, 50, 200, 25, 75, 300, 10],
),
]


class ConnectAllSiblingsTestCase(unittest.TestCase):
@parameterized.expand(CONNECT_ALL_SIBLINGS_TEST_CASES)
def test_connect_all_siblings(self, root: BinaryTreeNode, expected: List[int]):
actual = connect_all_siblings(root)
current = actual
for expected_val in expected:
self.assertEqual(current.data, expected_val)
current = current.next
self.assertIsNone(current)

@parameterized.expand(CONNECT_ALL_SIBLINGS_TEST_CASES)
def test_connect_all_siblings_ptr(self, root: BinaryTreeNode, expected: List[int]):
actual = connect_all_siblings_ptr(root)
current = actual
for expected_val in expected:
self.assertEqual(current.data, expected_val)
current = current.next
self.assertIsNone(current)


if __name__ == "__main__":
unittest.main()
4 changes: 2 additions & 2 deletions datastructures/trees/binary/tree/binary_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ def level_order_traversal(self) -> List[Any]:
return []

current_level: List[BinaryTreeNode] = [self.root]
levels: List[List[T]] = []
levels: List[List[Any]] = []

while current_level:
level: List[T] = []
level: List[Any] = []
next_level: List[BinaryTreeNode] = []

for node in current_level:
Expand Down
95 changes: 95 additions & 0 deletions datastructures/trees/binary/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional, Deque, List
from collections import deque
from datastructures.trees.binary.node import BinaryTreeNode


Expand Down Expand Up @@ -67,3 +69,96 @@ def lowest_common_ancestor_ptr(
ptr2 = ptr2.parent if ptr2.parent else node_one

return ptr1


def connect_all_siblings(root: Optional[BinaryTreeNode]) -> Optional[BinaryTreeNode]:
"""
Connects all siblings of a binary tree given the root, such that, the right most node is connected to the first node
on the next level using a 'next' pointer forming a kind of linked list data structure. The right most node on the
last level is set to None. On each level the nodes are pointed to each other via the next pointer
This assumes that the provided root is part of a perfect binary tree.

This uses a level order traversal utilizing a queue to traverse the nodes from the first level to the last level
adding the nodes to a list for further traversal. The levels list is then traversed connecting nodes to the next
node in the list via the next pointer. At this point the levels list will have the nodes in the correct order using
the level order traversal technique.

After the traversal, the root node will be the first element in the levels list and we simply return that which
will contain the modified tree with all siblings connected

This uses Space of O(n) as the levels list is required to handle the final iteration for the connections.
Time complexity is O(n) as we travers all the nodes in the tree.

Args:
root(BinaryTreeNode): root of a perfect binary tree
Returns:
BinaryTreeNode root such that the next pointer has been set to point to siblings
"""
if not root:
return root

# Queue will have the root node initially
queue: Deque[BinaryTreeNode] = deque([root])
levels: List[BinaryTreeNode] = [root]

while queue:
node = queue.popleft()
if node.left:
queue.append(node.left)
levels.append(node.left)
if node.right:
queue.append(node.right)
levels.append(node.right)

for idx in range(len(levels) - 1):
levels[idx].next = levels[idx + 1]

return levels[0]


def connect_all_siblings_ptr(
root: Optional[BinaryTreeNode],
) -> Optional[BinaryTreeNode]:
"""
Connects all siblings of a binary tree given the root, such that, the right most node is connected to the first node
on the next level using a 'next' pointer forming a kind of linked list data structure. The right most node on the
last level is set to None. On each level the nodes are pointed to each other via the next pointer
This assumes that the provided root is part of a perfect binary tree.

This performs a level order traversal using two pointers to traverse the tree utilizing constant space in the process
making it efficient in terms of space. However, this incurs a time complexity of O(n) as all the nodes in the tree
have to be traversed from the root to make the connections

Args:
root(BinaryTreeNode): root of a perfect binary tree
Returns:
BinaryTreeNode root such that the next pointer has been set to point to siblings

"""
# If the tree is empty, there's nothing to connect
if root is None:
return root

# Initialize two pointers:
# 'current' points to the current node being processed
# 'last' keeps track of the last node that was connected
current = root
last = root

# Loop continues until there are no more left children in the current level
while current.left:
# Connect the last node's 'next' to the current node's left child
last.next = current.left
# Update 'last' to point to this left child
last = last.next

# Connect the last node's 'next' to the current node's right child
last.next = current.right
# Update 'last' to point to this right child
last = last.next

# Move 'current' to the next node at the same level
current = current.next

# Return the root of the modified tree
return root
Loading