DSA 49: Binary Search Trees - Delete Review

A Thoroughly Commented Scenario-Style Coding of BST’s delete_node Function
data structures
algorithms
Author

Tony Phung

Published

March 6, 2025

1. delete_node Again! (ETA: A very long time)

2. TreeNode() class

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

3. insert_node() & insert_node_list() methods

def insert_node(root_node: TreeNode, target: int):
    current_node = root_node
    
    if target == current_node.data:
        print(f"Cant insert duplicates: t{target}c{current_node.data}")
        return
    
    if target < current_node.data: # go left
        if current_node.left:
            print(f"t{target}<-c{current_node.data}: going left...")
            current_node = current_node.left
            return insert_node(current_node, target)       
        else:
            # insert left
            current_node.left = TreeNode(target)
            print(f"t{target}<-c{current_node.data}: inserted left of c{current_node.data} at cl{current_node.left.data}",end="\n\n")
            return current_node.left
    else:
        if current_node.right:
            print(f"t{target}->c{current_node.data}: going left...")
            current_node = current_node.right
            return insert_node(current_node, target)       
        else:
            # insert right
            current_node.right = TreeNode(target)
            print(f"t{target}->c{current_node.data}: inserted right of c{current_node.data} at cl{current_node.right.data}",end="\n\n")
            return current_node.right    
        
def insert_node_clean2(root_node: TreeNode, target: int):
    current_node = root_node
    
    if target == current_node.data:
        print(f"Cant insert duplicates: t{target}c{current_node.data}")
        return
    
    if target < current_node.data: # go left
        if current_node.left:
            # print(f"t{target}<-c{current_node.data}: going left...")
            current_node = current_node.left
            return insert_node_clean2(current_node, target)       
        else:
            # insert left
            current_node.left = TreeNode(target)
            # print(f"t{target}<-c{current_node.data}: inserted left of c{current_node.data} at cl{current_node.left.data}",end="\n\n")
            return current_node.left
    else:
        if current_node.right:
            # print(f"t{target}->c{current_node.data}: going left...")
            current_node = current_node.right
            return insert_node_clean2(current_node, target)       
        else:
            # insert right
            current_node.right = TreeNode(target)
            # print(f"t{target}->c{current_node.data}: inserted right of c{current_node.data} at cl{current_node.right.data}",end="\n\n")
            return current_node.right    
        
def insert_node_list(root: TreeNode, node_list: list[int], show_results: bool=False):
    for node_int in node_list:
        if show_results:
            insert_node(root, node_int)
        else:
            insert_node_clean2(root, node_int)

4. traverse() method

def traverse(root: TreeNode):
    if not root:
        return
    traverse(root.left)
    print(root.data)
    traverse(root.right)

5. find_successor_node() method

5.1 Psuedo-Code Scenarios Only

def find_succ_node(target_node):
    print(f"t{target_node.data} has two kids[{target_node.left.data}][{target_node.right.data}]!...Find the successor child!")
    # Target has 2 kids: Determine s_node to replace t_node
    successor_node = target_node.right # because must be at least greater than target
    
    # [Case_1]  @[t.r]:             [s_node] has {0 left-child}, [s_node] replaces [t_node]
    # i. FIND S_SNODE:
    #                                 [50]t         - target to delete
    #                               X    [75]s      - current [s_node]
    # ii. REPLACE T_NODE:
    #                                 [75]s         - [target] <- [s_node] 
    #                               X    []         
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    
    # [Case_2]  @[t.r...l]:         [s_node] has {a left-child}, go to {left-most-child}
    #                               [s_node] is now {left-most-child} of {target.right}
    #                                 [50]t         - [target] to delete
    #                               X       [75]s   - prev [s_node] (target.right)
    #                                     [70]l...
    #                                    [60]l...   - new [s_node] (left-most-child of target.right)
    
    # [Case_2A] @[t.r...l]:         [s_node] has no {right-child}
    # i. FIND S_NODE:
    #                                 [50]t         - [target] to delete
    #                               X       [75]   - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is currently s_node}
    #                                    [60]s...   - current [s_node] (left-most-child of target.right)
    #                                   []  []      - [s_node] has no kids
    # ii. REPLACE T_NODE:
    #                                 [60]s         - [target] <- s_node
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is None} now!
    #                                    []...       
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    
    # [Case_2B] @[t.r...l]:         [s_node] has a {right-child}
    # i. FIND S_NODE:
    #                                 [50]t         - [target] to delete
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is currently s_node}
    #                                    [60]s...   - current [s_node] (left-most-child of target.right)
    #                                   []  [65]sr  - [s_node] has a [right-child]
    # ii. REPLACE T_NODE:
    #                                 [60]s         - [target] <- s_node
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is 65 (s_node.right)} 
    #                                    [65]sr...  - {s_node.right} replaces {s_node}       
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    
    
    
    target_node = s_node
    
    return s_node

5.2 The Code

def find_succ_node_final(t_node):
    print(f"t{t_node.data} has two kids[{t_node.left.data}][{t_node.right.data}]!...Find the successor child!")
    # Target has 2 kids: Determine s_node to replace t_node
    s_node = t_node.right # because must be at least greater than target
    
    # [Case_1]  @[t.r]:                 [s_node] has {0 left-child}, [s_node] replaces [t_node]
    if not s_node.left:             
        # i. FIND S_SNODE:
        #                                 [50]t         - target to delete
        #                               X    [75]s      - current [s_node]
        # ii. REPLACE T_NODE:
        #                                 [75]s         - [target] <- [s_node] 
        #                               X    []         
        # s_node = t_node
        print(f"[1]s_node[{s_node.data}] is our successor node!! will replace target_node[{t_node.data}]")
        t_node.data = s_node.data
        t_node.right = None

        # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
        return 
    
    # [Case_2]  @[t.r...l]:         [s_node] has {a left-child}, go to {left-most-child}
    while s_node.left:
        print(f"[2]s{s_node.data} has left_child[{s_node.left.data}]: keep going left...!")
        #                               [s_node] is now {left-most-child} of {target.right}
        #                                 [50]t         - [target] to delete
        #                               X       [75]s   - prev [s_node] (target.right)
        #                                     [70]l...
        #                                    [60]l...   - new [s_node] (left-most-child of target.right)
        parent_node = s_node
        s_node = s_node.left
    
    # [Case_2A] @[t.r...l]:         [s_node] has no {right-child}
    # i. FIND S_NODE:
    #                                 [50]t         - [target] to delete
    #                               X       [75]   - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is currently s_node}
    #                                    [60]s...   - current [s_node] (left-most-child of target.right)
    #                                   []  []      - [s_node] has right kids (and left thats alwyays cause we went the most left already)
    # ii. REPLACE T_NODE:
    if not s_node.right:
        print(f"[2A]s{s_node.data} has NO right child: p.left[{parent_node.left.data}] to None")
    #                                 [60]s         - [target] <- s_node
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is None} now!
    #                                    []...       
        parent_node.left = None
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    # [Case_2B] @[t.r...l]:         [s_node] has a {right-child}
    # i. FIND S_NODE:
    elif s_node.right:
        print(f"[2B]s{s_node.data} has a right child: p.left[{parent_node.left.data}] to [{s_node.right.data}]")
        parent_node.left = s_node.right
    #                                 [50]t         - [target] to delete
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is currently s_node}
    #                                    [60]s...   - current [s_node] (left-most-child of target.right)
    #                                   []  [65]sr  - [s_node] has a [right-child]
    #                                   [] [a] [b]
    # ii. REPLACE T_NODE:
    #                                 [60]s         - [target] <- s_node
    #                               X       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is 65 (s_node.right)} 
    #                                    [65]sr...  - {s_node.right} replaces {s_node}       
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    
    print(f"[3]s_node[{s_node.data}] is our successor node!! will replace target_node[{t_node.data}]")
    t_node.data = s_node.data

    return

6. delete_node() method

def delete_node_final(root_node: TreeNode, target: int):
    current_node = root_node
    target_node = None
    parent_node = None
    
    ### ----------------------------------- A. SEARCH ----------------------------------- ###
    while current_node:
        if target == current_node.data:
            target_node = current_node # t_node & c_node both point to target   
            print(f"t{target}c{current_node.data}: t_node{target_node.data} found, determine kids...")
            break
        elif target < current_node.data: # go left
            parent_node = current_node
            current_node = current_node.left
        else:
            parent_node = current_node
            current_node = current_node.right    
    
    if not target_node:
        print(f"{target} not found!")
        return
    # print("WTF")
    ### ----------------------------------- B. DELETE ----------------------------------- ###
    ### ----- I. 2 KIDS ----- ###
    if target_node.left and target_node.right:
        find_succ_node_final(target_node)
        return
    else:
        # root node: no parents
        targets_child_node = (target_node.left or target_node.right)
        if not parent_node:
            root_node.data = targets_child_node.data  # 50->25 cn and tn points
            
            ### ADD LOGIC: if Tchild.left or Tchild.right exists: assign
            root_node.left = targets_child_node.left
            root_node.right = targets_child_node.right
                #        [] no p - root
                #     [50]t  |     [25]c
                #   [25]c    |  {10}  {15}
                # 10 15      | 
            return
        elif parent_node.left == target_node: # target is non-root, left of parent
            parent_node.left = targets_child_node
                #       [45]p   or p
                #     [40]t  |  or p.left -> p.left=t.child
                #   [25]c    |  or t.child
                # 10 15      | 
            return
        elif parent_node.right == target_node: # target is non-root, right of parent
            parent_node.right = targets_child_node
            return
    return
# [Case_1]  @[t.r]:                 [s_node] has {0 left-child}, [s_node] replaces [t_node]
    # i. FIND S_SNODE:
    #                                 [50]t         - target to delete
    #                               25    [75]s      - current [s_node]
    # ii. REPLACE T_NODE:
    #                                 [75]s         - [target] <- [s_node] 
    #                               25    []         
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?
    
root = TreeNode(50)
node_list = [25,75]
insert_node_list(root, node_list)   
traverse(root) # 25,75
print()
delete_node_final(root, 50)
print()

print("expected: 25,75")
traverse(root) # 25,75
print()
print(f"{root.data},75")
print(f"{root.left.data},25")
# print(root.right.data)
25
50
75

t50c50: t_node50 found, determine kids...
t50 has two kids[25][75]!...Find the successor child!
[1]s_node[75] is our successor node!! will replace target_node[50]

expected: 25,75
25
75

75,75
25,25
    # [Case_2]  @[t.r...l]:         [s_node] has {a left-child}, go to {left-most-child}
        #                               [s_node] is now {left-most-child} of {target.right}
        #                                 [50]t         - [target] to delete
        #                              25       [75]s   - prev [s_node] (target.right)
        #                                     [70]l...
        #                                    [60]l...   - new [s_node] (left-most-child of target.right)
    
    # [Case_2A] @[t.r...l]:         [s_node] has no {right-child}
    # i. FIND S_NODE:
    #                                 [50]t         - [target] to delete
    #                              25       [75]   - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is currently s_node}
    #                                    [60]s...   - current [s_node] (left-most-child of target.right)
    #                                   []  []      - [s_node] has right kids (and left thats alwyays cause we went the most left already)
    # ii. REPLACE T_NODE:
    #                                 [60]s         - [target] <- s_node
    #                              25       [75]    - prev [s_node] (target.right)
    #                                     [70]p...  - {parent.left is None} now!
    #                                    []...       
    # iii. LOGIC CHECK:             is it next number after target in tree if in sequential order?

    
root = TreeNode(50)
node_list = [25,75,70,60]
insert_node_list(root, node_list)   
traverse(root) # 25,50,60,70,75
print()
delete_node_final(root, 50)
print()
print("expected: 25,60,70,75")
traverse(root) # 25,60,70,75
print()
print(f"{root.data},60") # 
print(f"{root.left.data},25") 
print(f"{root.right.data},75") 
print(f"{root.right.left.data},70") 
25
50
60
70
75

t50c50: t_node50 found, determine kids...
t50 has two kids[25][75]!...Find the successor child!
[2]s75 has left_child[70]: keep going left...!
[2]s70 has left_child[60]: keep going left...!
[2A]s60 has NO right child: p.left[60] to None
[3]s_node[60] is our successor node!! will replace target_node[50]

expected: 25,60,70,75
25
60
70
75

60,60
25,25
75,75
70,70
    # [Case_2B] @[t.r...l]:         [s_node] has a {right-child}
    # i. FIND S_NODE:

    #                         [50]t             - [target] to delete
    #                     25          [75]      - prev [s_node] (target.right)
    #                              [70]p...     - {parent.left is currently s_node}
    #                          [60]s...         - current [s_node] (left-most-child of target.right)
    #                         [] [65]sr         - [s_node] has a [right-child]
    # ii. REPLACE T_NODE:       
    #                        [60]s              - [target] <- s_node
    #                     25       [75]         - prev [s_node] (target.right)
    #                            [70]p...       - {parent.left is 65 (s_node.right)} 
    #                           [65]sr...       - {s_node.right} replaces {s_node}       
    # iii. LOGIC CHECK:                         - is it next number after target in tree if in sequential order?

root = TreeNode(50)
node_list = [25,75,70,60,65]
insert_node_list(root, node_list)   
traverse(root) # 25,50,60,70,75
print()
delete_node_final(root, 50)
print()
print("expected: 25,60,65,70,75")
traverse(root) # 25,60,65,70,75
print()
print(f"{root.data},60") # 
print(f"{root.left.data},25") 
print(f"{root.right.data},75") 
print(f"{root.right.left.data},70") 
print(f"{root.right.left.left.data},65") 
25
50
60
65
70
75

t50c50: t_node50 found, determine kids...
t50 has two kids[25][75]!...Find the successor child!
[2]s75 has left_child[70]: keep going left...!
[2]s70 has left_child[60]: keep going left...!
[2B]s60 has a right child: p.left[60] to [65]
[3]s_node[60] is our successor node!! will replace target_node[50]

expected: 25,60,65,70,75
25
60
65
70
75

60,60
25,25
75,75
70,70
65,65