DSA 45: Binary Search Trees - Delete [Part 8]

Wip Attempt 2: Zero, 1 or 2 Children
data structures
algorithms
Author

Tony Phung

Published

February 28, 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) 
root = None
root = TreeNode(50)
node_list = [25,75,10,33,56,89,4,11,30,40,52,61,82,95]
insert_node_list(root, node_list,show_outputs=True)
t[25]|c[50]:        Current[50] has NO left-child: INSERTED LEFT CurrentLeft[25]
t[75]|c[50]:        Current[50] has NO right-child: INSERTED right Currentright[75]
t[10]|c[50]:        Current[50] has ONE left-child : GO LEFT[25]
t[10]|c[25]:        Current[25] has NO left-child: INSERTED LEFT CurrentLeft[10]
t[33]|c[50]:        Current[50] has ONE left-child : GO LEFT[25]
t[33]|c[25]:        Current[25] has NO right-child: INSERTED right Currentright[33]
t[56]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[56]|c[75]:        Current[75] has NO left-child: INSERTED LEFT CurrentLeft[56]
t[89]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[89]|c[75]:        Current[75] has NO right-child: INSERTED right Currentright[89]
t[4]|c[50]:         Current[50] has ONE left-child : GO LEFT[25]
t[4]|c[25]:         Current[25] has ONE left-child : GO LEFT[10]
t[4]|c[10]:         Current[10] has NO left-child: INSERTED LEFT CurrentLeft[4]
t[11]|c[50]:        Current[50] has ONE left-child : GO LEFT[25]
t[11]|c[25]:        Current[25] has ONE left-child : GO LEFT[10]
t[11]|c[10]:        Current[10] has NO right-child: INSERTED right Currentright[11]
t[30]|c[50]:        Current[50] has ONE left-child : GO LEFT[25]
t[30]|c[25]:        Current[25] has ONE right-child : GO right[33]
t[30]|c[33]:        Current[33] has NO left-child: INSERTED LEFT CurrentLeft[30]
t[40]|c[50]:        Current[50] has ONE left-child : GO LEFT[25]
t[40]|c[25]:        Current[25] has ONE right-child : GO right[33]
t[40]|c[33]:        Current[33] has NO right-child: INSERTED right Currentright[40]
t[52]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[52]|c[75]:        Current[75] has ONE left-child : GO LEFT[56]
t[52]|c[56]:        Current[56] has NO left-child: INSERTED LEFT CurrentLeft[52]
t[61]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[61]|c[75]:        Current[75] has ONE left-child : GO LEFT[56]
t[61]|c[56]:        Current[56] has NO right-child: INSERTED right Currentright[61]
t[82]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[82]|c[75]:        Current[75] has ONE right-child : GO right[89]
t[82]|c[89]:        Current[89] has NO left-child: INSERTED LEFT CurrentLeft[82]
t[95]|c[50]:        Current[50] has ONE right-child : GO right[75]
t[95]|c[75]:        Current[75] has ONE right-child : GO right[89]
t[95]|c[89]:        Current[89] has NO right-child: INSERTED right Currentright[95]

2. Confirm Tree

#             50
#      25           75
#  10     33     56     89  
# 4  11 30  40 52  61 82  95
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. delete_node: 0 or 1 Child Nodes Only

def delete_node(root_node: TreeNode, target: int, parent_node=None):
    current_node = root_node
    if target == current_node.data:
        # 2. delete_node - delete cases 1, 2 & 3.
        print(f"[Part 1: Search] t[{target}]==c[{current_node.data}]: \t\t\t\tNode Found...")
        print(f"[Part 2: Delete] t[{target}]==c[{current_node.data}]: \t\t\t\tDetermine Number of Children...")

        if (not current_node.left and not current_node.right): # 0-kids
            # Case A: Target has 0-kid --- [(0 left + 0 right) 
            # only 1 parent per node(p.left or p.right)
            if parent_node.left: # if exists burn it
                parent_node.left = None
            elif parent_node.right: 
                parent_node.right = None
            
            print(f"[Part2A: Del_0Kid] t[{target}]==c[{current_node.data}]._0_kid: \t\tBurn'em!🔥[{target}], Parent[{parent_node.data}] will survive 🍀.")
            return
        
        elif (current_node.left and not current_node.right) or (not current_node.left and current_node.right):
            # Case B: 1-kid --- [(1 left + 0 right) OR (0 left + 1 right)]
            if current_node.left:
                target_sgl_child = current_node.left 
            else:
                target_sgl_child = current_node.right
                
            print(f"[Part2B: Del_1Kid] t[{target}]==c[{current_node.data}]._1_kid[{target_sgl_child.data}]: \tIs it Left or Right? kids_gramps[{parent_node.data}]")
            if parent_node.left == current_node:
                parent_node.left = target_sgl_child     ##### 2BI  REPLACE TARGET WITH TARGETS LEFT-CHILD
                print(f"[Part2B: Del_1Kid] targets.child[{target_sgl_child.data}] has now assumed the identity of the target t[{target}|c{current_node.data}] and is the kid of it's (previous) gramps[{parent_node.data}]: aka now gramps.leftkid[{parent_node.left.data}]")
            elif parent_node.right == current_node: # target can be a left or right child
                parent_node.right = target_sgl_child    ##### 2BII REPLACE TARGET WITH TARGETS LEFT-CHILD
                print(f"[Part2B: Del_1Kid] targets.child[{target_sgl_child.data}] has now assumed the identity of the target t[{target}|c{current_node.data}] and is the kid of it's (previous) gramps[{parent_node.data}]: aka now gramps.rightkid[{parent_node.right.data}]")
            return
            
        else:
            # Case C: 2-kid --- [(1 left + 1 right)
            # [deleting_node]: 2KIDS, 
            # * replace [target] with [successor_node]. 
            # The [s_node] is the [child_node]: least of all descendents that are greater than target
            # how to: [target + all descendants in ascending order], [s_node] is next number after [target]
            print(f"[Part2C: Del_2Kid] t[{target}]==c[{current_node.data}]._2_kids[{current_node.left.data},{current_node.right.data}]:\t\tGO RIGHT-CHILD[{current_node.right.data}]")
            
            #             50
            #       25           75
            #    11    33    [56]    89  
            # []  [] 30  40 52  61 82  95

            # STEP 1 FOR ALL NODES WITH 2 KIDS - GO RIGHT ONCE:
            # Step 1: [GO-RIGHT] (for all scenarios)
            target_node = parent_node = current_node
            current_node = current_node.right  # target(aka current) always has 2_kids (right always exists)

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

            # go left as possible til no leaf (or left child)
            # while current_node:     
            while current_node:
                if current_node.left:
                    print(f"[Part3A: 2K_SNodeLeft] t[{target}],c[{current_node.data}].left_child exists[{current_node.left.data}]: \tgo left...Parent[{parent_node.data}]")
                    parent_node=current_node
                    current_node=current_node.left
                elif current_node.right: #no left + yes right (S_Node is Right)
                    print(f"[Part3B: 2K_SNodeRite] t[{target}],c[{current_node.data}].right_child exists [{current_node.right.data}]: \t do something...1...Parent[{parent_node.data}]")
                    print(f"expected c[{current_node.data}]: to [expected 52] with right[{current_node.right.data}] as [expected 55]")
                          
                          
                    # S_NODE HAS RIGHT_CHILD
                    # S_NODE replaces TARGET
                    # RIGHT_CHILDr replaces S_NODE
                    
                    # insert_node_clean(root,55)
                    #             [X]
                    #       25            75
                    #    11    33     61     89  
                    # []  [] 30  40 {52}  [] 82  95    CN: {52} -> BECOMES SNODE
                    #                 [55]
                    
                    #             (52)
                    #       25            75
                    #    11    33     61     89  
                    # []  [] 30  40 (55)  [] 82  95    CN: {52} -> BECOMES SNODE
                    #                 [X]
                    s_node = current_node
                    target_node.data = s_node.data
                    current_node.data = s_node.right.data
                    current_node.right = None
                    print(f"[Part3B: 2K_SNodeRite] t[{target}] replaced w' s_node[{target_node.data}], s_node[{target_node.data}] replaced w' right_child[{current_node.data}] ")
                    return s_node.data # 
                else:
                    print(f"[Part3C: 2K_SNodeLeaf] t[{target}],c[{current_node.data}].no_children): \tParent[{parent_node.data}]")
                    #             50
                    #       25           75
                    #    11    33     [X]    89  
                    # []  [] 30  40 52  61 82  95
                    s_node = current_node
                    # delete target by simply replacing the data????
                    target_node.data = s_node.data
                    # remove pointer from parent to current_node
                    if parent_node.left == current_node:
                        print(f"[Part3C: 2K_SNodeLeaf] t[{target}],c[{current_node.data}].is_s_node[{s_node.data}]): replaces target[{target}] \tParent[{parent_node.data}].left[{parent_node.left.data}] to be deleted")
                        parent_node.left=None
                        return
                    elif parent_node.right == current_node: # IF S_NODE IS RIGHT_CHILD WITH A CHILD, REPLACE ITSELF WITH THE CHILD
                        print(f"[Part3C: 2K_SNodeLeaf] t[{target}],c[{current_node.data}].is_s_node[{s_node.data}]): replaces target[{target}] \tParent[{parent_node.data}].right[{parent_node.right.data}] to be deleted")
                        parent_node.right=None
                        return
                    
                    return s_node.data # current is S_Node, target is deleted, s_node replaced target
                    
            # Step 2:[LEFT EXISTS]: keep_going_left -> ... -> [Leaf.F_left_child]:
                # Step 2a:  [LEAF F_right_child]: leaf is [s_node]  -> replace [target]
                # Step 2bi: [LEAF T_right_child]: leaf is [s_node]  -> replace [target]
                # Step 2bii:                      and leaf.right_child  -> replace [leaf]
                    

            # test 1: c has two kids
            # if current_node.left and current_node.right:
                # print(f"[Part2C: Delete] t[{target}]==c[{current_node.data}]._2_kids: \t\twhat a shame! ☠️ [TBA IN FUTURE]")
    
    # 1. search_node 
    if target < current_node.data: # go left
        if current_node.left:
            print(f"[Part 1: Search] t[{target}]<c[{current_node.data}].left_child exists[{current_node.left.data}]: \tgo left...")
            parent_node = current_node
            current_node = current_node.left
            return delete_node(current_node, target,parent_node)
        else:
            print(f"[Part 1: Search] t[{target}]<c[{current_node.data}].left_child doesnt exists[{None}]: Node Not Found!")
            return
    else:
        if current_node.right:
            print(f"[Part 1: Search] t[{target}]>c[{current_node.data}].right_child exists[{current_node.right.data}]: \tgo right...")
            parent_node = current_node
            current_node = current_node.right
            return delete_node(current_node, target,parent_node)
        else:
            print(f"[Part 1: Search] t[{target}]>c[{current_node.data}].right_child doesnt exists[{None}]: Node Not Found!")
            return

4. Testing

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

delete_node(root,4)
#             50
#      25           75
#    10     33     56     89  
# [X]  11 30  40 52  61 82  95

print()
delete_node(root,10)
#             50
#      25           75
#    11     33     56     89  
# []  [X] 30  40 52  61 82  95

print()
delete_node(root,56)
#             50
#       25           75
#    11    33     [X]    89  
# []  [] 30  40 52  61 82  95

print()
delete_node(root,56)
#             50
#       25             75
#    11    33    [61]     89  
# []  [] 30  40 52  [X] 82  95

print(f"root.data[{root.data}]: expected[50]")
print(f"root.right.data[{root.right.data}]: expected[75]")
print(f"root.right.left.data[{root.right.left.data}]: expected[61]")
print(f"root.right.right.data[{root.right.right.data}]: expected[89]")
print(f"root.right.left.left.data[{root.right.left.left.data}]: expected[52]")
print(f"root.right.left.right.data[{root.right.left.right}]: expected[None]") 
print(f"root.right.right.left.data[{root.right.right.left.data}]: expected[82]")
print(f"root.right.right.right.data[{root.right.right.right.data}]: expected[95]")

print()
print("INSERT 55 (RIGHT OF 52)")
insert_node(root,55)
# insert_node_clean(root,55)
#             50
#       25            75
#    11    33     61     89  
# []  [] 30  40 52  [] 82  95
#                [55]

print(root.right.left.left.right.data)

delete_node(root,50)
# insert_node_clean(root,55)
#             50
#       25            75
#    11    33     61     89  
# []  [] 30  40 52  [] 82  95
#                [55]
[Part 1: Search] t[4]<c[50].left_child exists[25]:  go left...
[Part 1: Search] t[4]<c[25].left_child exists[10]:  go left...
[Part 1: Search] t[4]<c[10].left_child exists[4]:   go left...
[Part 1: Search] t[4]==c[4]:                Node Found...
[Part 2: Delete] t[4]==c[4]:                Determine Number of Children...
[Part2A: Del_0Kid] t[4]==c[4]._0_kid:       Burn'em!🔥[4], Parent[10] will survive 🍀.

[Part 1: Search] t[10]<c[50].left_child exists[25]:     go left...
[Part 1: Search] t[10]<c[25].left_child exists[10]:     go left...
[Part 1: Search] t[10]==c[10]:              Node Found...
[Part 2: Delete] t[10]==c[10]:              Determine Number of Children...
[Part2B: Del_1Kid] t[10]==c[10]._1_kid[11]:     Is it Left or Right? kids_gramps[25]
[Part2B: Del_1Kid] targets.child[11] has now assumed the identity of the target t[10|c10] and is the kid of it's (previous) gramps[25]: aka now gramps.leftkid[11]

[Part 1: Search] t[56]>c[50].right_child exists[75]:    go right...
[Part 1: Search] t[56]<c[75].left_child exists[56]:     go left...
[Part 1: Search] t[56]==c[56]:              Node Found...
[Part 2: Delete] t[56]==c[56]:              Determine Number of Children...
[Part2C: Del_2Kid] t[56]==c[56]._2_kids[52,61]:     GO RIGHT-CHILD[61]
[Part3C: 2K_SNodeLeaf] t[56],c[61].no_children):    Parent[56]
[Part3C: 2K_SNodeLeaf] t[56],c[61].is_s_node[61]): replaces target[56]  Parent[61].right[61] to be deleted

[Part 1: Search] t[56]>c[50].right_child exists[75]:    go right...
[Part 1: Search] t[56]<c[75].left_child exists[61]:     go left...
[Part 1: Search] t[56]<c[61].left_child exists[52]:     go left...
[Part 1: Search] t[56]>c[52].right_child doesnt exists[None]: Node Not Found!
root.data[50]: expected[50]
root.right.data[75]: expected[75]
root.right.left.data[61]: expected[61]
root.right.right.data[89]: expected[89]
root.right.left.left.data[52]: expected[52]
root.right.left.right.data[None]: expected[None]
root.right.right.left.data[82]: expected[82]
root.right.right.right.data[95]: expected[95]

INSERT 55 (RIGHT OF 52)
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[61]
t[55]|c[61]:        Current[61] has ONE left-child : GO LEFT[52]
t[55]|c[52]:        Current[52] has NO right-child: INSERTED right Currentright[55]
55
[Part 1: Search] t[50]==c[50]:              Node Found...
[Part 2: Delete] t[50]==c[50]:              Determine Number of Children...
[Part2C: Del_2Kid] t[50]==c[50]._2_kids[25,75]:     GO RIGHT-CHILD[75]
[Part3A: 2K_SNodeLeft] t[50],c[75].left_child exists[61]:   go left...Parent[50]
[Part3A: 2K_SNodeLeft] t[50],c[61].left_child exists[52]:   go left...Parent[75]
[Part3B: 2K_SNodeRite] t[50],c[52].right_child exists [55]:      do something...1...Parent[61]
expected c[52]: to [expected 52] with right[55] as [expected 55]
[Part3B: 2K_SNodeRite] t[50] replaced w' s_node[52], s_node[52] replaced w' right_child[55] 
55