树的遍历

遍历二叉树

在二叉树的一些应用中,常常要求在树中查找具有某种特征的结点,或者对树中全部结点逐一进行某种处理,这就提出了一个遍历二叉树的问题。即如何按某条搜索路径巡防树中每个结点,使得每个节点均被访问一次,而且仅被访问一次。 因为二叉树是由3个基本单元组成:根结点,左子树和右子树。因此,若能依次遍历这三个部分,便是遍历了整个二叉树。根据对这三个部分遍历顺序不同,那么我们就有了先序遍历,中序遍历,后序遍历。 先序遍历: 若二叉树为空则空操作(递归边界),否者: 1.访问根节点 2.先序遍历左子树 3.先序遍历右子树 中序遍历: 若二叉树为空则空操作,否者: 1.中序遍历左子树 2.访问根节点 3.中序遍历右子树 后序遍历: 若二叉树为空则空操作,否者: 1.后序遍历左子树 2.后序遍历右子树 3.访问根节点 这个过程明显是个递归的过程,《数据结构》给出了先序遍历的伪代码:

Status PreOrderTraverse( BiTree T, Status(*Visit)(ElemType) ) {
   // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数,
   // 先序遍历二叉树T的递归算法,对每个数据元素调用函数Visit。
   // 最简单的Visit函数是:
   //     Status PrintElement( ElemType e ) {  // 输出元素e的值
   //        printf( e );  // 实用时,加上格式串
   //        return OK;
   //     }
   // 调用实例:PreOrderTraverse(T, PrintElement);
   if (T) {
      if (Visit(T->data))
         if (PreOrderTraverse(T->lchild, Visit))
            if (PreOrderTraverse(T->rchild, Visit)) return OK;
      return ERROR;
   } else return OK;
} // PreOrderTraverse

对于上图的树(忽略他的线索线),如果我们采用不同的遍历方式,可以得到不同的结果: 先序遍历:-+ab-cd/ef 中序遍历:a+bc-d-e/f 后序遍历:abcd-*+ef/- 其实这就是前缀表达式(波兰式),中缀表达式,后缀表达式(逆波兰式)。 其实用树便可以实现前缀、中缀、后缀表达式相互转换,但是,一般我们采用栈的方式实现,至少不用建树。 仿照递归算法执行过程中递归工作栈的状态变化状态可直接写出相应的非递归算法。例如,从中序遍历递归算法执行过程中递归工作栈的状态可见: 1.工作记录中包含两项其一是递归调用的语句编号,其二是指向左子树根的指针进栈 2.若栈顶记录中的指针值为空,则应退至上一层,若是从左子树返回,则应访问当前层即栈顶记录中指针所指的根结点 3.若是从右子树返回,则表明当前层的遍历结束,应继续退栈。从另一角度,这意味着遍历右子树时不再需要保存当前层指针,直接修改栈顶记录中的指针就好。 非递归算法执行效率要比递归算法高些,有时候可以考虑。

Status InOrderTraverse(BiTree T, Status (*Visit)(ElemType)) {
  // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数。
  // 中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit。
  stack S;
  BiTree p;
  InitStack(S);  Push(S, T);  // 根指针进栈
  while (!StackEmpty(S)) {
    while (GetTop(S, p) && p) Push(S, p->lchild);  // 向左走到尽头
    Pop(S, p);                // 空指针退栈
    if (!StackEmpty(S)) {     // 访问结点,向右一步
      Pop(S, p);
      if (!Visit(p->data)) return ERROR;
      Push(S, p->rchild);
    }
  }
  return OK;
} // InOrderTraverse

Status InOrderTraverse(BiTree T, Status (*Visit)(ElemType)) {
  // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数。
  // 中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit。
  stack S;
  BiTree p;
  InitStack(S);  p = T;
  while (p || !StackEmpty(S)) {
    if (p) { Push(S, p);  p = p->lchild; }  // 非空指针进栈,继续左进
    else {       // 上层指针退栈,访问其所指结点,再向右进
      Pop(S, p);
      if (!Visit(p->data)) return ERROR;
      p = p->rchild;
    }
  }
  return OK;
} // InOrderTraverse

建立二叉树

遍历是二叉树各种操作的基础,可以在遍历过程中对结点进行各种操作,如:对一颗已知树可求结点的双亲,求结点的孩子结点,判断结点所在的层次等。 反之,也可以在遍历过程中生成结点,建立二叉树的存储结构。

BiTree CreateBiTree(BiTree &T) {
  // 按先序次序输入二叉树中结点的值(一个字符),空格字符表示空树,
  // 构造二叉链表表示的二叉树T。
  char ch;
  scanf("%c",&ch);
  if (ch=='#') T = NULL;
  else {
    if (!(T = (BiTNode *)malloc(sizeof(BiTNode)))) return ERROR;
    T->data = ch;              // 生成根结点
    CreateBiTree(T->lchild);   // 构造左子树
    CreateBiTree(T->rchild);   // 构造右子树
  }
  return T;
} // CreateBiTree

  对二叉树的遍历的搜索路径除了已经说的先序中序后序遍历,还可以从上到下(DFS),从左到右(BFS)按层次遍历。 但是遍历二叉树的算法中的基本操作时访问结点,则无论安那一种次序进行遍历,对含n个结点的二叉树,其时间复杂度均为O(n),所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n则空间复杂度也为O(n)。

回溯法与树的遍历

在程序设计中有一类求一组解或求全部解或求最优解的问题,比如八皇后问题,骑士周游问题,不是根据某种确定的计算法则,而是利用试探和回溯的搜索技术求解。 回溯法的实质是一个先序遍历一棵“状态树”的过程,只是这棵树不是遍历前预先建立的,而是隐含在遍历过程中的(边建立便遍历,随之释放)。 八皇后问题以后讨论,在数据结构中有一个很好玩的例题。

求含n个元素的集合的幂集 集合A的幂集是由集合A的所有子集所组合的集合。如:A={1,2,3},则A的幂集, p(A)={{1,2,3},{1,2},{1,3},{2,3},{1},{2},{3},∅}

但是这个答案肿么解出来,树中给出了一个树的思想,因为对每个元素只有舍或者留两种情况,那么,我们可以用二叉树,左支是留,右支是舍:

没有比代码更直观的了:

void GetPowerSet(int i, List A, List &B) {
   // 线性表A表示集合A,线性表B表示幂集ρ(A)的一个元素。
   // 局部量k为进入函数时表B的当前长度。
   // 第一次调用本函数时,B为空表,i=1。
   ElemType x;
   int k;
   if (i > ListLength(A)) Output(B); // 输出当前B值,即ρ(A)的一个元素
   else {
      GetElem(A, i, x);        k = ListLength(B);
      ListInsert(B, k+1, x);   GetPowerSet(i+1, A, B);
      ListDelete(B, k+1, x);   GetPowerSet(i+1, A, B);
   }
} // GetPowerSet

其实还有优化,比如加上状态压缩什么的。当然,这只是在竞赛中的小伎俩罢了。

2014/11/03 10:56 am posted in  算法&数据结构

线索二叉树概要

当用二叉链表作为二叉树的存储结构时,因为每个结点中只有指向其左、右儿子结点的指针,所以从任一结点出发只能直接找到该结点的左、右儿子。在一般情况下靠它无法直接找到该结点在某种遍历序下的前驱和后继结点。如果在每个结点中增加指向其前驱和后继结点的指针,将降低存储空间的效率。

我们可以证明:在n个结点的二叉链表中含有n+1个空指针。因为含n个结点的二叉链表中含有个指针,除了根结点,每个结点都有一个从父结点指向该结点的指针,因此一共使用了n-1个指针,所以在n个结点的二叉链表中含有n+1个空指针。

因此可以利用这些空指针,存放指向结点在某种遍历次序下的前驱和后继结点的指针。这种附加的指针称为线索,加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。为了区分一个结点的指针是指向其儿子的指针,还是指向其前驱或后继结点的线索,可在每个结点中增加两个线索标志。这样,线索二叉树结点类型定义为:

type
 TPosition=^thrNodeType;
 thrNodeType=record
              Label:LabelType;
              ltag,rtag:0..1;
              LeftChild,RightChild:TPosition;
             end;

线索二叉树

其中ltag为左线索标志,rtag为右线索标志。它们的含义是:

  • ltag=0,LeftChild是指向结点左儿子的指针;
  • ltag=1,LeftChild是指向结点前驱的左线索。
  • rtag=0,RightChild是指向结点右儿子的指针;
  • rtag=1,RihgtChild是指向结点后继的右线索。

例如图(a)是一棵中序线索二叉树,它的线索链表如图(b)所示。


(a)


(b)

图(b)中,在二叉树的线索链表上增加了一个头结点,其LeftChild指针指向二叉树的根结点,其RightChild指针指向中序遍历时的最后一个结点。另外,二叉树中依中序列表的第一个结点的LeftChild指针,和最后一个结点的RightChild指针都指向头结点。这就像为二叉树建立了一个双向线索链表,既可从第一个结点起,顺着后继进行遍历,也可从最后一个结点起顺着前驱进行遍历。

如何在线索二叉树中找结点的前驱和后继结点?

以图的中序线索二叉树为例。树中所有叶结点的右链是线索,因此叶结点的RightChild指向该结点的后继结点,如图中结点"b"的后继为结点""。当一个内部结点右线索标志为0时,其RightChild指针指向其右儿子,因此无法由RightChild得到其后继结点。然而,由中序遍历的定义可知,该结点的后继应是遍历其右子树时访问的第一个结点,即右子树中最左下的结点。例如在找结点""的后继时,首先沿右指针找到其右子树的根结点"-",然后沿其LeftChild指针往下直至其左线索标志为1的结点,即为其后继结点(在图中是结点"c")。类似地,在中序线索树中找结点的前驱结点的规律是:若该结点的左线索标志为1,则LeftChild为线索,直接指向其前驱结点,否则遍历左子树时最后访问的那个结点,即左子树中最右下的结点为其前驱结点。由此可知,若线索二叉树的高度为h,则在最坏情况下,可在O(h)时间内找到一个结点的前驱或后继结点。在对中序线索二叉树进行遍历时,无须像非线索树的遍历那样,利用递归引入栈来保存待访问的子树信息。

对一棵非线索二叉树以某种次序遍历使其变为一棵线索二叉树的过程称为二叉树的线索化。由于线索化的实质是将二叉链表中的空指针改为指向结点前驱或后继的线索,而一个结点的前驱或后继结点的信息只有在遍历时才能得到,因此线索化的过程即为在遍历过程中修改空指针的过程。为了记下遍历过程中访问结点的先后次序,可附设一个指针pre始终指向刚刚访问过的结点。当指针p指向当前访问的结点时,pre指向它的前驱。由此也可推知pre所指结点的后继为p所指的当前结点。这样就可在遍历过程中将二叉树线索化。对于找前驱和后继结点这二种运算而言,线索树优于非线索树。但线索树也有其缺点。在进行插人和删除操作时,线索树比非线索树的时间开销大。原因在于在线索树中进行插人和删除时,除了修改相应的指针外,还要修改相应的线索。

其实线索树是为了快速查询任意结点的前驱和后继而存在的。这也是它的优势。但是,如果删除插入频繁的话,反而不是好的选择。

这篇文章转载自HUST。

2014/11/01 15:48 pm posted in  算法&数据结构

二叉树

这一篇文章继续讲,二叉树是树的特殊,想要熟悉树的模型,就要从二叉树开始,而存储二叉树的二叉链表和三叉链表也是存储任意树的一般方式。

定义

二叉树是一种树型结构,它的特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。

而树的抽象数据类型:

ADT BinaryTree{

 //数据对象D:D是具有相同特性的数据元素的集合。
 //数据关系R:
 //  若D=Φ,则R=Φ,称BinaryTree为空二叉树;
 //  若D≠Φ,则R={H},H是如下二元关系;
 //    (1)在D中存在惟一的称为根的数据元素root,它在关系H下无前驱;
 //    (2)若D-{root}≠Φ,则存在D-{root}={D1,Dr},且D1∩Dr =Φ;
 //    (3)若D1≠Φ,则D1中存在惟一的元素x1,<root,x1>∈H,且存在D1
 //         上的关系H1 ⊆H;若Dr≠Φ,则Dr中存在惟一的元素xr,<root,xr>∈H,且存在上的关系Hr ⊆H;H={<root,x1>,<root,xr>,H1,Hr};
 //    (4)(D1,{H1})是一棵符合本定义的二叉树,称为根的左子树;(Dr,{Hr})是一棵符合本定义的二叉树,称为根的右子树。

基本操作:

 InitBiTree( &T )
     //操作结果:构造空二叉树T。

   DestroyBiTree( &T )
   //  初始条件:二叉树T已存在。
   //  操作结果:销毁二叉树T。

   CreateBiTree( &T, definition )
   //  初始条件:definition给出二叉树T的定义。
   //  操作结果:按definiton构造二叉树T。

   ClearBiTree( &T )
   //  初始条件:二叉树T存在。
   //  操作结果:将二叉树T清为空树。

   BiTreeEmpty( T )
   //  初始条件:二叉树T存在。
   //  操作结果:若T为空二叉树,则返回TRUE,否则返回FALSE。

   BiTreeDepth( T )
   //  初始条件:二叉树T存在。
   //  操作结果:返回T的深度。

   Root( T )
   //  初始条件:二叉树T存在。
   //  操作结果:返回T的根。

   Value( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:返回e的值。

   Assign( T, &e, value )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:结点e赋值为value。

   Parent( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:若e是T的非根结点,则返回它的双亲,否则返回“空”。

   LeftChild( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:返回e的左孩子。若e无左孩子,则返回“空”。

   RightChild( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:返回e的右孩子。若e无右孩子,则返回“空”。

   LeftSibling( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空”。

   RightSibling( T, e )
   //  初始条件:二叉树T存在,e是T中某个结点。
   //  操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空”。

   InsertChild( T, p, LR, c )
   //  初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空。
   //  操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点的原有左或右子树则成为c的右子树。

   DeleteChild( T, p, LR )
   //  初始条件:二叉树T存在,p指向T中某个结点,LR为0或1。
   //  操作结果:根据LR为0或1,删除T中p所指结点的左或右子树。

   PreOrderTraverse( T, visit() )
   //  初始条件:二叉树T存在,Visit是对结点操作的应用函数。
   //  操作结果:先序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。

   InOrderTraverse( T, visit() )
   //  初始条件:二叉树T存在,Visit是对结点操作的应用函数。
   //  操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。

   PostOrderTraverse( T, visit() )
   //  初始条件:二叉树T存在,Visit是对结点操作的应用函数。
   //  操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。

   LevelOrderTraverse( T, visit() )
   //  初始条件:二叉树T存在,Visit是对结点操作的应用函数。
   //  操作结果:层次遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。

}ADT BinaryTree
}

基本操作的实现:

//二叉树的结点结构体:

typedef struct{

    TElemType data;

    struct BiTNode *lchild,*rchild;

}BiTNode,*BiTree;

//二叉树的创建:

 /*******************************************/
 /*          按照前序遍历建立二叉树         */
 /*******************************************/
 Status CreateBiTree(BiTree &T)
 {
     //按先序次序输入二叉树中结点的值(一个字符),
     //空格字符表示空树,构造二叉链表表示的二叉树T。
     char ch;
     ch = getchar();//scanf("%c",&ch);
     if(ch == ' ')
         T = NULL;
     else
     {
         if(!(T = (BiTNode*)malloc(sizeof(BiTNode))))
             exit(OVERFLOW);
         T->data = ch;                //生成根结点
         CreateBiTree(T->lchild);     //构造左子树
         CreateBiTree(T->rchild);     //构造右子树
     }
     return OK;
 }

 /************************************************************/
 /*                  按层次顺序建立一棵二叉树 :队列         */
 /************************************************************/
 Status LevelCreateBiTree(BiTree &T)
 {
     BiTree p,s;//p指向父亲结点,s指向孩子结点
     Queue BiNodeQueue;
     char ch;
     ch=getchar();
     if(ch==' ')
     {
         return NULL;
     }
     T=(BiTNode*)malloc(sizeof(BiTNode)); //生成根结点
     T->data=ch;
     EnQueue(BiNodeQueue,T); //用队列实现层次遍历
     while(!BiNodeQueue.Empty())
     {
         DeQueue(BiNodeQueue,p);
         ch=getchar(); //为了简化操作,分别对左右子结点进行赋值。
         if(ch!=' ')//子树不空则进队列进行扩充。下同
         {
             s=(BiTNode*)malloc(sizeof(BiTNode));
             s->data=ch;
             p->lchild=s;
             EnQueue(BiNodeQueue,s);
         }
         else
         {
             p->lchild=NULL;
         }
         ch=getchar();
         if(ch!=' ')
         {
             s=(BiTNode*)malloc(sizeof(BiTNode));
             s->data=ch;
             p->rchild=s;
             EnQueue(BiNodeQueue,s);
         }
         else
         {
             p->rchild=NULL;
         }
     }
     return OK;
 }

//2.二叉树的前序遍历:

 /*******************************************/
 /*          按照前序递归遍历二叉树         */
 /*******************************************/
 Status PreOrderTraverse(BiTree T)
 {
     if(T)
     {
         printf("%d",T->data);
         PreOrderTraverse(T->lchild);
         PreOrderTraverse(T->rchild);
     }
     return OK;
 }

 /*******************************************/
 /*          按照前序非递归遍历二叉树:栈   */
 /*******************************************/
 Status PreOrderTraverse(BiTree T)
 {
     stack S;
     InitStack(S);
     BiTree p=T;  //p指向当前访问的结点
     while(p||!StackEmpty(S))
     {
         if(p)
         {
             printf("%c",p->data);
             Push(S,p);
             p=p->lchild;
         }
         else
         {
             Pop(S,p);
             p=p->rchild;
         }
     }
     return OK;
 }

 //3.二叉树的中序遍历:

 /*******************************************/
 /*          按照中序递归遍历二叉树         */
 /*******************************************/
 Status InOrderTraverse(BiTree T)
 {
     if(T)
     {
         InOrderTraverse(T->lchild);
         printf("%d",T->data);
         InOrderTraverse(T->rchild);
     }
     return OK;
 }

 /*******************************************/
 /*          按照中序非递归遍历二叉树       */
 /*******************************************/
 Status InOrderTraverse(BiTree T)
 {
     stack S;
     BiTree p;
     InitStack(S);  Push(S, T);
     while (!StackEmpty(S))
     {
         while (GetTop(S, p) && p)
             Push(S, p->lchild);   // 向左走到尽头
         Pop(S, p);                // 空指针退栈(叶子的左孩子)
         if (!StackEmpty(S))
         {   // 访问结点,向右一步
             Pop(S, p);
             printf("%d",p->data);   //当前根结点
             Push(S, p->rchild);
         }
     }
     return OK;
 }

 /*******************************************/
 /*          按照中序非递归遍历二叉树       */
 /*******************************************/
 Status InOrderTraverse(BiTree T)
 {
     stack S;
     InitStack(S);
     BiTree p=T;
     while (p || !StackEmpty(S))
     {
         if (p)
         {
             Push(S, p);
             p = p->lchild;
         }  // 非空指针进栈,继续左进
         else
         {  // 上层指针退栈,访问其所指结点,再向右进
             Pop(S, p);
             printf("%d",p->data);
             p = p->rchild;
         }
     }
     return OK;
 }

 //二叉树的后序遍历:
 /*******************************************/
 /*          按照后序递归遍历二叉树         */
 /*******************************************/
 Status PostOrderTraverse(BiTree T)
 {
     if(T)
     {
         PostOrderTraverse(T->lchild);
         PostOrderTraverse(T->rchild);
         printf("%d",T->data);
     }
     return OK;
 }

 /*******************************************/
 /*        按照后序非递归遍历二叉树         */
 /*******************************************/
 Status PostOrderTraverse(BiTree T)
 {
     stack S;
     InitStack(S);
     BiTree p=T,pre=NULL;
     while ( p || !StackEmpty(S))
     {
         if (p)
         {
             Push(S,p);
             p = p->left;
         }
         else
         {
             Pop(S,p);
             if (p->right!=NULL && pre!=p->right)
             { //pre指向上次访问的右结点,避免再次访问
                 p=p->right;
             }
             else
             {
                 printf("%d",p->data);
                 pre=p;
                 p=NULL;
             }
         }
     }
}

 /*******************************************/
 /*        按照后序非递归遍历二叉树         */
 /*******************************************/
 Status PostOrderTraverse(BiTree T)
 {
     BiTree p = T,last = NULL;
     stack S;
     InitStack(S);
     Push(S,p);
     while(!StackEmpty(S))
     {
         Pop(S,p);
         if(last == p->left || last == p->right)//左右子树已经访问完了,该访问根节点了
         {
             printf("%d",p->data);
             last = p;
         }
         else if(p->left || p->right) //左右子树未访问,当前节点入栈,左右节点入栈
         {
             Push(S,p);
             if(p->right)
                 Push(S,p->right);
             if(p->left)
                 Push(S,p->left);
         }
         else //当前节点为叶节点,访问
         {
             printf("%d",p->data);
             last = p;
         }
     }
 }

 //二叉树的层次遍历:
 /*******************************************/
 /*           按照层次遍历二叉树            */
 /*******************************************/
 void LevelOrderTraverse(BiTree T)
 {
     Queue BiNodeQueue;
     BiTree p=T;
     EnQueue(BiNodeQueue,p);
     while(!BiNodeQueue.Empty())
     {
         DeQueue(BiNodeQueue,p);
         if(p)
         {
             printf("%c",p->data);
             EnQueue(BiNodeQueue,p->lchild);
             EnQueue(BiNodeQueue,p->rchild);
         }
     }
 }

 //交换左右子树:

 /*******************************************/
 /*      递归法将二叉树的左右子树互换       */
 /*******************************************/
 void Exchange(BiTree T)
 {
     BiTree temp;
     if(T)
     {
         Exchange1(T->lchild);
         Exchange1(T->rchild);
         temp=T->lchild;
         T->lchild=T->rchild;
         T->rchild=temp;
     }
 }

 /*******************************************/
 /*      非递归法将二叉树的左右子树互换     */
 /*******************************************/
 void Exchange(BiTree T)
 {
     stack S;
     InitStack(S);
     BiTree p=T,temp;
     while(p||!StackEmpty(S))
     {
         if(p)
         {
             Push(S,p);
             temp=p->lchild;
             p->lchild=p->rchild;
             p->rchild=temp;
             p=p->lchild;
         }
         else
         {
             Pop(S,p);
             p->rchild;
         }
     }
 }

 //7.统计二叉树中叶子结点的数目:

 /*******************************************/
 /*        递归法求叶子结点个数             */
 /*******************************************/
 int LeavesNum(BiTree T)
 {
     if(T)
     {
         if(T->lchild==NULL&&T->rchild==NULL)
         {
             return 1;
         }
         return LeavesNum(T->lchild)+LeavesNum(T->rchild);
     }
     return 0;
 }

 /*******************************************/
 /*        非递归法求叶子结点个数           */
 /*******************************************/
 int LeavesNum(BiTree T)
 {
     int count=0;
     stack S;
     InitStack(S);
     BiTree p=T;
     while(p||!StackEmpty(S))
     {
         if(p)
         {
             Push(S,p);
             if(p->lchild==NULL&&p->rchild==NULL)
             {
                 count++;
             }
             p=p->lchild;
         }
         else
         {
             Pop(S,p);
             p=p->rchild;
         }
     }
     return count;
 }

 //8.求二叉树的深度:
 /**********************************************/
 /*                  求一棵树的高度            */
 /**********************************************/
 int Depth(BiTree T)
 {
     int  lh = rh =0 ;
     BiTree p=T;
     if(p==NULL)
     {
         return 0 ;
     }
     else
     {
         lh = Depth( p->lchild ) ;
         rh = Depth( p->rchild ) ;
         return ( lh > rh ? lh : rh ) + 1 ;
     }
  }

   //9.判断两棵树是否等价:
   /*******************************************************/
   /*                  判断两棵是否等价                   */
   /*******************************************************/
   int Is_equal( BiTree T1 , BiTree T2 )
 {
     int t=0;
     if(NULL == T1 && NULL == T2)
     {
         t=1;
     }
     else
     {
         if(NULL !=T1 &&NULL != T2 )
         {
             if(T1->data == T2->data)
             {
                 if(Is_equal(T1->lchild,T2->lchild))
                 {
                     t=Is_equal(T1->rchild,T2->rchild);
                 }
             }
         }
     }
     return t;
 }

 //10.查找某个信息是否在这棵树中
 /****************************************************/
 /*            查找某个信息是否在这棵树中            */
 /****************************************************/
 BiTree Locate(BiTree T,char x)
 {
     BiTree p=T;
     if(P==NULL)
         return NULL;
     else
     {
         if( P -> data == x )
             return P;
         else
         {
             p = Locate(P->lchild,x);
             if(p)
                 return p;
             else
                 return Locate(P->rchild,x);
         }
     }
 }

 //11.结点个数:
 /****************************************************/
 /*                  树的结点个数                    */
 /****************************************************/
 int  Num_Of_Node(BiTree t)
 {
     if(t==NULL)
         return 0 ;
     else
         return Num_Of_Node(t->lchild)+Num_Of_Node(t->rchild)+1;
 }

二叉树,或者说树都是一个递归的概念,他们的左右孩子一样还是二叉树。

在二叉树中,还有完全二叉树满二叉树的概念

一颗深度为k而且2^k - 1个结点的二叉树称为满二叉树(下图a便是满二叉树)。满二叉树的特点是每一层上的结点数都是最大结点数。


(a)

可以对满二叉树的结点进行连续编号,约定编号从根结点起,自上而下,自左而右。就此,可以引出完全二叉树的定义:

深度为k的,由n个结点的二叉树,当且仅当其每一个结点都与深度为k的慢二叉树中编号1至n的结点一一对应时,称之为完全二叉树(如下图b)。完全二叉树的特点是:


(b)

1.叶子结点只可能在层次最大的两层上出现

2.对任一结点,若其右分支下的子孙的最大层次为L,则其左下分支下的子孙的最大层次必为l或l+1。

一定要区分好满二叉树和完全二叉树。

在性质4、5将用到满二叉树和完全二叉树。

性质

1.在二叉树的第i层上至多有2^(i-1)个结点(i>=1)

2. 深度为k的二叉树至多有2^k - 1个结点(k >= 1)

3.对任何一颗二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1

4.具有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第一层到第[log2n]+1层,每层从左到右),则对任一结点i(11,则其双亲PARENT(i)是结点[i/2]

  • 如果2i>n。则结点i无左孩子(结点i为叶子结点);否者其左孩子LCHILD (i)是结点2i
  • 如果2i + 1 > n。则结点i无右孩子;否则其右孩子RCHILD(i)是结点2i + 1

(这个性质也方便了二叉树的线性存储)

存储结构

顺序存储结构

存储结构如名,按照顺序存储结构的的定义,用一组地址连续的存储单元依次自上而下,自左至右的存储完全二叉树上的结点元素,即将完全二叉树上的编号为i的结点元素存储在上述定义的一位数组中下标为i-1的分量中。

这种存储结构仅适用于完全二叉树,因为在最坏的情况下,一个深度为k ,且只有k个结点的单支树(树中不存在度为2的结点),却要使用长度为2^k - 1的一位数组。

(线段树就是用的这种存储结构,因为线段树分布比较均匀,虽然不是完全二叉树,)这种存储结构如果在树分布比较均匀的情况下,还是可以使用的,虽然特殊情况有些占用内存,但是操作十分简单,而且静态速度也快。

链式存储结构

要表示二叉树的链表中的结点至少包含3个域:数据域和左右指针域,有时为了方便还需要包含一个指向双亲的指针域。那么利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表。(三叉链表是为了方便寻找双亲结点而存在的,因为在包含n个结点的二叉链表中有n+1个空链域,为方便查找前驱后继,又引入线索链表的概念)。

上面讲的实现就是链式存储结构,这也是二叉树最基本最常用的存储结构。

在以后写树和森林的时候,再又特殊到一般总结树的存储方式。

2014/10/31 15:51 pm posted in  算法&数据结构

树的定义和基本术语

定义

树是n个结点的有限集。在任意一棵非空树中:

1.有且只有一个特定称为根的结点

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

和广义表一样,树的结构定义是一个递归 的定义,即在树的定义中又用到树的概念。

 基本术语

  1. 结点的度:一个结点含有的子树的个数称为该节点的度;
  2. 叶节点终端节点:度为零的节点称为叶节点;
  3. 非终端节点分支节点:度不为零的节点;
  4. 双亲节点:在含有孩子的节点中,这个节点称为孩子节点的双亲节点;
  5. 孩子节点:一个节点子树的根节点称为孩子节点;
  6. 兄弟节点:具有相同双亲节点的节点互称为兄弟节点;
  7. 树的度:一棵树中,最大的结点的度称为树的度;
  8. 结点的层次:从根开始定义起,根为第0层,根的孩子为第1层;
  9. 树的高度深度:树中节点的最大层次;
  10. 堂兄弟:双亲在同一层的节点互为堂兄弟;
  11. 节点的祖先:从根到该节点所经分支上的所有节点;
  12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
  13. 有序树:树中结点的各子树看成从左至右是有次序的(既不可互换),反之为无序树
  14. 森林:由m(m>=0)棵互不相交的树的集合称为森林;、

就逻辑结构而言,任何一棵树是一个二元组Tree(root,F),其中;root是数据元素,称作树的根结点;F是m(m>=0)棵树的森林,F=(T1,T2,....,Tm),其中Ti = (r,Fi)称作为根root 的第i棵子树;当m!=0时,在树根和其子树森林之间存在下列关系:

Rf = {|i = 1,2,...,m,m>0}。

这个定义将有助于得到森林和树与二叉树之间转换的递归定义。

2014/10/29 15:50 pm posted in  算法&数据结构

C++ 函数探幽

昨天刚制定好学习计划,今天的任务是C++第八章,╮(╯▽╰)╭C++拖得太久了,耽误了好多事情。

好不容易把引用变量之前的写完了,因为草稿保存故障,又要重写,真心伤不起。

第八章是第七章的继续,继讲了函数部分。如果说第七章讲的是C++与C语言函数部分的共同点和微妙差异的话那么第八章讲的就是C++函数的独特之处。C++较之C语言提供了许多新的函数特性,包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及函数模板。

内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的区别不在于编写方式,而在于C++编译器如何将它们组合到程序中的。As we all know,普通函数会在内存中占唯一的内存空间,每次调用函数的时候,CPU会终止当前运行的代码(并把当前运行地址保存到栈中),跳转到函数处继续执行,知道函数执行结束再跳转回来。然而跳转是需要时间的,尤其是跳转频繁的函数。因此,内联函数的目的就是把函数镶嵌到应当调用函数的代码块中,直接执行而不是跳转。虽然代价是消耗更多的内存空间来保存内联函数执行代码,但是却能节省下不少跳转时间。

也是因为内联函数节省的是跳转时间而且以内存作为代价,我们需要有选择的使用内联函数,如果执行函数代码时间比处理函数调用机制的时间长,则节省的时间将只占整个过程中很小的一部分,反之,如果代码执行时间很短,则时间优化很明显。我认为,对于调用频繁或代码短小的函数最好使用内联函数。

怎么使用内联函数

1.在函数声明前加上关键字inline

2.在函数定义前加上关键字inline

也就是说,如果想要使用内联函数,那么声明和定义前都需要加上关键字。当然,如果省略原型的函数,也就是声明加定义在一起的函数,写一次就够了。

总之,用不用内联函数是程序员的事,怎么实现内联函数是编译器的事。但是不是你认为使用了内联关键字就内联了,因为编译器不会总满足程序员的请求,如果编译器觉得函数代码过长或者内联函数调用了自己(内联函数不允许递归),因此不将其作为内联函数。当然这些标准的主动性在编译器。

因为内联函数和C语言的defin很像,但较之C的宏定义函数,还是内联函数更胜一筹,因为内联函数本来就是个函数,而define只不过是替换罢了,函数更容易查错和避免一些有歧义的语句。

引用变量

引用变量是C++新定义的一种复合类型。引用时已定义的变量的别名,我总感觉引用和指针差不多,只不过是对那些不先用*的安慰品。

其实引用最大的作用就是帮助函数传参,和指针类似,传递的实参没有被复制,而是直接访问原变量的内存空间,从而节省时间。(还有一个作用,像double在复制时也会有精度损失,引用传递double可以避免损失)。

创建引用变量,其实很简单,和指针类似

对于int a;

创建应用 int &b = a;

这时候b就可以和a一样使用了,他们两个控制的是同一个内存空间。

但是要注意,对新建一个引用,必须要在声明的时候把他定义,这一点有点像指针常量的声明。

也正是因为引用访问原变量的存储空间,在引用传参的时候,要和指针一样注意原数据的保护(不如,多多使用const)。

对于使用引用传参的函数,传递的必须是一个变量也不应是一个值(换句话说,就说传递的参数必须有个存储空间)。

比如 void fun(int &n)函数,传参fun(a + 3)是非法的。(有些老编译器只是警告,但是重新开辟了临时空间保存a+3的值)。

对于引用不匹配,编译器可能会开辟临时存储空间(和普通函数效果一样),但是开辟是有条件的。如果参数仅为const引用和满足一下条件时C++才允许这么做:

1.实参的类型不正确,但不是左值(不可以改变的常量)

2.实参的类型不正确但可以转换为正确的类型(比如int转double)。

但是,如果接受引用参数的函数的意图是修改作为参数传递的变量,那么创建临时变量将阻止这种意图的实现,因此此时要避免创建临时变量。

如果函数返回的是个引用,应该要注意,如果变量是在函数内定义的,如果返回这个变量的引用是危险的,因为这个变量在函数结束的时候便被释放了。而返回、访问一个被释放的变量的空间十分危险。

因为函数返回可以是引用,也就是可以修改的左值,那么 function(a,b) = c;这种语句是合法的。虽然高大上但有点费劲,避免这种语句的良策还是const返回类型。

C++有个有趣的现象,因为类可以继承,所以对基类的函数参数,既可以用基类,也可以用派生类。

如果需要显示小数模式则

setf(ios_base::showpoint); //将对象置于显示小数模式
precision(); //制定小数显示多少位

对于这些设置会一直保存,知道在修改回默认。

使用引用的原因有两个:

1.程序员能够修改调动函数中的数据对象

2.通过传递引用而不是整个数据对象可以提高程序运行速度

但要注意,对于本来就是传递地址的数据对象比如数组只能使用指针。

默认参数

默认参数是C++中十分炫酷的功能。默认参数指的是当函数调用中省略了实参是自动使用的一个值。

很吊大上的功能,使用起来也很方便。

char * left(const char *str,int n = 1);

当调用函数left的时候,如果只写了一个参数str,那么第二个参数默认n=1。

我认为很屌的功能但C++PP确认为并非编程方面的巨大突破,而只是提供了一种便捷的方式。在设计类时,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

对于使用函数默认参数,只有原形制定了默认值,而函数定义与没有默认参数时完全相同

因为默认参数调用时可以省略,导致如果你为一个参数设定默认值,你必须为其右边所有参数设定默认值!

函数重载

函数多态是C++在C语言基础上新增的功能。而函数多态能够使用多个同名函数。术语“多态”指的是有多种形式,因此函数多态允许函数可以有多种形式。类似的,术语“函数重载”指的是可以有多个同名函数,因此对名称进行了重载。这两个术语指的是同一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——让他们完成相同的工作,但使用不同的参数列表

函数重载的关键是函数的参数列表,也称之为函数特征标,所谓参数列表就是,函数的参数的数目和数据类型(不包括形参的变量名以及返回类型)。

对于重载不是完全匹配,编译器不会武断的停止匹配而是尝试使用标准类型转换强制进行匹配,比如,把int实参转化为double类型。但是,如果在不完全匹配的情况下,通过转换存在多种较为合理的解释,编译器便会保持。其实,不论是否非完全匹配,只要在函数重载中存在歧义,编译器都会报错

不止是在调用的时候,如果在声明了两个特征标相同的函数,编译器也是不允许的比如下面(不要把数据类型的引用和数据的类型本身会为一谈):

double cube(double x);

double cube(double &x);

不仅如此,特征标不区分是否是const和非const变量。因为正如前面,非const函数只能接受非const实参,而const函数能接受非const实参和const实参,因此使用范围有交集,也就是说有歧义。编译器不允许这么做。

重载引用参数存在特殊性。

void stove(double & r1);
void stove(const double & r2);
void stove(double && r3);

double x = 1.0;
const double y = 2.0;

stove(x);
stove(y);
stove(x + y);

对于第一个函数,要求一个可以修改的左值,对于第二个函数,要求一个不可修改的左值,最后一个左值引用参数r3将于一个左值匹配。

因此,x调用第一个函数,y调用第二个函数,x+y调用第三个函数。

但是,这并不是绝对的,因为,对于x+y,第二个函数也是可以行得通的(不可修改的左值)。因此,如果

void stove(double && r3);

不存在的话,编译器会自动匹配到

void stove(const double & r2);

这就是重载引用参数时,编译器会自动调用最匹配的版本。

函数重载虽然吸引人但也不要滥用,仅当函数基本上执行相同的任务,但是用不同的形式的数据时,才应采用函数重载。

如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,也可以考虑函数重载。

函数模板

函数模板也是C++新增特性,函数模板是通用的函数描述,也就是说它们使用泛型来定义函数,其中的泛型可用具体的类型如int,double替换。通过将类型转化为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型的方式编写程序,因此有时也被成为通用编程。由于累心是由参数表示的,因此模板特性有时也被成为参数化类型。

怎么使用函数模板

template
void Swap(AnyType &a,AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}

第一行指出,要建立一个模板,并将类型命名为AnyType,关键字template和typename是必须的。除非使用关键字class代替typename(在C++98添加typename之前,都是用class来创建模板的)。另外,必须使用尖括号,类型名可以任意选择(这里的AnyType)。

模板不会创建任何函数,只是告诉编译器如何定义函数,当调用实际参数的时候,编译器才会根据传递的参数类型自动生成函数。

如果声明和定义是分开的函数模板,声明部分和定义部分的前面都要加上template 以保证让编译器知道这是函数模板使用的数据类型。

注意:函数模板可能会缩短代码量但不会缩短可执行代码量,因为用不同的类型调用函数模板,会确确实实生成的不同函数,就像手工写了这两个函数一样。因此函数模板的本质就是给编译器一个自己写函数的模型。

函数模板的好处就是生成多个函数定义更简单更可靠。

更常见的形式是将模板放在头文件,并在需要时用的模板的文件中包含头文件。

需要对多种类型使用同一种算法时可以考虑函数模板,但是,并非所有的类型都使用相同的算法,因此,为了解决这个问题,可以像重载常规函数定义一样重载函数模板定义。但是同样要注意,要保证被重载的函数模板的特征标不同(在函数模板中,并非所有的参数为模板参数类型)。

函数模板的局限性

template
void f(T a,T b)
{
if(a > b)
a = b;
...
}

像这样的模板,如果a,b是普通的常规数据类型,也无大碍,但是如果a,b传入了数组,那么if比较的是两个地址没有意义,如果执行if里面的语句将是不合法的。

因此即使使用函数模板,但其中的小细节仍然需要注意语法。

对于上述也不是没有解决办法重载>和=运算符也可以实现。

显式具体化

如果想要交换两个值,传入的是两个结构,函数模板也能正常使用。但是我们传入两个结构,想交换其中两个结构的结构成员,那么我们就需要明确到底是什么结构的什么成员。这时候,单纯的函数模板就无法实现,我们需要显式具体化。

我们提供一个具体化函数定义,其中包含了所需代码,当编译器找到与函数匹配的具体化定义时,将使用该函数而不再寻找模板。

具体化方式有很多种,C++98采用了第三代具体化:

1.对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本

2.显示具体化的原型和定义应以template<>打头,并通过名称来指出类型

3.具体化优先于常规模板,而非模板函数优先于具体化和常规模板

如果交换job结构的示例:

//非函数模板
void Swap(job & a,job & b);

//常规函数模板
template
void Swap(T & a,T & b);

//显式具体化函数模板
template<> void Swap(job & a,job & b);

其中是可选的,因为函数的参数类型表明,这是job的一个具体化,也就是说可写可不写。但是,如果在参数类型中没有job,那么,写这个还是很关键的!

在使用显示具体化的时候和使用函数模板一样要声明定义都要加template<>

实例化

就像前面说的,模板不会生成函数,但可以调用模板之后,使模板实例化,从而生成实例函数,这被称为隐式实例化,早期的C++也只允许这么做,但现在C++允许显示实例化。这就意味着可以直接命令编译器创建特定的实例,耆域法师声明所需的种类用<>符号指示类型,并在声明前加上关键字template,如:

template void Swap(int ,int );

实现了这种特性的编译器看到上述声明之后将使用Swap模板生成一个使用int类型的实例。

当比较一下显式具体化:

template<> void Swap(job & a,job & b);
template<> void Swap(job & a,job & b);

还是有差别的,一定要注意,差别不仅仅是有无<>,更大的区别在于是否使用Swap模板创建函数,实例化是用Swap创建函数,而具体化是不使用模板创建一个新函数

实例化有什么用呢?

比如我们有int a,double b;

有个函数模板:

template
T add(T a,T b)
{
return a
}

int a = 1;
double b = 2;

cout (a,b)

这样就会强制使用double的实例。

隐式实例化,显示实例化,显式具体化统称具体化,他们的相同之处在于表示的都是适用具体类型的函数定义而不是通用描述。

最后,一定要注意,要区分使用template和template<>来使用显示实例化和显示具体化。

编译器选择使用哪个版本的函数

对于函数重载、函数模板和函数模板重载,C++需要有一个定义良好的策略来决定为函数调用使用哪个函数定义,尤其是有多个参数时,这个过程称为函数解析。过程:

1.创建候选函数列表,其中包括与被调用函数的名称相同的函数和函数模板

2.使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐形转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。

3.判断是否有最佳可行函数,否者报错。

那么,最后一步中的是否最佳的顺序如何确定呢?

1.完全匹配,但常规函数优先于模板

2.提升匹配,如(char,short提升为int后匹配)

3.标准转换,如(int转换为char,long转化为double)

4.用户定义的转换,如类声明中定义的转换。

C++允许某些无关紧要的匹配比如int匹配给const int,char[]和char *之类的。这意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配了。

然而,有时候即使两个函数都完全匹配了,然可完成重载解析,首先,直线非const数据的指针和引用优先于非const指针和引用参数匹配。然而,const和非const之间的区别值适用于指针和引用指向的数据(const的影响,更强调与指针和引用)。

对于函数只有模板,那么,更为具体的模板优先。

对于只有需要转换的函数,那么,转换最少的函数优先(更具体)。

其实我们可以手动选择,比如在调用函数模板的时候

add<>(a,b);

这时选择模板优先有常规函数。

对于多参数的函数情况更复杂,编译器必须考虑所有参数匹配的情况,什么是一个合适的函数呢?

1.其所有参数的匹配程度都必须不比其他函数差

2.同时至少有一个参数的匹配程度比其他函数都高

C++11给了模板函数更大的发展。主要的进步在于添加了decltype关键字,用来确定数据类型。

那么,这种模板就成为了可能:

template
void Add(T1 a,T2 b)
{
decltype(a + b) ans = a + b;
}

这个是在过程中出现不知类型的中间变量就使用decltype关键字,如果返回类型呢?

C++11允许返回值后置

template
auto Add(T1 a,T2 b)
{
decltype(a + b) ans = a + b;
return ans;
}

强大的自动类型auto啊!

编译器如何确定使用哪个函数是个十分复杂的机制,还需后续了解。

2014/10/22 01:07 am posted in  C++