数据结构第六章 树和二叉树

一、树

1.1、树的定义

1、树是由n(n≥0)个结点组成的有限集合(记为T)。

n=0,是一棵空树,这是树的特例。

n>0,这n个结点中存在(有仅存在)一个结点作为树的根结点(root),其余结点可分为m(m≥0)个互不相交的有限集T1、T2、…、Tm,其中每个子集本身又是一棵符合本定义的树,称为根结点的子树。

2、树是一种非线性数据结构,特点:

每一结点可以有零个或多个后继结点,但有且只有一个前驱结点(根结点除外)。 数据结点按分支关系组织起来,清晰地反映了数据元素之间的层次关系。

1.2、树的基本术语

1、结点的度:树中每个结点具有的子树数或者后继结点数称为该结点的度。

2、树的度:树中所有结点的度的最大值称之为树的度。

3、分支结点:度大于0的结点称为分支结点或非终端结点。度为1的结点称为单分支结点,度为2的结点称为双分支结点,依次类推。(通俗来说就是有分支的结点)

4、叶子节点:度为零的结点称为叶子结点或终端结点。(即没有分支的结点)

5、孩子结点:一个结点的后继称之为该结点的孩子结点。

6、双亲结点(父亲结点):一个结点称为其后继结点的双亲结点。

7、子孙结点:一个结点的子树中除该结点外的所有结点称之为该结点的子孙结点。

8、祖先结点:从树根结点到达某个结点的路径上通过的所有结点称为该结点的祖先结点(不含该结点自身)。

9、兄弟结点:具有同一双亲的结点互相称之为兄弟结点。

10、结点层次:树具有一种层次结构,根结点为第一层,其孩子结点为第二层,如此类推得到每个结点的层次。

11、树的高度:树中结点的最大层次称为树的高度或深度。

12、森林:零棵或多棵互不相交的树的集合称为森林。

 

在这张图中:

 1、A有三个分支,所以它的度为3

 2、树的度也为3

 3、A,C,D,E,均有分支,所以它们均为分支结点

 4、B,H,F,G为叶子结点

 5、结点A的孩子结点为B,C,D

 6、E,F的双亲结点均为C

 7、结点C的子孙结点为E,F,H

 8、结点F的祖先结点为A,C,注意C不仅可以是F的双亲结点,也可以是它的祖先节点

 9、E,F为兄弟结点

 10、树有4层,它的高度也为4

1.3、树的性质

(1) 树中的结点数等于所有结点的度数加1。

(2)度为m的树中第i层上至多有m^i-1个结点,这里应有i≥1。

(3) 高度为h的m次树至多有(m^h-1)/(m-1) 个结点。

(4)具有n个结点的m次树的最小高度为[logm(n(m-1)+1)]。

1.4、树的基本运算

 查找满足某种特定关系的结点,如寻找当前结点的双亲结点等;

插入或删除某个结点,如在树的当前结点上插入一个新结点或删除当前结点的第i个孩子结点等;

遍历树中每个结点。

(1)三种遍历方法:

 先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。

 后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。

 层次遍历:若树不空,则自上而下自左至右访问树中每个结点。

 1.5、树的存储结构

(1)双亲存储结构:顺序存储结构

 (2)孩子链存储结构:每个结点包含结点值和所有孩子结点指针,可按一个结点的度设计结点的孩子结点指针个数。

 (3)长子兄弟链存储结构:为每个结点设计三个域:一个数据元素域,一个指向该结点的长子的指针域,一个指向该结点的下一个兄弟结点指针域。

 (4)列表存储结构:

 二、二叉树

2.1、二叉树的定义

1、二叉树也称为二分树,它是有限的结点集合,这个集合或者是空,或者由一个根结点和两棵互不相交的称为左子树和右子树的二叉树组成。

在含n个结点的二叉树中,所有结点的度小于等于2,通常用n0表示叶子结点个数,n1表示单分支结点个数,n2表示双分支结点个数。

2、二叉树的五种形态

 3、满二叉树和完全二叉树

(1)、满二叉树:所有分支结点都有左孩子结点和右孩子结点,并且叶子结点都集中在二叉树的最下一层,这样的二叉树称为满二叉树。(具有2^h-1个结点)

 特点:叶子结点都在最下一层。

            只有度为0和度为2的结点。

            含n个结点的满二叉树的高度为log2(n+1),叶子结点个数为n/2+1,度为2的结点个数   为n/2。

(2)、完全二叉树:二叉树中最多只有最下面两层的结点的度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。

 特点:叶子结点只可能出现在最下面两层中。

            对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。

            如果有度为1的结点,只可能有一个,且该结点只有左孩子而无右孩子;

            按层序编号后,一旦出现某结点(其编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。

2.2、二叉树性质

 1、 非空二叉树上叶结点数等于双分支结点数加1。即n0=n2+1。

总结:①所有结点的度之和=n-1 ②所有结点的度之和=n1+2n2 ③n=n0+n1+n2。

2、 非空二叉树上第i层上至多有2i-1个结点,这里应有i≥1。

3、 高度为h的二叉树至多有2h-1个结点(h≥1)。

4、具有n个(n>0)结点的完全二叉树的高度为[log2(n+1)]或[log2n]+1。

 一棵完全二叉树中,由结点总数n可以确定其树形。

 n1只能是0或1,当n为偶数时,n1=1,当n为奇数时,n1=0。

 层序编号为i的结点层次恰好为log2(i+1)或者log2i+1。

 2.3、二叉树存储结构

1、顺序存储结构:采用字符串(假设每个结点值为单个字符)或者列表(数组)存放。

适合于完全二叉树或者满二叉树

2、链式存储结构: 使用链表存储,二叉树中每一个节点由链表中的一个链节点来存储

class BTNode:			#二叉链中结点类
  def __init__(self,d=None):    	#构造方法
    self.data=d			#结点值
    self.lchild=None		#左孩子指针
    self.rchild=None		#右孩子指针

 

 3、列表存储结构: 每个结点为“[结点值,左子树列表,右子树列表]”的三元组,当左或者右子树为空时取值None。

 2.4、二叉树的递归算法

 假设二叉树中所有结点值为整数,采用二叉链存储结构,求该二叉树b中所有结点值之和。

设f(b)为二叉树b中所有结点值之和。

则f(b.lchild)和f(b.rchild)分别求根结点b的左、右子树的所有结点值之和。

显然有f(b)=b.data+f(b.lchild)+f(b.rchild)。

当b=None时f(b)=0,从而得到以下递归模型:

def fun(b):		#计算以b为根的二叉树的结点值之和
  if b==None: 
      return 0;
  else: 
      return b.data+fun(b.lchild)+fun(b.rchild)

 2.5、二叉树的基本运算

class BTree:                  	#二叉树类
  def __init__(self,d=None):    	#构造方法
    self.b=None                 	#根结点指针
  #二叉树基本运算算法

  
#设置二叉树的根结点SetRoot(b)
def SetRoot(self,r):              #设置根结点为r
  self.b=r
#求二叉链的括号表示串DispBTree()
def DispBTree(self): 			#返回二叉链的括号表示串
  return self._DispBTree(self.b)

def _DispBTree(self,t):			#被DispBTree方法调用
  if t==None:                    	#空树返回空串
    return ""
  else:
    bstr=t.data				#输出根结点值
    if t.lchild!=None or t.rchild!=None:
      bstr+="("				#有孩子结点时输出"("
      bstr+=self._DispBTree(t.lchild)	#递归输出左子树
      if t.rchild!=None:
        bstr+=","			#有右孩子结点时输出","
      bstr+=self._DispBTree(t.rchild)	#递归输出右子树
      bstr+=")"				#输出")"
  return bstr
#查找值为x的结点FindNode(x)
# 设f(t,x)在以t为根结点的二叉树中查找值为x的结点,找到后返回其地址,否则返回None。
def FindNode(self,x):	                  #查找值为x的结点算法
  return self._FindNode(self.b,x)

def _FindNode(self,t,x):               	  #被FindNode方法调用
  if t==None: 
     return None				  #t为空时返回None
  elif t.data==x: 
     return t				  #t所指结点值为x时返回t
  else:
     p=self._FindNode(t.lchild,x)	  #在左子树中查找
     if p!=None: 
        return p				  #在左子树中找到p结点,返回p
     else:
        return self._FindNode(t.rchild,x)	  #返回在右子树中查找结果
#求高度Height()
#设以t为根结点二叉树的高度为f(t),空树高度为0,非空树高度为左、右子树中较大的高度加1。
def Height(self):			#求二叉树高度的算法
   return self._Height(self.b)

def _Height(self,t):                     	#被Height方法调用
  if t==None:
     return 0				#空树的高度为0
  else:
     lh=self._Height(t.lchild)		#求左子树高度lchildh
     rh=self._Height(t.rchild)		#求右子树高度rchildh
     return max(lh,rh)+1

 程序验证:

if __name__ == '__main__':
  b=BTNode('A')	        #建立各个结点      
  p1=BTNode('B');p2=BTNode('C');p3=BTNode('D')    
  p4=BTNode('E');p5=BTNode('F');p6=BTNode('G')
  b.lchild=p1		#建立结点之间的关系      
  b.rchild=p2;p1.lchild=p3
  p3.rchild=p6;p2.lchild=p4
  p2.rchild=p5;bt=BTree()
  bt.SetRoot(b)
  print("bt:",end=' ');print(bt.DispBTree())
  x='X';p=bt.FindNode(x)
  if p!=None: print("bt中存在"+x)
  else: print("bt中不存在"+x)
  x='G';p=bt.FindNode(x)
  if p!=None: print("bt中存在"+x)
  else: print("bt中不存在"+x)
  print("bt的高度=%d" %(bt.Height()))

 2.6、二叉树先序、中序、后序遍历

1、先序遍历:根节点,左子树,右子树

2、中序遍历:右子树,根节点,左子树

3、后序遍历:左子树,右子树,根节点

 

分别遍历的结果是:

先序:ABDGCEF

中序:DGBAECF

后序:GDBEFCA

4、算法实现

#先序
def PreOrder(bt):	        	#先序遍历的递归算法
  _PreOrder(bt.b)

def _PreOrder(t):              	#被PreOrder方法调用
  if t!=None:
    print(t.data,end=' ')	#访问根结点
    _PreOrder(t.lchild)		#先序遍历左子树
    _PreOrder(t.rchild)		#先序遍历右子树
#中序
def InOrder(bt):               	#中序遍历的递归算法
  _InOrder(bt.b)

def _InOrder(t):               	#被InOrder方法调用
  if t!=None:
    _InOrder(t.lchild)		#中序遍历左子树
    print(t.data,end=' ')	#访问根结点
    _InOrder(t.rchild)		#中序遍历右子树
#后序
def PostOrder(bt):               #后序遍历的递归算法
  _PostOrder(bt.b)

def _PostOrder(t):              	#被PostOrder方法调用
  if t!=None:
    _PostOrder(t.lchild)		#后序遍历左子树
    _PostOrder(t.rchild)		#后序遍历右子树
    print(t.data,end=' ')	#访问根结点

2.7、二叉树的层次遍历

1、过程:从根节点开始访问,逐层进行,从左到右

 2、层次遍历算法设计

与队列的先进先出特点吻合:先将根结点b进队,在队不空时循环:从队列中出队一个结点p,访问它;若它有左孩子结点,将左孩子结点进队;若它有左孩子结点,将左孩子结点进队。如此操作直到队空为止。

from collections import deque	#引用双端队列deque
def LevelOrder(bt):   		#层次遍历的算法
   qu=deque()                   	#将双端队列作为普通队列qu
   qu.append(bt.b)		#根结点进队
   while len(qu)>0:             	#队不空循环
      p=qu.popleft()		#出队一个结点
      print(p.data,end=' ')	#访问p结点
      if p.lchild!=None:		#有左孩子时将其进队
         qu.append(p.lchild)
      if p.rchild!=None:		#有右孩子时将其进队
      	 qu.append(p.rchild)

2.8、二叉树的构造

1、由先序/中序序列或后序/中序序列构造二叉树

一棵所有结点值不同的二叉树,其先序、中序、后序和层次遍历都是唯一的,也就是说一棵这样的二叉树,不可以有两种不同的先序遍历序列,也不可能有两种不同的中序序列。

 

 总结:由先序遍历序列和中序遍历序列能够唯一确定一棵二叉树。

            由后序遍历序列和中序遍历序列能够唯一确定一棵二叉树。

            由先序遍历序列和后序遍历序列不能唯一确定一棵二叉树。

 三、线索二叉树

1、定义:对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域。 利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。

2、遍历线索化二叉树

(1)求中序序列的开始结点:实际上该结点就是根结点的最左下结点。

(2)对于一个结点p,求其后继结点的过程是:    ① 如果p结点的rchild指针为线索,则rchild所指为其后继结点。    ② 否则p结点的后继结点是其右孩子q的最左下结点post。

四、哈夫曼树

1、定义:在n0个带权叶子结点构成的所有二叉树中,带权路径长度WPL最小的二叉树称为哈夫曼树(或最优二叉树)。

权——给树中的结点赋上一个有着某种意义的数值

带权路径长度——从树根结点到某个结点之间的路径长度与该结点权的乘积

2、哈夫曼算法

(1)根据给定的n0个权值W=(w1,w2,…,wn0),对应结点构成n0棵二叉树的森林T=(T1,T2,…,Tn0),其中每棵二叉树Ti(1≤i≤n0)中都只有一个带权值为wi的根结点,其左、右子树均为空。    

(2)在森林T中选取两棵根结点权值最小的子树作为左、右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根的权值之和。称为合并,每合并一次T中减少一棵二叉树。    

(3)重复(2)直到T只含一棵树为止。这棵树便是哈夫曼树。

 注意:每次都是两棵子树合并 , 哈夫曼树中没有单分支结点

 

 3、定理:对于具有n0个叶子结点的哈夫曼树,共有2n0-1个结点。

 4、构造哈夫曼树中采用静态数组ht存储哈夫曼树:

class HTNode: 		#哈夫曼树结点类
  def __init__(self,d=" ",w=None):    #构造方法
    self.data=d		#结点值
    self.weight=w	#权值
    self.parent=-1	#指向双亲结点
    self.lchild=-1	#指向左孩子结点
    self.rchild=-1	#指向右孩子结点
    self.flag=True	#标识是双亲的左(True)或者右(False)孩子
  
def CreateHT():	  		#构造哈夫曼树
  global ht,n0,D,W               #全局变量,存放哈夫曼树等信息
  ht=[None]*(2*n0-1)             #初始为含2n0-1个空结点
  heap=[]                       	#优先队列元素为[w,i],按w权值建立小根堆
  for i in range(n0):           	#i从0到n0-1循环建立n0个叶子结点并进队
     ht[i]=HTNode(D[i],W[i])    	#建立一个叶子结点
     heapq.heappush(heap,[W[i],i])          	   #将[W[i],i]进队
  for i in range(n0,2*n0-1):	#i从n0到2n0-2循环做n0-1次合并操作
     p1=heapq.heappop(heap)	#出队两个权值最小的结点p1和p2
     p2=heapq.heappop(heap)
     ht[i]=HTNode()		#新建ht[i]结点
     ht[i].weight=ht[p1[1]].weight+ht[p2[1]].weight  #求权值和
     ht[p1[1]].parent=i         	#设置p1的双亲为ht[i]
     ht[i].lchild=p1[1]         	#将p1作为双亲ht[i]的左孩子
     ht[p1[1]].flag=True
     ht[p2[1]].parent=i         	#设置p2的双亲为ht[i]
     ht[i].rchild=p2[1]         	#将p2作为双亲ht[i]的右孩子
     ht[p2[1]].flag=False 
     heapq.heappush(heap,[ht[i].weight,i])  	    #将新结点ht[i]进队

5、哈夫曼编码:规定哈夫曼树中的左分支为0,右分支为1。 从根结点到每个叶子结点所经过的分支对应的0和1组成的序列便为该结点对应字符的编码。这样的编码称为哈夫曼编码。(实质就是使用频率越高的采用越短的编码)

注意:在一组字符的哈夫曼编码中,任一字符的哈夫曼编码不可能是另一字符哈夫曼编码的前缀。

五、二叉树与树、森林之间的转换

1、树到二叉树的转换及还原

(1)转换:

特点: 根结点只有左子树而没有右子树。 左分支不变(左分支为最左孩子),兄弟变成右分支(右分支实为双亲的兄弟)。

(2)还原:

特点: 根结点不变。 左分支不变(左分支为最左孩子),右分支变成兄弟。

2、森林到二叉树的转换及还原

(1)转换:

 (2)还原

六、并查集

1、定义:给定n个结点的集合,结点编号为1~n,再给定一个等价关系,由等价关系产生所有结点的一个划分,每个结点属于一个等价类,所有等价类是不相交的。 需要求一个结点所属的等价类,以及合并两个等价类。

2、基本运算:

#(1)Init():初始化。
#(2)Find(int x):查找x结点所属的等价类。
#(3)Union(int x,int y):将x和y所属的两个等价类合并。

 3、实现:

并查集就是一个森林。 每个等价类用一棵树表示,包含该等价类的所有结点,即结点子集, 每个子集通过一个代表来识别,代表即该子集中的某个结点,通常选择根做这个代表。

4、基本存储结构:

MAXN=1005                        #最多结点个数
parent=[-1]*MAXN		        	#并查集存储结构
rank=[0]*MAXN			#存储结点的秩

5、基本运算算法:(查找中的路径压缩)

def Init():	           	#并查集初始化 
  global n
  global parent
  global rank
  for i in range(1,n+1):
    parent[i]=i
    rank[i]=0
def Init():	           	#并查集初始化 
  global n
  global parent
  global rank
  for i in range(1,n+1):
    parent[i]=i
    rank[i]=0

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值