数据结构-树形结构
首先,什么是树形结构,简单地说,树形结构就是你现在想的那样的结构,数据结构像树形的就是树形结构,典型的树形结构示例:Windows操作系统和Unix操作系统和文件系统均是树形结构的应用。
-
树的基本概念
**树(Tree)**是一个由一个或者一个以上的节点(Node)组成的,存在一个特殊的节点,称为树根(Root),Root很熟悉吧,Linux的根目录。每个节点是一些数据和指针组合而成的记录
A B C D A为根节点,B、C、D均是A的子节点
树还可以组成森林(forest),也就是说森林是n个互斥树的集合(n>=0),移动树根即是森林
简单地说,把根节点去掉,就是森林,够离谱
树形结构的专有名词
在树形结构中,有许多常用的专有名词
- 度数(Degree):每个节点所有字数的个数,比如A的度数是3
- 层数(level):树的层数,比如A在第一层,B、C、D在第二层
- 高度(Height):树的最大层数。比如上面这个高度就是2
- 树叶或者终端节点(Terminal Nodes):度数为零的节点就是树叶,B、C、D节点就是树叶
- 父节点(Parent):每一个节点有连接的上一层节点
- 子节点(Children):每一个节点有连接的下一层节点为子节点
- 祖先(Ancestor)和子孙(Descendent):所谓祖先,是指从树根到该节点路径上所包含的节点,而子孙则是在该节点往下追溯子树中的任一节点。(这些东西就和你生物学的族谱图是一样的,就看着类比吧)
- 兄弟节点(Siblings):有共同父节点的节点为兄弟节点
- 非终端节点(Nonterminal Nodes):树叶以外的节点
- 同代(Generation):在同一棵树中具有相同层数的节点
-
二叉树简介
一般树形结构在计算机内存中的存储方式是以链表(Linked List)为主的。对于n叉树(n-way 树)来说,因为每个节点的度数都不相同,所以我们必须为每个节点都预留存放n个链接字段的最大存储空间,因为每个节点的数据结构如下:
data link1 link2 link3 … … linkn 在这种情况下,这种n叉树十分浪费链接存储空间。假设此n叉树有m个节点,那么此树共有nxm个链接字段。另外,因为除了树根外,每一个非空链接都指向一个节点,所以得知空链接个数为nxm-(m-1) = m x (n-1) + 1,而n叉树的链接浪费率为[m x (n - 1) + 1]/m x n,因此可以得出以下结论:
n = 2 时,2叉树的链接浪费率约为 1 / 2
n = 3 时,3叉树的链接浪费率约为 1 / 3
n = 4 时,4叉树的链接浪费率约为 1 / 4
… …
… …
当n = 2 时,它的链接浪费率最低,所以为了改进存储空间浪费的缺点,最常使用二叉树(Binary Tree) 结构来取代其他树形结构
-
二叉树的定义
二叉树(又称为Knuth 树)是一个由有限节点所组成的集合,此集合可以为空集合,或由一个数根及其左右两个子树组成。简单地说,二叉树最多只能有两个子节点,就是度数小于或等于2.期计算机中的数据结构如下:
LLINK Data ALINK 二叉树和一般树的不同之处:
- 树不可以为空集合,但是二叉树可以
- 树的度数为d >= 0,但二叉树的节点度数为 0 <= d <= 2
- 树的子树间没有次序关系,二叉树则有
-
特殊二叉树
-
满二叉树(Full Binary Tree)
如果二叉树的高度为h,树的节点数为2^h - 1 ,h >= 0,我们就称此树为“满二叉树”
-
完全二叉树(Complete Binary Tree)
如果二叉树的高度为h,所含的节点数小于2^h - 1,但其节点的编号方式如同高度为h的满二叉树一样,从左到右、从上到下的顺序一一对应
对于完全二叉树而言,假设有N个节点,那么此二叉树的层数h为**[log2(N+1)]**
-
斜二叉树(Skewed Binary Tree):当一个二叉树完全没有有节点或左节点时,我们就把它称为左斜二叉树或右斜二叉树
-
严格二叉树(Strict binary Tree)
二叉树中的每一个非终端节点均有非空的左右子树
-
-
二叉树的存储方式
二叉树有很多的存储方式,在数据结构中,我们通常是用链表来表示二叉树,这样在删除或者增加节点时会很方便并且很便捷。但是也可以使用一维数组这样的连续存储空间来表示二叉树,不过在对树中的中间节点进行插入或者删除操作时,可能要大量移动数组中节点的存储位置来反应树节点的变动
-
一维数组表示法
使用有序的一维数组来表示二叉树,首先可将此二叉树假想为一个满二叉树,而且第K层具有2^k-1个节点,他们按照顺序存放在这个一维数组中。
(1)A(根节点) (1)B (3)无 (4)无 (5)C (6)无 (7)D 将上面的二叉树存放到一维数组中,是什么样的
索引值 1 2 3 4 5 6 7 内容值 A B C D 从二叉树和一位数组的索引值直接关系,可以看出
- 左子树索引值是父节点索引值*2
- 右子树索引值是父节点索引值*2+1
接着来看以一维数组建立二叉树的实例,事实上就是创建一个二叉查找树,这是一种很好的排序应用模型,因为在建立二叉树的同时,数据就经过初步的比较判断并按照二叉树的建立规则来存放数据了。二叉查找树具有一下特点:
- 可以是空集合,但若不是空集合,则节点上一定要有一个键值
- 每一个树根的值需要大于左子树的值
- 每一个树根的值需要小于右子树的值
- 左右子树也是二叉查找树
- 树的每个节点的值都不相同
例子:设计一个程序,按需输入一颗二叉树
注意,因为数组的大小是固定的,所以在data设计时很容易出现索引超限,所以找个data托配合一下
def Btree_create(btree,data,length): for i in range(1,length): level = 1 while btree[level] != 0: if data[i] > btree[level]: #判断data中的数据和树根的比较,大于放在右边 level = level * 2 + 1 #将level赋值为下一个节点,如果有数据循环遍历下一个 else: level = level * 2 #小于就放在左边,*2,将level赋值为下一个节点,如果有数据循环遍历下一个 btree[level] = data[i] #遍历得到了没有数值的level,将其赋值 data = [0,3,5,6,1,9,3,2,1] length = 9 btree = [0] * 16 print("原数组内容") for i in range(length): print('[%2d]'% data[i],end='') print('') Btree_create(btree,data,9) print('二叉树内容') for i in range(1,16): print('[%2d]'%btree[i],end='') print('')
-
链表表示法
链表表示法,就是使用链表来存储二叉树。使用链表来表示二叉树的好处就是对于节点的增加和删除相当容易,缺点时很难找到父节点,除非在每一节点多增加一个父字段,以存放整数的数据类型为例
class tree: def __init__(self): self.data = 0 self.left = None self.right = None
以链表方式建立二叉树
def create_tree(self,root,val): newnode = tree() #实例化一个节点 newnode.data = val #赋值data newnode.left = None #左节点指向None newnode.right = None #右节点指向None if root == None: root = newnode #如果没有父节点,将newnode赋值给root return root else: current = root while current != None: backup = current #将每次遍历的current值给到backup if current.data > val: #节点的值大于val,要把val放在左边,并且把current设为左侧下一个节点遍历 current = current.left #current为左侧下一个节点 else: #节点的值小于val,要把val放在右边,并且把current设为右侧下一个节点遍历 current = current.right #current为右侧下一个节点 if backup.data > val: #比较此时的backup(current)与val,决定newnode放到backup的哪侧 backup.left = newnode else: backup.right = newnode return root
总结一下以上的代码,while循环就是遍历节点current,直至找到下一个节点为空时放置newnode,第一个if是判断此current节点不为空时往左遍历还是往右遍历,第二个if是判断此节点为空时newnode应该放置到左侧还是右侧
设计程序,使用链表建立二叉树
class tree: def __init__(self): self.data = 0 self.left = None self.right = None def create_tree(root,val): newnode = tree() #实例化一个节点 newnode.data = val #赋值data newnode.left = None #左节点指向None newnode.right = None #右节点指向None if root == None: root = newnode #如果没有父节点,将newnode赋值给root return root else: current = root while current != None: backup = current #将每次遍历的current值给到backup if current.data > val: #节点的值大于val,要把val放在左边,并且把current设为左侧下一个节点遍历 current = current.left #current为左侧下一个节点 else: #节点的值小于val,要把val放在右边,并且把current设为右侧下一个节点遍历 current = current.right #current为右侧下一个节点 if backup.data > val: #比较此时的backup(current)与val,决定newnode放到backup的哪侧 backup.left = newnode else: backup.right = newnode return root data = [5,4,8,1,12,3,6,7,9] ptr = None root = None for i in range(9): ptr = create_tree(ptr,data[i]) print('左子树') root = ptr.left while root != None: print('%d' %root.data) root = root.left print('--------------') print('右子树') root = ptr.right while root != None: print('%d'%root.data) root = root.right print()
-
-
二叉树的遍历
线性数组或链表只能单向从头到尾遍历或者反向遍历。所谓二叉树的遍历简单的来说就是访问树中所有的节点各一次,并且在遍历后,将树中的数据转化为线性关系。二叉树可以将每个节点分为左右两个分支
二叉树的特性一律从左向右遍历,那么遍历的方式就分为了三种,分别是
- 中序遍历:左子树->树根->右子树
- 前序遍历:树根->左子树->右子树
- 后序遍历:左子树->右子树->树根
这三种方式的顺序也十分好记,简单来说“从左向右,树根最牛”,意思就是所有遍历的顺序都是从左向右,树根在哪个位置,就属于什么遍历,树根在前面就是前序遍历,树根在中间就属于中序遍历,树根在后面就属于后序遍历
下面分别介绍三种方式遍历的算法实现
-
中序遍历
中序遍历的顺序就是:左子树->树根->右子树
就是从树的左侧逐步向下方移动,直到无法移动,再访问此节点,并向右移动一个节点。如果无法向右移动,就可以返回上层的父节点,然后重复这个步骤
- 遍历左子树
- 遍历树根
- 遍历右子树
这种方式可以采用递归的方法,递归找到最左侧的节点,然后找到右侧,最后找到树根,出口就是节点为空时
""" 中序遍历 """ def inorder(ptr): if ptr != None: inorder(ptr.left) print('[%2d]' % ptr.data,end='') inorder(ptr.right)
-
后序遍历
后序遍历的顺序就是:左子树->右子树->树根
即是先遍历左子树,再遍历右子树,最后遍历根节点,反复重复这个步骤
- 遍历左子树
- 遍历右子树
- 遍历树根
""" 后序遍历 """ def postorder(ptr): if ptr != None: postorder(ptr.left) postorder(ptr.right) print('[%2d]' % ptr.data,end='')
-
前序遍历
前序遍历的顺序就是:树根->左子树->右子树
就是先从根节点遍历,再往左方移动,当无法移动时继续右方移动,接着重复执行此步骤
- 遍历树根
- 遍历左节点
- 遍历右节点
""" 前序遍历 """ def preorder(ptr): if ptr != None: print('[%2d]' % ptr.data,end='') preorder(ptr.left) preorder(ptr.right)
总结一下:
三种遍历方法的算法实现所需要实现的功能类似,只是步骤过程有先后,都是遍历左子树和右子树,不同的地方是看在哪一步之后做出处理
这个递归算法的运行过程细节以中序为例:
""" 中序遍历 """ def inorder(ptr): """ 判断此节点是否为空,不为空进入,如果第一个节点就为空,说明二叉树为空 """ if ptr != None: """ 将下一个左节点调用此函数,为空说明是子树的最左侧,不为空继续将一下个左节点调用,直至找到最左侧 """ inorder(ptr.left) """ 执行了上述代码,找到了最左侧的节点,输出该节点 """ print('[%2d]' % ptr.data,end='') """ 找下一个右节点,注意,执行到这一步,左、中节点已经输出,所以找右节点 """ inorder(ptr.right)
因为二叉树的规则是:左子树 < 根节点 < 右子树
所以使用中序遍历会发现输出的结果已经完成了从小到大的排序
例子:设计程序,中序、后序、前序遍历二叉树
-
class tree:
def __init__(self):
self.data = 0
self.left = None
self.right = None
"""
中序遍历
"""
def inorder(ptr):
if ptr != None:
inorder(ptr.left)
print('[%2d]' % ptr.data,end='')
inorder(ptr.right)
"""
后序遍历
"""
def postorder(ptr):
if ptr != None:
postorder(ptr.left)
postorder(ptr.right)
print('[%2d]' % ptr.data,end='')
"""
前序遍历
"""
def preorder(ptr):
if ptr != None:
print('[%2d]' % ptr.data,end='')
preorder(ptr.left)
preorder(ptr.right)
"""
建立二叉树
"""
def create_tree(root,val):
newnode = tree()
newnode.data = val
newnode.left = None
newnode.right = None
if root == None:
root = newnode
return root
else:
current = root
while current != None:
backup = current
if current.data > val:
current = current.left
else:
current = current.right
if backup.data > val:
backup.left = newnode
else:
backup.right = newnode
return root
"""
主程序
"""
data = [5,6,24,8,12,3,17,1,9]
ptr = None
root = None
for i in range(9):
ptr = create_tree(ptr,data[i])
print("--------------------------")
print("中序遍历")
inorder(ptr)
print('')
print('后序遍历')
postorder(ptr)
print('')
print('前序遍历')
preorder(ptr)
- 二叉树节点的插入与删除
在讲解二叉树的插入之前,先讨论如何在所建立的二叉树中查找单个节点的数据。从树根出发比较键值即可,如果比树根大就往右,否则往左,直到找到了要查找的值,如果找到None无法进行,就代表查无此值
```python
"""查找节点"""def searchnode(ptr,val): i = 1 while True: if ptr == None: return None if ptr.data == val: print('共查找了%3d次' %i) return ptr elif ptr.data > val: ptr = ptr.left else: ptr = ptr.right i += 1 print(ptr.data)
```
-
例子:实现一个二叉树的查找程序
class tree: def __init__(self): self.data = 0 self.left = None self.right = None """ 查找节点 """ def searchnode(ptr,val): i = 1 while True: if ptr == None: return None if ptr.data == val: print('共查找了%3d次' %i) return ptr elif ptr.data > val: ptr = ptr.left else: ptr = ptr.right i += 1 print(ptr.data) """ 建立二叉树 """ def create_tree(root,val): newnode = tree() newnode.data = val newnode.left = None newnode.right = None if root == None: root = newnode return root else: current = root while current != None: backup = current if current.data > val: current = current.left else: current = current.right if backup.data > val: backup.left = newnode else: backup.right = newnode return root """ 主程序 """ data = [5,6,24,8,12,3,17,1,9] ptr = None print('[原始数组内容]') for i in range(9): ptr = create_tree(ptr,data[i]) print('[%2d]' % data[i],end='') print() node = int(input("请输入查找值:")) if searchnode(ptr,node) != None: print('你找到的值【%3d】找到了!!' % node) else: print('您要找的值没找到!!')
- 二叉树节点的插入 二叉树节点插入的情况和查找相似,重点是插入后仍要保持二叉查找树的特性。如果插入的节点已经在二叉树中,就没有插入的必要了。而插入的值不在二叉树中,就会出现查找失败的情况,相当于找到了要插入的位置 ```python if serch(ptr,data) != None: print('二叉树中有这个节点了!')else: ptr = create_tree(ptr,data) inorder(ptr) ``` 实现一个程序,添加二叉树节点并中序遍历 ```python class tree: def __init__(self): self.data = 0 self.left = None self.right = None """ 查找节点 """ def searchnode(ptr,val): i = 1 while True: if ptr == None: return None if ptr.data == val: return ptr elif ptr.data > val: ptr = ptr.left else: ptr = ptr.right i += 1 """ 建立二叉树 """ def create_tree(root,val): newnode = tree() newnode.data = val newnode.left = None newnode.right = None if root == None: root = newnode return root else: current = root while current != None: backup = current if current.data > val: current = current.left else: current = current.right if backup.data > val: backup.left = newnode else: backup.right = newnode return root """ 中序遍历 """ def inorder(ptr): if ptr != None: inorder(ptr.left) print('[%2d]' % ptr.data,end='') inorder(ptr.right) """ 主程序 """ data = [5,6,24,8,12,3,17,1,9] ptr = None print('[原始数组内容]') for i in range(9): ptr = create_tree(ptr,data[i]) print('[%2d]' % data[i],end='') print() node = int(input("请输入插入的值:")) if searchnode(ptr,node) != None: print('该节点值在二叉树中已经存在') else: ptr = create_tree(ptr,node) inorder(ptr) ```
- **二叉树节点的删除**
二叉树节点的删除可以分为三种情况:
- 删除的节点为树叶,只要将其相连的父节点指向None即可
- 删除的节点只有一棵子树,删除之后就要将子树放到自己的位置连接上自己的父节点
- 删除的节点有两颗子树,删除的方式有两种,虽然结果不用,但是都符合二叉树的特性
- 找出中序立即先行者,即将欲删除节点的左子树中最大者向上提
- 找出中序立即后继者,即把要删除节点的右子树中最小者向上提
- **二叉运算树**
我们可以把中序法表达式按照运算符优先级的顺序建成一棵二叉运算数。之后再按二叉树的特性进行前、中、后序的遍历,即可得到前、中、后序法表达式。建立的方法可根据以下两种规则:
- 考虑表达式中运算符的结合性与优先级,再适当地加上括号,其中数也一定是操作数,内部节点一定时运算符
- 再从最内层的括号逐步向外,利用运算当树根,左边操作数当左子树,右边操作数当右子树,其中优先级最低的运算符作为此二叉运算树的树根
-
线索二叉树
相对于树来说,一个二叉树的存储方式可将指针字段(LINK)的存储空间浪费率从2/3降为1/2.不过,对于一个有n节点的二叉树,实际上用来指向左右节点的指针只有n-1个链接,另外的n+1个指针都是空链接
所谓线索二叉树,就是把这些空的链接加以利用,再指到树的其他节点,这些链接就称为线索,这棵树就称为二叉树。最大的好处时进行中序遍历时,不需要使用递归与堆栈,直接使用各个节点的指针即可
-
二叉树转为线索二叉树
在线索二叉树中,与二叉树最大的不同之处:为了分辨左右子树指针是线索还是正常的链接指针,我们必须在节点结构中再加上两个字段LBIT与RBIT来加以区别,而在所绘的图中,线索使用的虚线来表示,以便有别于一般的指针,二叉树转为线索二叉树:
- 先将二叉树按照中序遍历方式按序排出,再将所有空链接改成线索
- 如果线索链接是指向该节点的左指针,就将该线索指到中序遍历顺序下的前一个节点
- 如果线索链接时指向该节点的右指针,就将该线索指到中序遍历顺序下的后一个节点
- 指向一个空节点,并将此节点的右指针指向自己,而空节点的左子树是此线索二叉树
线索二叉树的基本结构如下:
LBIT LCHILD DATA RCHILD RBIT - LBIT:左控制位
- LCHILD:左子树链接
- DATA:节点数据
- RCHILD:右子树链接
- RBIT:右控制位
和链表所建立的二叉树的不同之处在于,为了区别正常指针和线索而加入了两个字段:LBIT和RBIT
- 若LCHILD为正常指针,则LBIT=1
- 若LCHILD为线索,则LBIT=0
- 若RCHILD为正常指针,则RBIT=1
- 若RCHILD为线索,则RBIT=0
class Node: def __init__(self): self.DATA=0 self.LBIT=0 self.RBIT=0 self.LCHILD=None self.RCHILD=None
使用线索二叉树的优缺点:
优点:
- 在二叉树进行中序遍历时,不需要使用堆栈处理,但是一般二叉树却需要
- 由于充分使用空链接,因此避免了链接闲置浪费的情况,另外,中序遍历时的速度也较快,节省了不少时间
- 任意一个节点都容易找出它的中序先行者与中序后继者,在中序遍历时可以不需使用堆栈或递归
缺点:
- 在加入或删除节点时的速度比一般二叉树慢
- 线索子树间不能共用
-
-
树的二叉树表示法
-
树转化为二叉树
对于一般树形结构转化为二叉树,使用的方法称为CHILD-SIBLING法则,以下是执行步骤:
- 将节点的所有兄弟节点用横线连接起来
- 删除所有与子节点间的链接,只保留与最左子节点的链接
- 顺时针旋转45度
-
二叉树转换为树
既然可以将树转换为二叉树,那么反过来肯定也是可以实现的,二叉树转换为树就是树转换为二叉树的逆向过程,首先是逆时针旋转45度,按照父子关系增加连接,同时删除兄弟节点之间的连接
-
森林转换为二叉树
除了一棵树可以转换为二叉树外,好几棵树所形成的森林也可以转换为二叉树
- 从左到右将每棵树的树根连接起来
- 仍然利用树转化为二叉树的方法操作
-
二叉树转换成森林
二叉树转换为森林的方法就是按照森林转换为二叉树的方法倒退回去,就是把原树逆时针旋转45度,再按照左子树为父子关系,右子树为兄弟关系的原则逐步划分
-
树与森林的遍历
除了二叉树的遍历可以有中序遍历、前序遍历和后序遍历三种方法外,树与森林的遍历也是这三种
假设树根为R,此树有n个节点,并可分为m个子树,分别为T1,T2,…,Tm
则遍历方法的步骤:
- 中序遍历
- 以中序法遍历T1
- 访问树根R
- 再以中序法遍历T2,T3,…,Tm
- 前序遍历
- 访问树根R
- 再以前序法依次遍历T1,T2,T3,…,Tm
- 后序遍历
- 以后序法依次访问T1,T2,T3,…,Tm
- 访问树根R
森林的遍历方式,则是从树的遍历衍生过来的
- 中序遍历
- 若森林为空,则直接返回
- 以中序遍历第一棵树的子树群
- 中序遍历森林中第一棵树的树根
- 按中序法遍历森林中其他树
- 前序遍历
- 若森林为空,则直接返回
- 遍历森林中的第一棵树的树根
- 按前序遍历第一棵树的子树群
- 按前序法遍历森林中其他树
- 后序遍历
- 若森林为空,则直接返回
- 按后序遍历第一棵树的子树
- 按后序法遍历森林中其他树
- 便利森林中第一棵树的树根
- 中序遍历
-
确定唯一二叉树
在二叉树的3种遍历方法中,如果有中序与前序的遍历结果或者中序与后序的遍历结果,即可从这些结果中求得唯一的二叉树。不过,若只具备前序与后序的遍历结果,则无法确定唯一的二叉树
-
优化二叉查找树
-
扩充二叉树
任何一个二叉树中,若具有n个节点,则有n-1个非空链接和n+1个空链接。如果在每一个空链接加上一个特定节点,就称为外节点,其余的节点称为内节点,因而定义这种树为扩充二叉树。另外定义:外径长=所有外节点到树根距离的总和,内径长=所有内节点到树根距离的总和
-
霍夫曼树
霍夫曼树经常用在处理数据压缩,是可以根据数据出现的频率来构建二叉树。
简单地说,如果有n个权值,且构成一个有n个节点的二叉树,每个节点的外部节点的权值为qi,则加权外径长度最小的就称为优化二叉树或“霍夫曼树”
-
平衡树
为了能够尽量降低查找所需要的时间,很快就找到所要的键值,或者很快就知道当前的树中没有我们要的键值,必须让树的高度越小越好
由于二叉查找树的缺点是无法永远保持在最佳状态,在加入数据部分已排序的情况下,极可能产生斜二叉树,因为使得树的高度增加,导致查找效率降低,因此一般的二叉查找树不适用于数据经常变动的情况
定义
所谓平衡树,又称为AVL树(Adelson-Velskii和Landis两人发明),它本身也是一棵二叉查找树,在AVL树中,每次在插入数据和删除数据后,必要的时候会对二叉树做一些高度的调整,而这些调整就是要让二叉查找树的高度随时维持平衡
-
-