二叉树结构与算法思路解析

2023-10-26

二叉树

介绍

主要内容

  • 二叉树的概念和性质
  • 二叉树的存储结构
  • 遍历二叉树
    • 递归遍历
    • 非递归遍历
  • 线索二叉树
  • 哈夫曼树
  • 树和森林
    • 树和森林的存储
    • 树和森林与二叉树的转换
    • 树和森林的遍历

树型结构特点

一对多

例:

  • 自然界,树
  • 人类社会,家谱,新政组织结构
  • 计算机领域
    • 操作系统的文件组织结构
    • 基于26个字母索引的查找树等
    • 编译原理中表达式求值操作等

一:树和二叉树的基本概念

  1. 树的定义

    树是n(n>=0)个结点的有限集合,在任一棵非空树中:

    • 有且仅有一个称为根的结点;
    • 其余结点可以分为多个互不相交的集合,而且这些集合中的每一集合都是本身又是一棵树,称为根的子树-----递归定义
  2. 树的基本术语
    • 结点:树中的元素,包含一个数据元素及若干指向子树的分支
    • 结点的度:结点拥有的子树的个数
    • 结点层:从根结点开始算起,根为第一层;根的孩子为第2层结点;…
    • 孩子:结点的子树的根称为该结点的孩子
    • 双亲:孩子结点的上层结点,称为这些结点的双亲
    • 兄弟: 同一双亲的孩子结点
    • 树的高度:树中最大的结点层
    • 树的度:树中最大的结点度
    • 叶子:也叫终端结点,是度为0的结点
    • 分支结点:度不为0的结点
    • 森林:互不相交的树集合
    • 有序树:子树有序的树,如家族树
    • 无序树:不考虑子树顺序的树
  3. 二叉树的概念

    二叉树定义: 二叉树是有限元素的集合,该集合或为空,或由一个称为根的元素及两个不相交、分别称为左子树和右子树的二叉树构成;左,右子树本身也是二叉树------递归定义。二叉树结点的度可以为0,1,或2,最大为2。

    二叉树优点:

    • 二叉树结构简单,规律性强
    • 所有的树都可以转换成唯一对应的二叉树

    满二叉树:如果深度为k的二叉树有2k -1 个结点,则称为满二叉树。特点:每一层上都含有最大结点数

    完全二叉树:如果二叉树除最后一层外每一层都是满的,且最后一层或者是满的,或者结点都连续地集中在该层的最左端,则称其为完全二叉树。特点:所有的叶子结点都出现在第k层或k-1层。如下图所示:

    在这里插入图片描述

    满二叉树一定是完全二叉树

  4. 二叉树特点
    • 二叉树中每个结点最多有两棵子树,二叉树每个结点度小于等于2
    • 二叉树是有序树:二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树还是右子树。

    注意:对于树来说,如果只有一个孩子,没有左右之分

    例:对于树和二叉树,如果有三个结点分别有哪几种形态?

在这里插入图片描述

  1. 二叉树性质
    • 性质1:对于非空二叉树,如果叶子结点数为n0,度为2的结点数为n2,则有n0 = n2 + 1

      证明:

      ​ 设二叉树中度为1的结点数为n1,二叉树中的总结点数为N,因为二叉树中所有结点均小于或等于2,所有有:

      ​ N = n0 + n1 + n2 (1式)

      ​ 再看二叉树中的分支数,处根结点外,其余结点都由一个分支与其双亲相连接,设B为二叉树中的分支总数,则有:

      ​ N = B + 1 (2式)

      ​ 由于这些分支都是由度为1和2的结点射出,所以有:

      ​ B = n1 + 2 * n2 (3式)

      即:N = B + 1 = n1 + 2 * n2 + 1 (4式)

      结合1式和4式可得:n0 + n1 + n2 = n1 + 2 x*n2 + 1

      即:n0 = n2 + 1

    • 性质2:一棵非空二叉树的第i层上最多有2(i-1)个结点(i>=1)

在这里插入图片描述

  • 性质3:一棵深度为k的二叉树中,最多有2k-1个结点(k>=1)

    证明:深度为k的二叉树取最多结点时,二叉树中的每层上均应取最多结点。根据性质2得到,每层上的最大结点数为2(i-1),则二叉树中的总结点数为:20 + 21 + … + 2(k-1) = 2k -1

  • 性质4:具有n个结点的完全二叉树的深度为:【log2n】+ 1。(这里【x】表示对x向下取整)

    证明:假设此二叉树的深度为k,则根据性质3及完全二叉树的定义得到:2(k-1) <= n <= 2k -1

    两边取对数得到:k-1 <= log2n < k

    因为k是整数,所以k = 【log2n】 + 1

  • 性质5:如果一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第【log2n】+1 层,每层从左到右),则对任一结点i(1<=i<=n):

    • 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则 其双亲是结点【i/2】

    • 如果2i<=n,则其左孩子是结点2i;否则i无左孩子,为叶子结点

    • 如果2i+1<=n,则其右孩子是结点2i+1;否则结点i无右孩子

在这里插入图片描述

二:二叉树的顺序存储

二叉树的顺序存储一般是按照从上至下、从左至右的顺序存储。但是,这样存储后结点在存储位置上的前驱、后继关系并不是他们在逻辑上的邻接关系,只有通过一些方法能够确定结点在逻辑上的前驱和后继结点,这种存储才有意义。

  1. 完全二叉树的顺序存储

    完全二叉树按照这种编号方式时,依据性质5,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用列表元素的下标值确定结点在二叉树中的位置以及结点之间的关系。所以完全二叉树适合于采用顺序存储方式。

    图解:

在这里插入图片描述

解析:

我们开辟一块连续的内存空间,将完全二叉树的的结点存放到连续的内存空间中去,依据性质5就使得这个连续的内存空间具有了逻辑性。

  1. 一般二叉树的顺序存储

    对于一般二叉树,只有按完全二叉树的形式增添一些并不存在的空结点,才能利用性质5使用顺序存储的方式

    图解:

在这里插入图片描述

解析:

​ 先将一般二叉树补全为完全二叉树,没有的部分以一个空结点补全。然后在内存中开辟一块足够大的连续空间。将二叉树的值填入到内存空间中,空结点以0来表示。空间的大小为最后一个结点所在的下标的值。

  1. 总结

    ​ 这种存储方式会造成空间的大量浪费,最坏的情况是单支树,一棵深度为k的右单支树,只有k个结点,却需要分配2k-1个存储单元。

    优点:结点之间的关系蕴含在其存储中。

    缺点:浪费空间。只适合用来存放完全二叉树。

三:二叉树的链式存储

  1. 二叉链表

    链表中每个结点包含三个域,数据域和两个指针域,指针域分别用来指示该结点的左孩子和右孩子所在的结点的存储地址。

    结点的存储结构为:

在这里插入图片描述

在n个结点的二叉链表中,有 n+ 1 个空指针域

二叉链表内存结构图解(xxx为空指针域):

在这里插入图片描述

  1. 三叉链表

    三叉链表中每个结点由4个域组成:数据域、双亲指针域、左指针域、右指针域。双亲域为指向该结点双亲结点的指针。

    存储结构为:

在这里插入图片描述

三叉链表既便于查找孩子结点,又便于查找双亲结点;但是,相对于二叉链表而言它增加了空间的开销。故二叉链表是最常用的二叉树存储方式,以下的遍历,查询操作都是针对二叉链表进行的。

三叉链表内存结构图解

在这里插入图片描述

  1. 遍历二叉树

​ 二叉树的遍历是按一定次序对二叉树中的每个结点做逐一访问,或查找具有某个特点的结点,然后对这些满足条件的结点进行处理。例如:求叶子结点个数,对每个结点值做一下修改,打印所有结点等等。

​ 由二叉树的定义可知,一棵二叉树是由根结点、根结点的左子树、根结点的右子树三部分组成。因此,只要依次遍历这三部分,就可以遍历整个二叉树。

​ 二叉树的遍历可以理解为:访问根,遍历左子树和遍历右子树。令L为遍历左子树,D为访问根结点,R为遍历右子树。对L,D,R进行排列共有6种排列顺序(自行排列),约定先左后右则有三种遍历方法:DLR,LDR,LRD,分别称为先序遍历、中序遍历、后序遍历。

  • 先序遍历

    若二叉树为空,遍历结束。否则:

    • 访问根结点
    • 先序遍历根结点的左子树
    • 先序遍历根结点的右子树

    如下图所示二叉树的先序遍历结果为:A B D E G C F

在这里插入图片描述

  • 中序遍历

    若二叉树为空,遍历结束。否则:

    • 中序遍历根结点的左子树
    • 访问根结点
    • 中序遍历根结点的右子树

    如下图所示二叉树的中序遍历结果为:D B G E A C F

在这里插入图片描述

  • 后序遍历

    若二叉树为空,遍历结束。否则:

    • 后序遍历根结点的左子树
    • 后序遍历根结点的右子树
    • 访问根结点

    如下图所示二叉树的后序遍历结果为:D G E B F C A

在这里插入图片描述

  • 扩展:

    用二叉树表示表达式:a+b*(c-d)-e/f (中缀表达式),我们平时在数学中见到的表达式都是以中缀表达式来定义的。上述表达式可用以下二叉树表示。

在这里插入图片描述

我们在看的时候从左下对应着表达式的计算顺序开始看起。

我们将上面的二叉树以先序遍历和后序遍历来显示表达式如下:

  • 先序序列:-+a*b-cd/ef

    前缀表达式(波兰式):前缀表达式的运算符位于操作数之前

  • 后序遍历:abcd-*+ef/-

    后缀表达式(逆波兰式):后缀表达式的运算符位于操作数之后

我们平时读一个表达式是以中缀表达式的方式来读的,但计算机无法识别中缀表达式的运算,需要先将中缀表达式转换为前缀表达式或后缀表达式,然后计算机才能够进行运算。

  1. 由遍历序列恢复二叉树

​ 任意一棵二叉树的先序序列和中序序列都是唯一的,如果已知结点的先序序列和中序序列,则可以唯一的确定这棵二叉树。如图所示:

在这里插入图片描述

​ 先序序列先遍历根,所以由二叉树先序序列的第一个结点确定树/子树的树根

​ 由得到的树根将中序序列分为左子树中序序列和右子树中序序列

​ 将左右子树按相同的方法进行恢复

​ 例:已知一棵二叉树的先序序列和中序序列分别为:

​ 先序序列:ABCDEFGHI

​ 中序序列:BCAEDGHFI

​ 恢复二叉树的过程如下图所示:

在这里插入图片描述

​ 通过恢复的二叉树我们可以得到它的后序序列为:CBEHGIFDA

​ 恢复二叉树的两个序列中必须有中序遍历序列来区分左子树序列和右子树序列,所以用两种序列恢复二叉树一共有两种方案:先序序列和中序序列恢复,后序序列和中序序列恢复。

​ 扩展:二叉树除了我们上面讲的三种序列外,还有一种序列叫层次遍历,即按从上到下,从左到右顺序遍历。

  1. 二叉树的遍历算法递归实现方式

因为我们目前还没有创建二叉树(创建二叉树在下一篇会讲到),所以我们在这里先讲一下它的实现思想,不涉及具体代码。以下代码仅供了解算法实现思想。

先序遍历算法:

def preorder(t):
	if t == None:
		return None
	print(t.data)
	preorder(t.lchild)
	preorder(t.rchild)

中序排列算法:

def midtraverse(t):
	if t == None:
		return None
	midtraverse(t.lchild)
	print(t.data)
	midtraverse(t.rchild)

后序排列算法

def postorder(t):
	if t == None:
		return None
	postorder(t.lchild)
	postorder(t.rchild )
	print(t.data)

我们可以看到,三种遍历算法的不同仅在于递归调用顺序的不同。

  1. 遍历算法的应用

统计二叉树中叶子结点的个数

如何判断叶子结点(二叉链表):

​ 当一个结点的左孩子和右孩子即左指针域和右指针域为空的时候,这个结点就是叶子结点。

如何找到叶子结点:

​ 定义一个计数器,然后遍历二叉树的每一个元素,遍历方式可以为先序、中序、后序任意一种。如果这个元素的左右指针域都为空时,则让计数器加1,直至遍历完所有结点。

注:因为我们在遍历时进行的是递归操作,需要在每一个函数中进行累加,所以计数器需设置为全局变量。

def countleaf(t):
	if t == None:
		return None
	if t.lchild == None and t.rchild == None:
		global n  # 全局变量
		n = n+1  # 叶子数n累加
	countleaf(t.lchild)
	countleaf(t.rchild)
  1. 建立二叉树二叉链表存储结构

​ 在以上的遍历等操作中,我们都是在二叉链表已经建立好的情况下进行的,但事实上我们并没有建立一个二叉链表,接下来我们就讲一下如何建立二叉链表。

​ 基本思想:设每个元素是一个字符。输入先序序列(在空字符串添加*),按先序遍历的顺序,建立二叉链表的所有结点并完成相应结点的链接。

​ 在遍历过程生成结点,建立二叉树的链式存储结构,需按先序遍历算法建立二叉树的存储结构,先建立根,再建立根的左右子树。

​ 注:在建立二叉树的过程中,只能依赖于先序遍历进行创建

定义结点类

class Node:
	def __init__(self,data=None,lchild=None,rchild=None):
		self.data = t
		self.lchild = lchild
		self.rchild = rchild

定义二叉树创建函数

# 先序递归遍历二叉树
def creatBT(self):
    ch = input("请从键盘上输入一个字符")
    if ch == "*":
    	T = None
    else:
        T = Node()
        T.data = ch
        T.lchild = creatBT()  # 建立左子树
        T.rchild = creatBT()  # 建立右子树
    return T  # 返回根结点

三:二叉树的列表存储

为什么可以使用列表来存放二叉树?

  • 二叉树是递归定义的
  • python的列表也是递归结构

步骤:

  • 空数用None表示
  • 非空二叉树用包含三个元素的列表[data,lchild,rchild]表示

把一棵二叉树映射到一种分层的list结构,每棵二叉树都有与之对应的列表。结构如图所示:

在这里插入图片描述

创建一个二叉树列表结构

初始化一个二叉树结点

# 创建二叉树结点类
class BinTreeList:
	def __init__(self,data,lchild=None,rchild=None):
		# 初始化一个结点
		self.btree = [data,lchild,rchild]
# 创建一个类对象
t = BinTreeList("A")

当我们实例化一个二叉树结点对象后,这个对象就是一个包含三个元素的列表,即为[“A”,None,None]

判断结点是否为空

def is_empty_bintree(self):
	return self.btree[0] is None

设置左孩子

def set_lchild(self,lchild):
	if self.is_empty_bintree():
		print("tree is empty")
	# 左右孩子同样为一个列表,需要用btree方法将列表取出来
	self.btree[1] = lchild.btree  

设置右孩子

def set_rchild(self,rchild):
	if self.is_empty_bintree():
		print("tree is empty")
	self.btree[2] = rchild.btree

递归遍历二叉树

先序遍历

def preorder(t):
	if t == None:
		return None
	print(t[0])
	preorder(t[1])
	preorder(t[2])

中序遍历

def preorder(t):
	if t == None:
		return None
	preorder(t[1])
	print(t[0])
	preorder(t[2])

后序遍历

def preorder(t):
	if t == None:
		return None
	preorder(t[1])
	preorder(t[2])
	print(t[0])

求二叉树中叶子结点个数

def leafnum(t):
	if t == None:
		return None
	if t[1] == None and t[2] == None:
		global n
		n = n + 1
	else:
		leafnum(t[1])
		leafnum(t[2])

查找某个元素

def searchdata(t,data):
	if t == None:
		return None
	if t[0] = data
		retrun 1
	else:
		searchdata(t[1],data)
		searchdata(t[2],data)

总结:

  • 列表是python的一种标准类型,二叉树结点是一个三元组
  • 二叉树是递归结构,python的列表也是递归结构
  • python的元组也可以实现这种二叉树的递归结构,但是实现的是非变动性二叉树结构,因为元组是不可变类型。

四:二叉树遍历的非递归算法

​ 由先序、中序、后序的遍历过程及代码可知,先序、中序、后序遍历过程中经过结点的路线是相同的,只是输出的时机不同。

​ 路线的形成:它们都是先从根结点开始,沿左子树深入,去判断左子树是否存在,如果存在则进一步判断,直至到达最低端。然后从最低端返回到相应的根结点处,从根结点处再判断右子树是否存在,直到最后从根结点的右子树返回到根结点。

​ 先序遍历:遇到结点就打印

​ 中序遍历:从左子树返回时遇到结点访问

​ 后序遍历:从右子树返回时遇到结点访问

​ 返回结点的顺序与深入结点的顺序相反,即后深入先返回,所以可以利用栈辅助实现遍历

​ 基本思想:

  • 在沿左子树深入时,深入一个结点压栈一个结点
  • 若为先序遍历,则在入栈前访问
  • 当沿左分支深入不下去时则返回,即从堆栈中弹出刚压栈的结点,若为中序遍历,则此时访问该结点,然后从该结点的右子树继续深入
  • 若为后序遍历,则将此结点再次入栈,然后从该结点的右子树继续深入…直到第二次从栈里弹出该结点,即从右子树返回时,才访问之。

我们以非递归中序遍历为例:

图解:

在这里插入图片描述

规律:

  • 只要栈中弹出了一个元素,下一步就是访问这个元素,即进入这个元素的右子树中
  • 当一个元素没有左子树时,就会将这个元素弹出并访问
  • 当一个元素没有右子树树时,就会将此时栈中最上面的元素弹出并访问

代码实现:

​ 关于栈的创建代码在以前的博客中有过详细介绍,这里不再复写。这里优先了解代码思路。

​ t最先指向根结点

def InOrder(t):
	p = t
	s = ListStack()  # 初始化一个栈
	while s.is_empty() != 1 or p != None:  # 如果栈或p不为空
		while p != None:  # 如果p不为空
			s.pushstack()  # 将当前节点入栈
			p = p.lchild  # 向左子树不断深入
			
		if not s.is_empty():
			p = s.popstack()  # 弹出栈顶元素,并将弹出元素赋值给p,相当于返回到当前树的根结点
			print(data)  # 打印当前节点元素
			p = p.rchild  # 遍历右子树

五:哈夫曼树

  1. 哈弗曼树的用途

    ​ 哈弗曼树是一种重要的二叉树,又称"最优树",或者"最优二叉树",在信息领域中有重要的理论和实际价值

  2. 哈夫曼树的定义
    • 路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;

    • 路径长度:路径分支上的分支数目称为路径长度

    • 结点的权:在许多应用中,常常将树中的结点赋上一个有着某种意义的实数,称此实数为该结点的权。

    • 结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积

    • 树的带权路径长度=树中所有叶子结点的带权路径之和,通常记作:
      W P L = ∑ i = 1 n w i ∗ l i WPL = \sum_{i=1}^{n}{w_i*l_i} WPL=i=1nwili
      wi为权值,li为根到结点的路径长度

    哈夫曼树:假设有n个权值(w1,w2,…,wn),构造有n个叶子结点的二叉树,每个叶子有一个wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。

  3. 哈夫曼树图解

    例:有4个结点,权值分别为7,5,2,4,构造有4个叶子结点a,b,c,d分别对应4个权值的二叉树

在这里插入图片描述

第一棵树的WPL为:
2 ∗ 4 + 3 ∗ 7 + 3 ∗ 5 + 2 = 46 2*4 + 3*7 + 3*5 + 2 = 46 24+37+35+2=46
第二棵树的WPL为:
2 ∗ 7 + 2 ∗ 5 + 2 ∗ 2 + 2 ∗ 4 = 36 2 * 7 + 2 * 5 + 2 * 2 + 2 * 4 = 36 27+25+22+24=36
第三棵树的WPL为:
7 + 2 ∗ 5 + 3 ∗ 2 + 3 ∗ 4 = 35 7 + 2 * 5 + 3 * 2 + 3 * 4 = 35 7+25+32+34=35
第三棵树的WPL最小,我们将类似于第三课树的二叉树称之为哈夫曼树

哈夫曼树特征:权值越大的叶子越靠近树根,权值越小的叶子越远离树根

  1. 哈夫曼树构建步骤

    将n个权值{w1,w2,…,wn}对应n个结点,构成具有n棵二叉树的森林F={T1,T2…Tn},其中每棵二叉树Ti(1<= i <=n)都只有一个权值为wi的根结点,其左,右子树均为空

    • 在森林F中选出两棵根结点的权值最小的树作为一棵新树的左、右子树,且新树的根结点的权值为其左、右子树上根结点的权值之和
    • 从F中删除构成新树的那两棵树,同时把新树加入F中
    • 重复2和3步,直到F中只含有一棵树为止,此树便是哈夫曼树

    例:利用w={5, 29, 7, 8, 14, 23, 3, 11}构建一棵哈夫曼树,详细创建过程如下图所示

在这里插入图片描述

注:哈夫曼树的结构不唯一,当我们在将两棵树合成一棵新树的时候,在没有约定的情况下不区分左右子树,所以哈夫曼树的结构不唯一。哈夫曼树虽然不唯一,但WPL是唯一的。

  1. 哈夫曼树构建算法
    • 哈夫曼树的存储结构

    在这里插入图片描述

    • 算法思路

      • 将n个叶子结点的数据存放到列表中
      • 对列表进行排序,找列表中两个权值最小的结点分别作为左右子树,创建一个新的树,新树的根的权值为左右两个子树权值之和,并修改这三个结点的指针指向;父结点的左右指针分别指向左右孩子,孩子结点的父指针指向父结点。
      • 把刚才的两个权值最小的结点从列表中删除,并把新的根结点加入到列表中
      • 重复2,3步骤,直到列表中只有一个元素为止,即只有根结点。
    • 算法描述

      例:构造以{7, 5 , 2, 4}为权值的哈弗曼树

    # 定义节点类
    class Node:
    	def __init__(self,data):
             self.lchild = None
             self.rchild = None
             self.parent = None
             self.data = data
    	def makeTree(self,nodes):
         nodes_list = nodes[:]
         while len(node_list) > 1:
         # 将列表进行排序
         nodes_list.sort(key = lambda item:item.data)
         # 取出最小的元素作为左子树值并将这个值删除
         node_left = nodes_list.pop(0)
         # 取出第二小的元素作为右子树值并将这个值删除
         nonde_right = nodes_list.pop(0)
         # 创建父结点,父结点的值为左右子树值的和
         node = Node(node_left + node_right)
         # 将父结点的左指针指向左子树
         node.lchild = node_left
         # 将父结点的右指针指向右子树
         node.rchild = node_right
         # 将左子树的父指针指向父结点
         node_right.parent = node
         # 将右子树的父指针指向父结点
         node_left.parent = node
         # 把新生的树加入到列表中
         nodes_list.append(node)
         # 最后一个元素是根节点,没有父结点
         nodes_list[0].parent = None
         # 返回根结点
         return nodes_list[0]
    
  2. 哈夫曼编码

    ​ 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)

    ​ 编码:在数据通信中,需要对传送的文字转化为二进制字符0,1组成的二进制串

    ​ 例如:设要发送电文"ABACCDA",其中只含有4种字符A,B,C,D

    • 等长编码

      编码二进制位数与字符的个数有关。以下图等长编码对电文进行编码后为:00010010101100

    在这里插入图片描述

    • 不等长编码

      将出现次数多的字符用较短的二级制位进行编码。以下图不等长编码对电文进行编码后位:010011010

    在这里插入图片描述

    此方案中当我们在对代码串进行译码时,会出现AC=D的情况,这是因为字符A的编码是字符D编码的前缀,具有二义性。

    最优编码要求:

    • 采用不等长编码,使报文尽量短

    • 任何一个字符的编码都不能是其它字符编码的前缀,保证译码的惟一性。

    解决方案:

    • 统计字符集中每个字符在电文中出现的概率,概率越大编码越短。
    • 利用哈夫曼树的特点,权越大的叶子离根越近,将每个概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
    • 在哈夫曼树的每个结点的左分支标0,右分支标1
    • 从根到每个叶子的路径上标号链接起来,作为该叶子代表的字符的编码

    例:设要传输额字符集D={C, A, S, T, ;},字符出现频率w={2, 4, 2, 3, 3}

    • 通过字符出现的频率即权值确定哈夫曼树

    在这里插入图片描述

    由上述哈夫曼图可得各个字符的编码:

    A:00 C:010 S:011 T:10 ;:11

    我们可以看出,哈夫曼编码完全契合编码要求

    哈夫曼编码总长最短原因:因为哈夫曼树的带权路径长度最短,即每个字符的编码长度与其出现的次数或频度的乘积之和最短,也就是电文的编码总长最短。

    哈夫曼树保证译码唯一性原因:因为在哈夫曼树中,每个字符结点都是叶子结点,它们不可能在根结点到其它字符结点的路径上。

    算法实现:

    在哈夫曼树的三叉链表结构中,从各个叶子结点开始,沿叶子结点的双亲链域退到根结点,在回退的过程中,根据分支的左右确定编码。

    注:由于一个字符的哈夫曼编码是从根结点到相应叶子结点所经过的路径上各分支组成的0,1序列,因此先得到的分支代码应为所求编码的低位码。

    过程:

    • 遍历nodelist,从第一个叶子结点开始回退
    • 遇到左分支设置为0,遇到右分支设置为1
    • 直到回退到根结点,再去找下一个叶子结点

    叶子结点图解:

在这里插入图片描述

代码实现:

nodes为一个保存有叶子结点的列表

def hufumanCode(nodes,root):
	# 初始code_list列表长度,列表长度即为叶子结点的个数
	code_list = [""] * len(nodes)
	# 对每个列表中每个结点进行求编码
    for i in range(len(nodes)):
    	# 判断当前节点是否是root结点
        while node[i] != root:
            # 判断是否有左子树
            if node[i].parent.left == node[i]:
            	# 左分支设置为0
            	code_list[i] = "0" + code_list[i]
            else:
            	# 右分支设置为1
            	code_list[i] = "1" + code_list[i]
            # node指向父节点
            node = node.parent
	return code_list

注意:我们在为左右分支分别设置0和1时,需要注意设置的顺序不能变。即0或1必须在code_list[i]的前面。因为我们编码需要从叶子结点先到达根结点然后在回溯时输出编码的值,而代码从叶子结点到根结点的过程中就开始记录编码的值,所以必须把后得到的值放在现有值前面才能得到正确的编码顺序。

六:树的基本性质

  1. 树的概念

​ 树是n(n>=0)个结点的有限集合,在任一棵非空树中:

​ 有且仅有一个称为根的结点

​ 其余结点可分为多个互不相交的集合,而且这些集合中的每一集合都本身又是一棵树,称为根的子树。

​ 森林是m(m>=0)个互不相交的树的集合

  1. 树的性质
    • n个结点的树中有n-1条边。

      证明:除树的根结点外每个结点有且只有一个直接前驱,除树的根结点之外的结点数等于所有结点的分支数。

    • 在度为k的树中,第i层至多有ki-1个结点。

      证明:最多的情况为,除了叶子结点,每个结点的度都是k

    • 度为k、高为h的树中至多有(kk-1/k-1)个结点

      证明:最多的情况为,除了叶子结点,每个结点的度都是k

    • 具有n个结点的度为k的树最小高度为 : logk(n*(k-1)+1)(如果取得小数则向上取整),最大高度为:n-k+1

      证明:当树为满k叉树时,高度是最小的,由第三个性质推导而来,n = (kk-1/k-1) ==> kh=n*(k-1)+1 ==> h = logk(n*(k-1)+1)。最大高度的情况为,从根结点开始,每一层只有一个结点,只有最后一层有k个结点。

  2. 树的存储

    ​ 树的存储结构有很多,既可以采用顺序存储结构,也可以采用链式存储结构。但无论采用哪种存储方式,都要求存储结构不仅能存储各结点本身的数据信息,还要能惟一地反映树中各结点之间的逻辑关系。

    ​ 常用的存储方式有:

    • 子节点引用表示
    • 父节点引用表示(双亲结点法)
    • 子节点表表示(孩子链表法)
    • 长子兄弟表示(二叉链表法)
    • list递归实现
  3. 子节点引用表示

    在这里插入图片描述

    在最大m度结点表示的n个结点的树中,我们为每个结点分配m+2个空间,其中第一个指针域用来存储数据,第二个指针域用来存储子结点的个数,剩下的指针域用来存储子节点的地址信息。

    性质:在这样的一棵树中,会产生n*(m-1)+1个空指针域

    证明:共有n个结点,每个结点有m个指针域,即共有n*m个指针域,由于根结点没有指针域,所以共用了n-1个指针域。所以共产生n*m-(n-1)个空指针域,即产生n*(m-1)+1个空指针域。

    优点:能直接反映子树的机构,操作方便灵活,很好的支持树结构的变动。

    缺点:出现大量空闲的结点指针域

  4. 父节点引用(双亲表示法)

    子节点引用会导致大量空闲的结点指针域,所以我们一般不会采用此种表示方法。父节点引用可以避免此类问题。

    用一种连续的存储空间存储树中的各结点,树中除根外的每个结点都有惟一的一个双亲结点,所以每个结点存储元素本身的信息和结点的双亲结点的位置。

在这里插入图片描述

如上图中的树,先将树按从上到下,从左到右的顺序排序,然后记录对应元素的父结点索引,从而得到一个存储着父节点数据和索引的列表。

优点:存储开销小,找双亲很容易,适合于寻找双亲的场合

缺点:找孩子难

# 双亲类结点类(data.parent)
class CNode:
	def __init__(self,data,parent):
		self.data = data
		self.parent = parent
nodes = []
  1. 子节点表表示(孩子链表法)

    ​ 孩子链表示法是通过保存每个结点的孩子结点的位置,表示树中结点之间的结构关系。每个结点的孩子结点用单链表存储,再用含n个元素的列表指向每个孩子链表。

    ​ 图解:

在这里插入图片描述

# 孩子结点类
class CNode:
	def __init__(self,child,next=None):
		self.child = child
		self.next = next
		
# 双亲结点类
class PNode:
	def __init__(self,data,firstchild=None):
	self.data = data
	self.firstchild = firstchild

# 存放双亲结点的列表
nodes = []

优点:找孩子容易,适合用于寻找孩子的场合

缺点:找双亲难

​ 如果既想要寻找子结点又想要寻找父结点,可以将父结点表示法与子结点法相结合的方法来构建。

图解:

在这里插入图片描述

  1. 长子兄弟表示法(二叉链表表示法)

    ​ 长子兄弟表示法用二叉链表作为树的存储结构。将树中的多支关系用二叉链表的双分支关系体现。

    ​ 结点的左指针指向它的第一个孩子(长子)结点

    ​ 右指针指向它的下一个兄弟结点

    ​ 图解如下:

    在这里插入图片描述

  2. list递归实现(python特有)

    树(根结点,一组子树) ===》用二元组来表示树:列表或元组

    存放方式与二叉树相似

    图解:

在这里插入图片描述

七:树,森林与二叉树之间的相互转换

​ 转换如图所示:

在这里插入图片描述

​ 由图可见:上图中的树与二叉树在内存中的存储结构是完全相同的,只是画图时为了区分树的层次而看起来有所不同。所以我们针对树的操作,比如遍历,增删结点等,都可将树先转换为二叉树然后再进行操作。

  1. 树转换为二叉树

    方法:在树中的长子即是二叉树中的左孩子,在树中的兄弟即是二叉树中的右孩子。具体如下

  • 加线:在兄弟之间加一连线

  • 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系

  • 旋转:以树的根结点为轴心,将整树顺时针转45

    转换图解如下:

在这里插入图片描述

由图中可以看出:树转换成的二叉树其右子树一定为空

  1. 森林转换为二叉树的方法:
  • 将各棵树分别转换为二叉树

  • 将每棵树的根节点用线相连

  • 以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转45

    转换图解如下:

在这里插入图片描述

  1. 二叉树转换为树
  • 加线:若p结点是双亲结点的左孩子,则将p结点的右孩子,右孩子的右孩子,…沿分支找到所有右孩子,都与p的双亲用线连起来

  • 抹线:抹掉原二叉树中双亲与右孩子之间的连线

  • 调整:将结点按层次排列,形成树结构

    转换图解如下:

在这里插入图片描述

  1. 二叉树转换为森林

    这里我们需要先搞清楚一个问题,即如何判断一个二叉树对应的是树还是森林。如果一个二叉树的根结点有右子树那么它对应的就是森林,没有则对应树。

  • 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。

  • 还原:将孤立的二叉树还原成树

    转换图解如下:

在这里插入图片描述

八:树、森林的遍历

​ 树和森林的遍历总共有3中,比二叉树的遍历少了一个中序遍历,如下:

  • 层次遍历:从上到下,从左到右依次遍历树中各个结点
  • 先根遍历:先访问根,然后依次先序遍历每一棵子树
  • 后根遍历:依次后序遍历根的每一棵子树,然后访问根
  1. 树的遍历

​ 如下图所示 :

在这里插入图片描述

​ 先根遍历序列:A B E F C G D H I

​ 后根遍历序列:E F B G C H I D A

​ 注:树的先根遍历与这棵树对应的二叉树的先序遍历是相同的,树的后根遍历对应二叉树中的中序遍历

  1. 森林的遍历
  • 先根遍历

    对森林中的每一棵树进行先序遍历;

    先根遍历森林中第一棵树的子树森林;

    先根遍历森林中(除第一棵树之外)其余树构成的森林

    如下图所示:
    在这里插入图片描述
    先根遍历序列为:A B C D E F G H I J

注:先根遍历森林和先序遍历与该森林对应的二叉树结果相同

  • 后根遍历

对森林中的每一棵树进行后序遍历

若森林不空,则

后根遍历森林中的第一棵树的子树森林;

访问森林中第一棵树的根结点;

后根遍历森林中(除第一棵树之外)其余树构成的森林

上图后根遍历的序列为:B C D A F E H J I G

注:后根遍历森林和中序遍历与该森林对应的二叉树结果相同

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

二叉树结构与算法思路解析 的相关文章

  • 如何使用keras打印神经网络中预测类的名称?

    我在 keras 中使用预先训练的模型 最终将类索引预测为一些整数值 但我似乎不明白如何打印这些类的名称 我使用的模型是 ResNet 50 看一下https martin thoma com image classification ht
  • Cython 中的抽象类(具有纯虚方法)

    快速版本 如何在 Cython 中声明抽象类 目标是只声明接口 以便其他类可以继承它 必须有没有实施这个班级的 接口 pxd cdef class IModel cdef void do smth self impl pyx from in
  • 使用 setup.py 编译 python 应用程序

    我已经指出了将 pygame 导出到可执行文件以进行分发的问题 我仍然有一个问题 当我运行 setup py 我使用 python 版本 3 7 0 并构建应用程序时 应用程序直接崩溃 我也无法打开 unix 可执行文件 这正是我到目前为止
  • OpenGL 说“from_param 收到了一个不连续的数组”

    安装 Yosemite 后 我必须升级 numpy PyOpenGL 等 现在 以前运行的程序给了我以下堆栈跟踪 file latebind pyx line 44 in OpenGL accelerate latebind Curry c
  • Plotly:如何使用日期时间索引绘制中心有一条线的范围?

    我想绘制一条周围有范围的线 就像这张照片所示 我发布了一个原始问题 但没有指定索引是日期时间索引 我以为这并不重要 但我错了 有一个答案用数字索引覆盖它 Plotly 如何制作具有多条线和标准差阴影区域的图形 https stackover
  • 如何在Python中将字符串转换为复数?

    我正在尝试将输入字符串转换为浮点数 但是当我这样做时 我不断收到某种错误 如下面的示例所示 gt gt gt a 3 3j gt gt gt b complex a Traceback most recent call last File
  • vscode python 远程解释器

    通过使用 VSCode Visual Studio Code 我在本地 Python Anaconda 解释器上执行 Python 代码 现在我想对其进行设置 以便能够在远程 Python 解释器上执行该代码 我有一个 Linux 设备 它
  • 无法获取POST参数

    我正在使用 WebApp2 作为框架在 Python 中开发一个 Web 应用程序 我无法获取通过填写表单提交的http POST请求参数 这是我创建的表单的 HTML 代码
  • python3导入找不到模块

    我正在尝试测试书中的一个例子 我得到了一个ImportError 该示例开始如下 from tkinter import from PP4E Gui Tools widgets import frame button entry 如果我放一
  • Python 元类有什么用?

    元类可以用其他方式做不到的事情做什么 Alex Martelli 表示 有些任务如果没有元类就无法完成Python 元类与类装饰器 https stackoverflow com questions 1779372 python metac
  • 在测试中检查 CLI 的退出代码

    我正在为命令行工具编写自动化测试 本质上 我想使用各种选项调用 CLI 并测试退出代码和 或输出 我的测试如下所示 from mymodule cli tool import main def test args capfd with py
  • py2exe + sqlalchemy + sqlite 问题

    在进入全速开发模式之前 我正在尝试让一些基本的东西在 Python 中工作 具体如下 Python 2 5 4 PyQt4 4 4 3 SqlAlchemy 0 5 2 py2exe 0 6 9 setuptools 0 6c9 pysql
  • 为什么 django-rest-frameworks request.data 有时是不可变的?

    在我宁静的CreateAPIView我变异我的request data字典 有时我会收到测试未捕获的错误 This QueryDict instance is immutable 例如这 class CreateView CreateAPI
  • PyQt5:如何将 QPushButton 连接到插槽?

    好吧 几乎所有教程 可理解的用人类语言编写的文档都是针对 PyQt4 的 但是 PyQt5 改变了整个 将按钮连接到插槽 的工作方式 但我仍然不知道如何做到这一点 我在 QtDesigner 中做了一个快速 gui 并且有一个 QPushB
  • 如何在Python 2.7中访问ODB文件

    我想在 Python 中访问 ODB 文件 使用 LibreOffice Base 创建 并提取一个表以供进一步使用 ODB包含多个表 一种关系设计和多种表单 是否可以在不使用任何 SQL 的情况下实现这一目标 Edit 由于我自己解析这种
  • 包含多个双引号的 CSV 拆分正则表达式

    我有一个包含文本的 CSV 列数据 每行用双引号分隔 一行中的示例文本类似于此 notice 新行和每行之前的空格是故意的 Lorem ipsum dolor sit amet consectetur adipisicing elit se
  • 如何在Python中使用JWK解码JWT令牌

    我正在开发一个应用程序 其中所有 API 都受 OAuth 保护 我已从客户端收到访问令牌 但无法解码和验证令牌 我有以下格式的 JWK keys kty RSA x5t S256 Some value e Some Value x5t S
  • SQLAlchemy - 如何从 ResultProxy 访问列名并写入 CSV 标题

    我正在尝试使用 SQLAlchemy 建立与 PostgreSQL 数据库的连接 执行 SQL 查询并将文件的输出打印到 Linux 中的文件中 from sqlalchemy import create engine import yam
  • 如何将焦点设置到 django 表单元素的 CharField

    我的登录页面使用 Django 表单 I want to set focus to the first textField username when the page loads 我尝试使用 Jquery 如下 但没有得到结果 forms
  • Pytorch ValueError:优化器得到一个空参数列表

    当尝试创建神经网络并使用 Pytorch 对其进行优化时 我得到了 ValueError 优化器得到一个空参数列表 这是代码 import torch nn as nn import torch nn functional as F fro

随机推荐

  • 计算机网络相关知识点

    计算机网络知识点 1 流量单位换算 2 概念和单位换算 3 计算机网络概述 4 例题 本文参考资料一 GitHub上的博客CS Notes 本文参考资料二 百度文库计算机网络知识点文档 1 流量单位换算 计算机中表示容量的单位有B KB M
  • MySQL5.7 下载安装

    一 下载 尽量使用压缩包解压缩方式安装 压缩包的解压后配置下环境变量就能使用 如果使用安装程序 msi安装程序 安装 卸载起来会比较麻烦 下载地址链接 各版本下载链接 二 安装 1 解压缩 下载的zip压缩包解压缩 我的mysql解压缩安装
  • AttributeError: ‘str‘ object has no attribute ‘parse‘

    今天 使用python提取版本号 pip3 install packaging from packaging import version A 3 5 2 version parse A 发现报错 AttributeError str ob
  • golang开发:类库篇(三)命令行工具cli的使用

    为什么要使用命令行 觉得这个问题不应该列出来 又觉得如果初次进行WEB开发的话 可能会觉得所有的东西都可以使用API去做 会觉得命令行没有必要 其实 一个生产的项目命令行是绕不过去的 比如运营需要导出报表 统计下付费用户 服务不稳定修改下订
  • 推荐引擎系统架构

    本文从互联网收集并整理了推荐系统的架构 其中包括一些大公司的推荐系统框架 数据流存储 计算 模型应用 可以参考这些资料 取长补短 最后根据自己的业务需求 技术选型来设计相应的框架 后续持续更新并收集 界面UI那一块包含3块东西 1 通过一定
  • vue3高德地图点击标记显示自定义提示框/地图平移过渡(panBy/panTo)

    上一篇文章有讲到点击标记显示窗口信息 但是在实际的项目需求中我们可能需要在某一个固定的地方显示自定义的内容 这里就需要我们自己动手了 4条消息 vue3高德地图多个点标记 窗口信息 点标记自定义图片不显示问题 奋斗不息 编码不止 的博客 C
  • 【MySQL】八,角色管理

    创建角色 引入角色的目的是方便管理拥有相同权限的用户 恰当的权限设定 可以确保数据的安全性 语法 CREATE ROLE role name host name role name host name 创建一个经理的角色 create ro
  • 基于Uniapp+SpringBoot+Vue的电影交流平台小程序设计与实现(源码+lw+部署文档+讲解等)

    前言 博主介绍 全网粉丝10W CSDN特邀作者 博客专家 CSDN新星计划导师 全栈领域优质创作者 博客之星 掘金 华为云 阿里云 InfoQ等平台优质作者 专注于Java 小程序技术领域和毕业项目实战 精彩专栏 推荐订阅 2023 20
  • FreeRTOS笔记(九)定时器

    定时器Timer 软件定时器是基于系统时钟中断且由软件来模拟的定时器 当经过设定的Tick 时钟计数值后会触发用户定义的回调函数 软件定时器不占用单片机宝贵的硬件资源和CPU资源 FreeRTOS提供了完善的软件定时器的支持 为了启用软件定
  • JAVA对象的toString方法

    一切类都是Object的子类 Object有toString方法 因此所有对象都有toString方法 打印一个对象时 打印的就是这个对象的toString方法返回值 值为 类名 hashCode 因此很多时候需要程序员重写此方法 推荐写法
  • Python运维开发(CMDB资产管理系统)——Python基础数据类型

    Python基础数据类型 字符串 可以通过单引号 双引号 三个双引号来表示 布尔 True和False 整数 浮点数 列表 定义一个列表 列表常用的一些函数 append 向列表中添加元素 元素可以是整数 浮点数 字符串等类型 count
  • 云财经服务器维护,云财经服务器维护

    云财经服务器维护 内容精选 换一换 云耀云服务器适用于对CPU 内存 硬盘空间和带宽无特殊要求 服务一般只需要部署在一台或少量的服务器上 一次投入成本少 后期维护成本低的场景 例如网站开发 Web应用 推荐使用云耀云服务器 主要提供均衡的计
  • 【vim工具的使用】

    目录 前言 一 普通 命令模式 1 文件中移动 1 2 文件中移动 2 3 复制 粘贴 剪切 删除 4 行内删除 5 撤回 6 替换 7 高亮选中 8 逐单词移动 3 二 底行模式 1 退出vim 2 设置行号 3 替换 4 搜索 3 不退
  • IntelliJ IDEA 创建spring boot项目报错:Cannot download 'https://start.spring.io'

    IEAD默认使用https start spring io 把上面地址改成http start spring io即可
  • 华为OD机试 - 矩阵扩散(Java)

    题目描述 存在一个m n的二维数组 其成员取值范围为0或1 其中值为1的成员具备扩散性 每经过1S 将上下左右值为0的成员同化为1 二维数组的成员初始值都为0 将第 i j 和 k l 两个个位置上元素修改成1后 求矩阵的所有元素变为1需要
  • Host key verification failed.

    一 问题描述 在 ssh 连接某台服务器的时候 报错如下 ECDSA host key for 172 xxx xxx xxx has changed and you have requested strict checking Host
  • valgrind

    http blog csdn net yanghao23 article details 7514587 valgrind通常用来成分析程序性能及程序中的内存泄露错误 一 Valgrind工具集简绍 Valgrind包含下列工具 1 mem
  • Mathtype7的安装及使用方法

    换了电脑 以前那个安装的mathtype无了 最近又要写论文 尝试了个新的mathtype安装方法 可提供参考 首先 下载正版mathtype7 下载链接 win系统下载win版 安装mathtype 以管理员身份运行 东西都默认即可 安装
  • dos命令操作mysql数据库的常用语句

    一 连接MYSQL 格式 mysql h主机地址 u用户名 p用户密码 1 连接到本机上的MYSQL 首先打开DOS窗口 然后进入目录mysql bin 再键入命令mysql u root p 回车后提示你输密码 注意用户名前可以有空格也可
  • 二叉树结构与算法思路解析

    二叉树 介绍 主要内容 二叉树的概念和性质 二叉树的存储结构 遍历二叉树 递归遍历 非递归遍历 线索二叉树 哈夫曼树 树和森林 树和森林的存储 树和森林与二叉树的转换 树和森林的遍历 树型结构特点 一对多 例 自然界 树 人类社会 家谱 新