树和二叉树

    • 1、树的定义
    • 2、树的基本术语
    • 3、二叉树的定义
    • 4、二叉树的性质和存储结构
    • 5、满二叉树、完全二叉树
      • **完全二叉树的性质**
    • 6、二叉树的存储
      • 顺序存储结构
      • 链式存储结构
    • 7、遍历二叉树演示
    • 8、二叉树相关算法
      • (1)遍历二叉树递归算法实现
      • (2)遍历二叉树非递归算法实现
      • (3)二叉树的建立
      • (4)复制二叉树
      • (5)计算二叉树的深度
      • (6)计算二叉树结点总数
      • (7)计算二叉树所有叶子结点
    • 9、线索二叉树
    • 10、树和森林
      • (1)树的存储结构
        • **双亲表示法**
        • 孩子链表
        • 孩子兄弟表示法
      • (2)树和二叉树的转换
      • (3)森林与二叉树的转换
      • (4)树和森林的遍历
    • 11、哈夫曼树
      • (1)哈夫曼树的构造算法
      • (2)哈夫曼树的实现

树为非线性结构,对比线性结构来说,每一结点有多个后继结点。

image-20230813162605498

1、树的定义

树(Tree) 是 n (n>0) 个结点的有限集。

  • 若 n = 0,称为空树
  • 若 n > 0,则它满足如下两个条件:

(1) 有且仅有一个特定的称为根(Root) 的结点

(2) 其余结点可分为 m (m > 0) 个互不相交的有限集 T1, T2, T3,…Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。

image-20230813163132661

2、树的基本术语

根节点: 树中最顶层的节点,图中A为根结点,树中只有一个根节点。

结点的度:结点拥有的子树数。A 有 T1,T2,T3三个子树,A结点的度为3,B结点有E、F俩个子树,B结点的度为2。

叶子结点/终端结点:度=0的结点,也就是说该结点下没有子树。

非叶子结点/非终端结点:度!=0的结点

孩子结点、双亲结点: 一个结点下的子结点被称为孩子结点,该结点为孩子结点的双亲结点。

树的深度:树中结点的最大层次

结点的祖先: 从根到该结点所经历分支上的所有结点

结点的子孙: 以某节点为根的子树中任一结点

image-20230813164911544

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

一棵树也是森林,可以看做特殊的森林。

将森林中的每棵树根结点连接起来就变成了一棵树

image-20230813165556865

3、二叉树的定义

为何要重点研究每结点最多只有两个“叉” 的树?

  • 二叉树的结构最简单,规律性最强
  • 可以证明,所有树都能转为唯一对应的二叉树: 不失一般性

普通树(多叉树) 若不转化为二叉树,则运算很难实现,二又树在树结构的应用中起着非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性.

二叉树是 n(n>0) 个结点的有限集,它或者是空集(n = 0)或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。

二叉树的特点

1、每个结点最多有俩孩子(二叉树中不存在度大于 2 的结点)

2、子树有左右之分,其次序不能颠倒。

3、二叉树可以是空集合,根可以有空的左子树或空的右子树

注意:二叉树不是树的特殊情况,他们是俩个概念。但是树和二叉树的基本术语是通用的。

二叉树的结点的子树要区分左子树右子树,即使只有一个子树也要进行区分,说明他是左子树还是右子树。

树当结点只有一个孩子时,无需区分他是左还是右的次序,因此俩者是不同的。

image-20230813171016694

思考

具有3个结点的二叉树可能有几种不同形态?普通树呢?

image-20230813171247263

二叉树的5种基本形态

image-20230813171419440

4、二叉树的性质和存储结构

(1)性质1:在二叉树的第 i 层上至多有 2i-1 个结点(i>=1), 至少有1个结点

image-20230813172322596

(2)性质2: 深度为 k 的二叉树至多有 2k -1个结点,至少有 k 个结点

(3)性质3: 对于一棵二叉树,如果其叶子结点数为 n0 , 度 为2的结点数为 n2 , 则有: n0 = n2 + 1

image-20230813173307702

5、满二叉树、完全二叉树

(1)满二叉树

一棵树深度为k且有 2k -1 个结点的二叉树称为满二叉树

image-20230813174019164

特点

1、每一层上结点数都是最大结点数

2、所有的叶子结点都在最后一层

(2)完全二叉树

深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满

二又树中编号为 1~n 的结点一一对应时,称之为完全二叉树。

image-20230813174733628

非完全二叉树中的F结点的位置对应满二叉树的G,因此不是完全二叉树

注: 在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树. 一定是连续的去掉!!!

image-20230813175651994

特点

1、叶子结点只可能分布在层次最大的俩层上。

2、对于任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为 i 或者 i + 1

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

完全二叉树的性质

image-20230814214334287

如下图共有 12 个结点,log212 ≈ 3,因此层数为 4

image-20230814214346085

image-20230814215156910

image-20230814215139789

下图中就可以直观的看出,各节点之间的关系:

image-20230814215235161

6、二叉树的存储

image-20230814215532101

顺序存储结构

实现: 按满二叉树的结点层次编号,依次存放二叉树中的数据元素

image-20230814215731416

举例说明

1、将下面的二叉树按顺序结构存储

image-20230814220706326

没有元素的位置在数组中也要空出来。

image-20230814220755286

2、若某二叉树采用顺序结构存储,如下图所示,还原出二叉树

image-20230814220613009

image-20230814220826687

顺序存储结构的缺点:

当二叉树中有非常多的空结点时,这些空结点需要在数组中存储空或者0,因此空间的利用率不高,浪费空间。因此顺序存储比较适合满二叉树和完全二叉树

image-20230814221131295

image-20230814221225442

链式存储结构

使用俩个指针分别指向该结点的左孩子结点和右孩子结点。如果没有左孩子或者右孩子则置为空。

image-20230814221559088

image-20230814221643854

案例演示

^ 表示空指针域

image-20230814222550076

思考

在 n 个结点的二叉链表中,有多少个空指针域?

乍一看好像也没有什么规律,我们可以试着反推以下,有n个结点,必然有 2n 个链域,其中除了头结点外, 每个结点必然有一个双亲,必然有一个指针存放双亲结点,因此非空的链域有 n-1 , 2n - (n-1)= n + 1 ,因此在n个结点的二叉链表中有 n + 1个空指针域

image-20230814223443971

三叉链表

相较于二叉链表,多了一个指针域,用于存储双亲结点

image-20230814224217692

image-20230814224248578

7、遍历二叉树演示

遍历的定义

遍历定义一 顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次 (又称周游)

“访问”的含义很广,可以是对结点作各种处理,如: 输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。

遍历的目的

得到树中所有结点的一个线性排列

遍历方法

无论二叉树多么复杂,都可以将它看成三部分。

image-20230814225206249

因此根据访问顺序的不同,就有了不同的遍历方式,如下:

假设:L遍历左子树、D:访问根节点、R:遍历右子树

整个二叉树的遍历方案:

DLR、LDR、LRD、DRL、RDL、RLD【重点研究前三种】

image-20230814225438677

先序遍历演示

若差二叉树为空,则无需操作,否则:

(1)访问根节点

(2)先序遍历左子树

(3)先序遍历右子树

image-20230814230217224

(1)先遍历根结点A

(2)子树不为空,左子树的根节点为B,遍历B结点,B的左右子树为空,左子树遍历完毕

(3)找到A的右子树,A的右子树的根节点为C,遍历C,C的左右子树为空,终止遍历。

最终结果:ABC

以下先序遍历结果:ABELDHMIJ

image-20230814231417911

中序遍历演示

若差二叉树为空,则无需操作,否则:

(1)中序遍历左子树

(2)访问根节点

(3)中序遍历右子树

image-20230814230938507

(1)找到A的左子树B,B的左子树为空,遍历左子树根结点B,B的右子树为空,左子树B遍历完毕

(2)遍历根节点A

(3)找到A的右子树C,C的左子树为空,遍历右子树的根节点C,C的右子树为空,右子树C遍历完毕

最终结果:BAC

以下按照中序遍历结果:ELBAMHIDJ

image-20230814231417911

后序遍历演示

若差二叉树为空,则无需操作,否则:

(1)后序遍历左子树

(2)后序遍历右子树

(3)访问根节点

image-20230814231712940

(1)找到A的左子树B,B的左右子树为空,遍历左子树根结点B,左子树B遍历完毕

(2)找到A的右子树C,C的左右子树为空,遍历右子树根结点C,右子树C遍历完毕

(3)访问根节点A

最终遍历结果:BCA

以下二叉树遍历结果:LEBMIHJDA

image-20230814231849455

根据遍历序列确定二叉树

  • 若二叉树中各结点的值均不相同,则二叉树结点的先序序列,中序序列和后序序列都是唯一的。
  • 由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一颗二叉树

举例说明一

已知二叉树的先序和中序序列,构造出相应的二叉树

先序: A B C DEFG HIJ

中序: C D B FE A IHGJ

由先序序列确定根;由中序序列确定左右子树

1、由先序知根为A,则由中序知左子树为CDBFE, 右子树为IHGJ

image-20230815215440909

2、再分别在左、右子树的序列中找出根、左子树序列、右子树序列

在先序得知左子树的根为B,在中序遍历中得知 CD为左子树,FE为右子树

同样,G为右子树的根,中序得知IH为左子树,J为右子树

image-20230815215833464

image-20230815220005966

3、由先序得知C是根,D在C的右边,D是C的右子树。同样,E为根,F为左子树。H为根,I为左子树

image-20230815220248250

举例说明二

中序序列: BDCEAFHG

后序序列: DECBHGFA,请画出这棵二叉树

分析

1、由后序遍历得知,A为整棵树的根。由中序遍历得知,根结点A左边的为左子树,右边的为右子树。

image-20230816214421889

2、看 BDCE 左子树,同样的方法,由后序遍历得知B为左子树根结点,在由中序遍历得知 DCE为B的右子树。DCE同样用此方法,C为根结点,D为子树,E为右子树

image-20230816215131163

3、A的右子树用相同方法求出

image-20230816215418021

8、二叉树相关算法

(1)遍历二叉树递归算法实现

先序遍历

先序遍历的顺序:(1)遍历根结点 (2)遍历左子树 (3)遍历右子树

二叉树的每个结点都可以有左子树和右子树,这种类似的结构可以采用递归的方式。可以先把整棵树分解开来,即对左子树和右子树先序遍历:

(1)若根结点不为空,遍历根结点

(2)若左子树不为空,遍历左子树

(3)若右子树不为空,遍历右子树

代码实现

public class PreOrderTraverse {public static void main(String[] args) {// 创建二叉树TreeNode root = new TreeNode("A");TreeNode left = new TreeNode("B");TreeNode right = new TreeNode("C");root.left = left;root.right = right;// 先序遍历root.preOrder(root);}
}
//  定义树的结点信息
class TreeNode{Object data;// data域TreeNode left; // 左结点TreeNode right; // 右结点// 初始化public TreeNode(Object data) {this.data = data;left = null;right = null;}/*** 先序遍历* */public void preOrder(TreeNode root){// 若根结点为空则停止递归if (root == null) return;// 先遍历根结点System.out.println(root.data);// 遍历左子树preOrder(root.left);// 遍历右子树preOrder(root.right);}
}

中序遍历

中序遍历顺序:

(1)若左子树不为空,遍历左子树

(2)若根结点不为空,遍历根结点

(3)若右子树不为空,遍历右子树

    /*** 中序遍历* */public void inOrder(TreeNode root){// 若根结点为空则停止递归if (root == null) return;// 遍历左子树preOrder(root.left);// 先遍历根结点System.out.println(root.data);// 遍历右子树preOrder(root.right);}

后序遍历

后序遍历顺序:

(1)若左子树不为空,遍历左子树

(2)若右子树不为空,遍历右子树

(3)若根结点不为空,遍历根结点

    /*** 后序遍历* */public void postOrder(TreeNode root){// 若根结点为空则停止递归if (root == null) return;// 遍历左子树preOrder(root.left);// 遍历右子树preOrder(root.right);// 先遍历根结点System.out.println(root.data);}

分析

如果去掉输出语句,从递归的角度看,三种算法是完全相同的,或说这三种算法的访问路径是相同的,只是访问结点的时机不同

image-20230817223211890

从图中可以看到,每个结点都被经过3次,第一次访问就是先序遍历,第二次访问就是中序遍历,第三次访问就是后序遍历。

时间效率:O(n) 每个结点都被访问一次

空间效率:O(n) 栈占用的最大辅助空间

(2)遍历二叉树非递归算法实现

利用栈实现非递归遍历二叉树,以中序遍历为例。

基本思想

(1)建立一个栈

(2)将根结点入栈,如果左子树不为空,遍历左子树

(3)如果左子树为空,弹出根结点,遍历右子树

演示

1、根结点不为空,将根结点A入栈

image-20230817225051586

2、访问左子树,同样将左子树的根结点B入栈

image-20230817225132161

3、访问B的左子树,发现为空,则弹栈B输出。接着访问B的右子树,同样将右子树的根结点D入栈

image-20230817225237355

4、访问D的左子树为空,则弹栈D并输出,右子树也为空

image-20230817225358406

5、左子树访问完毕,弹栈A输出。接着访问右子树,同样将右子树根结点C入栈

image-20230817225929485

6、结点 C 左子树为空,弹栈C输出。此时C结点的右子树为空,栈为空。访问完毕。

image-20230817230020225

代码实现

    /*** 非递归中序遍历* */public void traverse(TreeNode node){// 初始化栈Stack<TreeNode> stack = new Stack<TreeNode>();TreeNode p = node;while(p != null || !stack.isEmpty()) {if (p == null) {   // 左子树为空,弹栈并输出TreeNode pop = stack.pop();System.out.println(pop.data);// 遍历右子树p = pop.right;}else {// 压栈根结点stack.push(p);// 右子树为空p = p.left;}}}

(3)二叉树的建立

以先序遍历序列为例

例如:已知先序序列为:ABCDEGF

  • 从键盘输入二叉树的结点信息,建立二叉树的存储结构
  • 在建立二叉树的过程中按照二叉树的先序方式建立

如果只根据先序序列建立二叉树,所得到的二叉树是不唯一的。

image-20230820143827805

如果想得到一颗唯一的树,需要给树补充一些空结点。

image-20230820143936434

假设空结点用 # 标识,则上图中的先序序列为: ABC##DE#G##F###

此时通过这个序列就能创建出一颗唯一的二叉树!

代码实现

public class BinaryTreeCreate {public static void main(String[] args) {String preorder = "A,B,C,#,#,D,E,#,G,#,#,F,#,#,#";BinaryTreeCreate builder = new BinaryTreeCreate();TreeNode root = builder.buildTreeHelper(preorder.split(","));root.preOrder(root);}int index = 0;// 通过先序遍历建立二叉树public TreeNode buildTreeHelper(String[] nodes){if (index >= nodes.length || nodes[index].equals("#")) {index++; // 移动索引return null;}String value = nodes[index];index++; // 移动索引TreeNode node = new TreeNode(value);// 建立左子树node.left = buildTreeHelper(nodes);// 建立右子树node.right = buildTreeHelper(nodes);return node;}
}

(4)复制二叉树

算法步骤

  • 如果是空树,递归结束
  • 否则,复制根节点
    • 递归复制左子树
    • 递归复制右子树
public class BinaryTreeCreate {public static void main(String[] args) {BinaryTreeCreate builder = new BinaryTreeCreate();// 先建立一个二叉树// 创建原始二叉树TreeNode originalRoot = new TreeNode(1);originalRoot.left = new TreeNode(2);originalRoot.right = new TreeNode(3);originalRoot.left.left = new TreeNode(4);originalRoot.left.right = new TreeNode(5);System.out.println("按照先序遍历序列为:12453" );TreeNode newNode = builder.copyTree(originalRoot);newNode.preOrder(newNode);}// 复制二叉树public TreeNode copyTree(TreeNode node){if (node == null) return  null;// 复制根结点TreeNode treeNode = new TreeNode(node.data);// 复制左子树treeNode.left = copyTree(node.left);// 复制右子树treeNode.right = copyTree(node.right);// 返回新的二叉树return treeNode;}
}

(5)计算二叉树的深度

  • 如果是空树,则深度为0
  • 否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大值+1
    // 计算二叉树的深度public int deep(TreeNode node) {if (node == null) return 0;// 递归计算左子树int m = deep(node.left);// 递归计算右子树int n = deep(node.right);// +1 == 》根结点的层数return  m > n ? (m+1) : (n+1);}

(6)计算二叉树结点总数

  • 如果是空树,则结点个数为0
  • 否则: 左子树结点个数+右子树结点个数+1
    // 计算二叉树结点的个数public int treeNodeCount(TreeNode node) {if (node == null) return 0;int m = treeNodeCount(node.left);int n = treeNodeCount(node.right);return m + n + 1;}

(7)计算二叉树所有叶子结点

  • 如果是空树,则叶子结点个数为0;
  • 否则,为左子树的叶子结点个数+右子树的叶子结点个数

判断叶子节点的方法:根结点的左右子树都为空

    // 计算二叉树叶子节点个数public int leafCount(TreeNode node) {if (node == null) return 0;if (node.left ==  null && node.right == null) return 1;int m = leafCount(node.left);int n = leafCount(node.right);return m + n ;}

9、线索二叉树

为什么要研究线索二叉树呢?

当用二叉链表作为二叉树的存储结构时可以很方便地找到某个结点的左右孩

子,但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结

解决方法

  • 通过遍历寻找——浪费时间
  • 在增加俩个指针,前驱节点和后驱节点——浪费空间
  • 利用二叉树中的空指针域

具有 n 个结点的二叉树,有 n+1 个空指针域,我们可以利用这些空指针域存储结点的前驱和后驱节点:

  • 如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱

  • 如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继

这种改变指向的指针称为“线索”

如下图

A的右指针为空,理应存储A的后继,但是A是最后一个结点,因此不用存储

C的右指针为空,存储后继结点B

E的左指针为空,存储前驱结点 B

G的左右指针都为空,左指针存储E,右指针存储D

image-20230820161346388

在之前链式二叉树有俩个指针,分别指向左孩子和有孩子,线索二叉树中为了区分指针到底是指向前驱和后继,还是指向左右孩子,因此在增加俩个标志域 ltag、rtag:

ltag = 0 ,lchild 指向该结点的左孩子
ltag = 1,lchild 指向该结点的前驱
rtag = 0,rchild 指向该结点的右孩子
rtag = 1,rchild 指向该结点的后继

image-20230820161937147

案例演示

根结点A有左右孩子结点,正常存储即可。

B的左孩子结点为空,因此: ltag = 1,lchild = A

C的左右孩子结点为空,因此 ltag = 1,lchild = C,rtag=1,rchild=D

D的右孩子为空,rtag =1,rchild = E

E的左右孩子结点为空,因此 ltag = 1,lchild =D,rtag=1,rchild=null

image-20230820162238293

10、树和森林

image-20230820163430983

(1)树的存储结构

双亲表示法

实现: 定义结构数组存放树的结点每个结点含两个域:

  • 数据域: 存放结点本身信息
  • 双亲域: 指示本结点的双亲结点在数组中的位置

image-20230820163934155

对应的二叉树

image-20230820163951442

特点:找双亲结点容易,但是找孩子难

孩子链表

把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储。

则 n 个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的数组)存储。

image-20230820165431997

孩子链表的类型表示:

// 孩子存储结构
class ChildNode{// 孩子结点的下标int childIndex;// 下一个孩子的地址ChildNode nextChild;
}
// 双亲存储结构
class ParentNode {// data域Object data;// 第一个孩子ChildNode firstChild;
}// 树的存储结构
class CtTre{// 保存双亲结点的数组ParentNode[] nodes;// 根结点的下标,结点总数int r,n;
}

特点:找到孩子结点容易,找双亲难。

但是我们可以将 双亲表示法和孩子链表结合起来,在双亲存储结构中增加一个指针用于指向该结点的双亲结点的下标位置

// 孩子存储结构
class ChildNode{// 孩子结点的下标int childIndex;// 下一个孩子的地址ChildNode nextChild;
}
// 双亲存储结构
class ParentNode {// data域Object data;// 双亲的索引下标int parentIndex;// 第一个孩子ChildNode firstChild;
}// 树的存储结构
class CtTre{// 保存双亲结点的数组ParentNode[] nodes;// 根结点的下标,结点总数int r,n;
}

孩子兄弟表示法

又叫 二叉树表示法,二叉链表表示法

实现:用二又链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点下一个兄弟结点

与二叉树的区别:树没有左右孩子之分

image-20230820171006971

(2)树和二叉树的转换

由于树和又树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系

给定一棵树,可以找到唯一的一棵二叉树与之对应

image-20230820172440697

在图中可以发现,在树中兄弟结点之间并没有连线,而孩子的兄弟结点在二叉树中都变为了 右孩子结点。并没孩子的兄弟结点没有了和根结点的连接。

树和二叉树转换的步骤

(1)加线: 在兄弟之间加连线

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

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

兄弟相连留长子

image-20230820173054139

二叉树转换成树的步骤

(1) 加线: 若p结点是双亲结点的左孩子,则将p的右孩子,右孩的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来

(2)抹线: 抹掉原二叉树中双亲与右孩子之间的连线调整

(3)将结点按层次排列,形成树结构

左孩右右连双亲,去掉原来右孩线。

image-20230820173658559

(3)森林与二叉树的转换

森林转换二叉树

(1)将各棵树分别转换成二叉树
(2)将每棵树的根结点用线相连
(3)以第一棵树根结点为二叉树的根再以根结点为轴心,顺时针旋转
构成二叉树型结构

树变二叉根相连

image-20230820181256464

二叉树转换成森林

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

(2)还原: 将孤立的二又树还原成树

去掉全部右孩线,孤立二叉在还原

image-20230820181606875

(4)树和森林的遍历

树的遍历方式:

image-20230820181923667

森林的遍历

将森林看作由三部分构成

  • 森林中第一棵树的根结点
  • 森林中第一棵树的子树森林
  • 森林中其它树构成的森林。

image-20230820182552027

先序遍历

image-20230820182651871

中序遍历

image-20230820182714374

例子

image-20230820182729032

11、哈夫曼树

哈夫曼(Huffman)树又称最优树,是一类带权路径长度最短的树,在实际中有广泛的用途。

哈夫曼树的定义,涉及路径、路径长度、权等概念,下面先给出这些概念的定义,然后再介绍哈夫曼树。

路径: 从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。

结点的路径长度: 两结点间路径上的分支数

image-20230821214900834

树的路径长度:从树根到每一结点的路径长度之和。

image-20230821214956124

结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树,路径最短的二叉树不一定是完全二叉树

权(weight): 将树中结点赋给一个有着某种含义的数值则这个数值称为该结点的权

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

树的带权路径长度: 树中所有叶子结点的带权路径长度之和。通常记作:

image-20230821215757245

举例说明

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

求该二叉树的带权路径长度: WPL = 2 ∗ 7 + 2 ∗ 5 + 2 ∗ 4 + 2 ∗ 4 = 36 2*7 + 2*5+2*4+2*4 = 36 27+25+24+24=36

image-20230821215618833

第二种构造方法:WPL = 2 ∗ 4 + 3 ∗ 7 + 3 ∗ 5 + 2 = 46 2*4+3*7+3*5+2 = 46 24+37+35+2=46

image-20230821220101149

构造不同的二叉树它的带权路径是不一样的,哈夫曼树也称最优树,带权路径(WPL)最短的树。

带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等。

(1)哈夫曼树的构造算法

image-20230821220831159

构造哈夫曼树的方法

(1) 根据n个给定的权值 {w1,w2… w3} 构成n棵二叉树的森林 F={T1,T2,…Tn},其中Ti只有一个带权为 wi,的根结点.。【构造森林全是根】

(2) 在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。【选用两小造新树】

(3) 在F中删除这两棵树,同时将新得到的二又树加入森林中【删除两小添新人】

(4) 重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树

举例说明

有四个结点 a、b、c、d,权值分别为7、5、2、4,构造哈夫曼树

1、构造4个以a、b、c、d结点为根的树

2、选取权值最小的俩个结点 2、4构造一颗新树

3、此时有7、5、6三棵树,继续选取权值最小的树构造新树,直到剩下一棵树

image-20230821222545899

总结

包含 n 个叶子结点的哈夫曼树中共有2n- 1 个结点。

哈夫曼树的结点的度数为 0或2,没有度为 1 的结点。

包含n棵树的森林要经过 n-1 次合并才能形成哈夫曼树,共产生 n-1 个新结点

(2)哈夫曼树的实现

使用一维结构数组存储哈夫曼树的结点,定义四个变量,分别表示:权值、双亲结点下标、左右孩子结点下标

image-20230823213808674

初始化

根据权值构造n个为根节点的树,没有双亲结点和左右孩子结点。

初始化时请注意,对于结构型的数组,不赋值默认为null

class HuffmanNode {int weight; // 权值int lchild; // 左孩子int rchild; // 右孩子结点下标int parent; // 双亲结点的下标@Overridepublic String toString() {return "HuffmanNode{" +"weight=" + weight +", lchild=" + lchild +", rchild=" + rchild +", parent=" + parent +'}';}
}class HuffmanTree {HuffmanNode[] ht;// 初始化 weights==>权重集合public HuffmanTree(int[] weights) {// 权重的个数int n = weights.length;ht = new HuffmanNode[2*n - 1];// 设置0~n个元素的权重for (int i = 0; i < n; i++) {HuffmanNode node = new HuffmanNode();node.weight = weights[i];node.parent = node.lchild = node.rchild = 0;ht[i] = node;}// n~2*n-1初始化对象for (int i = n; i < ht.length; i++) {ht[i] = new HuffmanNode();}}
}

假设给定权值为:7, 19, 2, 6, 32, 3, 21, 10

初始化后:

image-20230823225223982

初始化完毕,开始将俩个结点进行合并,从下标 i = weights.length 开始,直到 2n-1:

  • 从weights数组中选取俩个从未选中的(parent=0)并且权重最小的。假设 s1,s2
  • 修改俩个较小结点的双亲结点:ht[s1].parent = i、ht[s2].parent = i
  • 修改新产生的结点:ht[i].weight = ht[s1].weight + ht[s2].weight、ht[i].lchild= s1、ht[i].rchild= s2
  public void createHuffmanTree(int[] weights) {for (int i = weights.length; i < 2 * weights.length - 1; i++) {// 获取俩个最小权重的结点下标List<Integer> list = getMinNode();System.out.println(list);Integer first = list.get(0);Integer second = list.get(1);// 更新俩个结点的双亲下标ht[first].parent = i;ht[second].parent = i;// 设置新生成的结点ht[i].weight = ht[first].weight + ht[second].weight;ht[i].lchild = first;ht[i].rchild = second;}}public List<Integer> getMinNode() {ArrayList<Integer> list = new ArrayList<>();// 假设俩个最小值int firstMinValue = Integer.MAX_VALUE;int secondMinValue = Integer.MAX_VALUE;// 最小值的下标int firstIndex = -1;int secondIndex = -1;for (int i = 0; i < ht.length; i++) {if (ht[i].parent == 0 && ht[i].weight != 0) {if (ht[i].weight < firstMinValue) {secondMinValue = firstMinValue; // 更新第二个最小值secondIndex = firstIndex;firstMinValue = ht[i].weight;  // 更新第一个最小值firstIndex = i;} else if (ht[i].weight < secondMinValue) {secondMinValue = ht[i].weight;secondIndex = i;}}}list.add(firstIndex);list.add(secondIndex);return list;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/135472.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mac电脑版矢量绘图推荐 Sketch for mac最新中文

Sketch软件特色 1、数字设计工具包 在Sketch中使用暗模式查找焦点。点亮灯光&#xff0c;失去分心&#xff0c;看着你的设计变得生动&#xff0c;让你专注于最重要的事情 - 你的工作。 2、为未来重新设计 Sketch 带来全新外观和更多。完全重新设计的界面使设计过程比以往更加…

人脸识别技术应用安全管理规定(试行)|企业采用人脸打卡方式,这4条规定值得关注

近日&#xff0c;为规范人脸识别技术应用&#xff0c;国家互联网信息办公室起草了&#xff0c;并向全社会公开征求意见。该规定一共列举了25条&#xff0c;企业如借助人脸识别技术采集考勤打卡数据&#xff0c;以下4条规定值得关注。 第四条 只有在具有特定的目的和充分的必要…

Python接口自动化测试post请求和get请求,获取请求返回值

引言 我们在做python接口自动化测试时&#xff0c;接口的请求方法有get,post等&#xff1b;get和post请求传参&#xff0c;和获取接口响应数据的方法&#xff1b; 请求接口为Post时&#xff0c;传参方法 我们在使用python中requests库做接口测试时&#xff0c;在做post接口测试…

notepad++配合正则表达式分组模式处理文本转化为sql语句

一、正则分组知识点补充 正则分组和捕获 ()&#xff1a;用于分组和捕获子表达式。 大白话就是()匹配到的数据&#xff0c;通过美元符号加下标可以获取该数据&#xff0c;例如$1、$2, 下标从1开始。 下面的案例就采用该模式处理文本数据 二、使用正则的需求背景 有一份报表…

KPM算法

概念 KMP&#xff08;Knuth–Morris–Pratt&#xff09;算法是一种字符串匹配算法&#xff0c;用于在一个主文本字符串中查找一个模式字符串的出现位置。KMP算法通过利用模式字符串中的重复性&#xff0c;避免无意义的字符比较&#xff0c;从而提高效率。 KMP算法的核心思想是…

0.UML

1.图 1.1类图含义 第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法或行为。注意前面的符号, ,表示public,-,表示private,#,表示protected。 1.2接口图 与类图的区别主要是顶端有<< interface >…

24v转5v稳压芯片-5A大电流输出ic

这款24V转5V5A汽车充电芯片具有以下特性和参数&#xff1a; - 宽输入电压范围&#xff1a;4.5V至36V - 最大输出电流&#xff1a;5.0A - 高达92%的转换效率 - 恒流/恒压模式控制 - 最大占空比100% - 可调输出电压 - 2%的输出电压精度 - 集成40mΩ高侧开关 - 集成18mΩ低侧开关 …

【Redis 多机服务的简单认识】

目录 主从同步 哨兵模式 集群服务 随着业务的不断发展&#xff0c;单机 Redis 的性能已经不能满⾜我们的需求了&#xff0c;此时我们需要将单机 Redis 扩展为多机服务&#xff0c;Redis 多机服务主要包含以下 3 个内容&#xff1a; Redis 主从同步Redis 哨兵模式Redis 集群…

Android高德地图截屏功能(可包含自定义控件)

一、不包含自定义控件 地图 SDK 支持对当前屏幕显示区域进行截屏&#xff0c;可以对地图、覆盖物&#xff08;包含信息窗口&#xff09;、Logo进行截取屏幕&#xff0c;这其中不包括地图控件、Toast窗口。 详细示例如下&#xff1a; // 对地图进行截屏aMap!!.getMapScreenSho…

vue2-x6-dag自定义vue组件节点

效果如图 官方案例 人工智能建模 DAG 图 vue2中自定义节点 代码 1.dag.json [{"id": "1","shape": "dag-node","x": 290,"y": 110,"data": {"label": "读数据","status&q…

【iOS】push与present Controller的区别

文章目录 前言一、push方法二、pop方法三、present方法四、dismiss方法五、dismiss多级的方法举例动画 前言 iOS推出与退出界面有两种方式——push与present&#xff0c;接下来笔者分别介绍这两种方式 一、push方法 SecondViewController *second [[SecondViewController all…

【C++】AVL树

个人主页&#xff1a;&#x1f35d;在肯德基吃麻辣烫 我的gitee&#xff1a;C仓库 个人专栏&#xff1a;C专栏 文章目录 前言一、什么是AVL树&#xff1f;设计AVL树的原因 二、AVL树的性质三、二叉树节点的定义四、AVL树的插入旋转1&#xff09;右单旋2&#xff09;左单旋3&…

目标检测中生成锚框函数详解

%matplotlib inline import torch from d2l import torch as d2l torch.set_printoptions(2) # 让pytorch打印张量时&#xff0c;只打印到小数点后两位将设一张图片&#xff0c;宽和高为2,2 X torch.rand(size(1,3,2,2)) Y generate_anchors(X,sizes[0.75,0.5,0.25],ratios[…

HPC集群自动弹性扩缩的两种实现方式

常青藤 HPC常青园 2023-07-28 19:48 发表于北京 弹性扩缩技术正在成为HPC集群中的一项重要技术。它可以根据实际需求动态调整集群资源&#xff0c;应对用户负载的波动。对于运维团队来说&#xff0c;自动弹性扩缩能够减轻集群运维负担&#xff0c;提高集群资源利用率&#xff0…

小程序Saas平台源码:开启电商小程序新时代,可视化后台自由DIY的无限可能

在当今数字化的时代&#xff0c;小程序已成为各行各业开展业务的重要工具。尤其在电商领域&#xff0c;小程序能有效地缩短消费者与商品之间的距离&#xff0c;提升营销效率。给大家分享一款针对电商行业的小程序Saas平台源码&#xff0c;它具有一键生成电商小程序、可视化后台…

Go基础八股

【Go面试】Go slice深拷贝和浅拷贝_哔哩哔哩_bilibili 基础篇 1.Go方法值接收者和指针接收者的区别 简单言之就是是否会影响调用的结构体&#xff0c;方法接收者是指针会影响 2.返回局部变量的指针 一般来说&#xff0c;局部变量会在函数返回后被销毁&#xff0c;因此被返回…

flink的网络缓冲区

背景 在flink的taskmanager进行数据交互的过程中&#xff0c;网络缓冲区是一个可以提升网络交换速度的设计&#xff0c;此外&#xff0c;flink还通过网络缓冲区实现其基于信用值credit的流量控制&#xff0c;以便尽可能的处理数据倾斜问题 网络缓冲区 在flink中每个taskmana…

C++ 太卷,转 Java?

最近看到知乎、牛客等论坛上关于 C 很多帖子&#xff0c;比如&#xff1a; 2023年大量劝入C 2023年还建议走C方向吗&#xff1f; 看了一圈&#xff0c;基本上都是说 C 这个领域唯一共同点就是都使用 C 语言&#xff0c;其它几乎没有相关性。 的确是这样&#xff0c;比如量化交…

微信小程序遇到的一些问题及解决方法(设备安装)

微信小程序遇到的一些问题及解决方法 1、[js将字符串按照换行符分隔成数组](https://blog.csdn.net/pgzero/article/details/108730175)2、[vue byte数组](https://www.yzktw.com.cn/post/1202765.html)3、使用vant-weapp的文件上传capture"camera" 无法直接调用摄像…

OPC UA协议报文,基础介绍+Hello报文解析

消息主要分为&#xff1a;消息头和附加字段 通讯过程 协议标准第一部分进行总体介绍&#xff1b;协议标准第四部分有详细介绍通讯过程 流程介绍 整体流程 连接套接字》Hello》打开安全信道》创建会话》关闭安全信道》关闭套接字 订阅等事件 服务器审核行为 聚合的服务器审…