树的定义
树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中: (1)有且仅有一个特定的称为根( Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、.... Tm,其中每一个集合本身又是一棵树,并且称为根的子树( SubTree)。
结点
- 结点拥有的子树数量称为结点的度(Degree).
- 度为0的结点称为叶结点(Leaf) 或终端结点;
- 度不为0的结点称为非终端结点或分支结点。
- 除根结点之外,分支结点也称为内部结点。
- 树的度是树内各结点的度的最大值。
下图树结点度的最大值是节点D的度,为3.所以树的度为3.
结点间的关系
结点的子树的根称为该结点的孩子(Child), 相应地,该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling) 。
其他概念
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。树中结点的最大层次称为树的深度(Depth) 或高度,当前树的深度为4。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
二叉树定义
二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树特点
- 每个结点最多有两棵子树
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中结点只有一棵子树,也要区分是左子树还是右子树。
特殊二叉树
斜树
所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<i<n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵:二叉树称为完全二叉树。满二叉树是一棵完全二叉树,但完全二叉树不一定是满的。
二叉树的性质
在二叉树的第i层上至多有2i-1个结点(i≥1)。
深度为k的二叉树至多有2k-1个结点(k≥1)。
对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 =n2 + 1
具有n个结点的完全二叉树的深度为[log2n]+1 ([x]表示不大于x 的最大整数)。
如果对一棵有n个结点的完全二叉树(其深度为log2n+1) 的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),对任一结点i (1<i<n)有:
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1, 则其双亲是结点li/2]。
2.如果2i>n, 则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1.
二叉树存储结构
顺序存储
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
顺序存储结构一般只适用于完全二叉树。否则会对存储空间造成浪费。
二叉链表
二叉树每个节点最多有两个孩子,所以设计为一个数据域和两个指针域。我们称这样的链表叫做二叉链表
data是数据域。lchild和rchild分别存放左孩子和右孩子。
遍历二叉树
二叉树的遍历( traversing binary tree )是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
前序遍历(根结点->左子树->右子树)
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。遍历的顺序为: ABDGHCEIF。
代码
static void PreOrderTraverse(Node root) {
if(root == null) {
return;
}
printNode(root);
PreOrderTraverse(root.lchild);
PreOrderTraverse(root.rchild);
}
中序遍历(左子树->根结点->右子树)
若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。遍历的顺序为: GDHBAEICF.
代码
static void InOrderTraverse(Node root) {
if(root == null) {
return;
}
InOrderTraverse(root.lchild);
printNode(root);
InOrderTraverse(root.rchild);
}
后序遍历(左子树->右子树->根结点)
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。遍历的顺序为: GHDBIEFCA。
代码
static void PostOrderTraverse(Node root) {
if(root == null) {
return;
}
PostOrderTraverse(root.lchild);
PostOrderTraverse(root.rchild);
printNode(root);
}
前序、中序、后序遍历代码差异仅仅是打印数据的时机的差异
创建二叉树
为了能让每个结点确认是否有左右孩子,我们将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#"。 我们称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。
代码
static int createTree(Node node, String[] treeDatas, int n) {
if("#".equals(treeDatas[n])) {
return n + 1;
}
node.data = treeDatas[n];
node.lchild = new Node();
int left = createTree(node.lchild, treeDatas, n + 1);
node.rchild = new Node();
int right = createTree(node.rchild, treeDatas, left);
return right;
}
测试结果
public static void main(String[] args) {
Node root = new Node();
String[] treeDatas = "AB#D##C##".split("");
createTree(root, treeDatas, 0);
System.out.print("前序遍历:");
PreOrderTraverse(root);
System.out.println();
System.out.print("中序遍历:");
InOrderTraverse(root);
System.out.println();
System.out.print("后序遍历:");
PostOrderTraverse(root);
}
- 前序遍历 A B D C
- 中序遍历 B D A C
- 后序遍历 D B C A
线索二叉树
我们把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
线索二叉树的结构
树转换为二叉树
-
加线。在所有兄弟结点之间加一条连线。
-
去线。对树中每个结点,保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
-
层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度, 使之结构层次分明。
注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树、二叉树转换为树、二叉树转换为森林就不在此介绍了,有空可以去看看。
赫夫曼树及其应用
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。树的路径长度就是从树根到每一结点的路径长度之和。如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
带权路径长度WPL最小的二叉树称做赫夫曼树。
二叉树a根结点到结点D的路径长度就为4
二叉树b中根结点到结点D的路径长度为2。
二叉树a的树路径长度就为1+1+2+2+3+3+4+4=20。
二叉树b的树路径长度就为1+2+3+3+2+1+2+2=16。
二叉树a的WPL=5X 1+15X2+40X3+30X4+10X4=315。
二叉树b的WPL=5X3+15X3+40X 2+30X2+10X2=220
构建赫夫曼树
-
根据给定的n个权值{ w1,w2···,wn}构成n棵二叉树的集合F={ T1,T2···,Tn},其中每棵二叉树Ti中只有一个带权为Wi的根结点,其左右子树均为空。
-
在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
-
在F中删除这两棵树,同时将新得到的二叉树加入F中。
-
重复2和3步骤,直到F只含一棵树为止。这棵树便是赫夫曼树。
例: W = {5, 15, 40, 30, 10}。F = {A, B, C, D, E}