DSA 46: Binary Search Trees - Delete [Part 9]

Cleaner Implementation with Less Code
data structures
algorithms
Author

Tony Phung

Published

March 3, 2025

1. TreeNode, Binary Tree & insert_node: Setup

Introduced previously.

class TreeNode:
    def __init__(self, data, left=None, right=None):
        self.data = data
        self.left = left
        self.right = right


def insert_node(root_node: TreeNode, target: int):
    current_node = root_node
    if target == current_node.data:
        print(f"t[{target}]|c[{current_node.data}]: \t\tCannot insert duplicates")
        return  
    if target < current_node.data: # left
        if current_node.left: # go left-child: if exists
            print(f"t[{target}]|c[{current_node.data}]: \t\tCurrent[{current_node.data}] has ONE left-child : GO LEFT[{current_node.left.data}]")
            current_node=current_node.left
            return insert_node(current_node, target)
        else: # insert left-child: if not exist
            current_node.left = TreeNode(target)
            print(f"t[{target}]|c[{current_node.data}]: \t\tCurrent[{current_node.data}] has NO left-child: INSERTED LEFT CurrentLeft[{current_node.left.data}]")
            return current_node.left
    else:
        if current_node.right: # go right-child: if exists
            print(f"t[{target}]|c[{current_node.data}]: \t\tCurrent[{current_node.data}] has ONE right-child : GO right[{current_node.right.data}]")
            current_node=current_node.right
            return insert_node(current_node, target)
        else: # insert right-child: if not exist
            current_node.right = TreeNode(target)
            print(f"t[{target}]|c[{current_node.data}]: \t\tCurrent[{current_node.data}] has NO right-child: INSERTED right Currentright[{current_node.right.data}]")
    pass

def insert_node_clean(root_node: TreeNode, target: int):
    current_node = root_node
    if target == current_node.data:
        return  
    if target < current_node.data: # left
        if current_node.left: # go left-child: if exists
            current_node=current_node.left
            return insert_node_clean(current_node, target)
        else: # insert left-child: if not exist
            current_node.left = TreeNode(target)
            return current_node.left
    else:
        if current_node.right: # go right-child: if exists
            current_node=current_node.right
            return insert_node_clean(current_node, target)
        else: # insert right-child: if not exist
            current_node.right = TreeNode(target)
    pass

# create a basic tree
#             50
#      25           75
#  10     33     56     89  
# 4  11 30  40 52  61 82  95  
def insert_node_list(root_node:TreeNode, node_list: list[int], show_outputs:bool=True):
    for node in node_list: 
        if show_outputs:
            insert_node(root_node, node) 
        else:
            insert_node_clean(root_node, node) 

def create_tree_from_root_node(root_node = TreeNode(50),node_list=[25,75,10,33,56,89,4,11,30,40,52,61,82,95]):
    insert_node_list(root_node, node_list,show_outputs=False)
    

2. Confirm Tree

#             50
#      25           75
#  10     33     56     89  
# 4  11 30  40 52  61 82  95
root = TreeNode(50)
create_tree_from_root_node(root)
print(f"[L1] root.data: \t\t\t[{root.data}] (expected: 50)")
print()
print(f"[L2] root.left.data: \t\t\t[{root.left.data}] (expected: 25)")
print(f"[L2] root.right.data: \t\t\t[{root.right.data}] (expected: 75)")
print()
print(f"[L3] root.left.left.data:      \t\t[{root.left.left.data}] (expected: 10)")
print(f"[L3] root.left.right.data:     \t\t[{root.left.right.data}] (expected: 33)")
print(f"[L3] root.right.left.data:     \t\t[{root.right.left.data}] (expected: 56)")
print(f"[L3] root.right.right.data:    \t\t[{root.right.right.data}] (expected: 89)")
print()
print(f"[L4] root.left.left.left.data: \t\t[{root.left.left.left.data}]  (expected:  4)")
print(f"[L4] root.left.left.right.data:  \t[{root.left.left.right.data}] (expected: 11)")
print(f"[L4] root.left.right.left.data:  \t[{root.left.right.left.data}] (expected: 30)")
print(f"[L4] root.left.right.right.data: \t[{root.left.right.right.data}] (expected: 40)")
print()
print(f"[L4] root.right.left.left.data:  \t[{root.right.left.left.data}] (expected: 52)")
print(f"[L4] root.right.left.right.data: \t[{root.right.left.right.data}] (expected: 61)")
print(f"[L4] root.right.right.left.data: \t[{root.right.right.left.data}] (expected: 82)")
print(f"[L4] root.right.right.right.data:\t[{root.right.right.right.data}] (expected: 95)")
[L1] root.data:             [50] (expected: 50)

[L2] root.left.data:            [25] (expected: 25)
[L2] root.right.data:           [75] (expected: 75)

[L3] root.left.left.data:           [10] (expected: 10)
[L3] root.left.right.data:          [33] (expected: 33)
[L3] root.right.left.data:          [56] (expected: 56)
[L3] root.right.right.data:         [89] (expected: 89)

[L4] root.left.left.left.data:      [4]  (expected:  4)
[L4] root.left.left.right.data:     [11] (expected: 11)
[L4] root.left.right.left.data:     [30] (expected: 30)
[L4] root.left.right.right.data:    [40] (expected: 40)

[L4] root.right.left.left.data:     [52] (expected: 52)
[L4] root.right.left.right.data:    [61] (expected: 61)
[L4] root.right.right.left.data:    [82] (expected: 82)
[L4] root.right.right.right.data:   [95] (expected: 95)

3. find_successor_node

def find_successor_node(target_node: TreeNode):
    successor_node  = target_node.right # go right    
    print(f"t{target_node.data}.tr[{successor_node.data}]: go right...{successor_node.data}")        
    
    if not successor_node.left:
        # [75] to succeed [50]
        if successor_node.right:
            print(f"s{successor_node.data} has no left kids: s will t[{target_node.data}], t.right -> s.right[{successor_node.right.data}]")        
            target_node.data = successor_node.data # no need to update targets parents because only replacing data not nodes
            target_node.right = successor_node.right
        else:
            target_node.data = successor_node.data
            print(f"s{successor_node.data} has no left kids: s will succeed t[{target_node.data}], t.right -> s.right[{None}]")        
        pass

    while successor_node.left:
        print(f"s{successor_node.data}.l[{successor_node.left.data}]: s has a left_child, go left...")        
        
        parent_node = successor_node
        successor_node = successor_node.left
    
    print(f"s{successor_node.data}: s no further left_childs, check is s has a right_child...")        
    
    # successor_node is not left_most child
    # 1.  has no_right_child: no further action
    #   i.    parent_left = None instead of parent_left = s_node (removing s_node to replace target_node)
    #   ii    target.data = s_node.data
    
    if not successor_node.right:
        print(f"s{successor_node.data}: s has 0 right_child...p[{parent_node.data}] from p.left[{parent_node.left.data}] -> p.left[{None}]")        
        parent_node.left = None
        
    if successor_node.right:
        print(f"s{successor_node.data}: s has 1 right_child...p[{parent_node.data}] from p.left[{parent_node.left.data}] -> s[{successor_node.data}].right[{successor_node.right.data}]")        
        # print(f"s{successor_node.data}: succesor has 1 right_child...parent.left -> successors.right[{successor_node.right.data}]")        
        parent_node.left = successor_node.right
        
    # 2. has snode has_right_child
    #   i.  parent_left = snode_right_child
    #   ii  target.data = s_node.data
    
    print(f"s{successor_node.data} to succeed t[{target_node.data}]")
    target_node.data = successor_node.data
    return successor_node


# NO RIGHT CHILD
#           [t50]
#      25         [s75]

# NO RIGHT CHILD
#           [s75]
#      25         []

# RIGHT CHILD
#           [t50]
#      25         [s75]
#                       [sc80]

# RIGHT CHILD
#           [s75]
#      25         [sc80]
#                        [scc81]

4. delete_node

def delete_node(root_node: TreeNode, target: int):
    current_node = root_node
    parent_node = None
    target_node = None
    while current_node:
        if target == current_node.data:
            target_node = current_node
            print(f"t{target}==c{current_node.data}: target found! [Determine Children Count]...")        
            break
        elif target < current_node.data: # go left
            print(f"t{target}<-c{current_node.data}: go left...")        
            parent_node = current_node
            current_node = current_node.left
        else: # go right
            print(f"t{target}->c{current_node.data}: go right...")        
            parent_node = current_node
            current_node = current_node.right
    if not target_node:
        print(f"t{target}!=p{parent_node.data}: target not found because node has no more children!")        
        return
    
    if target_node.left and target_node.right:
        print(f"t{target}.lr[{target_node.left.data},{target_node.right.data}]: target has two kids! [Find Successor Node]...")        
        s_node = find_successor_node(target_node)
        return
    else: # 0 or 1 kid
        targets_child = (target_node.left or target_node.right)
        print(f"t[{target}]: target has 0 or 1 children! [Find Successor Node]...")        
        
        if not parent_node: # root node has no parents: replace parent (and its kids) with child (and its kids)
            print(f"t{target}.tc[{targets_child.data}]:tc[{targets_child.data}] succeeds t[{target}], no parents ptrs to update")        
            target_node.data = targets_child.data
            target_node.left = targets_child.left
            target_node.right = targets_child.right
        elif parent_node.left == target_node:
            if targets_child:
                print(f"t{target}.tc[{targets_child.data}]:tc[{targets_child.data}] succeeds t[{target}], p[{parent_node.data}]l[{parent_node.left.data}] updated to point to tc[{targets_child.data}]")        
                parent_node.left = targets_child
                print(f"t{target}.tc[{targets_child.data}]:final result p[{parent_node.data}]l[{parent_node.left.data}]")        
            else: 
                print(f"t{target} has no kids: {target} deleted, parent_node.left pointing to [None]")        
                parent_node.left = targets_child
            return 

        elif parent_node.right == target_node:
            if targets_child:
                print(f"t{target}.tc[{targets_child.data}]:tc[{targets_child.data}] succeeds t[{target}], p[{parent_node.data}]r[{parent_node.right.data}] updated to point to tc[{targets_child.data}]")        
                parent_node.right = targets_child
                print(f"t{target}.tc[{targets_child.data}]:final result p[{parent_node.data}]r[{parent_node.right.data}]")        
            else: 
                print(f"t{target} has no kids: {target} deleted, parent_node.right pointing to [None]")        
                parent_node.right = targets_child
                
            return 
        
    return 
#             50
#      25           75
#  10     33     56     89  
# 4  11 30  40 52  61 82  95 
root = None
root = TreeNode(50)
create_tree_from_root_node(root)
delete_node(root, 50)
# delete_node(root, 10)
t50==c50: target found! [Determine Children Count]...
t50.lr[25,75]: target has two kids! [Find Successor Node]...
t50.tr[75]: go right...75
s75.l[56]: s has a left_child, go left...
s56.l[52]: s has a left_child, go left...
s52: s no further left_childs, check is s has a right_child...
s52: s has 0 right_child...p[56] from p.left[52] -> p.left[None]
s52 to succeed t[50]
root = None
root = TreeNode(50)
create_tree_from_root_node(root, node_list=[25])
delete_node(root,50)


root.data
t50==c50: target found! [Determine Children Count]...
t[50]: target has 0 or 1 children! [Find Successor Node]...
t50.tc[25]:tc[25] succeeds t[50], no parents ptrs to update
25
root = None
root = TreeNode(50)
create_tree_from_root_node(root, node_list=[25,10])
delete_node(root,25)
t25<-c50: go left...
t25==c25: target found! [Determine Children Count]...
t[25]: target has 0 or 1 children! [Find Successor Node]...
t25.tc[10]:tc[10] succeeds t[25], p[50]l[25] updated to point to tc[10]
t25.tc[10]:final result p[50]l[10]
root = None
root = TreeNode(50)
create_tree_from_root_node(root, node_list=[51,52])
delete_node(root,51)
t51->c50: go right...
t51==c51: target found! [Determine Children Count]...
t[51]: target has 0 or 1 children! [Find Successor Node]...
t51.tc[52]:tc[52] succeeds t[51], p[50]r[51] updated to point to tc[52]
t51.tc[52]:final result p[50]r[52]
root = None
root = TreeNode(50)
create_tree_from_root_node(root)
delete_node(root, 4)
t4<-c50: go left...
t4<-c25: go left...
t4<-c10: go left...
t4==c4: target found! [Determine Children Count]...
t[4]: target has 0 or 1 children! [Find Successor Node]...
t4 has no kids: 4 deleted, parent_node.left pointing to [None]
root = None
root = TreeNode(50)
create_tree_from_root_node(root, node_list=[25,75])
delete_node(root,75)


# NO RIGHT CHILD
#           [t50]
#      25         [s75]

# NO RIGHT CHILD
#           [s75]
#      25         []
t75->c50: go right...
t75==c75: target found! [Determine Children Count]...
t[75]: target has 0 or 1 children! [Find Successor Node]...
t75 has no kids: 75 deleted, parent_node.right pointing to [None]

# RIGHT CHILD
#           [t50]
#      25         [s75]
#                       [sc80]

# RIGHT CHILD
#           [s75]
#      25         [sc80]
#                        [scc81]
#             [50]X
#      25            [75]
#  10     33      [56]     89  
# 4  11 30  40 [52]  61 82  95 
root = None
root = TreeNode(50)
create_tree_from_root_node(root)
delete_node(root, 50)


#             [52]
#      25            [75]
#  10     33      [56]     89  
# 4  11 30  40 []  61 82  95 

print(root.data) # expected 52
print(root.right.left.data) # expected 56
print(root.right.left.left.data) # expected error
t50==c50: target found! [Determine Children Count]...
t50.lr[25,75]: target has two kids! [Find Successor Node]...
t50.tr[75]: go right...75
s75.l[56]: s has a left_child, go left...
s56.l[52]: s has a left_child, go left...
s52: s no further left_childs, check is s has a right_child...
s52: s has 0 right_child...p[56] from p.left[52] -> p.left[None]
s52 to succeed t[50]
52
56
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[20], line 18
     16 print(root.data) # expected 52
     17 print(root.right.left.data) # expected 56
---> 18 print(root.right.left.left.data) # expected error

AttributeError: 'NoneType' object has no attribute 'data'
#             [50]X
#      25            [75]
#  10     33      [56]     89  
# 4  11 30  40 [52]  61 82  95 
#                [55]
root = None
root = TreeNode(50)
create_tree_from_root_node(root)
insert_node(root, 55)
delete_node(root, 50)

root.right.left.left.data # expected 55


#             [52]
#      25            [75]
#  10     33      [56]     89  
# 4  11 30  40 [55]  61 82  95 
#                  []
t[55]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[55]|c[75]:        Current[75] has ONE left-child : GO LEFT[56]
t[55]|c[56]:        Current[56] has ONE left-child : GO LEFT[52]
t[55]|c[52]:        Current[52] has NO right-child: INSERTED right Currentright[55]
t50==c50: target found! [Determine Children Count]...
t50.lr[25,75]: target has two kids! [Find Successor Node]...
t50.tr[75]: go right...75
s75.l[56]: s has a left_child, go left...
s56.l[52]: s has a left_child, go left...
s52: s no further left_childs, check is s has a right_child...
s52: s has 1 right_child...p[56] from p.left[52] -> s[52].right[55]
s52 to succeed t[50]
55