Skip to content

Commit d74b74f

Browse files
authored
Merge pull request #174 from BrianLusina/feat/datastructures-binary-tree
feat(datastructures, trees): binary tree branch sum and node depth
2 parents cbcb729 + 80ffd18 commit d74b74f

11 files changed

Lines changed: 413 additions & 107 deletions

File tree

datastructures/trees/binary/node.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(
1717
right: Optional["BinaryTreeNode"] = None,
1818
key: Optional[Any] = None,
1919
parent: Optional["BinaryTreeNode"] = None,
20-
next: Optional["BinaryTreeNode"] = None,
20+
nxt: Optional["BinaryTreeNode"] = None,
2121
) -> None:
2222
"""
2323
Constructor for BinaryTreeNode class. This will create a new node with the provided data and optional
@@ -30,7 +30,7 @@ def __init__(
3030
right (Optional[BinaryTreeNode]): Right child of the node
3131
key (Optional[Any]): Key for the node, if not provided a hash of the data is used
3232
parent (Optional[BinaryTreeNode]): Parent of the node
33-
next (Optional[BinaryTreeNode]): Next child of the node which is the sibling of the node. The sibling is the
33+
nxt (Optional[BinaryTreeNode]): Next child of the node which is the sibling of the node. The sibling is the
3434
node on the same level as this node. If this is the rightmost node in the tree that is not on the last level
3535
of the tree, then this is the next node on the next level starting from the left. If this is the last node
3636
in the tree, then this is None.
@@ -42,7 +42,7 @@ def __init__(
4242
# node on a given level, then it is connected to the first node on the next level. If this node is the last node
4343
# in the tree on the last node, then it is pointed to None. By default, it is set to None.
4444
# Note that if this is the root node, it is connected to the left most node on the next level.
45-
self.next: Optional[BinaryTreeNode] = next
45+
self.next: Optional[BinaryTreeNode] = nxt
4646

4747
def insert_node(self, data: T) -> None:
4848
"""

datastructures/trees/binary/tree/binary_tree.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,3 +960,144 @@ def longest_uni_value_path(self) -> int:
960960
The length of the path between two nodes is represented by the number of edges between them.
961961
"""
962962
return longest_uni_value_path(self.root)
963+
964+
def branch_sum(self) -> List[Any]:
965+
"""
966+
Retrieves the sum of branches ordered from left most branch sum to right most branch sum. A branch sum is the
967+
sum of all values in a binary tree branch. A binary tree branch is a path of nodes in a tree that starts at the
968+
root node and ends at any leaf node.
969+
970+
For example given the tree below
971+
972+
1
973+
/ \
974+
2 3
975+
/ \ / \
976+
4 5 6 7
977+
/ \ /
978+
8 9 10
979+
980+
The output should be:
981+
[15, 16, 18, 10, 11]
982+
983+
15 == 1 + 2 + 4 + 8
984+
16 == 1 + 2 + 4 + 9
985+
18 == 1 + 2 + 5 + 10
986+
10 == 1 + 3 + 6
987+
11 == 1 + 3 + 7
988+
989+
The time complexity for the solution is O(n) because we have to traverse each element in the tree and the space
990+
complexity is also O(n) because of the recursion.
991+
"""
992+
if not self.root:
993+
return []
994+
995+
branch_totals = []
996+
997+
def preorder_traversal(
998+
node: Optional[BinaryTreeNode], running_sum: Any
999+
) -> None:
1000+
nonlocal branch_totals
1001+
if not node:
1002+
return
1003+
1004+
total_sum = running_sum + node.data
1005+
if not node.left and not node.right:
1006+
branch_totals.append(total_sum)
1007+
1008+
preorder_traversal(node.left, total_sum)
1009+
preorder_traversal(node.right, total_sum)
1010+
1011+
preorder_traversal(self.root, 0)
1012+
return branch_totals
1013+
1014+
def sum_of_node_depths(self) -> int:
1015+
"""
1016+
Returns the sum of depths of all nodes in the binary tree.
1017+
Node Depth is the distance between a node in a binary tree and the tree's root.
1018+
For example given the tree below
1019+
1020+
1
1021+
/ \
1022+
2 3
1023+
/ \ / \
1024+
4 5 6 7
1025+
/ \
1026+
8 9
1027+
Output should be: 16
1028+
The depth of node with value 2 is 1
1029+
The depth of node with value 3 is 1
1030+
The depth of node with value 4 is 2
1031+
The depth of node with value 5 is 2
1032+
etc ...
1033+
Summing all of these depths yields 16
1034+
1035+
This uses an iterative approach with a stack to get the sum of all the node depths. The stack will store the
1036+
node alongside the depth of the node and be used to calculate the total depth. This incurs a time complexity
1037+
cost of O(n) as we traverse all the nodes of the tree and space complexity cost of O(h) where h is the height
1038+
of the tree as we store all the nodes in the stack.
1039+
1040+
Returns:
1041+
int: Sum of depths of all nodes in the binary tree.
1042+
"""
1043+
1044+
if not self.root:
1045+
return 0
1046+
1047+
stack: List[Tuple[BinaryTreeNode, int]] = [(self.root, 0)]
1048+
total_depth: int = 0
1049+
1050+
while stack:
1051+
node, depth = stack.pop()
1052+
total_depth += depth
1053+
if node.left:
1054+
stack.append((node.left, depth + 1))
1055+
if node.right:
1056+
stack.append((node.right, depth + 1))
1057+
1058+
return total_depth
1059+
1060+
def sum_of_node_depths_recursive(self) -> int:
1061+
"""
1062+
Returns the sum of depths of all nodes in the binary tree using recursion.
1063+
Node Depth is the distance between a node in a binary tree and the tree's root.
1064+
For example given the tree below
1065+
1066+
1
1067+
/ \
1068+
2 3
1069+
/ \ / \
1070+
4 5 6 7
1071+
/ \
1072+
8 9
1073+
Output should be: 16
1074+
The depth of node with value 2 is 1
1075+
The depth of node with value 3 is 1
1076+
The depth of node with value 4 is 2
1077+
The depth of node with value 5 is 2
1078+
etc ...
1079+
Summing all of these depths yields 16
1080+
1081+
This uses a recursive approach to get the sum of all the node depths. This incurs a time complexity
1082+
cost of O(n) as we traverse all the nodes of the tree and space complexity cost of O(h) where h is the height
1083+
of the tree as we call the recursion stack h times on the height of the tree.
1084+
1085+
Returns:
1086+
int: Sum of depths of all nodes in the binary tree.
1087+
"""
1088+
1089+
if not self.root:
1090+
return 0
1091+
1092+
def sum_of_node_depths_helper(
1093+
node: Optional[BinaryTreeNode], depth: int
1094+
) -> int:
1095+
if not node:
1096+
return 0
1097+
return (
1098+
depth
1099+
+ sum_of_node_depths_helper(node.left, depth)
1100+
+ sum_of_node_depths_helper(node.right, depth)
1101+
)
1102+
1103+
return sum_of_node_depths_helper(self.root, 0)

datastructures/trees/heaps/__init__.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from typing import Any, List
2+
from typing import Any, List, Optional
33

44

55
class Heap(ABC):
@@ -8,12 +8,19 @@ class Heap(ABC):
88
"""
99

1010
@abstractmethod
11-
def insert_data(self, data: Any):
11+
def insert(self, data: Any):
1212
"""
1313
Inserts a data value into the heap
1414
"""
1515
raise NotImplementedError("Not yet implemented")
1616

17+
@abstractmethod
18+
def peak(self) -> Optional[Any]:
19+
"""
20+
Returns data at the top of the heap without removing it
21+
"""
22+
raise NotImplementedError("Not yet implemented")
23+
1724
@abstractmethod
1825
def delete(self) -> Any:
1926
"""
@@ -38,37 +45,46 @@ def get_right_child_index(i: int) -> int:
3845

3946
class ArrayBasedHeap(Heap):
4047
"""
41-
Heap datastructure that uses an array as the underlying datastructure to build a heap.
48+
Heap data structure that uses an array as the underlying data structure to build a heap.
4249
"""
4350

44-
def __init__(self):
51+
def __init__(self, data: Optional[List[Any]] = None):
4552
super().__init__()
46-
self.data: List[Any] = []
53+
if data is None:
54+
data = []
55+
self.heap: List[Any] = data
4756

4857
def __len__(self):
49-
return len(self.data)
58+
return len(self.heap)
5059

5160
@property
52-
def root_node(self):
61+
def root_node(self) -> Optional[Any]:
62+
"""
63+
Retrieves the root node of the Heap
64+
"""
65+
if len(self.heap) == 0:
66+
raise Exception("Heap is empty")
67+
return self.heap[0]
68+
69+
def peak(self) -> Optional[Any]:
5370
"""
5471
Retrieves the root node of the Heap
55-
:return:
5672
"""
57-
if len(self.data) == 0:
73+
if len(self.heap) == 0:
5874
raise Exception("Heap is empty")
59-
return self.data[0]
75+
return self.heap[0]
6076

6177
@property
6278
def last_node(self):
6379
"""
6480
Returns the last node of the heap
6581
:return:
6682
"""
67-
if len(self.data) == 0:
83+
if len(self.heap) == 0:
6884
raise Exception("Heap is empty")
69-
return self.data[len(self.data) - 1]
85+
return self.heap[len(self.heap) - 1]
7086

71-
def insert_data(self, data: Any):
87+
def insert(self, data: Any):
7288
"""
7389
Inserts a value into the heap
7490
:param data: element to insert into the heap

datastructures/trees/heaps/binary/max_heap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def __build_heap(self, array: List[HeapNode]) -> List[HeapNode]:
9191

9292
return array
9393

94-
def insert_data(self, data: Any):
94+
def insert(self, data: Any):
9595
"""
9696
Inserts a value into the heap
9797
"""

datastructures/trees/heaps/binary/max_heap/max_array_heap.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,42 @@ class MaxArrayBasedHeap(ArrayBasedHeap):
1111
def __init__(self):
1212
super().__init__()
1313

14-
def insert_data(self, value: Any):
14+
def insert(self, value: Any):
1515
"""
1616
Inserts a value into the heap
1717
"""
1818
# add the value into the last node
19-
self.data.append(value)
19+
self.heap.append(value)
2020

2121
# keep track of the index of the newly inserted node
22-
new_node_index = len(self.data) - 1
22+
new_node_index = len(self.heap) - 1
2323

2424
# the following executes the "trickle up" algorithm. If the new node is not in the root position and it's greater
2525
# than its parent node
2626
while (
2727
new_node_index > 0
28-
and self.data[new_node_index]
29-
> self.data[self.get_parent_index(new_node_index)]
28+
and self.heap[new_node_index]
29+
> self.heap[self.get_parent_index(new_node_index)]
3030
):
3131
# swap the new node with the parent node
3232
(
33-
self.data[self.get_parent_index(new_node_index)],
34-
self.data[new_node_index],
33+
self.heap[self.get_parent_index(new_node_index)],
34+
self.heap[new_node_index],
3535
) = (
36-
self.data[new_node_index],
37-
self.data[self.get_parent_index(new_node_index)],
36+
self.heap[new_node_index],
37+
self.heap[self.get_parent_index(new_node_index)],
3838
)
3939

4040
# update the index of the new node
4141
new_node_index = self.get_parent_index(new_node_index)
4242

4343
def delete(self) -> Any:
44-
if len(self.data) == 0:
44+
if len(self.heap) == 0:
4545
raise Exception("Heap is empty")
4646

4747
# we only ever delete the root node from a heap, so we pop the last node from the array and make it the root node
48-
root_node = self.data[0]
49-
self.data[0] = self.data.pop()
48+
root_node = self.heap[0]
49+
self.heap[0] = self.heap.pop()
5050

5151
# track the current index of the "trickle node". This is the node that will be moved into the correct position
5252
trickle_node_index = 0
@@ -57,9 +57,9 @@ def delete(self) -> Any:
5757
larger_child_index = self.__calculate_larger_child_index(trickle_node_index)
5858

5959
# swap the trickle node with its larger child
60-
self.data[trickle_node_index], self.data[larger_child_index] = (
61-
self.data[larger_child_index],
62-
self.data[trickle_node_index],
60+
self.heap[trickle_node_index], self.heap[larger_child_index] = (
61+
self.heap[larger_child_index],
62+
self.heap[trickle_node_index],
6363
)
6464

6565
trickle_node_index = larger_child_index
@@ -76,20 +76,20 @@ def __has_greater_child(self, index: int) -> bool:
7676
left_child_index = self.get_left_child_index(index)
7777
right_child_index = self.get_right_child_index(index)
7878

79-
left_child_exists = left_child_index < len(self.data)
80-
right_child_exists = right_child_index < len(self.data)
79+
left_child_exists = left_child_index < len(self.heap)
80+
right_child_exists = right_child_index < len(self.heap)
8181

8282
if left_child_exists and right_child_exists:
83-
left_child = self.data[left_child_index]
84-
right_child = self.data[right_child_index]
83+
left_child = self.heap[left_child_index]
84+
right_child = self.heap[right_child_index]
8585

86-
return left_child > self.data[index] or right_child > self.data[index]
86+
return left_child > self.heap[index] or right_child > self.heap[index]
8787
elif left_child_exists and not right_child_exists:
88-
left_child = self.data[left_child_index]
89-
return left_child > self.data[index]
88+
left_child = self.heap[left_child_index]
89+
return left_child > self.heap[index]
9090
elif right_child_exists and not left_child_exists:
91-
right_child = self.data[right_child_index]
92-
return right_child > self.data[index]
91+
right_child = self.heap[right_child_index]
92+
return right_child > self.heap[index]
9393
else:
9494
return False
9595

@@ -100,14 +100,14 @@ def __calculate_larger_child_index(self, index: int) -> int:
100100
:return: The position of the larger child
101101
"""
102102
# if there is no right child
103-
if not self.data[self.get_right_child_index(index)]:
103+
if not self.heap[self.get_right_child_index(index)]:
104104
# return the left child index
105105
return self.get_left_child_index(index)
106106

107107
# if right child value is greater than left child value
108108
if (
109-
self.data[self.get_right_child_index(index)]
110-
> self.data[self.get_left_child_index(index)]
109+
self.heap[self.get_right_child_index(index)]
110+
> self.heap[self.get_left_child_index(index)]
111111
):
112112
# return the right child index
113113
return self.get_right_child_index(index)

0 commit comments

Comments
 (0)