5. 树

 

广义树概念

子节点表描述方法

树的“左子节点/右兄弟节点”描述方法
 

 


 

 

   本章将扩展前几章的内容,讨论一种较为复杂的数据结构,即树(tree)结构。前面所讨论的线性表、堆栈等数据结构中,跟每个节点相连的节点的个数都是有限的。本章讨论的树结构中,节点可以有任意数目的子节点。这是的数在实际应用中具有更大的作用,但其结构更复杂。

树的定义

   一棵树T是由一个或一个以上节点组成的有限集合,其中节点有分为根节点、叶节点和中间节点。树具有层结构,根节点(R)之外的节点集合{T-R},可以划分为一些不相交子集{T1,…,Tn}。这些子集又可以分别构成子树,并拥有相应的根节点。一般将位于根节点左边的子节点称作左子节点,位于右边的子节点称作右子节点。位于右边的子节点称为最左边子节点的右兄弟节点。节点的出度定义为其子节点的个数。

图示意了图结构,其找R为根节点,根节点下有连个子节点,P和P1。其中P有三个子节点,V,V1,V2。以V为父节点的子集为例,构成了一个子树。V1和V2是V的有兄弟节点。

 

 

 

图 广义树示例

     在树结构中,根节点之外的节点都有唯一的父节点。所以,在一棵节点总数为n的树中,边的个数为n-1。在本章即将详细讨论的二叉树是一种特殊的树结构,具有非常丰富的结构性,例如二叉树的中间节点个数与叶节点个数。

本章内容

     本章将从广义树开始讨论树结构,并介绍两种广义树的实现方式;将详细讨论二叉树的性质和四种遍历算法;最后一部分将介绍huffman编码树。

 

广义树的描述方法

    本章只讨论总节点有限的广义树,所有节点存放在大小确定的数组中。节点之间的连接关系用节点在数组中的标号之间的连接关系表示。

广义树中每个父节点可能包含多个子节点,并且各个父节点的子节点个数也不尽相同。针对这种结构,每个节点除了存放节点元素项和节点的父节点之外,我们可以对为每个节点设计一个单链表,链表中的元素项表示该节点所指向的所有子节点在数组中的标号。如图示意了用这种方法对上图中树结构的描述。这种方法通常称为“子节点表方法”。

 

 

 

图 子节点表方法描述树

 

     子节点表方法在每个节点记录了该节点的所有子节点的信息,这种描述没有充分挖掘同一个父节点的子节点之间连接关系的传递性。另一种常见的描述方式为“左节点/右兄弟节点方法”。这种方法中,每个节点都存放了该节点的子节点中最左边的子节点的指针以及与该节点相邻的右兄弟节点。这种方法利用了同一父节点的子节点之间连接关系的传递性。

 

 

 

 

 

图 左子节点/右兄弟节点描述方法

 

   利用“左子节点/右兄弟节点”描述方法,在每个节点处所保存的连接信息只需要三个指针,而不需要链表结构。下面我们将详细讨论这两种描述方法的实现过程。

 

子节点表描述方法

   上面我们已经初步了解到,利用子节点表描述树结构时,关键的设计步骤是设计节点的数据结构。这里节点的成员变量包括:节点的所包含的内容信息、该节点的父节点在节点数组中的序号、表示该节点的子节点的链表和该节点在节点数组中的序号。其中子节点链表中链表节点的元素项为子节点的序号,子节点的顺序由左向右。在创建节点对象时,需要同时设置四个成员变量。

   节点数据结构的定义如下:

 

复制代码
package Tree;

import Element.ElemItem;
import List.SingleLink2;

/**
 * 基于链表的树结构(子节点表示法),
 * 这里的的链表是高效单链表
 
*/
public  class LinkTNode{
     // 节点自身的元素项
     private ElemItem elem;
     // 节点父节点
     private  int parentNode;
     // 每个树节点中包含一个单链表,
    
// 链表中每个节点中的元素项是LinkTNode类型
     private SingleLink2 link_idx;
     // 节点在节点数组中的位置idx
     private  int idx;
    
     // 无参数的构造函数
     public LinkTNode(){
        elem =  null;
        parentNode = -1;
        link_idx =  null;
        idx = -1;
    }
    
     // 带参数的构造函数
     public LinkTNode(ElemItem e,  int pn,
                SingleLink2 sk,  int _idx){
        elem = e;
        parentNode = pn;
        link_idx = sk;
        idx = _idx;
    }
    
     // 设置节点元素项
     public  void setElem(ElemItem _elem) {
        elem = _elem;    
    }
     // 获取节点的元素项
     public ElemItem getElem(){
         return elem;
    }
    
     // 判断是否是叶节点
     public  boolean isLeaf() {
         // 若节点中的链表是空,则是叶节点
         return link_idx ==  null;
    }

     // 返回父节点
     public  int parent() {
         return parentNode;
    }

     // 设定父节点
     public  void setParent( int pn) {
        parentNode = pn;
    }
    
     // 返回所有子节点对应的链表
     public SingleLink2 getChildLink(){
         return link_idx;
    }
    
     // 重新设置节点的子节点
     public  void setChildLink(SingleLink2 slk){
        link_idx = slk;
    }
    
     // 获取当前节点在节点数组中的位置
     public  int getIdx(){
         return idx;
    }
     // 设置当前节点在节点数组中的序号
     public  void setIdx( int _idx){
        idx = _idx;
    }
     // 返回元素项String形式
     public String toString(){
         return elem.getElem().toString();
    }
    
}
复制代码

 

    上述节点类的描述中还包含了toString()函数,其目的是为了更方便地显示该节点中的内容。

节点连接表的描述方法中,树的节点全部存放在节点类型的数组中,节点之间的连接关系通过每个节点所保存的子节点链表来表示。树的数据结构中包含节点个数、节点数组、当前树中有效的节点个数和树的根节点在数组中的序号。创建树时,需要确定树的节点个数,即节点数组的大小。

对树的操作通常包括节点的添加、节点的删除和树的遍历等。

在树中插入包含元素项elem的新节点时,需要确定待插入节点在数组中的序号self_idx和父节点在数组中的序号p_idx。如果其父节点为空,则直接将p_idx位置的节点中的元素项设为elem,而self_idx的值无效。如果父节点不为空,则将在节点数组的self_idx处新建元素项为elem的节点,并将self_idx添加到p_idx处节点的子节点链表中去。如果当前p_idx的子节点链表不为空,则直接将self_idx添加到链表中,否则,需要首先创建p_idx的子节点链表,然后再添加self_idx。添加新节点后需要更新节点的个数。

从上面的插入节点过程可以看出,插入新的节点最多只会影响一个已存在的节点的子节点链表,但是删除节点相对节点插入过程会更复杂一点。因为删除一个节点时,需要同时删除该节点对应的子节点以及子节点的子节点,以此类推。即需要递归地删除该节点的所有子节点。

如果待删除的节点没有子节点,即其子节点链表为空,则直接将该节点从其父节点的子节点链表中删除,同时更新树中节点的个数。这个过程首先需要在待删除节点父节点的子节点链表中找到该节点的序号,由于使用的是单链表,则需要从链表首部开始逐个判断。

如果待删除的节点有子节点,其子节点链表不为空,则需要遍历它的所有子节点,并递归地对每个子节点执行删除节点过程。

打印树结构时需要遍历树中的所有节点,在上一章中,堆的遍历算法包括前序、中序和后续。由于广义树中每个节点的子节点个数不一定为两个,所以这里的遍历算法主要是前序和后续遍历。我们将分别讨论这两种遍历方法。

后序遍历算法,首先访问节点的所有子节点,然后再访问节点本身。一般的实现方法是递归遍历。首先对节点的子节点链表中所有节点执行递归后序遍历,直到子节点再没有子节点位为止,然后再访问节点自身。

前序遍历算法,首先访问节点自身,然后再对节点子节点链表中的每个节点递归地执行前序遍历,直到子节点再没有子节点为止。这种遍历算法对广义树而言更有意义,例如我们通常使用的文件资源管理结构中,文件夹下通常包含子文件夹,并层层包含,如图。在遍历文件资源管理器的树状结构时,我们通常会自然地采用前序遍历算法。

在打印树结构及其中的节点时,为了体现出节点之间的层次性,需要控制节点内容显示的位置。节点所在的层次越深,其显示的位置越靠后。

 

 

 

 

图 文件资源管理器的树状结构

    基于上面的讨论,树的节点连接表描述类实现如下:

 

复制代码
     package Tree;
    
     import Element.ElemItem;
     import List.SingleLink2;
    
     /**
     * 链表树,基于数组的节点存储方式
     
*/
     public  class LinkTree {
         // 私有成员变量,LinkNode类型的数组,固定长度
         private  int len; // 节点最大个数
        
// 私有成员变量,树节点个数
         private LinkTNode node_array[];
         // 私有成员变量,树的根节点在数组中序号
         private  int root;
         // 树中当前节点的个数
         private  int curr_num;
        
         // 有参数的构造函数
         public LinkTree( int _len){
            len = _len;
            node_array =  new LinkTNode[_len];
            root = -1;        
             // 位置0处为树根前驱节点
            node_array[0] =  new LinkTNode(
                     new ElemItem<Integer>(0),
                    -1,     null, 0);
             for( int i  = 1; i < _len; i++){
                node_array[i] =  new LinkTNode();
            }
        }
        
         // 设置树的根节点
         public  void setRoot( int r){
            root = r;
            node_array[r].setParent(0);
            SingleLink2 slk =  new SingleLink2();
            slk.insert( new ElemItem<Integer>(r));
            node_array[0].setChildLink(slk);
        }
        
         // 清除数组中的元素项
         public  void clear(){
            root = -1;
             for( int i  = 0; i < len; i++){
                node_array[i].setElem( null);
            }
            curr_num = 0;
        }
        
         // 数组中第n_idx个节点添加子节点(类型的元素项),
        
// n_idx为父节点号
        
// 自身的节点号为self_idx
         public  boolean addChild( int p_idx, 
                                 int self_idx, 
                                ElemItem e){
             if(p_idx < 0 || p_idx >= len || 
                self_idx < 0 || self_idx >= len ||
                curr_num > len){
                 return  false;
            }
             // 如果节点数组在父节点号处没有元素项,
            
// 则直接将元素项e赋值到数组的p_idx处,self_idx无效用
             if(node_array[p_idx].getElem() ==  null){
                node_array[p_idx].setElem(e);
                node_array[p_idx].setChildLink( null);
                node_array[p_idx].setParent(-1);
                node_array[p_idx].setIdx(p_idx);
                curr_num++;
                 return  true;
            }
             // 如果节点数组在父节点号处有元素项了
            
// 则将e赋值到数组self_idx处,同时设置相关参数
            node_array[self_idx].setElem(e);
            node_array[self_idx].setIdx(self_idx);
            node_array[self_idx].setParent(p_idx);
            node_array[self_idx].setChildLink( null);
             // 如果父节点的子树链表是空的,则新建链表并插入self_ode处的节点
             if (node_array[p_idx].getChildLink() ==  null){
                node_array[p_idx].setChildLink(
                         new SingleLink2());
            }
             // 在节点的子节点
            node_array[p_idx].getChildLink().insert(
                     new ElemItem<Integer>(self_idx));
            node_array[p_idx].getChildLink().next();
            curr_num++;
             return  true;
        }
        
         // 删除pIdx父节点的子节点序号sIdx,即链表清除
         protected  void removeSon( int pIdx,  int sIdx){
            node_array[pIdx].getChildLink().goFirst();
             // 定位父节点的子节点链表中要删除的子节点的序号的位置
             while(((Integer)(node_array[pIdx].getChildLink(
                ).getCurrVal().elem)).intValue()!= sIdx){
                node_array[pIdx].getChildLink().next();
            }
             // 删除该位置上的子节点的序号(链表节点)
            node_array[pIdx].getChildLink().remove();
            curr_num--;
        }
        
         // 删除位置为nidx的节点的子节点
         public  boolean removeChild( int nidx){
             if(nidx < 0 ||
                node_array[nidx].getElem() ==  null){
                 return  false;
            }
             // 子节点不为空,则将子节点一一清空
             if(node_array[nidx].getChildLink() !=  null){
                node_array[nidx].getChildLink().goFirst();
                 while(node_array[nidx].getChildLink().getCurrVal()
                        !=  null){
                     int idx = 
                        ((Integer)(node_array[nidx].getChildLink(
                            ).getCurrVal().getElem())).intValue();
                     // 递归调用
                    removeChild(idx);
                }
                
            }
            
             // 清除当前节点
            
// 首先清除
            removeSon(node_array[nidx].parent(), nidx);
             // 链表和元素都清空
            node_array[nidx] =  new LinkTNode();        
             return  true;
        }
        
         // 在特定高度打印一个元素项
         protected  void printnode( int pos,  int h){
             for( int i = 0; i < h; i++)
                System.out.print("\t");
            System.out.println(
                "|—— " +node_array[pos] + "(" + pos+ ")");
        }
        
         // 后序遍历树中元素项
         protected  void posOrder( int pos,  int h){
             if(pos < 0 || 
                 // pos > curr_num ||
                node_array[pos].getElem() ==  null
                 return;
             // 访问左子节点
            SingleLink2 lftMostChild = 
                node_array[pos].getChildLink();
             if(lftMostChild !=  null){
                lftMostChild.goFirst();
                 while(lftMostChild.getCurrVal() !=  null){
                     int idx = ((Integer)(
                        lftMostChild.getCurrVal().getElem())
                        ).intValue();
                    posOrder(idx, h + 1);
                    lftMostChild.next();
                    
                }
            }
             // 访问父节点
            printnode(pos, h);
        }
        
         // 前序遍历树中元素项
         protected  void preOrder( int pos,  int h){
             if(pos < 0 || 
                 // pos > curr_num ||
                node_array[pos].getElem() ==  null
                 return;
             // 访问父节点
            printnode(pos, h);
             // 访问左子节点
            SingleLink2 lftMostChild 
                = node_array[pos].getChildLink();
             if(lftMostChild !=  null){
                lftMostChild.goFirst();
                 while(lftMostChild.getCurrVal() !=  null){
                     int idx = ((Integer)(
                        lftMostChild.getCurrVal().getElem())
                        ).intValue();
                    preOrder(idx, h + 1);
                    lftMostChild.next();        
                }
            }
        }
        
         // 后序打印树
         public  void post_print_tree(){
             if(curr_num == 0) {
                System.out.println("当前树中没有节点。");
                 return;
            }
            System.out.println(curr_num + " 节点,后序遍历打印:");
            posOrder(1, 0);
        }
         // 前序打印树
         public  void ford_print_tree(){
             if(curr_num == 0) {
                System.out.println("当前树中没有节点。");
                 return;
            }
            System.out.println(curr_num + " 节点,前序遍历打印:");
            preOrder(1,0);
        }
    }
复制代码

 

类中包含四个私有成员变量,分别为树中最多包含的节点的个数len,节点类型的数字node_array[],树根root和树中当前节点的个数curr_num。

节点添加函数addChild函数,首先判断添加节点self_idx的父节点p_idx是否为空,若为空则直接将p_idx节点的内容设置为待添加元素项。否则,设置self_idx处节点内容为待添加元素项,并设置其父节点为p_idx,其子节点链表为空,如果p_idx的子节点链表为空则创建新的子节点链表并添加self_idx,如果p_idx的子节点链表不为空,则在子节点链表中添加self_idx。

节点删除过程包含两个函数,removeSon和removeChild。其中removeChild为递归函数,如果节点的子节点链表不为空,则递归地调用removeSon删除节点的所有子节点。如果节点的子节点链表为空,则调用removeSon函数。removeSon函数的入参包括待删除的节点的父节点在节点数组pIdx中的序号和待删除节点自身在节点数组中的序号sIdx。在执行时,首先在pIdx节点的子节点链表中找到sIdx,然后在链表中删除它并更新树中的节点个数curr_num。removeSon执行后,还需要将节点数组中的待删除节点的序号处赋值为空。

两种树的遍历打印过程都需要调用节点打印函数printnode,该函数具有两个入参,即节点在节点数组中的位置pos和节点所在的层数h。函数执行时首先打印h个制表位,然后再打印节点的内容。

后序打印函数post_print_tree,调用后续遍历函数posOrder,从树第一个节点开始打印。执行时,posOrder首先递归地调用自身,遍历节点的所有子节点,递归调用时需要改变的是子节点所在的层数。如果子节点为空则调用printnode函数打印该节点。然后调用printnode函数打印节点自身。

前序打印函数ford_print_tree,调用前序遍历函数preOrder,也从树的第一个节点开始打印。执行时,preOrder函数首先调用printnode函数打印节点自身,然后递归地调用preOrder,遍历所有子节点,递归调用时子节点所在层数也需要变化。如果子节点为空,则调用printnode函数打印该节点。

下面介绍一个实例程序,进一步说明子节点表描述方法类。

 

复制代码
     package Tree;
    
     import Element.ElemItem;
    
     /**
     * 链表树结构测试示例程序,
     * 测试树中插入、删除节点以及打印树中元素项
     * ExampleLinkTree.java
     * 
     
*/
     public  class ExampleLinkTree {
         public  static  void main(String args[]){
            LinkTree ltree =  new LinkTree(100);
            ltree.addChild(0, 1, 
                     new ElemItem<String>("Root"));
            ltree.setRoot(1);
            ltree.addChild(1, 2, 
                     new ElemItem<String>("C:"));
            ltree.addChild(1, 3, 
                     new ElemItem<String>("D:"));
            ltree.addChild(1, 4, 
                     new ElemItem<String>("E:"));
            ltree.addChild(2, 5, 
                     new ElemItem<String>("System"));
            ltree.addChild(2, 6, 
                     new ElemItem<String>("Programme"));
            ltree.addChild(3, 7, 
                     new ElemItem<String>("EBook"));
            ltree.addChild(7, 13, 
                     new ElemItem<String>("Communication Network"));
            ltree.addChild(3, 8, 
                     new ElemItem<String>("Game"));
            ltree.addChild(8, 14, 
                     new ElemItem<String>("LianLianKan"));
            ltree.addChild(8, 15, 
                     new ElemItem<String>("LianLianKan2"));
            ltree.addChild(8, 16, 
                     new ElemItem<String>("LianLianKan3"));
            ltree.addChild(8, 17, 
                     new ElemItem<String>("LianLianKan4"));
            ltree.addChild(4, 9, 
                     new ElemItem<String>("Work"));
            ltree.addChild(4, 10, 
                     new ElemItem<String>("Paper"));
            ltree.addChild(9, 11, 
                     new ElemItem<String>("Java Algo"));
            ltree.addChild(9, 12, 
                     new ElemItem<String>("Java Code"));
             // 调用前序、后序打印函数打印该树
            ltree.ford_print_tree();
            ltree.post_print_tree();
             // 删除节点8和节点12
            ltree.removeChild(8);
            ltree.removeChild(12);
             // 前序打印节点删除后的树
            System.out.println("前序打印节点8和12被删除后的树:");
            ltree.ford_print_tree();
        }
    }
  
复制代码

 

 该实例程序中首先创建一个树,其结构如下:

 

 

复制代码
17 节点, 前序遍历打印
|—— Root(1)
    |—— C:(2)
        |—— System(5)
        |—— Programme(6)
    |—— D:(3)
        |—— EBook(7)
            |—— Communication Network(13)
        |—— Game(8)
            |—— LianLianKan(14)
            |—— LianLianKan2(15)
            |—— LianLianKan3(16)
            |—— LianLianKan4(17)
    |—— E:(4)
        |—— Work(9)
            |—— Java Algo(11)
            |—— Java Code(12)
        |—— Paper(10)
17 节点,后序遍历打印:
        |—— System(5)
        |—— Programme(6)
    |—— C:(2)
            |—— Communication Network(13)
        |—— EBook(7)
            |—— LianLianKan(14)
            |—— LianLianKan2(15)
            |—— LianLianKan3(16)
            |—— LianLianKan4(17)
        |—— Game(8)
    |—— D:(3)
            |—— Java Algo(11)
            |—— Java Code(12)
        |—— Work(9)
        |—— Paper(10)
    |—— E:(4)
|—— Root(1)
复制代码

 

可见,前序打印的结果更符合树的自然观察次序,很类似于中的树结构。

本示例程序还示例了在树中删除节点的过程,包括叶节点12的删除和非叶节点8的删除,删除这两个节点后树的前序打印结果如下:

 

复制代码
前序打印节点8和12被删除后的树:
11 节点,前序遍历打印:
|—— Root(1)
    |—— C:(2)
        |—— System(5)
        |—— Programme(6)
    |—— D:(3)
        |—— EBook(7)
            |—— Communication Network(13)
    |—— E:(4)
        |—— Work(9)
            |—— Java Algo(11)
        |—— Paper(10)
复制代码

 

从删除节点后树中的总节点树可以发现在删除节点8的同时其子树中的所有子节点也被成功删除。

 

树的“左子节点/右兄弟节点”描述方法

 

    上面我们讨论了基于子节点表的树的描述方法。这种方法的关键思想是将树的所以节点保存在数组中,每一个节点与其它节点的连接关系用链表来描述。树的另一种描述方法是“左子节点/右兄弟节点”描述法。这种方法中,树中的节点同样保存在一个数组中,每个节点除了保存其父节点之外,只保存该节点的一个子节点和一个兄弟节点,每个节点的信息量比较均等,并且避免了链表结构的使用。

一下将“左子节点/右兄弟节点”描述方法简写为LsRs (Left son Right sibling)方法。LsRs的节点类中将包含五个私有变量,即节点的内容、节点在节点数组中序号、节点的父节点的序号、节点的左子节点序号以及节点的右兄弟节点序号。

   节点类的实现如下:

 

复制代码
package Tree;

import Element.ElemItem;

/**
    * 左子树-右兄弟树节点数据结构,
 
*/
public  class LcRsTNode{
     // 节点元素项
     private ElemItem elem;
     // 节点序号
     private  int idx;
     // 节点的父节点
     private  int parentNode;
     // 节点的左子节点
     private  int leftMostChildNode;
     // 节点的右兄弟节点
     private  int rightSiblingNode;
    
     // 无参数构造函数
     public LcRsTNode(){
        elem =  null;
        parentNode = -1;
        leftMostChildNode = -1;
        rightSiblingNode = -1;
    }
    
     // 有参数构造函数
     public LcRsTNode(ElemItem _elem, 
                     int _parentNode,
                     int _leftMtChild, 
                     int _rightSibling){
        elem = _elem;
        parentNode = _parentNode;
        leftMostChildNode = _leftMtChild;
        rightSiblingNode = _rightSibling;
    }
    
     // 设置节点的序号
     public  void setIdx( int _idx){
        idx = _idx;
    }
    
     // 获取节点的序号
     public  int getIdx(){
         return idx;
    }
    
     // 获取元素项
     public ElemItem getElem() {
         return elem;
    }
    
     // 设置元素项
     public  void setElem(ElemItem _elem) {
        elem = _elem;
    }
    
     // 获取节点的父节点
     public  int parent() {
         return parentNode;
    }
    
     // 获取节点的最左子节点
     public  int leftMostChild() {
         return leftMostChildNode;
    }

     // 获取节点的右兄弟节点
     public  int rightSibling() {
         return rightSiblingNode;
    }
    
     // 重设节点的父节点
     public  void setParent( int n) {
        parentNode = n;
    }
    
     // 重设节点的最左子节点
     public  void setLeftMostChild( int n) {
        leftMostChildNode = n;
    }
    
     // 重设节点的右兄弟节点
     public  void setNextSibling( int n) {
        rightSiblingNode = n;
    }
    
     // 节点是否是叶节点
     public  boolean isLeaf() {
         return leftMostChildNode == -1;
    }
    
     // 返回元素项String形式
     public String toString(){
         return elem.getElem().toString();
    }
}
复制代码

 

   由于节点之间连接关系的表示方法与子节点表描述法有所不同,对用LsRs法描述的树的操作方法也有所变化。

向树中插入新的节点时,同样需要指明带插入的节点在节点数组中的序号self_idx及其父节点的序号p_idx。如果父节点为空,则直接设置p_idx处节点的内容。否则,首先设置self_idx的内容,此时self_idx处节点的左子节点和右兄弟节点都为空,其父节点序号为p_idx。关键的步骤是将self_idx赋值给当前p_idx的最右子节点的右兄弟节点,即将self_idx设置为p_idx处节点的新的最右兄弟节点。这一过程需要迭代地寻找当前的最右兄弟节点。这一过程的实现复杂度较节点表描述法要高一些,因为在节点表描述法中p_idx处的最右子节点可以直接通过获取。

节点删除包含两个函数,removeChild和removeSon。在删除序号为nidx的节点时,首先要先删除该节点的所有子节点,然后再删除节点自身。在删除该节点的子节点时,需要首先找到该节点的最左子节点,然后递归调用删除节点的函数删除左子节点和它的右兄弟节点(及其子节点)。如果该节点没有子节点,即其最左子节点为空,则直接调用removeSon函数删除该节点。removeSon函数在删除节点nidx时,首先找到该节点的父节点p,如果nidx是p的最左子节点则将p的最左子节点重新置为nidx的右兄弟节点,并将当前树中节点数减一。如果nidx不是p的最左子节点,则需要先找到右兄弟节点为nidx的节点idx,并将idx的右兄弟节点重置为nidx的兄弟节点,同时将树中当前的节点数减一。

最后讨论一下树的这种实现方法下前序打印树中各个节点的函数,即preOrder函数。preOrder函数也是一个递归函数,该函数从高度为h的节点pos开始该打印,并递归地在适当的高度打印pos节点的子节点。该函数函数调用了函数printnode,printnode函数的功能是在特定的高度h打印节点的内容。在打印整个树的函数为ford_print_tree,该函数中将preOrder的输入初始节点序号置为根节点,高度置为0即可实现打印整棵树的功能。

下面用类似于“子节点表”法的示例树结构测试“左子节点/右兄弟节点”方法。实例代码如下:

 

 

复制代码
     package Tree;
    
     import Element.ElemItem;
    
     /**
     *
     * 左子节点/右兄弟节点描述法示例程序,
     * 包括树的创、节点删除和树的前序遍历打印
     
*/
     public  class ExampleLcRsTree {
         public  static  void main(String[] args){
            LcRsTree lrTree =  new LcRsTree(100);
            lrTree.addChild(0, 0, 
                     new ElemItem<String>("Root"));
            lrTree.addChild(0, 1, 
                     new ElemItem<String>("C:"));
            lrTree.addChild(0, 2, 
                     new ElemItem<String>("D:"));
            lrTree.addChild(0, 3, 
                     new ElemItem<String>("E:"));
            lrTree.addChild(1, 4, 
                     new ElemItem<String>("System"));
            lrTree.addChild(1, 5, 
                     new ElemItem<String>("Programme"));
            lrTree.addChild(2, 6, 
                     new ElemItem<String>("EBook"));
            lrTree.addChild(2, 7, 
                     new ElemItem<String>("Game"));
            lrTree.addChild(3, 8, 
                     new ElemItem<String>("Work"));
            lrTree.addChild(3, 9, 
                     new ElemItem<String>("Paper"));
            lrTree.addChild(8, 10, 
                     new ElemItem<String>("Java Algo"));
            lrTree.addChild(8, 11, 
                     new ElemItem<String>("Java Code"));
            lrTree.addChild(6, 12, 
                     new ElemItem<String>("Communication Network"));
            lrTree.addChild(7, 13, 
                     new ElemItem<String>("LianLianKan"));
            lrTree.addChild(7, 14, 
                     new ElemItem<String>("LianLianKan2"));
            lrTree.addChild(7, 15, 
                     new ElemItem<String>("LianLianKan3"));
            lrTree.addChild(7, 16, 
                     new ElemItem<String>("LianLianKan4"));
            System.out.println("整棵树的节点如下:");
            lrTree.ford_print_tree();
            System.out.println("删除节点二以及其子树后树的节点如下:");
            lrTree.removeChild(2);
            lrTree.ford_print_tree();
        }
    }
复制代码

 

该实例程序中,同样先调用addChild函数向树中添加节点,从而构建整个树。调用ford_print_tree函数打印整棵树。然后调用removeChild函数删除树中的第二个节点及其子树。实例代码的运行结果如下:

 

 

复制代码
整棵树的节点如下:
17 节点,前序遍历打印:
|—— Root(0)
    |—— C:(1)
        |—— System(4)
        |—— Programme(5)
    |—— D:(2)
        |—— EBook(6)
            |—— Communication Network(12)
        |—— Game(7)
            |—— LianLianKan(13)
            |—— LianLianKan2(14)
            |—— LianLianKan3(15)
            |—— LianLianKan4(16)
    |—— E:(3)
        |—— Work(8)
            |—— Java Algo(10)
            |—— Java Code(11)
        |—— Paper(9)
删除节点二以及其子树后树的节点如下:
9 节点,前序遍历打印:
|—— Root(0)
    |—— C:(1)
        |—— System(4)
        |—— Programme(5)
    |—— E:(3)
        |—— Work(8)
            |—— Java Algo(10)
            |—— Java Code(11)
        |—— Paper(9)
复制代码

 

 

从运行结果可以看出,树被前序打印出来,其显示方式符合我们对树结构的正常观察顺序。删除节点2后树中的节点数从原来的17个变为9个,减少的8个节点正好为节点2和其子树的节点个数。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值