结点:
- 树的结点包含一个数据元素及若干指向其子树的分支。
- 结点拥有的子树数称为结点的度。度为零的结点称为叶节点或终端节点;度不为零的结点称为非终端结点或分支节点。除根以外,分支结点也称为内部结点。
- 树的度是树内部各结点的度的最大值。
树的其他相关概念:
- 结点的层次从根开始定义,根为第一层,根的孩子为第二层,依次类推。
- 树中结点的最大层数称为树的深度或高度。树T的高度=1+树T最高子树的高度。
- 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
- 森林是m(m>=0)课互不相交的树的集合。对数中每个结点而言,其子树的结合即为森林。
树的存储结构:
- 双亲表示法:假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1。该存储结构可以很容易找到结点的双亲结点,但不容易找到结点的孩子,为此我们可以改进一下,增加一个结点最左边孩子的域,这样就可以很容易得到结点的孩子。如果是没有孩子的结点,则这个域设置为-1。
- 孩子表示法:每个结点有多个指针域,其中每个指针指向一棵子树的根节点。这种方法称为多重链表表示法。由于每个树的孩子个数不同,因此有两种解决方案:一种是指针域的个数就等于树的度。另一种是每个结点的指针域的个数等于该节点的度,并且专门去一个位置来存储结点指针域的个数。孩子表示法是把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。
- 孩子兄弟表示法:设置两个指针,分别指向该结点的第一个孩子和此节点的右兄弟。该表示法的最大好处是它把一颗复杂的树变成了一颗二叉树。
1:二叉树
定义:
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集,或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
特点:
- 每个结点最多有两棵子树,即二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
特殊二叉树:
- 斜树:所有的结点都只有左子树的二叉树叫做左斜树。所有结点都只有右子树的二叉树叫做右斜树。两者统称为斜树。斜树的特点是每一层只有一个结点,结点的个数与二叉树的深度相同。
- 满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
- 完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。特点:叶子结点只能出现在最下两层;最下层的叶子一定集中在左部连续位置;
- 倒数第二层如果有叶子结点,一定都在右部连续位置;如果结点的度为1,则该结点只有左孩子,即不存在只有右子树的情况;同样结点数的二叉树,完全二叉树的深度最小。
性质:
- 在二叉树的第i层上至多有2的i-1次方个结点。
- 深度为k的二叉树之多有2的k次方-1个结点。
- 对任何一颗二叉树T,如果其叶子结点个数为n0,度为2的结点个数为n2,则n0=n2+1。
- 具有n个结点的完全二叉树的深度为⌊log2 n⌋+1。证明过程如图所示:
存储结构:
二叉链表:含有一个数据域和两个指针域。
class BinaryNode{
int data;
BinaryNode left;
BinaryNode right;
BinaryNode(int x){
data=x;
}
}
遍历二叉树:
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点都被访问一次且仅被访问一次。
前序遍历:先访问根结点,然后前序遍历左子树,再前序遍历右子树。
//递归算法
public void PreOrderTraverse(BinaryNode root){
if(root==null){
return;
}
System.out.println(root.data);
PreOrderTraverse(root.left);
PreOrderTraverse(root.right);
}
//迭代算法
public void PreOrderTraverse(BinaryNode root){
Deque<BinaryNode> deque=new LinkedList();
BinaryNode cur=root;
while(cur!=null||!deque.isEmpty()){
if(cur!=null){
System.out.println(cur.data);
deque.push(cur);
cur=cur.left;
}
else{
cur=deque.pop();
cur=cur.right;
}
}
}
中序遍历:先中序遍历根结点的左子树,然后访问根结点,再中序遍历根节点的右子树。
//递归算法
public void InOrderTraverse(BinaryNode root){
if(root==null){
return;
}
InOrderTraverse(root.left);
System.out.println(root.data);
InOrderTraverse(root.right);
}//迭代算法
public void InOrderTraverse(BinaryNode root){
Deque<BinaryNode> deque=new LinkedList();
BinaryNode cur=root;
while(cur!=null||!deque.isEmpty()){
if(cur!=null){
deque.push(cur);
cur=cur.left;
}
else{
cur=deque.pop();
System.out.println(cur.data);
cur=cur.right;
}
}
}
后序遍历:先后序遍历根结点的左子树,然后后序遍历根节点的右子树,再访问根结点。
//递归算法
public void PreOrderTraverse(BinaryNode root){
if(root==null){
return;
}
PreOrderTraverse(root.left);
ProOrderTraverse(root.right);
System.out.println(root.data);
}
//迭代算法
public void PostOrderTraverse(BinaryNode root){
Deque<BinaryNode> deque=new LinkedList();
BinaryNode cur=null,pre=null;
deque.push(root);
while(!deque.isEmpty()){
cur=deque.peek();
if((cur.left==null&&cur.right==null)||(pre!=null&&(pre==cur.left||pre==cur.right))){
System.out.println(cur.data);
deque.pop();
pre=cur;
}
else{
if(cur.right!=null){
deque.push(cur.right);
}
if(cur.left!=null){
deque.push(cur.left);
}
}
}
}
2:线索二叉树
对于一个有n个结点的二叉链表,共有n+1个空指针域。可以考虑利用这些空地址,存放指向结点在某种遍历次序下的前去和后继结点的地址。
定义:
这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
线索二叉树结构实现:
//二叉线索树存储结构
class BiThrNode{
int data;
BiThrNode left,right;
boolean lFlag,rFlag; //左右标志位,true表示线索,false表示孩子
public BiThrNode(int x){
data=x;
left=null;
right=null;
lFlag=false;
rFlag=false;
}
}
public class Test {
public static BiThrNode pre=null; //全局变量,指向刚访问过的结点
public static void main(String argv[]){
BiThrNode T=new BiThrNode(0); //头结点
BiThrNode node1=new BiThrNode(1);
BiThrNode node2=new BiThrNode(2);
BiThrNode node3=new BiThrNode(3);
BiThrNode node4=new BiThrNode(4);
BiThrNode node5=new BiThrNode(5);
BiThrNode node6=new BiThrNode(6);
BiThrNode node7=new BiThrNode(7);
BiThrNode node8=new BiThrNode(8);
BiThrNode node9=new BiThrNode(9);
BiThrNode node10=new BiThrNode(10);
T.left=node1;//头结点的left指向二叉树的根节点
T.rFlag=true;
T.right=node7;//头结点的right指向二叉树中序访问的最后一个节点
node1.left=node2;
node1.right=node3;
node2.left=node4;
node2.right=node5;
node3.left=node6;
node3.right=node7;
node4.left=node8;
node4.right=node9;
node5.left=node10;
pre=T;
InThreading(node1);
node7.rFlag=true;//令二叉树中序序列中最后一个节点的right指向头结点
node7.right=T;
InOrderTraverse(T);
}
//中序线索化过程
public static void InThreading(BiThrNode p){
if(p!=null){
InThreading(p.left); //递归左子树线索化
if(p.left==null){ //没有左孩子
p.lFlag=true; //前驱线索
p.left=pre; //左孩子指向前驱
}
if(pre.right==null){ //前驱没有右孩子
pre.rFlag=true; //后继线索
pre.right=p; //前驱右孩子指向后继
}
pre=p; //更新pre
InThreading(p.right); //递归右子树线索化
}
}
//中序遍历
public static void InOrderTraverse(BiThrNode T){
BiThrNode p=T.left; //p指向根节点
while(p!=T){ //空树或遍历结束时,p==T
while(!p.lFlag){ //当p.lFlag为true时循环到中序序列第一个结点
p=p.left;
}
System.out.println(p.data);//显示结点或其他操作
while(p.rFlag&&p.right!=T){
p=p.right;
System.out.println(p.data);
}
p=p.right; //p进至其右子树
}
}
}
3:树,森林,二叉树之间的转化
树转换为二叉树:
1. 加线。在所有兄弟结点之间加一条连线。
2. 去线。对树中的每个结点,只保留它与第一个孩子节点的连线,删除他与其他孩子结点之间的连线。
3. 层次调整。以树的根结点为轴心,调整树的结构。注意,二叉树结点的左孩子是它的第一个孩子,二叉树结点的右孩子是它的兄弟。
森林转换为二叉树:
1. 把每个树转换为二叉树。
2. 第一棵二叉树不同,从第二课二叉树开始,依次吧后一棵二叉树的根节点作为前一刻二叉树根节点的右孩子,用线连起来。
二叉树转换为树:
1. 加线。若某结点的左孩子结点存在,则将该左孩子的所有右孩子结点都作为该结点的孩子。将该结点与这些有孩子结点用线连起来。
2. 去线。删除原二叉树中所有结点与其有孩子结点的连线。
3. 层次调整。
二叉树转换为森林:
判断一颗二叉树是转换为一棵树还是森林的标准是看这棵二叉树的根节点有没有右孩子,有就是森林,没有就是一棵树。
1. 从根节点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除………,知道所有右孩子连线都删除为止,得到分离的二叉树。
2. 再将二叉树转换为树。
树有两种遍历:先根遍历和后根遍历
森林有两种遍历:前序遍历和后序遍历
森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。
4:赫夫曼树
从树中一个结点到另一个结点之间的分支构成连个节点之间的路径,路径上的分支数目成做路径长度。
树的路径长度就是从树根到每一结点的路径长度之和。
带权路径长度最小的二叉树称做赫夫曼树。
一般的,设需要编码的字符集为{d1,d2,……,dn},各个字符在电文中出现的次数或平率集合为{w1,w2,……,wn},以d1,d2,……,dn作为叶子结点,以w1,w2,……,wn作为相应叶子结点的权值来构造一颗赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根节点到叶子结点所经过的路径分支组成的0和1的序列便成为该结点对应字符的编码,这就是赫夫曼编码。