Tree


Tree

基本概念:子树,度,叶子(终端节点),分支节点(非终端节点),孩子,双亲,兄弟,子孙……
节点层次:注意从根开始定义,根为第一层;
树的深度(高度):树中节点的最大层次;
以及如节点各子树有次序,称其为有序树,否则为无序树
森林为n棵互不相交的树的集合;

二叉树

二叉树是一种树形结构,各个节点至多只有两棵子树,且各子树有次序,为有序树;

存储结构

  • 顺序存储结构
    #define MAX_TREE_SIZE 100                   //二叉树最大节点数
    typedef TElemType SqBiTree[MAX_TREE_SIZE];  //0号存储根节点
    SqBiTree bt;
  • 链式存储结构
    typedef struct BiTNode
    {
      TElemType data;
      struct BiTNode *lchild, *rchild;
    }BiTNode, *BiTree;

遍历二叉树及线索二叉树

遍历二叉树

首先标明三种概念,先序遍历中序遍历,以及后序遍历。实际上,其分别对应于波兰式,中缀表达式,逆波兰式;

  • 先序:根->左->右
  • 中序:左->根->右
  • 后序:左->右->根

可以均从左向右历遍,根据经过该节点的次数判断是否遍历,首先经过,即先序,历遍该节点的左子树后,再次经过该节点,即中序,再历遍右子树后返回该节点,即后序;

递归算法

以先序遍历为例,如下:

Status PreOrderTraverse(BiTree T, Status (*Visit)(TElemType e))
{
  if (T)
  {
    if (T->data)
    {
      if (PreOrderTraverse(T->lchild, Visit))
      {
        if (PreOrderTraverse(T->rchild, Visit))
          return OK;
      }
    }
    return ERROR;
  }
  else
    return OK;
}

中序遍历以及后序遍历即将根节点的访问置于中间或最后,在此不做赘述;

非递归算法

  • 中序遍历的非递归算法
    Status InOrderTraverse(BiTree T, Status (*Visit)(TElemType e))
    {
      SqStack *S = (SqStack *)malloc(sizeof(SqStack));
      InitStack(S);
      Push(S, T);
      while (!StackEmpty(*S))
      {
        BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
        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;
    }
    另一种形式如下:
    Status InOrder_Traverse(BiTree T, Status (*Visit)(TElemType e))
    {
      SqStack *S = (SqStack *)malloc(sizeof(SqStack));
      InitStack(S);
      BiTNode *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;
    }
    两种方法均在于在于每次向左历遍后退栈空指针,进行中序遍历访问,再向右一步进行继续历遍;
  • 先序遍历的非递归算法

相较于中序遍历,先序遍历的非递归算法仅仅改变了访问节点的时刻;

Status PreOrder_Traverse(BiTree T, Status (*Visit)(TElemType e))
{
  SqStack *S = (SqStack *)malloc(sizeof(SqStack));
  InitStack(S);
  BiTNode *p = T;
  while (p || !StackEmpty(*S))
  {
    if (p)
    {
      if (!Visit(p->data))
        return ERROR;
      Push(S, p);
      p = p->lchild;
    }
    else
    {
      Pop(S, p);
      p = p->rchild;
    }
  }
  return OK;
}
  • 后序遍历的非递归算法

其中主要是根据右子树以及是否访问过进行出栈访问的判断,要注意存在一个r标志,表示该节点已被访问过,相当于该节点的父节点的右子树为空;

Status PostOrder_Traverse(BiTree T, Status (*Visit)(TElemType e))
{
  SqStack *S = (SqStack *)malloc(sizeof(SqStack));
  InitStack(S);
  BiTNode *p = T, *r = NULL;
  while (p || !StackEmpty(*S))
  {
    if (p)
    { // 同样先遍历左子树
      Push(S, p);
      p = p->lchild;
    }
    else
    { // 先不出栈,根据栈顶元素的右子树情况进行判断
      GetTop(*S, p);
      if (p->rchild && p->rchild != r)
      { // 如果右子树存在,并且未被访问过
        p = p->rchild;
        Push(S, p);
        p = p->lchild;
      }
      else
      { // 否则便可以直接出栈并访问
        Pop(S, p);
        if (!Visit(p->data))
          return ERROR;
        r = p;    // 标记该节点已被访问,便于其父节点判断
        p = NULL; // 修改p,直接跳入对其父节点判断
      }
    }
  }
  return OK;
}

层次遍历

对于层次遍历,需要借助一个队列,先将二叉树根节点入队,然后出队访问,将其左右子树根节点入队,循环以上过程,直至队列为空……

Status LevelOrder(BiTree T)
{
  SqQueue *Q = (SqQueue *)malloc(sizeof(SqQueue));
  InitQueue(Q);
  BiTree p;
  EnQueue(Q, T);
  while (!(Q->front == Q->rear))
  { //若队列不空
    DeQueue(Q, p);
    Visit(p);
    if (p->lchild != NULL)
      EnQueue(Q, p->lchild);  //左子树
    if (p->rchild != NULL)
      EnQueue(Q, p->rchild);  //右子树
  }
  return OK;
}

构造二叉树

  • 先序序列和中序序列确定二叉树

    在中序序列中找到依次找到先序序列中元素作为根节点,则该节点左侧所有元素即属于左子树,同理,右侧均属于右子树;

  • 后序序列和中序序列确定二叉树

    同样是分割中序序列,由后序序列的最后一个节点如同先序序列的第一个节点,构造二叉树;

  • 层序序列和中序序列确定二叉树

    由层序序列同样可以得到各子树根节点,由此分割中序序列得到二叉树;

线索二叉树

试作如下规定:若节点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱;若节点有右子树,则其rchild域指示其右孩子,否则令rchild域指示其后继。
根据此改变节点结构,增加两个标志域如下:

$$LTag = \begin{cases}
0,lchild域指示节点的左孩子
\1,lchild域指示节点的前驱
\end{cases}$$
$$LTag = \begin{cases}
0,rchild域指示节点的右孩子
\1,lrchild域指示节点的后继
\end{cases}$$

以此节点结构构成的二叉链表作为二叉树的存储结构,称其为线索链表,指向节点前驱和后级的指针称为线索,加上线索的二叉树称之为线索二叉树,对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化

存储表示

typedef enum PointerTag
{
  Link,
  Thread
};
typedef struct BiThrNode
{
  TElemType data;
  struct BiThrNode *lchild, *rchild;
  PointerTag LTag, RTag; // 左右标志
} BiThrNode, *BiThrTree;

同时仿照线性表的存储结构,在二叉树线索链表上添加一个头结点,其lchild域的指针指向二叉树的根节点,其rchild域的指针指向中序遍历时访问的最后一个节点;同时中序序列中的第一个节点的lchild域指针和最后一个节点的rchild域指针均指向头结点;

线索二叉树的遍历

  • 中序遍历二叉线索树
Status InOrderTraverse_Thr(BiThrTree T, Status (*Visit)(TElemType))
{ // 中序遍历二叉线索树T的非递归算法
  BiThrNode *p = T->lchild; // T为头结点,p指向树的根节点
  while (p != T)
  {
    while (p->LTag == Link)
      p = p->lchild;
    if (!Visit(p->data))
      return ERROR;
    while (p->RTag == Thread && p->rchild != T)
    { //访问后继节点,直到存在右子树的节点或指向头指针的节点
      p = p->rchild;
      Visit(p->data);
    }
    p = p->rchild;    //对其右子树再次中序遍历
  }
  return OK;
}

二叉树的线索化

  • 中序遍历二叉树,并将其中序线索化
Status InOrderThreading(BiThrTree *Thrt, BiThrTree T)
{
  if (!(Thrt = (BiThrTree *)malloc(sizeof(BiThrTree))))
    return ERROR;
  (*Thrt)->LTag = Link;
  (*Thrt)->RTag = Thread; // 建立头结点
  (*Thrt)->rchild = Thrt; // 右指针回指
  if (!T)
    (*Thrt)->lchild = Thrt; // 若二叉树空则左指针同样回指
  else
  {
    (*Thrt)->lchild = T;
    BiThrNode *pre = Thrt;
    InThreading(T, pre);
    pre->rchild = Thrt;
    pre->RTag = Thread;
    (*Thrt)->rchild = pre;
  }
  return OK;
}

void InThreading(BiThrTree p, BiThrNode *pre)
{
  if (p)
  {
    InThreading(p->lchild, pre); // 左子树线索化
    if (!p->lchild)
    { // 前驱线索
      p->LTag = Thread;
      p->lchild = pre;
    }
    if (!pre->rchild)
    { // 后继线索
      pre->RTag = Thread;
      pre->rchild = p;
    }
    pre = p;     //保持前驱更新
    InThreading(p->rchild, pre); // 右子树线索化
  }
}

其中InThreading算法类似于中序遍历的递归算法,仅将访问节点改为了实现线索化;

  • 先序和后序线索化二叉树
    类似于中序线索化二叉树,将递归算法中访问功能更改为线索化实现,即可得到相应的线索化算法;

树的存储结构

如下有三种常用的链表结构:

双亲表示法

#define MAX_TREE_SIZE 100
typedef struct PTNode
{ // 节点结构
  TElemType data;
  int parent; // 双亲位置域
} PTNode;
typedef struct
{
  PTNode nodes[MAX_TREE_SIZE];
  int r, n; // 根的位置和节点数
} PTree;

这种结构利用了各节点(除根外)仅有一个双亲的性质;根据此性质便于实现对于父节点的查找,但是要求节点的孩子时则需要遍历整个结构;

孩子表示法

typedef struct CTNode
{ // 孩子节点
  int child;
  struct CTNode *next;
} *ChildPtr;
typedef struct
{
  TElemType data;
  ChildPtr firstchild; // 孩子链表头指针
} CTBox;
typedef struct
{
  CTBox nodes[MAX_TREE_SIZE];
  int n, r; // 节点数和根的位置
} CTree;

类似于双亲表示法,孩子表示法便于实现关于孩子的操作,考虑到两种方式的优劣性,故可拓展为带双亲的孩子链表;

孩子兄弟表示法

或称二叉树表示法,二叉链表表示法

typedef struct CSNode
{
  TElemType data;
  struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;

树&森林&二叉树

森林与二叉树的转换

由上述的二叉链表表示法可导出树与二叉树之间的一个对应关系,由此引出如下的转换方式:
森林->二叉树

  • 在兄弟节点之间加一条线(各树根可视作兄弟关系);
  • 对于每个节点,只保留其与第一个孩子的连线,其与抹除;
  • 以树根为轴心,顺时针旋转45度;

二叉树->森林

类似于如上的逆过程……

树和森林的遍历

树的遍历

  • 先根遍历:先访问树的根节点,然后一次先根遍历根的每棵子树;(类似先序)
  • 后根遍历:先依次后根遍历每棵子树,然后访问根节点;(类似中序)

森林的遍历

  • 先序遍历森林:第一棵树的根节点->先序遍历第一棵树中根节点的子树森林->先序遍历除第一棵树所构成的森林;
  • 后序遍历森林:第一棵树的根节点的子树森林->访问第一棵树的根节点->后序遍历除第一棵树所构成的森林;

树与等价问题

Huffman树

概念

考虑到带权路径长度的概念,引出如下的huffman树最优二叉树

  • 树的路径长度是从树根到每一节点的路径长度之和;
  • 树的带权路径长度为树中所有叶子节点的带权路径长度之和,记作
    $$ WPL = \sum_{k=1}^{n}w_{k}l_{k} $$

Huffman编码

根据huffman树的原理使得二进制前缀编码的权重和最小,有如下实现:

存储表示

#define OK 1
#define ERROR -1
#define MAX_FILENAME 100 // 文件名最大长度
#define MAX_NUM 9999999
// 统计字符频度的临时结点
typedef struct
{
  unsigned char str;   // 存储该节点对应字符
  unsigned int weight; // 该字符频度
} StrNode;

// 哈夫曼树结点
typedef struct
{
  unsigned char str;          // 以8bits为单元的无符号字符
  unsigned int weight;        // 每类(以二进制编码区分)字符出现频度
  char *code;                 // 字符对应的哈夫曼编码(动态分配存储空间)
  int parent, lchild, rchild; // 定义双亲和左右孩子
} HufNode, *HufTree;

算法实现

// 选择最小的两个结点
void select(HufNode *huftree, unsigned int n, int *s1, int *s2)
{
  unsigned int i;
  unsigned int min = MAX_NUM; // 初始化最小值
  for (i = 0; i < n; ++i)
    if (huftree[i].parent == 0 && huftree[i].weight < min)
    {
      // 选择父节点未被标记以及权重最小的节点
      min = huftree[i].weight;
      *s1 = i;
    }
  huftree[*s1].parent = 1; // 标记该节点已有父节点
  min = MAX_NUM;           // 重新初始化便于寻找
  for (i = 0; i < n; ++i)
    if (huftree[i].parent == 0 && huftree[i].weight < min)
    {
      // 选择父节点未被标记以及权重第二小的节点
      min = huftree[i].weight;
      *s2 = i;
    }
}

// 建立赫夫曼树以及赫夫曼编码
void HuffmanCoding(HufNode *huftree, unsigned int kinds)
{
  unsigned int i;
  int s1, s2;
  for (i = kinds; i < 2 * kinds - 1; i++)
  { // 建立Huffman树
    select(huftree, i, &s1, &s2);
    // 找到最小的两个节点
    huftree[s1].parent = huftree[s2].parent = i;
    huftree[i].lchild = s1;
    huftree[i].rchild = s2;
    huftree[i].weight = huftree[s1].weight + huftree[s2].weight;
  }

  // 构建Huffman编码
  int c, f, start;
  char *cd = (char *)malloc(kinds * sizeof(char)); // 存储编码kinds种
  cd[kinds - 1] = '\0';                            // 编码结束符

  for (i = 0; i < kinds; ++i)
  { // 逐个字符求其对应的Huffman编码

    start = kinds - 1;                                                       // 编码结束符位置
    for (c = i, f = huftree[i].parent; f != 0; c = f, f = huftree[f].parent) // 从叶子到根逆向求编码
      if (huftree[f].lchild == c)
        cd[--start] = '0'; // 左子树为0
      else
        cd[--start] = '1'; // 右子树为1

    huftree[i].code = (char *)malloc((kinds - start) * sizeof(char)); // 为第i个字符编码分配存储空间
    strcpy(huftree[i].code, &cd[start]);                              // 从cd复制编码串到编码域
  }
  free(cd); // 释放工作空间
}

二叉排序树

或是空树,或满足下列条件:

  • 若它的左子树不空,则左子树所有节点的值均小于它根节点的值;
  • 若它的右子树不空,则右子树所有节点的值均大于它根节点的值;
  • 它的左右子树也分别为二叉排序树;

其类型定义可类似于二叉链表如下:

typedef struct BiTNode
{
  int data;
  struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

SearchBST

BiTree SearchBST(BiTree T, keyType key)
{ //二叉排序树查找算法
  if ((!T) || key == T->data)
    return T;
  else if (key < T->data) // 关键字在左子树上
    return SearchBST(T->lchild, key);
  else
    return SearchBST(T->rchild, key);
}

InsertBST

在原有的查找算法中进行改进,用指针p返回访问时的最后的一个节点,便于最后进行插入;

Status SearchBST(BiTree T, keyType key, BiTree f, BiTree *p)
{ // 二叉排序树查找算法,p指向访问路径的最后一个节点
  if (!T)
  {
    p = f;
    return ERROR;
  }
  else if (key == T->data)
  {
    p = T;
    return OK;
  }
  else if (key < T->data)
    return SearchBST(T->lchild, key, T, p);
  else
    return SearchBST(T->rchild, key, T, p);
}

Status InsertBST(BiTree *T, int e)
{ // 当二叉排序树中不存在关键字为e的数据元素是,插入并返回OK
  BiTree p = (BiTree)malloc(sizeof(BiTNode));
  if (!SearchBST(T, e, NULL, &p))
  {
    BiTree s = (BiTree)malloc(sizeof(BiTNode));
    s->data = e;
    s->lchild = s->rchild = NULL;
    // 此时根据查找时访问的最后一个节点插入
    if (!p)
      *T = s;
    else if (e < p->data)
      p->lchild = s;
    else
      p->rchild = s;
    return OK;
  }
  else
    return ERROR;
}

DeleteBST

其中Delete函数实现对于节点的删除以及左右子树的重连;

Status DeleteBST(BiTree *T, keyType key)
{ // 删除关键字为key元素
  if (!*T)
    return ERROR;
  else
  {
    if (key == (*T)->data) // 进行删除
      return Delete(T);
    else if (key < (*T)->data) // 对左子树进行查找
      return DeleteBST((*T)->lchild, key);
    else
      return DeleteBST((*T)->rchild, key);
  }
  return OK;
}

Status Delete(BiTree *p)
{ // 删除节点p,并重连它的左右子树
  BiTree q = (BiTree *)malloc(sizeof(BiTree));
  BiTree s = (BiTree *)malloc(sizeof(BiTree));
  if (!(*p)->rchild)
  { // 重接左子树
    q = *p;
    *p = (*p)->lchild;
    free(q);
  }
  else if (!(*p)->lchild)
  { // 重接右子树
    q = *p;
    *p = (*p)->rchild;
    free(q);
  }
  else
  {
    q = *p;
    s = (*p)->lchild; // 先向左
    while (s->rchild)
    { // 再向右到尽头,找到待删节点的前驱
      q = s;
      s = s->rchild;
    }
    (*p)->data = s->data; // 将待删节点的值换为其前驱的值
    //分别重接q的左右子树
    if (q != *p)
      q->rchild = s->lchild;
    else
      q->lchild = s->lchild;
    delete (s); //删除前驱结点
  }
  return OK;
}

平衡二叉树

平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1;则显然,其要么是一颗空树,要么它的左右子树均是平衡二叉树,且深度之差绝对值不超过1;同时我们将二叉树上节点的左子树深度减去右子树深度的值称为**平衡因子BF(Balance Factor)**,则所有节点的平衡因子只可能是-1,0,1;

平衡二叉树的插入

平衡二叉树的查找

B-树和B+树

键树

未完待续……


文章作者: HOUCQ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 HOUCQ !
评论
  目录