Python实现二叉搜索树的删除功能

二叉搜索树(二叉查找树,Binary Search Tree)又称为排序二叉树、有序二叉树。

本文使用 Python 实现二叉搜索树的删除功能,在此之前必须先知道二叉搜索树的特性:

1. 如果二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。

2. 如果二叉树的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。

3. 如果独立地看,左子树、右子树也分别为二叉搜素树。

一、准备二叉搜索树类

在实现二叉搜索树的删除功能前,先实现一个二叉搜索树的类 SearchBinaryTree 。

# coding=utf-8
class Node(object):
"""节点类"""
def __init__(self, data, left_child=None, right_child=None):
self.data = data
self.parent = None
self.left_child = left_child
self.right_child = right_child


class SearchBinaryTree(object):
"""二叉树类"""
def __init__(self):
self.__root = None
self.prefix_branch = '├'
self.prefix_trunk = '|'
self.prefix_leaf = '└'
self.prefix_empty = ''
self.prefix_left = '─L─'
self.prefix_right = '─R─'

def is_empty(self):
return not self.__root

@property
def root(self):
return self.__root

@root.setter
def root(self, value):
self.__root = value if isinstance(value, Node) else Node(value)

def show_tree(self):
if self.is_empty():
print('空二叉树')
return
print('-' * 20)
print(self.__root.data)
self.__print_tree(self.__root)
print('-' * 20)

def insert(self, root, value):
"""二叉搜索树插入节点-递归"""
node = value if isinstance(value, Node) else Node(value)
if self.is_empty():
self.root = node
return
if root is None:
root = node
elif node.data < root.data:
root.left_child = self.insert(root.left_child, value)
root.left_child.parent = root
elif node.data > root.data:
root.right_child = self.insert(root.right_child, value)
root.right_child.parent = root
return root

def __print_tree(self, node, prefix=None):
if prefix is None:
prefix, prefix_left_child = '', ''
else:
prefix = prefix.replace(self.prefix_branch, self.prefix_trunk)
prefix = prefix.replace(self.prefix_leaf, self.prefix_empty)
prefix_left_child = prefix.replace(self.prefix_leaf, self.prefix_empty)
if self.has_child(node):
if node.right_child is not None:
print(prefix + self.prefix_branch + self.prefix_right + str(node.right_child.data))
if self.has_child(node.right_child):
self.__print_tree(node.right_child, prefix + self.prefix_branch + ' ')
else:
print(prefix + self.prefix_branch + self.prefix_right)
if node.left_child is not None:
print(prefix + self.prefix_leaf + self.prefix_left + str(node.left_child.data))
if self.has_child(node.left_child):
prefix_left_child += ' '
self.__print_tree(node.left_child, self.prefix_leaf + prefix_left_child)
else:
print(prefix + self.prefix_leaf + self.prefix_left)

def has_child(self, node):
return node.left_child is not None or node.right_child is not None

def __str__(self):
return str(self.__class__)

上面的代码实现了一个节点类 Node,实现了二叉搜索树的类 SearchBinaryTree。在 SearchBinaryTree 中,实现了判断二叉搜索树是否为空的 is_empty() 方法、一对供实例对象调用的 root() 方法、按树形结构打印二叉搜索树的 show_tree() 方法和添加数据到二叉搜索树中的 insert(root, value)方法。 

现在先在树中添加一些节点,供后面的删除方法使用。

if __name__ == '__main__':
tree = SearchBinaryTree()
data = [50, 77, 55, 29, 10, 30, 66, 80, 51, 18, 90, 78, 79]
for i in data:
tree.insert(tree.root, i)
tree.show_tree()

运行结果:

--------------------
50
├─R─77
| ├─R─80
| | ├─R─90
| | └─L─78
| | ├─R─79
| | └─L─
| └─L─55
| ├─R─66
| └─L─51
└─L─29
├─R─30
└─L─10
├─R─18
└─L─
--------------------

添加数据后的二叉搜索树如下图:

Python实现二叉搜索树的删除功能_二叉树的删除功能

添加完成节点后,继续准备删除节点时需要使用到的方法。返回二叉搜索树的高度的方法 height(root),用于判断被删除的节点在树的第几层。在二叉搜索树中查找节点的方法 search(root, data),要从二叉搜索树中删除节点,首先要保证节点是属于二叉搜索树的,所以先搜索节点是否在二叉搜索树中。返回二叉搜索树中值最大的节点的方法 get_max(root) 和返回值最小的节点的方法 get_min(root) ,用于寻找被删除节点的前继节点或后继节点。

def height(self, root):
"""二叉树的深度"""
if root.data is None:
return 0
if root.left_child is None and root.right_child is None:
return 1
if root.left_child is not None and root.right_child is None:
return 1 + self.height(root.left_child)
if root.left_child is None and root.right_child is not None:
return 1 + self.height(root.right_child)
if root.left_child is not None and root.right_child is not None:
return 1 + max(self.height(root.left_child), self.height(root.right_child))

def search(self, root, data):
"""二叉搜索树的查询操作"""
if root is None:
return
if root.data == data:
return root
elif data < root.data:
return self.search(root.left_child, data)
elif data > root.data:
return self.search(root.right_child, data)

def get_max(self, root):
"""查找二叉搜索树值最大的节点"""
if self.is_empty():
return
return self.get_max(root.right_child) if root.right_child else root

def get_min(self, root):
"""查找二叉搜索树值最小的节点"""
if self.is_empty():
return
return self.get_min(root.left_child) if root.left_child else root

二、二叉搜索树删除节点

如果被删除的节点是叶节点,比较好办,找到这个节点,然后删除。如果被删除的节点不是叶节点,节点有左子树或右子树,或两者都有,删除节点后,左子树和右子树会跟着节点一起从树中“断”掉,那就不仅仅是删除节点了,而是把子树给全部删除了。所以,删除非叶节点时,必须从子树中选择一个节点来填补被删除的节点位置,避免树的断裂,也避免“牵连”到其他的节点,还要保证删除节点后的二叉树依然是一棵二叉搜索树,满足二叉搜索树的特性。

由简到繁,根据被删除节点是否有左右子树,二叉搜索树的删除可以分为三种情况,被删除节点为叶节点、被删除节点只有一棵子树和被删除节点有两棵子树。

1. 被删除节点为叶节点,即被删除节点没有左子树和右子树。删除叶节点后,不会破坏整棵树的结构,只要找到此节点,然后将节点从二叉树中“断开”(父节点的指针指向空)即可。在上面添加数据后的二叉搜索树中,随机选一个叶节点,如 66。

Python实现二叉搜索树的删除功能_Python二叉树的删除功能_02

def _delete_no_child(self, node):
"""删除叶节点"""
if node == self.root:
self.root = None
return
if node == node.parent.left_child:
node.parent.left_child = None
else:
node.parent.right_child = None
node.parent = None

_delete_no_child(node): 删除叶节点的方法。这个方法只属于删除的部分功能,所以在前面加一个下划线,表示等删除功能完全实现后,不会再直接使用。

node = tree.search(tree.root, 66)
tree._delete_no_child(node)
tree.show_tree()

运行结果:

--------------------
50
├─R─77
| ├─R─80
| | ├─R─90
| | └─L─78
| | ├─R─79
| | └─L─
| └─L─55
| ├─R─
| └─L─51
└─L─29
├─R─30
└─L─10
├─R─18
└─L─
--------------------

删除节点 66 后的二叉搜索树结构如下图。

Python实现二叉搜索树的删除功能_二叉搜索树的删除功能_03

2. 被删除节点只有一棵子树,即被删除节点只有左子树或只有右子树。删除节点后,为了保证节点的子树不被“牵连”,要将节点的子树进行“补位”。如删除下图中的节点 10,被删除节点是其父节点的左子节点,被删除节点有右子树,则删除节点后,直接将被删除节点的右子树补位成为其父节点的左子树。

Python实现二叉搜索树的删除功能_二叉树的删除功能_04

def _delete_one_child(self, node):
"""删除有一个子节点的节点"""
if node.left_child:
if node.parent and node.parent.left_child == node:
node.left_child.parent = node.parent
node.parent.left_child = node.left_child
elif node.parent and node.parent.right_child == node:
node.left_child.parent = node.parent
node.parent.right_child = node.left_child
else:
self.root = node.left_child
node.left_child.parent = None
node.left_child = None
else:
if node.parent and node.parent.left_child == node:
node.right_child.parent = node.parent
node.parent.left_child = node.right_child
elif node.parent and node.parent.right_child == node:
node.right_child.parent = node.parent
node.parent.right_child = node.right_child
else:
self.root = node.right_child
node.right_child.parent = None
node.right_child = None
node.left_child = None
node.parent = None

_delete_one_child(node): 删除只有一棵子树的节点。同理,这个方法也只属于删除的部分功能,所以在前面加一个下划线,表示删除功能完全实现后,不会直接使用。

node = tree.search(tree.root, 10)
tree._delete_one_child(node)
tree.show_tree()

运行结果:

--------------------
50
├─R─77
| ├─R─80
| | ├─R─90
| | └─L─78
| | ├─R─79
| | └─L─
| └─L─55
| ├─R─66
| └─L─51
└─L─29
├─R─30
└─L─18
--------------------

删除节点 10 后的二叉搜索树结构如下图。

Python实现二叉搜索树的删除功能_Python二叉树的删除功能_05

3. 被删除节点有两棵子树,即被删除节点同时有左子树和右子树。删除节点后,为了避免两棵子树从树中“断裂”,必须找到一个节点来填补被删除节点的位置。如删除下图中的节点 77,必须找一个节点来补位,否则两棵子树也会被删除。

Python实现二叉搜索树的删除功能_Python二叉树的删除_06

为了保证删除节点后二叉树仍然是一棵二叉搜索树,补位节点有两种选择方式,选择被删除节点的前继节点或后继节点,前继节点一定存在左子树中,后继节点一定存在右子树中,选择两种方式都可以,本文选择后继节点。

那什么是后继节点和前继节点?如果将二叉搜索树中的节点按存储的数值的大小升序排列,排在当前节点的后一个节点就是后继节点,排在当前节点的前一个节点就是前继节点。节点 77 的后继节点是 78。

Python实现二叉搜索树的删除功能_二叉搜索树的删除功能_07

将节点删除后,找到它的后继节点,将后继节点补位到被删除节点的位置,这样就保证了被删除节点的两棵子树没有断裂。但是还没有完,后继节点补位之后,相当于是将后继节点从它原来的位置删除了。节点 78 补位到 77 的位置后,相当于将节点 78 从原来的位置删除了。

所以还要对后继节点进行处理,后继节点可能是叶节点,可以用情况1,后继节点可能有右子树,可以用情况2,如节点 78,后继节点不可能有左子树,不用考虑。

为什后继节点一定没有左子树?假设后继节点有左子树,根据二叉搜索树的特性,后继节点的值比当前节点大,后继节点一定在当前节点的右子树中,而后继节点的左子树中的节点值要小于后继节点的值,这样,就存在比当前节点大又比后继节点小的节点,与后继节点的定义矛盾,所以假设不成立。

当然这里并不是说二叉搜索树中所有节点的后继节点都没有左子树,而是当一个节点有两棵子树时,它的后继节点不可能有左子树。

到这里,删除节点 77 分为两步,第一步是找到后继节点 78,将节点 78 补位到 77 的位置,第二步是删除节点 78 。

Python实现二叉搜索树的删除功能_二叉树的删除功能_08

def delete(self, value):
"""二叉搜索树删除节点"""
if self.height(self.root) >= 1:
node_delete = self.search(self.root, value)
if node_delete:
self._delete(node_delete)
else:
raise KeyError("Error, value not in this tree")
else:
raise KeyError("Error, value not in this tree")

def _delete(self, node):
"""删除节点"""
if not node.left_child and not node.right_child:
self._delete_no_child(node)
elif node.left_child and not node.right_child or not node.left_child and node.right_child:
self._delete_one_child(node)
else:
rear_node = self.get_min(node.right_child)
node.data = rear_node.data
self._delete(rear_node)

_delete(node): 删除节点的方法。该方法中通过 get_min() 方法找到当前节点的后继节点 rear_node ,然后进行补位操作。而删除后继节点的操作,只要递归一次就可以,因为删除后继节点的方法一定属于前面的两种情况之一。

delete(node): 删除节点的通用方法。前面提过,要删除节点,首先节点要属于当前二叉搜索树。所以无法指定节点来删除,只能根据节点中存储的值,先在树中进行搜索,找到了再执行删除操作。

tree.delete(77)
tree.show_tree()

运行结果:

--------------------
50
├─R─78
| ├─R─80
| | ├─R─90
| | └─L─79
| └─L─55
| ├─R─66
| └─L─51
└─L─29
├─R─30
└─L─10
├─R─18
└─L─
--------------------

删除节点 77 后的二叉搜索树结构如下图。

Python实现二叉搜索树的删除功能_Python二叉树的删除功能_09

三、完整代码

# coding=utf-8
class Node(object):
"""节点类"""
def __init__(self, data, left_child=None, right_child=None):
self.data = data
self.parent = None
self.left_child = left_child
self.right_child = right_child


class SearchBinaryTree(object):
"""二叉树类"""
def __init__(self):
self.__root = None
self.prefix_branch = '├'
self.prefix_trunk = '|'
self.prefix_leaf = '└'
self.prefix_empty = ''
self.prefix_left = '─L─'
self.prefix_right = '─R─'

def is_empty(self):
return not self.__root

@property
def root(self):
return self.__root

@root.setter
def root(self, value):
self.__root = value if isinstance(value, Node) else Node(value)

def show_tree(self):
if self.is_empty():
print('空二叉树')
return
print('-' * 20)
print(self.__root.data)
self.__print_tree(self.__root)
print('-' * 20)

def insert(self, root, value):
"""二叉搜索树插入节点-递归"""
node = value if isinstance(value, Node) else Node(value)
if self.is_empty():
self.root = node
return
if root is None:
root = node
elif node.data < root.data:
root.left_child = self.insert(root.left_child, value)
root.left_child.parent = root
elif node.data > root.data:
root.right_child = self.insert(root.right_child, value)
root.right_child.parent = root
return root

def delete(self, value):
"""二叉搜索树删除节点"""
if self.height(self.root) >= 1:
node_delete = self.search(self.root, value)
if node_delete:
self._delete(node_delete)
else:
raise KeyError("Error, value not in this tree")
else:
raise KeyError("Error, value not in this tree")

def _delete(self, node):
"""删除节点"""
if not node.left_child and not node.right_child:
self._delete_no_child(node)
elif node.left_child and not node.right_child or not node.left_child and node.right_child:
self._delete_one_child(node)
else:
rear_node = self.get_min(node.right_child)
node.data = rear_node.data
self._delete(rear_node)

def _delete_no_child(self, node):
"""删除叶节点"""
if node == self.root:
self.root = None
return
if node == node.parent.left_child:
node.parent.left_child = None
else:
node.parent.right_child = None
node.parent = None

def _delete_one_child(self, node):
"""删除有一个子节点的节点"""
if node.left_child:
if node.parent and node.parent.left_child == node:
node.left_child.parent = node.parent
node.parent.left_child = node.left_child
elif node.parent and node.parent.right_child == node:
node.left_child.parent = node.parent
node.parent.right_child = node.left_child
else:
self.root = node.left_child
node.left_child.parent = None
node.left_child = None
else:
if node.parent and node.parent.left_child == node:
node.right_child.parent = node.parent
node.parent.left_child = node.right_child
elif node.parent and node.parent.right_child == node:
node.right_child.parent = node.parent
node.parent.right_child = node.right_child
else:
self.root = node.right_child
node.right_child.parent = None
node.right_child = None
node.left_child = None
node.parent = None

def height(self, root):
"""二叉树的深度"""
if root.data is None:
return 0
if root.left_child is None and root.right_child is None:
return 1
if root.left_child is not None and root.right_child is None:
return 1 + self.height(root.left_child)
if root.left_child is None and root.right_child is not None:
return 1 + self.height(root.right_child)
if root.left_child is not None and root.right_child is not None:
return 1 + max(self.height(root.left_child), self.height(root.right_child))

def search(self, root, data):
"""二叉搜索树的查询操作"""
if root is None:
return
if root.data == data:
return root
elif data < root.data:
return self.search(root.left_child, data)
elif data > root.data:
return self.search(root.right_child, data)

def get_max(self, root):
"""查找二叉搜索树值最大的节点"""
if self.is_empty():
return
return self.get_max(root.right_child) if root.right_child else root

def get_min(self, root):
"""查找二叉搜索树值最小的节点"""
if self.is_empty():
return
return self.get_min(root.left_child) if root.left_child else root

def __print_tree(self, node, prefix=None):
if prefix is None:
prefix, prefix_left_child = '', ''
else:
prefix = prefix.replace(self.prefix_branch, self.prefix_trunk)
prefix = prefix.replace(self.prefix_leaf, self.prefix_empty)
prefix_left_child = prefix.replace(self.prefix_leaf, self.prefix_empty)
if self.has_child(node):
if node.right_child is not None:
print(prefix + self.prefix_branch + self.prefix_right + str(node.right_child.data))
if self.has_child(node.right_child):
self.__print_tree(node.right_child, prefix + self.prefix_branch + ' ')
else:
print(prefix + self.prefix_branch + self.prefix_right)
if node.left_child is not None:
print(prefix + self.prefix_leaf + self.prefix_left + str(node.left_child.data))
if self.has_child(node.left_child):
prefix_left_child += ' '
self.__print_tree(node.left_child, self.prefix_leaf + prefix_left_child)
else:
print(prefix + self.prefix_leaf + self.prefix_left)

def has_child(self, node):
return node.left_child is not None or node.right_child is not None

def __str__(self):
return str(self.__class__)


if __name__ == '__main__':
tree = SearchBinaryTree()
data = [50, 77, 55, 29, 10, 30, 66, 80, 51, 18, 90, 78, 79]
for i in data:
tree.insert(tree.root, i)
# tree.show_tree()

# node = tree.search(tree.root, 66)
# tree._delete_no_child(node)
# tree.show_tree()

# node = tree.search(tree.root, 10)
# tree._delete_one_child(node)
# tree.show_tree()

tree.delete(80)
tree.show_tree()

 

Python实现二叉搜索树的删除功能_二叉树的删除功能_10