1. 二叉树结构可视化
思路
为了避免重合,二叉树左右子树的距离是关键
让每一个节点占用一个列空间,这样就不会重合了
所以
左节点与父节点在X轴上的距离为 左节点 的 右子树宽度+1 乘以一个水平距离常数
右节点与父节点在X轴上的距离为 右节点 的 左子树宽度+1 乘以一个水平距离常数
每当画好一个节点点,确定其左右孩子在X轴上的距离,再画这个节点连接孩子的边
from matplotlib import pyplot as plt
class Node(object):
"""二叉树节点,节点有左右孩子,左孩子left_child,右孩子right_chile"""
def __init__(self, value):
"""
初始化节点
:param value: 节点中所保存的值
"""
self.left_child = None # 左孩子
self.right_child = None # 右孩子
self.value = value
def __str__(self):
return self.value
class BiTree(object):
def __init__(self, d_hor=4, d_vec=8, radius=1.5, figsize =(11, 9)):
"""
对所展示二叉树的一些元素参数的设置
:param d_hor: 节点与节点的水平距离
:param d_vec: 节点与节点的垂直距离
:param radius: 节点的半径
:param radius: 画布大小,用一个元祖表示画布width和high,单位为inch
"""
self.root = None
self.d_hor = d_hor
self.d_vec = d_vec
self.radius = radius
self.figsize = figsize
def get_left_width(self, root):
"""获得根左边宽度,也是根的左子孙节点数"""
return self.get_width(root.left_child)
def get_right_width(self, root):
"""获得根右边宽度,也是根的右子孙节点数"""
return self.get_width(root.right_child)
def get_width(self, root):
"""获得树的宽度,也是该树的节点数"""
if root:
return self.get_width(root.left_child) + 1 + self.get_width(root.right_child)
else:
return 0
def get_height(self, root):
"""获得二叉树的高度"""
if root:
return max(self.get_height(root.left_child), self.get_height(root.right_child)) + 1
else:
return 0
def get_w_h(self, root):
"""获得树的宽度和高度"""
w = self.get_width(root)
h = self.get_height(root)
return w, h
def __draw_a_node(self, x, y, value, ax):
"""画一个节点"""
c_node = plt.Circle((x, y), radius=self.radius, color="#65DDFF")
ax.add_patch(c_node)
plt.text(x, y, value, ha='center', va='center', fontsize=25, )
def __draw_a_edge(self, x1, y1, x2, y2):
"""画一条边"""
x = (x1, x2)
y = (y1, y2)
plt.plot(x, y, 'g-')
def __create_win(self, root):
"""创建窗口"""
# WEIGHT: 树宽,HEIGHT: 树高
WEIGHT, HEIGHT = self.get_w_h(root)
# WEIGHT:树宽 + 1
WEIGHT = (WEIGHT+1)*self.d_hor
# HEIGHT = 树高+1
HEIGHT = (HEIGHT+1)*self.d_vec
# print(WEIGHT, HEIGHT)
# fig = plt.figure(figsize=(a, b), dpi=dpi)
# 设置图形的大小,a 为图形的宽, b 为图形的高,单位为英寸
# dpi 为设置图形每英寸(inch)的点数
# 1点(英美点)=0.3527毫米=1/72英寸(Office里面的点)。
# 线条,标记,文本等大多数元素都有以磅(point即点)为单位的大小。因1inch = 72point,则72dp/inch=1dp/point、144dp/inch=2dp/point
fig = plt.figure(figsize=self.figsize)
ax = fig.add_subplot(111) # 表示整个figure分成1行1列,共1个子图,这里子图在第一行第一列
plt.xlim(0, WEIGHT) # 设定x座标轴的范围,当前axes上的座标轴。
plt.ylim(0, HEIGHT) # 设定y座标轴的范围,当前axes上的座标轴。
x = (self.get_left_width(root) + 1) * self.d_hor # x, y 是第一个要绘制的节点坐标,由其左子树宽度决定
y = HEIGHT - self.d_vec
return fig, ax, x, y
def __print_tree_by_preorder(self, root, x, y, ax):
"""通过先序遍历打印二叉树"""
if not root:
# 根节点为空返回
return
# 画节点
self.__draw_a_node(x, y, root.value, ax)
# 画左右分支
lx = rx = 0
ly = ry = y - self.d_vec
if root.left_child:
lx = x - self.d_hor * (self.get_right_width(root.left_child) + 1) # x-左子树的右边宽度
self.__draw_a_edge(x, y, lx, ly)
# print(root.left_child, (lx, ly))
if root.right_child:
rx = x + self.d_hor * (self.get_left_width(root.right_child) + 1) # x-右子树的左边宽度
# print(root.right_child, (rx, ry))
self.__draw_a_edge(x, y, rx, ry)
# 递归打印
self.__print_tree_by_preorder(root.left_child, lx, ly, ax)
self.__print_tree_by_preorder(root.right_child, rx, ry, ax)
def show_BiTree(self):
"""可视化二叉树"""
_, ax, x, y = self.__create_win(self.root)
self.__print_tree_by_preorder(self.root, x, y, ax)
plt.show()
def __create_tree(self):
value = input("输入节点数据:").strip()
if value:
node = Node(value)
node.left_child = self.__create_tree()
node.right_child = self.__create_tree()
return node
else:
return None
def create_tree(self):
self.root = self.__create_tree()
if __name__ == '__main__':
bi_tree = BiTree()
bi_tree.create_tree()
print("+++++++++++++++++++")
bi_tree.show_BiTree()
测试
输入节点数据:5
输入节点数据:6
输入节点数据:
输入节点数据:2
输入节点数据:
输入节点数据:
输入节点数据:1
输入节点数据:
输入节点数据:3
输入节点数据:0
输入节点数据:
输入节点数据:
输入节点数据:
+++++++++++++++++++
Process finished with exit code 0
[参考博客]
用pyplot打印二叉树,实现二叉树的可视化
2. 二叉树中一些其它常用方法
# BiTree
from matplotlib import pyplot as plt
from collections import deque
class Node(object):
"""二叉树节点,节点有左右孩子,左孩子left_child,右孩子right_chile"""
def __init__(self, value):
"""
初始化节点
:param value: 节点中所保存的值
"""
self.left_child = None # 左孩子
self.right_child = None # 右孩子
self.value = value
def __str__(self):
return self.value
class BiTree(object):
"""二叉树及其常用方法"""
def __init__(self, d_hor=4, d_vec=8, radius=1.5, figsize =(11, 9)):
"""
对所展示二叉树的一些元素参数的设置
:param d_hor: 节点与节点的水平距离
:param d_vec: 节点与节点的垂直距离
:param radius: 节点的半径
:param radius: 画布大小,用一个元祖表示画布width和high,单位为inch
"""
self.root = None
self.d_hor = d_hor
self.d_vec = d_vec
self.radius = radius
self.figsize = figsize
def get_left_width(self, root):
"""获得根左边宽度,也是根的左子孙节点数"""
return self.get_width(root.left_child)
def get_right_width(self, root):
"""获得根右边宽度,也是根的右子孙节点数"""
return self.get_width(root.right_child)
def get_width(self, root):
"""获得树的宽度,也是该树的节点数。使用的是中序遍历方式"""
if root:
return self.get_width(root.left_child) + 1 + self.get_width(root.right_child)
else:
return 0
def get_height(self, root):
"""获得二叉树的高度, 使用后序遍历"""
if root:
return max(self.get_height(root.left_child), self.get_height(root.right_child)) + 1
else:
return 0
def get_w_h(self, root):
"""获得树的宽度和高度"""
w = self.get_width(root)
h = self.get_height(root)
return w, h
def __draw_a_node(self, x, y, value, ax):
"""画一个节点"""
c_node = plt.Circle((x, y), radius=self.radius, color="#65DDFF")
ax.add_patch(c_node)
plt.text(x, y, value, ha='center', va='center', fontsize=25, )
def __draw_a_edge(self, x1, y1, x2, y2):
"""画一条边"""
x = (x1, x2)
y = (y1, y2)
plt.plot(x, y, 'g-')
def __create_win(self, root):
"""创建窗口"""
# WEIGHT: 树宽,HEIGHT: 树高
WEIGHT, HEIGHT = self.get_w_h(root)
# WEIGHT:树宽 + 1
WEIGHT = (WEIGHT+1)*self.d_hor
# HEIGHT = 树高+1
HEIGHT = (HEIGHT+1)*self.d_vec
# print(WEIGHT, HEIGHT)
# fig = plt.figure(figsize=(a, b), dpi=dpi)
# 设置图形的大小,a 为图形的宽, b 为图形的高,单位为英寸
# dpi 为设置图形每英寸(inch)的点数
# 1点(英美点)=0.3527毫米=1/72英寸(Office里面的点)。
# 线条,标记,文本等大多数元素都有以磅(point即点)为单位的大小。因1inch = 72point,则72dp/inch=1dp/point、144dp/inch=2dp/point
fig = plt.figure(figsize=self.figsize)
ax = fig.add_subplot(111) # 表示整个figure分成1行1列,共1个子图,这里子图在第一行第一列
plt.xlim(0, WEIGHT) # 设定x座标轴的范围,当前axes上的座标轴。
plt.ylim(0, HEIGHT) # 设定y座标轴的范围,当前axes上的座标轴。
x = (self.get_left_width(root) + 1) * self.d_hor # x, y 是第一个要绘制的节点坐标,由其左子树宽度决定
y = HEIGHT - self.d_vec
return fig, ax, x, y
def __print_tree_by_preorder(self, root, x, y, ax):
"""通过先序遍历打印二叉树"""
if not root:
# 根节点为空返回
return
# 画节点
self.__draw_a_node(x, y, root.value, ax)
# 画左右分支
lx = rx = 0
ly = ry = y - self.d_vec
if root.left_child:
lx = x - self.d_hor * (self.get_right_width(root.left_child) + 1) # x-左子树的右边宽度
self.__draw_a_edge(x, y, lx, ly)
# print(root.left_child, (lx, ly))
if root.right_child:
rx = x + self.d_hor * (self.get_left_width(root.right_child) + 1) # x-右子树的左边宽度
# print(root.right_child, (rx, ry))
self.__draw_a_edge(x, y, rx, ry)
# 递归打印
self.__print_tree_by_preorder(root.left_child, lx, ly, ax)
self.__print_tree_by_preorder(root.right_child, rx, ry, ax)
def show_BiTree_1(self):
"""可视化二叉树"""
_, ax, x, y = self.__create_win(self.root)
self.__print_tree_by_preorder(self.root, x, y, ax)
plt.show()
def show_BiTree_2(self):
"""采用逆中序方法+矩阵转置"""
def inner_func(root, layer):
if root:
inner_func(root.left_child, layer + 1)
print(f"{' ' * layer}{root.value}")
inner_func(root.right_child, layer + 1)
return inner_func(self.root, 1)
def __create_tree(self):
value = input("输入节点数据:").strip()
if value:
node = Node(value)
node.left_child = self.__create_tree()
node.right_child = self.__create_tree()
return node
else:
return None
def create_tree(self):
"""先序方式创建二叉树"""
self.root = self.__create_tree()
def create_tree_extension(self, seq):
"""
利用扩展先序遍历序列创建二叉树
扩展先序遍历序列:AB.DF..G..C.E.H..
创建过程:首先读入当前根节点的数据,如果是“.”则将当前树根置为空,否则申请一个新节点,存入当前根节点的数据,
分别用当前根节点的左子域和右子域进行递归调用,创建左右子树。
:param seq: 扩展先序遍历序列
:return: 创建好的一棵树
"""
if seq:
index = 0
def inner_function():
nonlocal index
try:
if seq[index] == ".":
index += 1
return
else:
node = Node(seq[index])
index += 1
node.left_child = inner_function()
node.right_child = inner_function()
return node
except IndexError as e:
pass
self.root = inner_function()
def pre_order_1(self):
"""先序递归遍历"""
trav_seq = deque()
def inner_func(root):
if root:
nonlocal trav_seq
trav_seq.append(root.value)
inner_func(root.left_child)
inner_func(root.right_child)
else:
return
inner_func(self.root)
return trav_seq
def in_order_1(self):
"""中序递归遍历,方法1"""
trav_seq = deque()
def inner_func(root):
if root:
nonlocal trav_seq
inner_func(root.left_child)
trav_seq.append(root.value)
inner_func(root.right_child)
else:
return
inner_func(self.root)
return trav_seq
def post_order_1(self):
"""后序递归遍历"""
trav_seq = deque()
def inner_func(root):
if root:
nonlocal trav_seq
inner_func(root.left_child)
inner_func(root.right_child)
trav_seq.append(root.value)
else:
return
inner_func(self.root)
return trav_seq
def pre_order_2(self):
"""
先序非递归遍历,方法2
算法思想:
从根节点开始,只要当前节点存在,或者栈不空,则重复下面的操作。
(1)如果当前根节点存在,则访问当前结点并遍历左子树
(2)否则退栈,然后遍历右子树
算法时间复杂度:
对有n个结点二叉树,该算法每循环一次,p指向一个节点或空(无左孩子或无右孩子的节点的空链域),
因此指向空的次数为n+1,n为节点个数,故循环次数为n+(n+1)=2n+1,因此算法的复杂度为O(n).
空间复杂度:
所需栈的空间最多等于二叉树K乘以每个节点所需空间数,记作O(K)
总结:入栈顺序是先序,在入栈前访问结点,访问后将当前结点设置为当前结点的左孩子
:return: 先序遍历序列
"""
def inner_func(root):
from collections import deque
temp_stack = deque()
trav_seq = deque() # 用于存放访问过的结点
p = root # 当前结点
while p or temp_stack:
if p:
trav_seq.append(p.value) # 访问当前结点
temp_stack.append(p) # 将当前结点入栈
p = p.left_child # 当前结点更新为当前结点的左孩子
else:
p = temp_stack.pop() # 将栈顶结点出栈
p = p.right_child # 将当前结点设置为栈顶结点的右孩子,为了将栈顶结点右子树入栈
return trav_seq
return inner_func(self.root)
def in_order_2(self):
"""
中序非递归遍历,方法2
算法思想:
从根节点开始,只要当前节点存在,或者栈不空,则重复下面的操作。
(1)如果当前根节点存在,则进展并遍历左子树
(2)否则退栈并访问,然后遍历右子树
算法时间复杂度:
对有n个结点二叉树,该算法每循环一次,p指向一个节点或空(无左孩子或无右孩子的节点的空链域),
因此指向空的次数为n+1,n为节点个数,故循环次数为n+(n+1)=2n+1,因此算法的复杂度为O(n).
空间复杂度:
所需栈的空间最多等于二叉树K乘以每个节点所需空间数,记作O(K)
总结:入栈顺序是先序,在出栈时立即访问结点,访问后将当前结点设置为当前结点的右孩子
:return: 中序遍历序列
"""
def inner_func(root):
from collections import deque
temp_stack = deque()
trav_seq = deque() # 用于存放访问过的结点
p = root # 当前结点
while p or temp_stack:
if p:
temp_stack.append(p) # 将当前结点入栈
p = p.left_child # 当前结点更新为当前结点的左孩子
else:
p = temp_stack.pop() # 将栈顶结点出栈,获取当前结点
# print(root.value)
trav_seq.append(p.value) # 访问栈顶结点
p = p.right_child # 将当前结点设置为栈顶结点的右孩子,为了将栈顶结点右子树入栈
return trav_seq
return inner_func(self.root)
def post_order_2(self):
"""
后序非递归遍历,方法2
算法思想:
从根节点开始,只要当前结点存在,或者栈不空,则重复下面的操作:
(1) 从当前结点开始,进栈并遍历左子树,直到左子树为空。
(2) 如果栈顶结点的右子树为空,或者栈顶结点的右孩子为刚被访问过的结点(表明栈顶结点的右子树已被访问过),
则退栈并访问,然后将当前结点指针置为空。
(3) 否则,遍历右子树 。
总结:入栈顺序是先序,通过对访问条件的是否满足的判断使得访问顺序是后序。
:return: 后序遍历序列
"""
def inner_func(root):
from collections import deque
temp_stack = deque()
trav_seq = deque() # 用于存放访问过的结点
temp_root = None # 临时结点,用于保存刚刚访问过的节点
p = root # 当前结点
while p or temp_stack:
if p:
temp_stack.append(p) # 将当前结点入栈
p = p.left_child # 当前结点更新为当前结点的左孩子
else:
p = temp_stack.pop() # 将栈顶结点出栈,获取当前结点
temp_stack.append(p) # 由于此时并不是真正的出栈只是为了获取栈顶结点,因此还需再次入栈
if not p.right_child or p.right_child is temp_root: # 满足两个条件中任意一个,则访问栈顶结点
trav_seq.append(p.value) # 访问栈顶结点
temp_stack.pop() # 栈顶结点已被访问,此时需要出栈
temp_root = p # 将刚访问过的栈顶结点保存起来,以便下一次用于判断
p = None # 将当前结点设置为空,避免对当前结点的左子树再次入栈
else:
p = p.right_child # 将当前结点设置为当前结点的右孩子,为了将当前结点右子树入栈
return trav_seq
return inner_func(self.root)
def leaf_number_1(self):
"""获取叶子节点数目,通过后序遍历方法一"""
leaf_number_ = 0
def inner_func(root):
if root:
nonlocal leaf_number_
inner_func(root.left_child)
inner_func(root.right_child)
if not root.leaf_child and not root.right_child:
leaf_number_ += 1
inner_func(self.root)
return leaf_number_
def leaf_number_2(self):
"""
获取叶子节点数目,通过后序遍历方法二
对比leaf_number_1,去除了if 嵌套
"""
def inner_func(root):
if not root:
return 0
elif not root.left_child and not root.right_child:
return 1
else:
return inner_func(root.left_child) + inner_func(root.right_child)
return inner_func(self.root)
def get_high_pre(self):
"""
获取树的高度,先序遍历方法
精髓在于树的高度的迭代更新
:return: 树的高度
"""
deepth = 0 # 临时保存上一个节点的层级
def inner_func(root, high):
"""
:param root: 当前根节点,假设当前根节点是存在的
:param high: 表示当前根节点root的层级
:return:
"""
if not root: # 判断当前根节点是否真的存在
return
else:
nonlocal deepth
if deepth < high:
deepth = high
else:
pass
inner_func(root.left_child, high+1)
inner_func(root.right_child, high+1)
inner_func(self.root, 1)
return deepth
def get_high_in(self):
"""
获取树的深度,中序遍历方法
精髓在于树的高度的迭代更新
:return: 树的高度
"""
deepth = 0 # 临时保存上一个节点的层级
def inner_func(root, high):
"""
:param root: 当前根节点,假设当前根节点是存在的
:param high: 表示当前根节点root的层级
:return: None
"""
if root: # 判断当前根节点是否真的存在
nonlocal deepth
inner_func(root.left_child, high+1)
if deepth < high:
deepth = high
inner_func(root.right_child, high+1)
inner_func(self.root, 1)
return deepth
def get_high_post_1(self):
"""
获取树的高度,后序遍历方法1
精髓在于树的高度的迭代更新
:return: 树的高度
"""
deepth = 0 # 临时保存上一个节点的层级
def inner_func(root, high):
"""
:param root: 当前根节点,假设当前根节点是存在的
:param high: 表示当前根节点root的层级
:return: None
"""
if root: # 判断当前根节点是否真的存在
inner_func(root.left_child, high+1)
inner_func(root.right_child, high+1)
nonlocal deepth
if deepth < high:
deepth = high
inner_func(self.root, 1)
return deepth
def get_high_post_2(self):
"""
获取树的高度,后序遍历方法2
精髓在于对(root层级+左子树高度)与(root层级+右子树高度)进行对比
由于后序遍历不需要保留root的层级,也就不需要high参数了
:return: 树的高度
"""
def inner_func(root):
"""
:param root: 当前根节点,当前根节点是否存在并知道,没有做出假设
:return: 树的高度
"""
if root: # 判断当前根节点是否存在
h_left = inner_func(root.left_child)
h_right = inner_func(root.right_child)
return max(h_left, h_right) + 1
else:
return 0
return inner_func(self.root)
def left_right_swap_pre(self):
"""
先序递归遍历,交换左右子树
:return:
"""
def inner_func(root):
if root:
root.left_child, root.right_child = root.right_child, root.left_child
inner_func(root.left_child)
inner_func(root.right_child)
inner_func(self.root)
def left_right_swap_post(self):
"""
后序递归遍历,左右子树交互
注意:
左右子树的交换不能使用中序遍历。原因:假设对当前根节点来说,以其左孩子为根节点的左子树已交换成功,
此时在交换当前根结点的左右子树,交换完后。导致左子树还未交换,而右子树已交换。在接下来的会交换右子树,
交换后导致整棵树还是最初的结构,并未实现左右子树的交换。
"""
def inner_func(root):
if root:
inner_func(root.left_child)
inner_func(root.right_child)
root.left_child, root.right_child = root.right_child, root.left_child
inner_func(self.root)
def like(bi_tree1, bi_tree2):
"""
判断两棵子树相似性
算法思想:
like(bi_tree1, bi_tree2)=True,若t1=t2=None;
like(bi_tree1, bi_tree2)=False,若t1与t2之一为None,另一个不为None;
like(bi_tree1, bi_tree2)=like(bi_tree1.left_child, bi_tree2.left_child) and like(bi_tree1.right_child, bi_tree2.right_child),若t1、t2均不为空
:param bi_tree1: 第一棵子树
:param bi_tree2: 第二棵子树
:return: True or False
"""
if not bi_tree1 and not bi_tree1:
return True
elif bi_tree1 and bi_tree2:
like1 = like(bi_tree1.left_child, bi_tree2.left_child)
like2 = like(bi_tree1.right_child, bi_tree2.right_child)
return like1 and like2
else:
return False
def path(bi_tree, bi_node):
"""
获取从二叉树根节点到指定结点之间的路径
算法思想:
(1) 由于后序遍历访问到bi_node所指结点时,此时栈种所有结点均为bi_node所指结点的祖先,由这些祖先便
构成了一条从根节点到bi_node所指结点之间的路径,古应采用后序遍历方法。
(2) 由于递归方式的战区是由系统隐士控制,为了能在找到结点bi_node时控制输出,需要将递归方式中系统
隐含的栈变成为用户自己控制的栈,故应使用非递归的后序遍历算法。此算法与后序非递归算法相似。
:param bi_tree: 二叉树根节点
:param bi_node: 指定结点
:return: 路径
"""
from collections import deque
path = deque() #
temp_root = None
p = bi_tree
while p or path:
if p:
path.append(p) # 将当前结点入栈
p = p.left_child # 当前结点更新为当前结点的左孩子
else:
# print("++++++++++")
p = path.pop() # 将栈顶结点出栈,获取当前结点
path.append(p) # 由于此时并不是真正的出栈只是为了获取栈顶结点,因此还需再次入栈
if not p.right_child or p.right_child is temp_root: # 满足两个条件中任意一个,则访问栈顶结点
# 访问栈顶结点
if p.value == bi_node.value: # 判断当前结点是否是bi_node结点
return path # 返回路径
else:
path.pop() # 栈顶结点已被访问,此时需要出栈
temp_root = p # 将刚访问过的栈顶结点保存起来,以便下一次用于判断
p = None # 将当前结点设置为空,避免对当前结点的左子树再次入栈
else:
p = p.right_child # 将当前结点设置为当前结点的右孩子,为了将当前结点右子树入栈
def layer_order(bi_tree):
"""
任务:层次遍历二叉树
问题分析:
实现层次遍历,需要设置一个队列Q,暂存某层已访问过的结点,同时也保存了该层结点访问的先后次序。按照对
该层结点访问的先后次序实现对其下层孩子结点的按次序访问。
算法思想:
(1) 创建一个空队列;
(2) 若二叉树bi_tree为空树,则直接返回;
(3) 将二叉树的根结点指针bi_tree放入队列Q;
(4) 若队列非空,则重复如下操作。
a. 对头元素出队列访问该元素;
b. 若该结点的左孩子非空,则将该结点的左孩子节点人对;
c. 若该结点的右孩子非空,则将该结点的右孩子结点指针入队。
:param bi_tree: 二叉树根
:return: 层次遍历二叉树的序列
"""
if not bi_tree:
return
from collections import deque
temp_queue = deque() # 用于暂存已遍历到的结点,并保存了不同层的层级关系和同层结点的先后关系
trav_seq = deque() # 存放访问到的结点
temp_queue.append(bi_tree) # 二叉树根节点入队
while temp_queue:
p = temp_queue.popleft() # 对头结点出队
trav_seq.append(p) # 保存访问到的结点
if p.left_child: # 判断当前结点左孩子是否存在
temp_queue.append(p.left_child) # 若存在,将当前结点左孩子入队
if p.right_child: # 判断当前结点右孩子是否存在
temp_queue.append(p.right_child) # 若存在,将当前结点右孩子入队
return trav_seq # 返回层次遍历二叉树的序列