一、树
1、什么是树?
树是一种数据结构,比如:目录结构。
树是一种可以递归定义的数据结构。
定义:树是由n个节点组成的集合:
如果n=0,那这是一棵空树;
如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
2、相关概念
根节点: 根节点(root)是树的一个组成部分,也叫树根。它是同一棵树中除本身外所有节点的祖先,没有父节点。
叶子节点(终端节点):一棵树当中没有子节点(即度为0)的结点称为叶子结点,简称“叶子”。 叶子是指度为0的结点,又称为终端结点。
树的深度(高度):树中节点的最大层次。
节点的度:一个节点含有的子树的个数称为该节点的度。
树的度:一棵树中,最大的节点的度称为树的度。
父节点(双亲节点):若一个节点含有子节点,则这个节点称为其子节点的父节点;
子树:设T是有根树,a是T中的一个顶点,由a以及a的所有后裔(后代)导出的子图称为有向树T的子树。
3、树的实例——模拟文件系统
class Node:
def __init__(self, name, type='dir'):
self.name = name
self.type = type # 类型可以是"dir"或"file"
self.children = []
self.parent = None
"""链式存储"""
def __repr__(self):
return self.name
class FileSystemTree:
def __init__(self):
self.root = Node("/") # 根目录
self.now = self.root # 当前目录
def mkdir(self, name):
"""创建目录"""
if name[-1] != "/":
name += "/" # 判断当不是以"/"结尾,添加"/"
node = Node(name) # 创建文件夹
self.now.children.append(node)
node.parent = self.now
def ls(self):
"""展示当前目录下的所有目录"""
return self.now.children
def cd(self, name):
"""切换路径"""
if name[-1] != "/":
name += "/" # 判断当不是以"/"结尾,添加"/"
if name == "../":
self.now = self.now.parent
return
for child in self.now.children:
if child.name == name:
self.now = child
return
raise ValueError("invalid dir")
tree = FileSystemTree()
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
print(tree.root.children) # [var/, bin/, usr/]
print(tree.ls()) # [var/, bin/, usr/]
tree.cd("bin/")
tree.mkdir("python/")
print(tree.ls()) # [python/]
tree.cd("../")
print(tree.ls()) # [var/, bin/, usr/]
树绝大多数的存储都是和链表一样链式存储。往后指child;往前指parent。通过节点和节点间相互连接的关系来组成这么一个数据结构。
二、二叉树
二叉树:度不超过2的树。如下所示:
每个节点最多有两个孩子节点,两个孩子节点被区分为左孩子节点和右孩子节点。
1、特殊二叉树——满二叉树
一个二叉树如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。
2、特殊二叉树——完全二叉树
叶节点只能出现在最下层和次下层,并且最下面一层的节点都集中在该层最左边的若干位置的二叉树。
满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。堆是一个特殊的完全二叉树。
三、二叉树的存储方式(表示方式)
二叉树这种数据结构在计算机中的存储方法。
1、链式存储方式
二叉树的链式存储:将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
(1)节点定义
class BiTreeNode:
def __init__(self, data): # data就是传进去的节点值
self.data = data
self.lchild = None
self.rchild = None
(2)根据给定图片生成二叉树
代码如下:
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
# 创建二叉树节点
a = BiTreeNode("A")
b = BiTreeNode("B")
c = BiTreeNode("C")
d = BiTreeNode("D")
e = BiTreeNode("E")
f = BiTreeNode("F")
g = BiTreeNode("G")
# 节点连接
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
# 指定根节点
root = e
print(root.lchild.rchild.data) # C
2、顺序存储方式
所谓顺序存储方式就是二叉树用列表来存储。如下图所示就是用列表来存储二叉树。
如上图二叉树标出了元素所对应的索引,则可以有以下结论:
(1)父节点和左孩子节点的编号下标有什么关系?
父与左子下标关系:0-1 1-3 2-5 3-7 4-9
i (父)——>2i+1 (子)
如果已知父亲节点为i,那么他的左孩子节点为2i+1
(2)父节点和右孩子节点的编号下标有什么关系?
父与右子下标关系:0-2 1-4 2-6 3-8 4-10
i (父)——>2i+2 (子)
如果知道父亲节点为i,那么他的右孩子节点为2i+2
(3)知道孩子找父亲规律?
知道左孩子求父节点:(n-1)/2=i
知道右孩子求父节点:(n-2)/2=i
四、二叉树的遍历方式
1、前序遍历:EACBDGF
访问根节点操作发生在遍历其左右子树之前。
def pre_order(root):
"""前序遍历"""
if root: # 如果不为空(递归条件)
print(root.data, end=',') # 访问自己
pre_order(root.lchild) # 递归左子树
pre_order(root.rchild) # 递归右子树
pre_order(root) # E,A,C,B,D,G,F,
2、中序遍历:ABCDEGF
访问根节点的操作发生在遍历其左右子树之间。
def in_order(root):
"""中序遍历"""
if root:
in_order(root.lchild) # 递归左子树
print(root.data, end=',') # 访问自己
in_order(root.rchild) # 递归右子树
in_order(root) # A,B,C,D,E,G,F,
3、后序遍历:BDCAFGE
访问根节点的操作发生在遍历其左右子树之后。
def post_order(root):
"""后序遍历"""
if root:
post_order(root.lchild) # 递归左子树
post_order(root.rchild) # 递归右子树
print(root.data, end=",") # 访问自己
post_order(root) # B,D,C,A,F,G,E,
4、层次遍历:EAGCFBD
层次遍历很好理解,需要利用到队列。不仅适用二叉树也适用多叉树。
用一个队列保存被访问的当前节点的左右孩子以实现层序遍历。
from collections import deque
def level_order(root):
"""层次遍历"""
queue = deque()
queue.append(root)
while len(queue) > 0: # 只要队不空
node = queue.popleft() # 出队
print(node.data, end=',')
if node.lchild:
queue.append(node.lchild)
if node.rchild:
queue.append(node.rchild)
level_order(root) # E,A,G,C,F,B,D,
5、给定一个树的两种遍历方式,就可推导出这个树
例如:前序遍历——EACBDGF;中序遍历——ABCDEGF。
由此可知E是根节点,E的左边包含ABCD,右边包含GF。且A是根节点的左节点、G是根节点的右节点。
BCD是A的子节点,由于中序遍历ABCD可知A的左节点是空的,右节点包含BCD,由前序ACBD可知C是A的右子节点。再由中序遍历BCD可知B是C的左节点,D是C的右节点。
GF是根节点右边节点,G是右节点,F是G的子节点。由中序GF可知F是G节点的右节点。至此推导出整个树。
五、二叉树应用——二叉搜索树
二叉搜索树是一颗二叉树且满足性质:设x是二叉树的一个节点。如果y是x左子树的一个节点,那么y.key <= x.key;如果y是x右子树的一个节点,那么y.key >= x.key。
总结来说:二叉搜索树的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有节点的值均大于它根节点的值;它的左右子树也都是二叉搜索树。
1、二叉搜索树的插入
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None # 加了parent就是双链表
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
def insert(self, node, val):
"""
递归插入
:param node: 节点
:param val: 要插入的值
:return:
"""
if not node:
node = BiTreeNode(val)
elif val < node.data:
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.lchild,val)
node.rchild.parent = node
# else: # "=" else不用写了
return node
def insert_no_rec(self, val):
"""非递归插入"""
p = self.root
if not p: # 空树的情况处理
self.root = BiTreeNode(val)
return
while True:
if val < p.data: # 小于根节点往左边走
if p.lchild: # 如果左孩子存在
p = p.lchild
else: # 左子树不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data: # 大于根节点往右边走
if p.rchild: # 如果右孩子存在
p = p.rchild
else: # 右子树不存在
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else: # 等于的时候,什么都不干(类似集合)
return
def pre_order(self, root):
"""前序遍历"""
if root: # 如果不为空(递归条件)
print(root.data, end=',') # 访问自己
self.pre_order(root.lchild) # 递归左子树
self.pre_order(root.rchild) # 递归右子树
def in_order(self, root):
"""中序遍历"""
if root:
self.in_order(root.lchild) # 递归左子树
print(root.data, end=',') # 访问自己
self.in_order(root.rchild) # 递归右子树
def post_order(self, root):
"""后序遍历"""
if root:
self.post_order(root.lchild) # 递归左子树
self.post_order(root.rchild) # 递归右子树
print(root.data, end=",") # 访问自己
tree = BST([4,6,7,9,2,1,3,5,8])
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
print("")
tree.post_order(tree.root)
"""
4,2,1,3,6,5,7,9,8,
1,2,3,4,5,6,7,8,9, # 注意中序是有序的
1,3,2,5,8,9,7,6,4,
"""
可以注意到中序遍历输出的是有序的,做如下验证:
import random
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.in_order(tree.root) # 0,1,2,3,4,5,...,496,497,498,499
这是因为二叉搜索树的性质导致二叉搜索树的左孩子一定是最小的,因此它的中序序列一定是升序的。
2、二叉搜索树的查询操作
class BST:
"""代码省略"""
def query(self, node, val):
"""
递归查询
:param node: 要递归的节点
:param val: 要查询的值
:return:
"""
if not node: # 如果node是空,则找不到
return None # 递归终止条件
if val > node.data: # 大于node的值往右边找
return self.query(node.rchild, val)
elif val < node.data: # 小于node的值往左边找
return self.query(node.lchild, val)
else:
return node # 值相同返回当前节点
def query_no_rec(self, val):
"""非递归查询"""
p = self.root
while p: # 如果树不为空
if p.data < val: # 大于p的值往右边找
p = p.rchild
elif p.data > val: # 小于p的值往左边找
p = p.lchild
else:
return p
return None # 树为空,递归终止条件
import random
li = list(range(0, 500, 2))
random.shuffle(li)
tree = BST(li)
print(tree.query_no_rec(3)) # None
print(tree.query_no_rec(6)) # <__main__.BiTreeNode object at 0x103d01cc0>
print(tree.query_no_rec(6).data) # 6
3、二叉搜索树的删除操作
(1)如果要删除的节点是叶子节点
操作方法是:直接删除
(2)如果要删除的节点只有一个孩子
操作方法是:将此节点的父亲与孩子连接,然后删除该节点。
(3)如果要删除的节点有两个孩子
操作方法:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。
(4)代码实现如下所示:
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None # 加了parent就是双链表
class BST:
"""代码省略"""
def __remove_node_1(self, node):
"""情况1:node是叶子节点"""
if not node.parent: # 此叶子节点没有父节点,说明树中就这一个节点
self.root = None # 将这唯一的节点删除
if node == node.parent.lchild: # node是父亲的左孩子
node.parent.lchild = None # 父亲与node断联系
node.parent = None # node与父亲断联系(这句可写可不写)
else: # node是父亲的右孩子
node.parent.rchild = None # # 父亲与node断联系
def __remove_node_21(self, node):
"""情况2-1:node只有一个左孩子"""
if not node.parent: # 如果node是根节点
self.root = node.lchild # 将node的左孩子置为根节点
node.lchild.parent = None # 将新根节点的父亲设为空
elif node == node.parent.lchild: # 如果node是它父亲的左孩子
node.parent.lchild = node.lchild # node父节点的左孩子设为node的左孩子
node.lchild.parent = node.parent # node左孩子的父节点设为node的父节点
else: # 如果node是它父亲的右孩子
node.parent.rchild = node.lchild # node父节点的右孩子指向node的左孩子
node.lchild.parent = node.parent # node左孩子的父亲指向node的父节点
def __remove_node_22(self, node):
"""情况2-2:node只有一个右孩子"""
if not node.parent: # 如果node是根节点
self.root = node.rchild # 将node的右孩子置为根节点
node.rchild.parent = None # 将新根节点的父亲设为空
elif node == node.parent.lchild: # 如果node是父亲的左孩子
node.parent.lchild = node.rchild # 将node父节点的左孩子指向node的右孩子
node.rchild.parent = node.parent
else: # 如果node是父亲的右孩子
node.parent.rchild = node.rchild # 将node父节点的右孩子指向node的右孩子
node.rchild.parent = node.parent
def delete(self, val):
if self.root: # 如果不是空树
node = self.query_no_rec(val)
if not node: # 如果node不存在
return False
if not node.lchild and not node.rchild: # 如果node是叶子节点
self.__remove_node_1(node)
elif not node.rchild: # 如果没有右孩子(只有一个左孩子)
self.__remove_node_21(node)
elif not node.lchild: # 如果没有左孩子(只有一个右孩子)
self.__remove_node_22(node)
else: # 如果两个孩子都有
min_node = node.rchild
while min_node.lchild: # 一直查找node右孩子的左子树的左孩子,直到没有为止
min_node = min_node.lchild
node.data = min_node.data # 将min_node.data的值赋给node.data
# 删除min_node
if min_node.rchild: # 如果min_node只有右孩子
self.__remove_node_22(min_node)
else: # 如果min_node没有孩子
self.__remove_node_1(min_node)
tree = BST([1,4,2,5,3,8,6,9,7])
tree.in_order(tree.root) # 1,2,3,4,5,6,7,8,9,
print("")
tree.delete(4)
tree.in_order(tree.root) # 1,2,3,5,6,7,8,9,
print("")
tree.delete(1)
tree.delete(8)
tree.in_order(tree.root) # 2,3,5,6,7,9,
4、二叉搜索树的效率
平均情况下,二叉搜索树进行搜索的时间复杂度为O(logn)。
最坏情况下,二叉搜索树可能非常偏斜,时间复杂度退化到O(n)。如下所示:
解决方案:
(1)随机化的二叉搜索树(打乱顺序插入),有时是是不是插入的那打乱插入就不好用。
(2)AVL树
六、AVL树
七、二叉搜索树扩展应用——B树
B树(B-Tree):B树是一棵自平衡的多路搜索树。常用于数据库的索引,最常用数据库的索引就是哈希表、B树。
如下所示,一个节点存了两个值,分成了三路。