您的位置:首页 > 理论基础 > 数据结构算法

树的存储结构和运算

2017-11-10 17:01 1211 查看
一、树的抽象数据类型

       这里所说的树是指度大于等于3的树,通常称为多元树或多叉树。

       树的抽象数据类型的数据部分为一棵普通的k叉树,操作部分包括初始化树、建立树、遍历树、查找树、输出树、清除树、判空树等一些常用运算。

       根据普通k叉树的抽象数据类型,给出在Java语言中对应的接口定义如下:

public interface GeneralTree {

//由model数组提供遍历普通k叉树的3种不同的访问次序(先序,后序,层次)
final String []model={"preOrder","postOrder","levelOrder"};
//根据参数字符串gt中保存的普通树的广义表表示在计算机中建立对应的存储结构
boolean createGTree(String gt);
//按照字符串s所给定的次序遍历一棵普通树,每个结点均被访问一次
void traverseGTree(String s);
//返回普通树的度数k
int degree();
//从树中查找值为obj的结点,若存在则返回完整值否则返回空值
Object findGTree(Object obj);
//求出并返回一棵树的深度
int depthGTree();
//求出并返回一棵树的结点数
int countGTree();
//按照树的一种表示方法输出一棵树
void printGTree();
//判断树是否为空,若是则返回真否则返回假
boolean isEmpty();
//清除树中的所有结点,使之变为一棵空树
void clearGTree();
}


二、 树的存储结构

1、树的顺序存储结构

       树的顺序存储结构同样需要使用一个一维数组,存储方法是:首先对树中每个结点进行编号,然后以各结点的编号为下标,把结点值对应存储到相应元素中。

        假定待存储的树的度为k,即它是一棵k叉树,则结点编号规则为:树根结点的编号为1,然后按照从上到下、每一层再从左到右的次序依次对每个结点编号。若一个结点的编号为i,则k个孩子接待的编号依次为k*i-(k-2)、k*i-(k-3)、。。。、k*i+1。例如,对于三叉树,若双亲结点的编号为i,则3个孩子结点的编号依次为3*i-1、3*i、3*i+1。又如,对于四叉树,若双亲结点的编号为j,则四个孩子结点的编号依次为4*j-2、4*j-1、4*j、4*j+1。

      若k叉树中一个结点的编号为j,则它的父亲结点的编号为(j-2)/k+1,即等于j-2除以k得到的整数商再加上1。例如,当k=3时,父亲结点的编号为(j-2)/3+1,若j=10,则父节点的编号为3.

      树的顺序存储适合满树和完全树的情况,否则非常浪费存储空间。故在实际应用中很少使用,这里也不作深入讨论。

2、树的链接存储结构

     树的链接存储结构通常采用如下3种方式。

(1)标准方式

       在这种方式中,树中的每个结点除了包含存储数据元素 的值域外,还包含k个指针域,用来分别指向k个孩子结点,或者说,用来分别链接k棵子树,其中,k为树的度。结点的类型可定义为:

//普通树结点类定义
public class GTreeNode {
Object element;                //元素值域
GTreeNode[] next;              //保存k个指针(引用)的域
public GTreeNode(Object obj)
{
element=obj;
next=new GTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=null;
}
}
public GTreeNode(Object obj, GTreeNode[] nt)
{
element=obj;
next=new GTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=nt[i];
}
}
}


      由于在普通树结点类GTreeNode的定义中,使用了表示树的度数的常量k,而k应该作为常量数据成员而被定义在普通树类之中,所以该结点类应作为普通树类的内部类,不能作为独立的结点类。

(2)广义标准方式

        广义标准方式是在标准方式的每个结点中增加一个指向其双亲结点的指针域。结点类型可定义为:

public class PGTreeNode {
Object element;                 //元素值域
PGTreeNode[] next;              //保存k个指针(引用)的域
PGTreeNode parent;              //双亲指针
public PGTreeNode(Object obj)
{
element=obj;
next=new PGTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=null;
}
parent=null;
}
public PGTreeNode(Object obj, PGTreeNode[] nt,PGTreeNode pt)
{
element=obj;
next=new PGTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=nt[i];
}
parent=pt;
}
}
       下图(a)是一棵有序二叉树。在此图中,若一个指针箭头不指向任何结点,则表明该指针为空。在有序树中,任何一个指针域都可能为空,对于叶子结点,因为指针全为空,所以均不画出指针箭头。若用标准方式来链接存储有序三叉树,则如下图(b)所示。若采用广义标准方式来链接存储图(a)所示的有序三叉树,只要在图(b)的每个存储结点中增加一个表示双亲的指针域,并让它指向父结点,而树根结点的双亲指针域为空。



(3)二叉树方式

      这种方式需要把树转换为对应的二叉树形式,然后再采用二叉链表存储这棵二叉树。

      将树转化为二叉树的规则是:将树中每个接待的第一个孩子结点转换为二叉树中对应结点的左孩子,将第二个孩子结点转换为这个左孩子的右孩子,将第三个孩子结点转换为这个右孩子的右孩子。也就是说,转换后得到的二叉树中的每个结点及右孩子在转换前的树中互为兄弟。例如,对于下图a所示的三叉树T,对应的二叉树表示如图b所示。若用二叉树方式,则不能表示有序的三叉树。



      在树的以上三种链接存储表示方式中,标准方式和广义表方式能够表示任何树,但二叉树方式只适合表示无序树,以及前面不缺少兄弟结点的有序树,不能表示前面缺少星弟,而又存在后面兄弟的有序树。

3、树的链接存储类

三、树的运算

1、建立树的存储结构

       建立树的存储结构就是在内存中生成一课树的标准方式的存储结构,即k叉链表。同二叉链表的生成过程一样,首先要确定输入树的方法,然后再写出相应的算法。假定仍采用广义表的形式输入,对于图(a)所示的三叉树,得到的广义表表示为:

                            A(B(D,E(,H,I)F),,C(,,G))

       在树的广义表表示中,若一个结点的前面子树为空,而后面不为空,则子树之间的逗号分隔符不能省略。

       在树的生成算法中,需要设置两个栈,一个用来保存被访问过的结点的指针(引用),以便待访问的孩子结点向双亲结点的相应指针域链接之用;另一个用来保存待链接的孩子结点应链接到双亲结点相应指针域的位置序号,以便能正确地链接到双亲结点的相应指针域。假定这两个栈分别用sck和ord表示,它们的栈长度都不会大于整个树的深度。

       树的生成算法与二叉树生成算法类似,假定结点值仍为字符类型char,整个k叉树用一个广义表形式的字符串str表示,则具体算法描述为:

public boolean createGTree(String str) {
Stack sck=new SequenceStack();         //定义和创建一个保存结点指针的栈
Stack ord=new SequenceStack();         //定义和创建一个保存待链接指针域序号的栈
root =null;                            //把树根指针置为空,即从空树开始
GTreeNode p=null;                      //定义p为指向k叉树结点的指针
char []a=str.toCharArray();            //将字符串内容转换为字符数组a的内容
for(int i=0;i<a.length;i++)
{
switch(a[i])
{
case ' ':                      //对空格不做任何处理
break;
case '(':                      //处理左括号
sck.push(p);                   //其前面的结点引用进栈,以便孩子结点进行链接
ord.push(0);                   //序号0进栈,表明待处理的结点将链接到p.next[0]中
break;
case ')':                      //处理右括号,表明一棵子树已经链接完成
if(sck.isEmpty())
{
System.out.println("普通树广义表字符串错,返回假!");
return false;
}
sck.pop();                 //两个栈同时进行退栈处理
ord.pop();
break;
case ',':                      //处理逗号,使ord的栈顶元素增1,表明将处理下一课子树
int x=(Integer)ord.pop();
ord.push(x+1);
break;
default:                       //扫描到的必为字母,即结点值
if((a[i]>='a'&&a[i]<='z'||(a[i]>='A'&&a[i]<='Z')))
{
//处理元素结点
p=new GTreeNode(a[i]); //根据a[i]生成一个结点
if(root==null)
{
root=p;            //若p结点为树根则把引用赋给root
}
else
{
//若p为非树根则链接得双亲结点
((GTreeNode).sck.peek(0)).next[(Integer)ord.peek()]=p;
}
}
else
{
System.out.println("普通树广义表中存在非法字符,返回假!");
return false;
}
}
}//for 结束
if(sck.isEmpty())
{
return true;
}
else
{
return false;
}
}

2、树的遍历

      树的遍历包括先根遍历(或称深度优先遍历)、后跟遍历和层次遍历(或称广度优先遍历)3种。

     先根遍历定义为:先访问根结点,然后从左到右依次先根遍历每棵子树,此遍历过程是一个递归过程。例如:先根遍历(a)所示的树(上面图所示的树),得到的结点序列为:

                                                      ABDEHIFCG

      后根遍历定义为:从左到右依次后根遍历根结点的每棵子树,然后再访问根结点,此遍历过程也是一个递归过程。例如:后根遍历(a)所示的图,得到的结点序列为:

                                                       DHIEFBGCA

      按层遍历定义为:先访问第一层结点(即树根结点),再从左到右访问第二层结点,依次按层访问,直到全树中国的所有结点都被访问为止,或者说直到访问完最深一层结点为止。例如,按层遍历图(a)所示的树,得到的结点序列为:

                ABCDEFGHI

      1、 同二叉树的先序遍历算法类似,下面首先给出树的先根遍历算法:

//k叉树先根遍历的递归算法
private void preOrder(GTreeNode gt)
{
if(gt!=null)
{
System.out.print(gt.element+" ");     // 访问根结点
for(int i=0;i<k;i++)                  //依次先根遍历每棵子树
{
preOrder(gt.next[i]);
}
}
}

     2、 树的后跟遍历算法如下:

//后跟遍历的递归算法
private void postOrder(GTreeNode gt)
{
//k叉树的后跟遍历的递归算法
if(gt!=null)
{
for(int i=0;i<k;i++)                             //依次后跟遍历每棵子树
{
postOrder(gt.next[i]);
}
System.out.print(gt.element+" ");                //访问根结点
}
}
     3、 在树的层次遍历算法中,需要设置一个队列,假定用que表示,算法开始时将q初始化为空,接着树根指针入队;然后每从队列中删除一个元素(即为结点引用)时,都会输出它的值并 且依次使非空的孩子指针入队;然后每从队列中删除一个元素(即为结点引用)时,都输出它的值并且依次使非空的孩子指针入队;这样反复进行下去,直到队列为空时止,此算法是一个非递归算法,算法的具体描述为:

//k叉树层次遍历的非递归算法
private void levelOrder(GTreeNode gt)
{
Queue que=new SequenceQueue();                       //定义并创建一个空队列
GTreeNode p=null;                                    //定义结点引用p
que.enter(gt);                                       //首先将树根指针入队
while(!que.isEmpty())
{
p=(GTreeNode)que.leave();                        //删除队首元素赋给p
System.out.print(p.element+" ");                 //输出p所指结点的值
for(int i=0;i<k;i++)
{
if(p.next[i]!=null)
{
que.enter(p.next[i]);
}
}
}
}
 4、从树中查找结点值

       此算法要求:从树中查找值为obj的结点时,若存在则返回该结点的完整值,否则返回空值表示查找失败。此算法类似于树的先根遍历,它首先访问根结点,若相等则返回结点值,否则依次查找每个子树。具体算法描述为:

//从以gt为树根指针的一棵k叉树中查找值为x的结点
private Object findGTree(GTreeNode gt,Object x)
{
if(gt==null)
{
return null;                       //查找失败返回空值
}
else if(gt.element.equals(x))
{
return gt.element;                 //查找成功返回该结点的值
}
else
{
Object y;
for(int i=0;i<k;i++)               //从每棵子树中查找
{
if((y=findGTree(gt.next[i],x))!=null)
{
return y;                  //在子树中查找均失败返回空值
}
}
}
return null;
}

5、树的输出

        假定要求输出格式为树的广义表形式。此算法同样类似于树的先跟遍历,它首先输出树根结点的值,接着依次扫描查找出子树不为空的最大序号,然后依次输出每棵子树,当然在输出第一棵子树之前要输出子表的左括号,输出每棵子树后要输出一个逗号,子树输出完后要输出一个右括号。该算法描述为:

//输出k叉树的广义表表示
public void printGTree(GTreeNode gt)
{
if(gt!=null)                           //树为空时结束递归,否则执行如下操作
{
System.out.print(gt.element);      //输出根结点的值
int i,j=-1;
for(i=0;i<k;i++)                   //求出序号最大的非空子树
{
if(gt.next[i]!=null)
{
j=i;                        //此序号保存在j中
}
}
if(j>=0)                            //当存在子树时则输出子表
{
System.out.print('(');          //输出左括号
printGTree(gt.next[0]);         //输出第一棵子树
for(i=1;i<j;i++)
{
System.out.print(',');
printGTree(gt.next[i]);
}
System.out.print(')');          //输出右括号
}
}
}

6、求树的深度

        若树为空则深度为0,否则它等于所有子树的最大深度加1。为此需要设置一个整型变量,用来保存已求过的子树中最大深度,当所有子树都求过后,返回该变量加1即可。具体算法描述为:

//求以gt为树根指针的一棵k叉树的深度
private int depthGTree(GTreeNode gt)
{
if(gt==null)
{
return 0;                          //对于空树,返回0并结束递归
}
else
{
int max=0;                         //用来保存子树中的最大深度,初值为0
for(int i=0;i<k;i++)               //依次求出每棵子树的深度,保留最大值
{
int dep=depthGTree(gt.next[i]);
if(dep>max)
{
max=dep;
}
}
return max+1;                      //返回树的深度
}
}

7、求出树中的结点数

      若树为空则节点数为0,否则它等于所有子树的节点数之和再加上1。此算法具体描述如下:

//求出并返回以gt为树根指针的一棵k叉树中的结点数
private int countGTree(GTreeNode gt)
{
if(gt==null)
{
return 0;
}
else
{
int c=0;
for(int i=0;i<k;i++)
{
c+=countGTree(gt.next[i]);
}
return c+1;
}
}
        所有算法的类如下:

    

public class LinkGeneralTree implements GeneralTree{

protected int k; //用常变量k保存普通树的度数
protected GTreeNode root; //定义普通树的树根指针(引用)

//普通树结点类的定义,作为内部类使用
public class GTreeNode {
Object element; //元素值域
GTreeNode[] next; //保存k个指针(引用)的域
public GTreeNode(Object obj)
{
element=obj;
next=new GTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=null;
}
}
public GTreeNode(Object obj, GTreeNode[] nt)
{
element=obj;
next=new GTreeNode[k];
for(int i=0;i<k;i++)
{
next[i]=nt[i];
}
}
}

//普通树类构造方法的定义
public LinkGeneralTree(int kk)
{
k=kk; //把参数kk的值赋给k作为树的度
root=null; //初始设置普通树为空
}

//返回普通树的度数k
public int degree() {
// TODO Auto-generated method stub
return k;
}

//k叉树先根遍历的递归算法
private void preOrder(GTreeNode gt)
{
if(gt!=null)
{
System.out.print(gt.element+" "); // 访问根结点
for(int i=0;i<k;i++) //依次先根遍历每棵
{
preOrder(gt.next[i]);
b87c

}
}
}

//后跟遍历的递归算法 private void postOrder(GTreeNode gt) { //k叉树的后跟遍历的递归算法 if(gt!=null) { for(int i=0;i<k;i++) //依次后跟遍历每棵子树 { postOrder(gt.next[i]); } System.out.print(gt.element+" "); //访问根结点 } }

//k叉树层次遍历的非递归算法 private void levelOrder(GTreeNode gt) { Queue que=new SequenceQueue(); //定义并创建一个空队列 GTreeNode p=null; //定义结点引用p que.enter(gt); //首先将树根指针入队 while(!que.isEmpty()) { p=(GTreeNode)que.leave(); //删除队首元素赋给p System.out.print(p.element+" "); //输出p所指结点的值 for(int i=0;i<k;i++) { if(p.next[i]!=null) { que.enter(p.next[i]); } } } }

//从以gt为树根指针的一棵k叉树中查找值为x的结点 private Object findGTree(GTreeNode gt,Object x) { if(gt==null) { return null; //查找失败返回空值 } else if(gt.element.equals(x)) { return gt.element; //查找成功返回该结点的值 } else { Object y; for(int i=0;i<k;i++) //从每棵子树中查找 { if((y=findGTree(gt.next[i],x))!=null) { return y; //在子树中查找均失败返回空值 } } } return null; }

//求以gt为树根指针的一棵k叉树的深度 private int depthGTree(GTreeNode gt) { if(gt==null) { return 0; //对于空树,返回0并结束递归 } else { int max=0; //用来保存子树中的最大深度,初值为0 for(int i=0;i<k;i++) //依次求出每棵子树的深度,保留最大值 { int dep=depthGTree(gt.next[i]); if(dep>max) { max=dep; } } return max+1; //返回树的深度 } }
//求出并返回以gt为树根指针的一棵k叉树中的结点数 private int countGTree(GTreeNode gt) { if(gt==null) { return 0; } else { int c=0; for(int i=0;i<k;i++) { c+=countGTree(gt.next[i]); } return c+1; } }

//输出k叉树的广义表表示 public void printGTree(GTreeNode gt) { if(gt!=null) //树为空时结束递归,否则执行如下操作 { System.out.print(gt.element); //输出根结点的值 int i,j=-1; for(i=0;i<k;i++) //求出序号最大的非空子树 { if(gt.next[i]!=null) { j=i; //此序号保存在j中 } } if(j>=0) //当存在子树时则输出子表 { System.out.print('('); //输出左括号 printGTree(gt.next[0]); //输出第一棵子树 for(i=1;i<j;i++) { System.out.print(','); printGTree(gt.next[i]); } System.out.print(')'); //输出右括号 } } }

//根据参数字符串gt中保存的普通树的广义表表示在计算机中建立对应的存储结构
public boolean createGTree(String str) {
Stack sck=new SequenceStack(); //定义和创建一个保存结点指针的栈
Stack ord=new SequenceStack(); //定义和创建一个保存待链接指针域序号的栈
root =null; //把树根指针置为空,即从空树开始
GTreeNode p=null; //定义p为指向k叉树结点的指针
char []a=str.toCharArray(); //将字符串内容转换为字符数组a的内容
for(int i=0;i<a.length;i++)
{
switch(a[i])
{
case ' ': //对空格不做任何处理
break;
case '(': //处理左括号
sck.push(p); //其前面的结点引用进栈,以便孩子结点进行链接
ord.push(0); //序号0进栈,表明待处理的结点将链接到p.next[0]中
break;
case ')': //处理右括号,表明一棵子树已经链接完成
if(sck.isEmpty())
{
System.out.println("普通树广义表字符串错,返回假!");
return false;
}
sck.pop(); //两个栈同时进行退栈处理
ord.pop();
break;
case ',': //处理逗号,使ord的栈顶元素增1,表明将处理下一课子树
int x=(Integer)ord.pop();
ord.push(x+1);
break;
default: //扫描到的必为字母,即结点值
if((a[i]>='a'&&a[i]<='z'||(a[i]>='A'&&a[i]<='Z')))
{
//处理元素结点
p=new GTreeNode(a[i]); //根据a[i]生成一个结点
if(root==null)
{
root=p; //若p结点为树根则把引用赋给root
}
else
{
//若p为非树根则链接得双亲结点
((GTreeNode).sck.peek()).next[(Integer)ord.peek()]=p;
}
}
else
{
System.out.println("普通树广义表中存在非法字符,返回假!");
return false;
}
}
}//for 结束
if(sck.isEmpty())
{
return true;
}
else
{
return false;
}
}

//按照字符串s所给定的次序遍历一棵普通树,使每个结点均被访问一次
public void traverseGTree(String s) {
if(s.equals(model[0]))
{
preOrder(root); //进行先序遍历
}
else if(s.equals(model[1]))
{
postOrder(root); //进行后序遍历
}
else if(s.equals(model[2]))
{
levelOrder(root); //进行层次遍历
}
System.out.println(); //进行一种遍历后输出换行
}

//从树中查找值为obj的结点,若存在则返回完整值否则返回空值
public Object findGTree(Object obj) {
return findGTree(root,obj); //调用相应的递归算法完成查找任务
}

//求出并返回一棵树的深度
public int depthGTree() {
return depthGTree(root); //调用相应的递归算法完成求深度任务
}

//求出并返回一棵树的结点数
public int countGTree() {
return countGTree(root); //调用相应的递归算法完成任务
}

//按照树的一种表示方法输出一棵树
public void printGTree() {
printGTree(root); //调用相应的递归算法完成任务
System.out.println(); //输出之后换行
}

//判断树是否为空,若是则返回真否则返回假
public boolean isEmpty() {
return root==null; //判断k叉树是否为空,是返回真,否则返回假
}

//清除树中的所有结点,使之变为一棵空树
public void clearGTree() {
root=null; //清除k叉树,即给root赋空值
}

}


       上面讨论的树的一些运算都需要访问树中的所有结点,并且每个结点的值仅被访问一次,访问时也只是做些简单的操作,所以每个算法的时间复杂度均为O(logkn),最差情况为O(n)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息