“ 忠诚、笃学、严谨、守纪 ”

点击任意处进入

数据结构笔记3

4 树和二叉树

树的逻辑结构、定义和基本术语
二叉树的逻辑结构和基本性质
二叉树的存储结构及实现
二叉树的遍历操作及应用

线索二叉树
树的存储结构
树、森林与二叉树的转换
哈夫曼树及哈夫曼编码

4.1 树

树是一种十分重要的非线性结构(1->n)

4.1.1 树型结构

  • 树的定义

    树是由n (n >= 0)个结点组成的有限集合;
    如果n = 0,称为空树;如果n > 0,则有一个特定的结点称为根(root)结点
    除根以外的其它结点划分为m (m >=0)个互不相交的有限集合T0, T1, …, Tm-1,每个集合又是一棵树,并且称之为根的子树(subTree)。

1778026105943.png
  • 树上结点的关系描述

    树(Tree)是n(n>=0)个结点的有限集合。这些结点:
    (1)有且仅有一个结点没有直接前驱;该结点称为根(Root)
    (2)其他结点有且仅有一个直接前驱;
    (3)所有结点可以有m(m>=0)个直接后继;
    (4)除根结点外,其他结点都存在唯一一条从根到该结点的路径;
    (5)凡是无直接后继的结点称为叶结点

4.1.2 树的特点

  • 非线性结构:除根结点以外的每个结点可以有一个前驱,树上每个结点可以有多个后继结点

  • 递归性:树是递归结构,在树的定义中又用到树的概念(每棵子树又都是一树,完全符合树的定义)

1778026526562.png

术语 英文 定义说明
结点 node 数据元素 + 指向其子树的分支
分支(边) - 结点之间的连接关系
结点的度 degree 该结点拥有的子树个数
树的度 - 树中所有结点的度的最大值
根结点 root 树的最顶层结点,无父结点
叶结点 leaf 度为 0 的结点(终端结点)
父结点 parent 某结点的上层直接前驱结点
子结点 child 某结点的下层直接后继结点
兄弟结点 sibling 同一父结点下的子结点
祖先结点 ancestor 从根到该结点路径上的所有结点
子孙结点 descendant 某结点所有子树中的结点
分支结点 - 度不为 0 的结点(非终端结点)
结点的层次 level 根结点在第 1 层,其余结点层次 = 父结点层次 + 1
树的深度 depth 树中结点的最大层次数
结点的高度 height 该结点到其最远叶结点的路径长度,叶结点高度为 1
树的高度 height 根结点的高度(树中所有结点的最大高度)
路径和路径长度 - 路径:两结点间的结点序列;路径长度:路径上的边数
有序树 - 树中各子树的顺序不可交换
无序树 - 树中各子树的顺序可以交换
森林 - 由若干棵互不相交的树组成的集合

4.2 二叉树

4.2.1 定义

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上两棵互不相交的、分别称为左子树和右子树的二叉树组成。

1778027844185.png

(二叉树的五种形态)


4.2.2 二叉树的特点

  • 二叉树中每个结点最多有两棵子树,二叉树每个结点的度小于等于2

  • 左、右子树不能颠倒——有序树

  • 二叉树是递归结构,在二叉树的定义中又用到了二叉树的概念

4.2.3 二叉树的性质

  1. 在非空二叉树的第 i层上,最多有$ (2^{i-1})$ 个结点 ((i >=1))。

  2. 深度为k的二叉树中,最多有 \(2^{k}-1\)个结点(k >=1)。

  3. 对于任何一棵非空的二叉树, 如果其叶结点(度为0)个数为 n0,度数为2的结点数为 n2,则有 n0=n2+1。

n0+n1+n2=2*n2+n1+1

4.2.4 两个定义

  1. 满二叉树(Perfect Binary Tree)

    如果一棵二叉树的所有分支结点都存在左右子树且所有叶结点都在同一层上,则此二叉树称为满二叉树(完美二叉树)。

1778029711352.png
  • 满二叉树的性质:叶子结点的个数比分支结点个数多1
  1. 完全二叉树(Complete Binary Tree)

    若设二叉树的高为k,除第k层外,其它各层的结点数都达到最大个数,第k层从右向左连续缺若干结点,这就是完全二叉树。[叶结点只出现在倒数第1层和倒数第2层]

1778030013957.png
  • 完全二叉树的性质:

    1. 具有n个结点的完全二叉树的高度k为[log2n+1]

      1778030314351.png
    2. 对于有n个结点的完全二叉树,如果按从上到下、从左到右顺序对二叉树中所有结点从0开始到n-1进行编号0,1, 2, …, n-1,则对于任意结点i (0<=i <=n-1),有

    • 若i = 0, 则 该 i 结点是树根,它无父结点;如果i>0,则它的父结点在下标(i-1)/2;

    • 若2i +1<=n-1, 则下标为 i 的结点左孩子结点的下标为2i+1,否则下标为i的结点没有左孩子结点 ;

    • 若2i +2<=n-1, 则下标为 i的结点的右孩子结点的下标为2i+2,否则下标为i的结点没有右孩子结点。

4.2.5 二叉树的存储结构及实现

1778030941786.png

顺序存储定义

typedef struct SeqBinTree {
    int MAXNUM;
    int n;
    DataType *nodelist;
}*PSeqBinTree;

PSeqBinTree t;
1778205728653.png

链式存储定义

typedef struct BinTreeNode {
     DataType  info;
     struct BinTreeNode *Left;
     struct BinTreeNode *Right;
}*PBinTreeNode,*BinTree;

4.2.6 二叉树的遍历

遍历:按某种搜索路径访问二叉树的每个结点,而且每个结点仅被访问一次。
访问:对结点的各种操作,如输出结点中的数据,修改结点中的数据等。
遍历是各种数据结构最基本的操作,许多其他的操作可以在遍历上实现。

  • 二叉树由根、左子树、右子树三部分组成

  • 二叉树的遍历可以分解为:访问根,遍历左子树和遍历右子树

    L: 遍历左子树
    D: 访问根结点
    R: 遍历右子树

约定先左后右,有三种遍历方法:DLR,LDR,LRD,分别称为先序遍历、中序遍历、后序遍历

  • 基于递归
先序遍历算法
1778206945693.png 1778207150784.png
void PreOrder(BinTree t) {
  //采用二叉链表存储结构,这里访问就是输出数据元素
    if (t!=NULL) {                       //二叉树不为空
             printf(“%d”,  t -> info);  //访问根结点
             PreOrder ( t->Left);       //先序遍历T的左子树
             PreOrder( t->Right);       //先序遍历T的右子树
           }
} //PreOrder
中序遍历算法
1778207745704.png
void InOrder(BinTree t) {
  //采用二叉链表存储结构,这里的访问就是输出数据元素
    if (t!=NULL) {                    //二叉树不为空
         InOrder(t -> Left);         //中序遍历T的左子树
         printf(“%d”, t -> info);    //访问根结点
         InOrder(t -> Right);        //中序遍历T的右子树
         }
} //InOrder
后序遍历算法
1778208077942.png
void PostOrder(BinTree t) {
  //采用二叉链表存储结构
   if (t!=NULL) {                   //二叉树不为空
       PostOrder(t ->Left);         //后序遍历T的左子树
       PostOrder(t -> Right);      //后序遍历T的右子树
       Visit(t -> info);           //访问根结点
       }
} //PostOrder

1778208238305.png

  • 非递归

    • 中序非递归算法

      // 二叉树 中序遍历 非递归算法
      void InOrder(BinTree t) {
          BinTree bt;           // 工作指针,用来遍历节点
          Stack s = CreateStack();  // 创建一个空栈,用来保存走过的节点
          bt = t;               // 一开始指向根节点
      
          // 大循环:只要 当前节点不为空 或 栈不为空,就继续
          while (bt || !IsEmpty(s)) {
      
              // 内层循环:一路向左走到底,把经过的节点全部压栈
              while (bt) {
                  Push(s, bt);   // 把当前节点存进栈里
                  bt = bt->Left; // 继续往左走
              }
      
              // 左边走到空了,开始弹出节点
              bt = Pop(s);       // 弹出栈顶节点(最左节点)
              printf("%d", bt->info);  // 访问这个节点(中序核心)
      
              bt = bt->Right;    // 转向右子树,重复整个过程
          }
      }
      
    • 层序遍历算法

      // 层序遍历:一层一层访问二叉树
      void LevelOrder(BinTree t) {
          Queue q;        // 定义一个队列
          BinTree bt;     // 工作指针,保存当前取出的节点
      
          if (!t) return; // 如果是空树,直接返回
      
          q = CreatQueue();   // 创建一个空队列
          AddQ(q, t);         // 根节点先入队
      
          // 只要队列不为空,就继续循环
          while (!IsEmpty(q)) {  
              bt = DeleteQ(q);       // 出队一个节点
              printf("%d", bt->Data); // 访问(打印)这个节点
      
              // 如果有左孩子,左孩子入队
              if (bt->Left)  
                  AddQ(q, bt->Left);
      
              // 如果有右孩子,右孩子入队
              if (bt->Right)
                  AddQ(q, bt->Right);
          }
      }
      

4.2.7 二叉树遍历算法的应用

  1. 求二叉树叶结点个数

    输入:二叉树的二叉链表
    输出:二叉树的叶结点个数, 用全局变量n计数

    void leaf(BinTree BT) {
    //采用二叉链表存储结构,n是叶子结点个数计数变量
    //先序遍历二叉树T
     if (BT) {                                          //二叉树不为空
         if (BT -> Left==NULL && BT -> Right==NULL) n=n+1;//叶结点, 计数
         leaf (BT -> Left); 
         leaf( BT -> Right);
         } 
    } //leaf
    
  2. 查找数据元素Search(bt,x)

    BinTree Search(BinTree bt, DataType x) {
    //在bt为根结点指针的二叉树中查找数据元素x
    BinTree p;
    if (bt) {
       if (bt->Data==x) return bt;  //查找成功
       if (bt->Left!=NULL)   //在左子树查找x
          {  p=Search(bt->Left, x); if (p) return p;}
       if (bt->Right!=NULL)   //在右子树查找x
          {  p=Search(bt->Right, x); if (p) return p; }
    }//if
    return NULL;
    }
    
    • 求二叉树的深度——先序遍历法

      void BinTreeDepth(BinTree bt, int h, int *depth) {
      //h为bt指向的结点所在层次,bt指向二叉树的根,则h的初值为1;
      //depth指向单元为当前求得的最大层数,初值为0;
      if (bt) {
          if (h > *depth) *depth=h;
          BinTreeDepth(bt->Left, h+1, depth);
          BinTreeDepth(bt->Right, h+1, depth);
         }
      }//BinTreeDepth
      
    • 求二叉树的高度——后序遍历法

      int BinTreeDepth(BinTree BT) {
      //后序遍历求BT所指二叉树的高度
      //BT指向二叉树的根
      //hL为左子树高度,hR为右子树高度
      if (!BT) return 0;
      else {
          hL=BinTreeDepth(BT->Left);
          hR=BinTreeDepth(BT->Right);
          if (hL>=hR) return hL+1;
          else return hR+1;
         }
      }//BinTreeDepth
      

思考题

试问以下算法结构是什么?
求二叉树上度为1或为2的结点的个数。
查找二叉树上第一个值为x的结点。
在二叉树上求位于先序序列中第k个位置的结点的值。
求二叉树上以元素值为x的结点为根的子树的高度。
求二叉树中所有结点的左、右子树相互交换的二叉树。

4.2.8 表达式数及其遍历

1778630760522.png

按先序遍历、中序遍历、后序遍历如上表达式树

先序遍历序列:– + a * b – c d / e f 根结点在序列最前端
中序遍历序列:a + b * c – d – e / f 根结点划分左右子树
后序遍历序列:a b c d - * + e f / – 根结点在序列最末端
按层遍历序列:- + / a * e f b – c d 根结点在序列最前端

例 用二叉树表示算术表达式

1778630923154.png
遍历方式 遍历结果 别称 / 说明
先序遍历 + * * / A B C D E 前缀表示法(波兰式)
中序遍历 A / B * C * D + E 中缀表示法
后序遍历 A B / C * D * E + 后缀表示法(逆波兰式)

练习

1、图示出表达式(A-BC)(D+E/F)的二叉树表示。

2、将算术表达式((a+b)+c(d+e)+f)(g+h)转化为二叉树。

3、设a=6,b=4,c=2,d=3,e=2,则后缀表达式abc-/de*+的值为( )。

4.2.9 推导二叉树结构

已知结点的先序和中序序列分别为
先序序列:A B C D E F G
中序序列:C B E D A F G
试画出这棵二叉树。

1778631378706.png

若二叉树中各结点的值均不相同,则由二叉树的前序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树,但由前序序列和后序序列却不一定能唯一的确定一棵二叉树。

4.2.10 二叉树的创建(第二次测试:二叉链表操作)

编写算法生成二叉树结点,从而实现二叉树链表的创建。

输入:二叉树结点中的数据字符串,它们的顺序是二叉树的先序遍历结果。
1778632954491.png

输出:二叉树链表

BinTree createBinTree( )   //树的建立  
{    
   char ch;    
   BinTree t;    
   ch=getchar();            //输入二叉树数据  
   if(ch==' ')     t=NULL;    
   else                       //二叉树不为空 
   {    
        t=(BinTree)malloc(sizeof(struct BinTreeNode));  //生成根结点  
        t->data=ch;    
        t->Left=createBinTree();    
        t->Right=createBinTree();    
    }    
    return t;    
}

已有二叉树T:

void createBinTree(BinTree &T) {     //构造二叉树链表表示的二叉树T
    char ch; 
    ch=getchar( );
    if (ch==‘   ‘)  T=NULL;
    else {                                          //二叉树不为空
            T=(BinTree)malloc(sizeof(struct BinTreeNode));  //生成根结点
            T -> data =ch;
            createBinTree (T ->Left);       //构造左子树
            createBinTree (T ->Right);  //构造右子树
            } 
}

4.2.11 线索二叉树(不考)

二叉树链式存储特点:

用二叉链表法(l_child, r_child)存储 包含n个结点的二叉树,结点的指针区域中
会有\(n+1\)个空指针

对二叉树进行某种遍历之后,将得到一个线性 有序的序列。
例如对某二叉树的中序遍历结果是B D C E A F H G,意味着已将该树转为线性排列,显然其中结点具有唯一前驱和唯一后继。

二叉树中容易找到结点的左右孩子信息,但该结点在某一序列中的直接前驱和直接后继只能在某种遍历过程中动态获得。
先依遍历规则把每个结点某一序列中对应的前驱和后继线索预存起来,这叫做“线索化”。
意义:从任一结点出发都能快速找到其某一序列中前驱和后继,且不必借助堆栈。

ppt4:树和二叉树,p61


4.3 树、森林与二叉树的转换

4.3.1 树的存储结构

  • 双亲表示法

1778635555451.png1778635590505.png

#define MAX_TREE_SIZE 100
typedef struct TNode {//结点结构
    TElemType data;
    int parent;   //双亲位置域
}PTNode;
typedef struct PTree{  //树结构
    PTNode nodes[MAX_TREE_SIZE];
    int r,n;  //根的位置和结点数
}*PPTree;
  • 孩子表示法
1778635867123.png
typedef struct CTNode {   //孩子结点
   int child;
   struct CTNode *next;
}*ChildPtr;
typedef struct {
   TElemType data;
   ChildPtr firstchild;  //孩子链表头指针
}CTBox;
typedef struct CTree{
   CTBox nodes[MAX_TREE_SIZE];
   int n,r;    //结点数和根的位置
}*PCTree;

孩子兄弟表示法

1778635555451.png1778636112795.png

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

4.3.2 森林到二叉树的转换

1778636267231.png

练习:

后序列:DGJHEBIFCA, 中序列:DBGEHJACIF,
求:1、画出该二叉树;
2、画出该二叉树的先序线索树;
3、画出该二叉树对应的森林。

树型结构的实际问题:

计算机语言中子程序之间的调用结构;
计算机资源管理器中资源结构;
单位的组织结构;
面向对象高级语言的控件分类;
棋类游戏的状态变化图;
网络引擎搜索方法;

4.3.3 哈夫曼树

哈夫曼树(Huffman),又称为最优二叉树,是一类带权路径长度最短的树。

  • 路径:从树中一个结点到另一个结点之间的分支构成该两结点之间的路径。

  • 路径长度:路径上分支数目。

  • 树的路径长度:从树根到每个结点的路径长度之和。

<img src="https://free.picui.cn/free/2026/05/15/6a068381dab59.png" title="" a1778811829191.pnglt="1778811780985.png" width="249">1778811829191.png

实际问题

通信中如何对待传送的信息进行编码,可以减少对通信信道带宽的需要?

例如:某段待发送的信息中含有字母A, E, I, O, U, D, T, B,它们各自出现的概率是:0.05, 0.29, 0.07, 0.08, 0.14, 0.23, 0.03, 0.11,问如何对该8个字母编码,可以使得传送的信息编码最短?

ASCII 7位+1位,等长码
二进制码 3位

哈夫曼编码举例

如:要对字符A B C D编码,且当发送字符串’0000’时 ,请对字符串进行译电。

1778812321963.png

前缀码:长短不一,且任意一个字符的编码都不是另一个字符的编码的前缀。

设计电文总长度最短的二进制前缀码即为以n种字符出现的频率作权,设计一棵哈夫曼树的问题。由此得到的二进制前缀码称为哈夫曼编码。

最优二叉树定义
  • 给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。

  • 哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

哈夫曼树的特点

  1. 二叉树的WPL最小

  2. 一组数据的哈夫曼树不唯一

  3. 哈夫曼树上无度为1的结点

  4. 一棵有\(n\)个叶子节点的哈夫曼树共有\(2n-1\)个节点

举例:构造哈夫曼树

1778814059689.png

哈夫曼编码的编码步骤
  • 构造哈夫曼树

    • 从叶结点开始按构造哈夫曼树的算法构造哈夫曼树;(确立双亲、孩子关系,建立度为2的结点,填写双亲、孩子关系信息,完成从下到上的结点链接)
  • 编写哈夫曼码

    • 从根结点开始设二叉树的左子树编码为‘0’,右子树编码为‘1’,依次编码下去直到叶结点,然后从根到每个叶结点依次写出叶结点的编码——哈夫曼编码。(按照孩子信息完成从上到下的结点编码)
结点结构设计
  • 由于一棵n个叶结点的哈夫曼树共有2n-1个结点,故可以用一个大小为2n-1的一维数组存储哈夫曼树。具体数组元素的结构是:

    weight parent llink rlink

1778814622673.png

1778814826061.png

4.3.4 哈夫曼树的存储定义

//动态申请存储结点的空间
typedef struct HTreeNode{
    int weight;  //权值
    int parent, llink, rlink;
 }HTNode;     //Huffman树上结点结构
typedef struct HuffmanTree {
     int n;      //Huffman树上叶子个数
     int root;
     HTNode *ht;     //存放 2*n-1个结点
}HTree, *PHTree;    //Huffman树结构及其指针

哈夫曼算法

PHTree huffman(int n, int *w) 
{//w一组权值
    PHTree pht;
    int i, j, x1, x2, m1,m2;
    //申请huffman树存储空间
    pht=(PHTree)malloc (sizeof(struct HuffmanTree));
    if (pht==NULL) {printf(“no space\n”); return pht;}
    pht->ht=(HTNode *)malloc(sizeof(HTNode)*(2*n-1));
     if (pht->ht==NULL) {printf(“no space\n”); return pht;}
     //初始化huffman树存储的数组空间
     for(i=0;  i<2*n-1;  i++) {
            pht->ht[i].llink=pht->ht[i].rlink=pht->ht[i].parent=-1;
           if (i<n) pht->ht[i].weight=w[i]; else pht->ht[i].weight=-1;
    }   
    //构造Huffman树
   for (i=n;  i<2*n-1;  i++) {
      x1=x2=-1;
     SelectMin( pht, i, x1, x2);//选2个最小权不在huffman树上结点
     pht->ht[ x1 ].parent=i;  
     pht->ht [x2 ].parent=i;//填写父结点位置
     pht->ht[i].weight = pht->ht[x1].weight + pht->ht[x2].weight;
     pht->ht[i].llink=x1;
     pht->ht[i].rlink=x2;
  }
  pht->root=2*n-2;
  return pht;
}           

查找两个取值最小的无父结点的结点

void SelectMin( PHTree pht, int i, int &x1, int &x2) {
     m1=m2=MAXINT;//m1存放最小权值,m2次小权值
    for (j=0; j<i; j++)
      if (pht->ht[j].weight<m1 && pht->ht[j].parent==-1) 
         { m2=m1; x2=x1; m1=pht->ht[j].weight; x1=j; }
       else
           if (pht->ht[j].weight<m2 && pht->ht[j].parent==-1)
             { m2=pht->ht[j].weight; x2=j; }
    return ;
}   

哈夫曼编码算法实现

void HuffmanEncoding(PHTree pht,  HuffmanCode &HC)
{//根据哈夫曼树pht求哈夫曼编码表HC
   HC=(HuffmanCode)malloc(n*sizeof(char *));//分配n个字符编码的头指针向量
   cd=(char *)malloc(n*sizeof(char));//分配求编码的工作空间
   cd[n-1]=‘\0’;                                 //编码结束符
   for (i=0; i<n; ++i) {//对n个叶子结点编码
     start=n-1;
     for (c=i, f=pht->ht[i].parent; f!=-1; c=f, f=pht->ht[f].parent)//从叶到根逆编码
           if (pht->ht[ f ].llink==c) cd[--start]=‘0’;//为左子树结点
           else cd[--start]=‘1’; //为右子树结点
     HC[ i ]=(char *)malloc((n-start)*sizeof(char));//为第i个字符编码分配空间
     strcpy(HC[ i ], &cd[start]);              //从cd复制编码串到HC
    }
   free(cd);                                      //释放工作空间
}//HuffmanEnCoding
posted @ 2026-05-06 08:48  alonep  阅读(17)  评论(0)    收藏  举报