算法——树和二叉树
一、树
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树。
如下所示,一个节点存了两个值,分成了三路。