数据结构——二叉树(一)

167 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

二叉树的概念和性质

二叉树的定义

二叉树(Binary Tree)是含有n(n≥0)个结点(node)的有限集合。当n = 0时称为空二叉树。在非空二叉树中:

  • 有且仅有一个称为根(root)的结点
  • 其余结点划分为两个互不相交的子集L和R,其中L和R也是一棵二叉树,分别称为左子树(left subtree)和右子树(right subtree),且其次序不能颠倒。(其余结点0个的话,划分为两个空集——两棵空子树)

二叉树的基本术语

  • 二叉树的结点包含一个数据元素及指向其左右子树的两个分支,分别称为左分支和右分支。
  • 结点的左、右子树的根称为该结点的左、右孩子,统称为孩子(children),相应地,该结点称为孩子的双亲(parent)
  • 同一个双亲的孩子之间可互称为兄弟(sibling)
  • 结点的孩子个数称为结点的度(degree)
  • 度为0的结点称为叶子结点(leaf)
  • 非叶子结点称为内部结点或分支结点(internal node)
  • 结点的层次(level): 从根结点开始定义,根为第1层,根的孩子为第2层,如此计数,直到该结点为止
  • 二叉树的深度(depth)或高度(height): 二叉树中结点的最大层次。

二叉树上的性质

  • 性质1 :在二叉树的第 i 层上至多有2^(i-1) 个结点。(i≥1)
  • 性质 2 :深度为 k 的二叉树上最多含 2^k-1 个结点(k≥1)
  • 性质 3:对于任意一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0 = n2+1

    •   两类特殊的二叉树

    • 满二叉树:深度为k且含有2k-1个结点的二叉树。(除最后一层外,每一层上的所有结点都有两个子结点)

    • 完全二叉树:二叉树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应

  • 性质 4:具有 n 个结点的完全二叉树的深度为 [log2n] +1。
  • 性质5: 对于含n个结点的完全二叉树中编号为i(1≤i≤n)的结点:

    • (1) 如果i = 1,则i结点是这棵完全二叉树的根,没有双亲;否则其双亲的编号为 [i/2] 。

    • (2)如果2i>n,则i结点没有左孩子;否则其左孩子的编号为2i。

    • (3)如果2i+1>n,则i结点没有右孩子;否则其右孩子的编号为2i+1。

二叉树的存储结构

顺序结构储存完全二叉树

可用一维数组存储完全二叉树(0号单元不用),结点的编号对应于结点的下标,且每个结点的双亲、左孩子和右孩子在数组中的下标均可根据性质5计算而得。

 //完全二叉树的顺序存储结构的类型定义
typedef char TElemType;    
   // 假设二叉树结点的元素类型为字符
typedef struct {
   TElemType *elem;     // 0号单元闲置
   int lastIndex;              // 二叉树最后一个结点的编号
} SqBiTree;    // 顺序存储的二叉树

链式存储结构

typedef struct  BiTNode {
    TElemType      data;     // 数据域
    struct BiTNode  *lchild,*rchild;  // 左、右孩子指针
} BiTNode,*BiTree;   // 二叉链表

创建二叉树

  • 分为两步:
    • 生成一个新结点t,其数据域的值为e
    • 创建一棵二叉树T,其中根结点为t,L和R分别作为左子树和右子树
BiTree MakeBiTree(TElemType e, BiTree L, BiTree R) {
     BiTree t;
     t = (BiTree)malloc(sizeof(BiTNode));
     if(NULL==t) return NULL;
     t->data = e;              // 根结点的值为e
     t->lchild = L;           // L作为t的左子树
     t->rchild = R;           // R作为t的右子树
     return t;
}

三叉链表:

typedef struct TriTNode {
    TElemType data; // 数据域
    TriTNode *parent, *lchild, *rchild;    
           // 双亲、左、右孩子指针
} TriTNode, *TriTree; //三叉链表

遍历二叉树

对“二叉树”而言,可以有三条搜索路径:

  • 先左(子树)后右(子树)的遍历;
  • 先右(子树)后左(子树)的遍历;
  • 先上后下的层次遍历。

限定先左后右,则有三种实现方案:先序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)

递归遍历:以中序为例

若二叉树不为空,则依次进行以下操作:

  1. 遍历根结点的左子树
  1. 访问根结点
  1. 遍历根结点的右子树
Status InOrderTraverse(BiTree T, Status (*visit)(TElemType e)) {
    if(NULL==T) return OK;
    if(ERROR==InOrderTraverse(T->lchild, visit))
        return ERROR;    // 递归遍历T的左子树
     if(ERROR==visit(T->data)) return ERROR;  // 访问结点的数据域
     return InOrderTraverse(T->rchild, visit);  // 递归遍历T的右子树
}

从三种遍历算法可以知道:如果将visit语句抹去,从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。

非递归遍历

可利用栈实现

  • 前序:根结点T先进栈,随后栈顶出栈,之后先后将T的右子树左子树压栈,周而复始重复操作,直到出栈结点没有左右子树

    • void PreOrder(BiTree T, Status (*visit)(TElemType))
      { 
        Stack s;
        InitStack(s);
        if (T)
          Push(s, T);
        while (StackEmpty(s)!=TRUE)
        {
          BiTree t;
          Pop(s, t);
          visit(t->data);
          if (t->rchild)
            Push(s, t->rchild);
          if (t->lchild)
            Push(s, t->lchild);
        }
      
      }
      
  • 中序:从根节点T开始一直往左下走,沿途结点入栈,返回最左下的结点p,若p结点的右孩子存在,则令p指向右孩子,然后向左走到底,并依次将指向沿途结点的指针入栈。否则判断栈是否为空?若非空则将保留在栈顶的指针退栈并赋给p,周而复始。

    • void InorderTraverse_I(BiTree T, Status(*visit) (TElemType e)) { 
          LStack S;    InitStack_LS(S);
          BiTree p;    p = GoFarLeft(T, S);  // 找到最左下的结点
          while(p!=NULL) {
             visit(p->data);  // 访问结点
             if(p->rchild!=NULL)  // 令p指向其右孩子为根的子树的最左下结点
                 p = GoFarLeft(p->rchild, S); 
             else if(StackEmpty_LS(S)!=TRUE) Pop_LS(S,p); // 栈不空时退栈
             else  p = NULL; // 栈空表明遍历结束
          } 
        } 
      
  • 后序:需要用到两个栈,一个操作栈一个收集栈,根结点T先进操作栈,随后操作栈栈顶出栈到收集栈,之后先后将T的左子树右子树压操作栈,周而复始重复操作,直到出栈结点没有左右子树,之后收集栈出栈到空

    • void PostOrder(BiTree T, Status (*visit)(TElemType))
      {   // Add your code here
        SElemType t,l,r;
        if(T==NULL) return;
        Stack s1,s2;//两个栈,分别是操作栈和收集栈
        InitStack(s1);
        InitStack(s2);
        t.ptr=T;
        Push(s1,t);
        while(!StackEmpty(s1)){
          Pop(s1,t);
          Push(s2,t);
          if(t.ptr->lchild!=NULL){
            l.ptr=t.ptr->lchild;
            Push(s1,l);
          }
          if(t.ptr->rchild!=NULL){
            r.ptr=t.ptr->rchild;
            Push(s1,r);
          }
        }
        while(!StackEmpty(s2)){    //遍历收集栈
          Pop(s2,t);
          visit(t.ptr->data);
        }
      
      }