- 树
是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。
特点:
1)每个节点有零个或多个子节点;
2)没有父节点的节点称为根节点;
3)每一个非根节点有且只有一个父节点;
4)除了根节点外,每个子节点可以分为多个不相交的子树;
-
树的术语
节点的度:一个节点含有的子树的个数称为该节点的度;
树的度:一棵树中,最大的节点的度称为树的度;
叶节点或终端节点:度为零的节点;
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的深度:树中节点的最大层次;
堂兄弟节点:父节点在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林; -
树的种类
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;
1)二叉树:每个节点最多含有两个子树的树称为二叉树;
2)完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树。
3)满二叉树:所有叶节点都在最底层的完全二叉树;
4)平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
5)排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树);
霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。 -
树的储存与表示
1)顺序存储:将数据结构存储在固定的数组中,不好根据二叉树结点数目的增多而动态扩展。如果内存空间分配过大,造成内存空间的浪费;如果分配过小,则会产生数组溢出的问题。方便查找和遍历但不方便插入和删除,故不常用。
2)链式存储:由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数最多为2。 可以动态分配内存空间,插入和删除节点更加方便,是更为常用的储存方式。
-
树的常见应用场景
1)xml,html解析器
2)路由协议
3)mysql数据库索引
4)文件系统的目录结构
5)很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构 -
树的实现
创建树节点类:# 创建树节点 class Node(object): def __init__(self, item = None, lchild = None, rchild = None): self.item = item self.lchild = lchild self.rchild = rchild
创建具有添加节点功能的树:
# 创建树 class Tree(object): def __init__(self, root = None): self.root = root # 指定根节点创建树 def add(self, item): # 加入节点 node = Node(item) if not self.root: # 如果根节点为空,直接将根节点取为插入节点 self.root = node return queue = [self.root] #构建队列,完成树已有节点遍历 while queue: cur = queue.pop(0) # 从队列头取出一个节点 if cur.lchild == None: # 如果左节点为空,直接插入,退出循环 cur.lchild = node return else: queue.append(cur.lchild) # 否则,将左节点尾插到队列中 if cur.rchild == None: # 同样判断右节点 cur.rchild = node return else: queue.append(cur.rchild)
-
广度优先遍历(Breadth First Search,BFS)
从树的根节点开始,从上倒下,从左到右的遍历树的节点。
运用队列实现:
def breadth_travel(self): # 广度优先遍历
if not self.root: # 如果树为空,返回空
return
queue = [self.root] # 用队列方式遍历树
while queue:
cur = queue.pop(0)
print(cur.item, end = ' ')
if cur.lchild is not None:
queue.append(cur.lchild)
if cur.rchild is not None:
queue.append(cur.rchild)
-
深度优先遍历(Depth First Search,DFS)
沿着树的深度遍历树的节点,尽可能深的搜索树的分支。根据访问次序不同分为先序遍历(preorder),中序遍历(inorder)和后序遍历(postorder)。
1)先序遍历
根节点 -> 左节点 ->右节点
实现方式:def preorder(self, node): # 深度优先遍历(先序遍历) if node == None: # 递归终止条件 return print(node.item, end = ' ') # 根节点 self.preorder(node.lchild) # 左节点 self.preorder(node.rchild) # 右节点 def preorder_stack(self, root): # 利用栈实现深度优先遍历(先序遍历) if root == None: return stack = [root] # 创建栈 while stack: cur = stack.pop() # 打印当前节点 print(cur.item, end = ' ') if cur.rchild is not None: # 先将右节点压入,则后输出 stack.append(cur.rchild) if cur.lchild is not None: # 后将左节点压入,将先输出 stack.append(cur.lchild)
2)中序遍历
左节点 -> 根节点 ->右节点
实现方式:
def inorder(self, node): # 深度优先遍历(中序遍历)
if node == None: # 递归终止条件
return
self.inorder(node.lchild) # 左节点
print(node.item, end = ' ') # 根节点
self.inorder(node.rchild) # 右节点
def inorder_stack(self, root): # 利用栈实现深度优先遍历(中序遍历)
if root == None:
return
stack = [root] # 创建栈
node = root.lchild
while stack or node: # stack为空时,仍有最后一个可能不为空的node需要输出
while node: # 一直压入左节点
stack.append(node)
node = node.lchild
# 退出循环时,栈内最后一个节点没有左节点,node=None
cur = stack.pop()
print(cur.item, end = ' ') # 打印该节点
node = cur.rchild # 开始搜索该节点右子树
3)后序遍历
左节点 -> 右节点 ->根节点
实现方式:
def postorder(self, node): # 深度优先遍历(后序遍历)
if node == None: # 递归终止条件
return
self.postorder(node.lchild) # 左节点
self.postorder(node.rchild) # 右节点
print(node.item, end = ' ') # 根节点
def postorder_stack(self, root): # 利用栈实现深度优先遍历(后序遍历)
if root == None:
return
stack1 = [root] # 用来遍历树
stack2 = [] # 用来记录遍历的序号
while stack1:
node = stack1.pop()
if node.lchild is not None:
stack1.append(node.lchild)
if node.rchild is not None:
stack1.append(node.rchild)
stack2.append(node) # stack2 实际记录顺序为根节点->右节点->左节点
while stack2: # 以栈的方式依次弹出stack2中的节点,输出顺序为记录顺序的倒序,即为后序遍历
cur = stack2.pop()
print(cur.item, end = ' ')
-
广度优先与深度优先比较
深度优先搜索:不全部保留结点,占用空间少;主要通过栈或递归实现,有回溯操作(即有入栈、出栈操作),运行速度慢。
广度优先搜索:保留全部结点,占用空间大; 主要通过队列实现,无回溯操作(即无入栈、出栈操作),运行速度快。 -
根据已有排序画出树
先序,中序,后序中任意包括中序的两个可以确定一个树。必须要中序因为中序排列中根节点区分了左右节点。
要点在于:利用前序或后序确定根节点,在中序中找到该节点,完成左右子树区分,再代回前序或后序找到左右子树的根节点,以此类推。