树和二叉树

树和二叉树


文章目录

  • 树和二叉树
    • @[toc]
    • 树的基本术语
    • 二叉树
      • 定义
      • 特点
      • 特殊二叉树
          • 斜树
          • 满二叉树
            • 介绍
            • 特点
          • 完全二叉树
            • 定义
            • 特点
      • 基本性质
      • 二叉树的顺序存储结构
      • 二叉树的链式存储表示
        • 二叉链表
          • 基本思想
          • 结构
          • 特点
        • 三叉链表
          • 基本思想
          • 结构
        • 双亲链表
          • 结构
        • 线索链表
      • 二叉树遍历
        • 遍历实现
        • 由遍历序列求二叉树
      • 二叉树递归应用
      • 线索二叉树
        • 概念
        • 线索链表
    • 树的存储结构
      • 双亲表示法
      • 孩子链表表示法
      • 孩子兄弟表示法
    • 森林和二叉树的转化
      • 森林化二叉树
        • 转化思路
        • 具体操作
      • 二叉树化森林
      • 遍历性质
    • 哈夫曼树及哈夫曼编码
      • 相关概念
      • 基本思想
      • 代码实现

树的基本术语

  • 节点的度:节点所拥有的子树的个数。
  • 树的度:树中各节点度的最大值。
  • 叶子节点:度为0的节点,也称为终端节点。
  • 分支节点:度不为0的节点,也称为非终端节点。
  • 孩子、双亲:树中某节点子树的根节点称为这个节点的孩子节点,这个节点称为它孩子节点的双亲节点;
  • 兄弟:具有同一个双亲的孩子节点互称为兄弟。
  • 路径:如果树的节点序列n1, n2, …, nk有如下关系:节点ni是ni+1的双亲(1<=i
  • 祖先、子孙:在树中,如果有一条路径从节点x到节点y,那么x就称为y的祖先,而y称为x的子孙。
  • 节点所在层数根节点的层数为1;对其余任何节点,若某节点在第k层,则其孩子节点在第k+1层。
  • 树的深度:树中所有节点的最大层数,也称高度。
  • 层序编号:将树中节点按照从上层到下层、同层从左到右的次序依次给他们编以从1开始的连续自然数。
  • 有序树、无序树:如果一棵树中节点的各子树从左到右是有次序的,称这棵树为有序树;反之,称为无序树。
  • 森林:m(m≥0)棵互不相交的树的集合。
  • 同构:对两棵树,若通过对节点适当地重命名,就可以使这两棵树完全相等(节点对应相等,节点对应关系也相等),则称这两棵树同构。
  • 树的遍历:从根节点出发,按照某种次序访问树中所有节点,使得每个节点被访问一次且仅被访问一次。

二叉树

定义

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

特点

  1. 每个节点最多有两棵子树;

  2. 二叉树是有序的,其次序不能任意颠倒。

特殊二叉树

斜树
  1. 所有节点都只有左子树的二叉树称为左斜树;
  2. 所有节点都只有右子树的二叉树称为右斜树;
  3. 左斜树和右斜树统称为斜树。
  4. 在斜树中,每一层只有一个节点;
  5. 斜树的节点个数与其深度相同。
满二叉树
介绍

在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上。

特点
  1. 叶子只能出现在最下一层。
  2. 只有度为0和度为2的节点
  3. 满二叉树在同样深度的二叉树中节点个数最多。
  4. 满二叉树在同样深度的二叉树中叶子节点个数最多。
完全二叉树
定义

对一棵具有n个节点的二叉树按层序编号,如果编号为i(1≤i≤n)的节点与同样深度的满二叉树中编号为i的节点在二叉树中的位置完全相同。

简单来讲就是k层的完全二叉树前k-1层和k-1层的满二叉树一样,在第k层自左端数起,叶子节点都是连续的。

或者说在满二叉树中,从最后一个节点开始,连续去掉任意个节点,即是一棵完全二叉树。

特点
  1. 叶子节点只能出现在最下两层,且最下层的叶子节点都集中在二叉树的左部。

  2. 完全二叉树中如果有度为1的节点,只可能有一个,且该节点只有左孩子。

  3. 深度为k的完全二叉树在k-1层上一定是满二叉树。

基本性质

  1. 二叉树的第 i 层上最多有2i-1个节点(i≥1)。

  2. 一棵深度为k的二叉树中,最多有2k-1个节点,最少有k个节点。

  3. 深度为k且具有2k-1个节点的二叉树一定是满二叉树,

  4. 深度为k且具有k个节点的二叉树不一定是斜树。(可能左右交错)

  5. 在一棵二叉树中,如果叶子节点数为n0,度为2的节点数为n2,则有: n0=n2+1。 (简单证明如下:

    1. 二叉树总节点数 == 度为0的节点数+度为1的节点数+度为2的节点数
    2. 除根节点之外每个节点均有一个入度,而入度来自于度数不为零的节点的贡献(总节点数-1 == 度为1的节点数+2*度为2的节点数)
    3. 1和2联立消去度数为1的节点数以及总节点数即可
  6. 具有n个节点的完全二叉树的深度为[log2n]+1 ([ ]表示向下取整)

  7. 若将n个节点的完全二叉树从1开始层序标号,则有如下性质:

    1. 根节点标号为1.

    2. 节点 i(i>1) 的双亲节点是i/2,根节点无双亲节点

    3. 节点 i 的左儿子标号为 2*i,右儿子为 2*i+1 ,若左(右)儿子标号超过n,则无左(右)儿子

二叉树的顺序存储结构

二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且节点的存储位置(下标)应能体现节点之间的逻辑关系——父子关系

普通二叉树需按照完全二叉树的编号方式编号,然后以完全二叉树的形式存储到一维数组中,造成很多浪费,故二叉树的顺序存储结构一般仅存储完全二叉树.

二叉树的链式存储表示

二叉链表

基本思想

令二叉树的每个节点对应一个链表节点,链表节点除了存放与二叉树节点有关的数据信息外,还要设置指示左右孩子的指针。

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

空间浪费: 具有n个节点的二叉链表中,有n+1个空指针

三叉链表

基本思想

在二叉链表的节点上多加一个双亲指针.便于找到此节点的双亲.

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

双亲链表

这个书中并没有详细介绍,经由查询,这种存储方式便于寻找某节点的双亲节点,不便于进行寻找儿子或兄弟的操作,仅仅适用于少数场景,是一种静态链表(也可写作动态)

数组下标 数据 双亲位置 左/右标记
0 B 2 L
1 C 0 R
2 A -1(根节点无双亲节点)
3 D 2 R
4 E 3 R
5 F 4 L
结构
typedef struct BPTNode{//节点结构
    TElemType data;
    int  parent;//双亲的下标
    char LRTag;//左,右孩子的标志域
}BPTNode;

typedef struct BPTree{//树结构
    BPTNode *nodes; // 后续需申请空间
    int num_node;//节点数目
    int root;//根节点的位置
}BPTree;

线索链表

这个后续线索二叉树会讲到,此处按下不表

二叉树遍历

遍历实现

具体遍历方法已在博客中书写过,不再重复讲述

传送门: 二叉树遍历

由遍历序列求二叉树

  1. 根据前序序列的第一个元素建立根节点;

  2. 在中序序列中找到该元素,确定根节点的左右子树的中序序列;

  3. 在前序序列中确定左右子树的前序序列;

  4. 由左子树的前序序列和中序序列建立左子树;

  5. 由右子树的前序序列和中序序列建立右子树。

(4,5条基本就是递归求解的意思)

二叉树递归应用

二叉树递归应用

线索二叉树

概念

  • 线索:将二叉链表中的空指针域指向前驱节点和后继节点的指针被称为线索;
  • 线索化:使二叉链表中节点的空链域存放其前驱或后继信息的过程称为线索化
  • 线索二叉树:加上线索的二叉树称为线索二叉树。

线索链表

0 1
LTag lchild指向该结点的左孩子 lchild指向该结点的前驱结点
RTag rchild指向该结点的右孩子 rchild指向该结点的后继结点
typedef enum { Link, Thread } PointerThr; // 左右孩子指针(0) or 线索(1)   
typedef struct BiThrNod {
   TElemType        data;			// 数据域
   struct BiThrNode  *lchild, *rchild;  // 左右指针
   PointerThr         LTag, RTag;    // 左右标志
} BiThrNode, *BiThrTree;

线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。


树的存储结构

这里指的是更普遍意义上的树,不仅仅局限于二叉树

双亲表示法

基本思想:用一维数组来存储树的各个节点(一般按层序存储),数组中的一个元素对应树中的一个节点,包括节点的数据信息以及该节点的双亲在数组中的下标。

与前文表示的双亲链表没区别,还把那个"左右标志"去掉了.

查找双亲复杂度O(1),查找孩子和兄弟的复杂度都是O(n),n是元素个数.

孩子链表表示法

此方法思路主要分为两种,第一种是让每个节点都有多个指针域,

  1. 如果每个节点指针域都相同且等于最大的度数,则会造成巨大的浪费
  2. 如果每个节点指针域都恰好等于自己的度数,则节省空间的同时会造成结构不统一.

故换个思路:

基本思想:将每个节点的孩子节点都用线性表(链表)存储起来,然后n个节点就得到了n条链表,再将这个n条链表的头指针组成一个线性表(数组)

struct CTNode{	// 孩子节点   
     int child;		// 孩子下标
     CTNode *next;	// 下一个孩子节点
};

struct CBNode{	// 表头节点     
    TElemType data;		// 数值域
    CTNode *firstchild;  // 第一个孩子
};

struct CTree{	// 数组结构
    CBNode * nodes;	// 数组头指针
    int  n;	// 节点数量
    int r;	// 树根位置
}

但这种方法查找孩子简单,查找双亲就变得困难.故可以考虑其与双亲表示法结合一下.

// 双亲孩子表示法
struct CBNode{	// 表头节点     
    TElemType data;		// 数值域
    int parent;         //双亲的下标
    CTNode *firstchild;  // 第一个孩子
};

孩子兄弟表示法

又称二叉树表示或者二叉链表表示法,即以二叉链表作树的存储结构.链表中节点的两个链域分别指向该节点的第一个孩子节点和下一个兄弟节点

struct TNode{	// 孩子兄弟表示法
     TElemType data;	// 数据域
     TNode  *firstchild, *rightsib;		//第一个孩子和下一个兄弟
};

既可方便查找孩子,又可方便查找兄弟,若增设双亲域,也便于查找其双亲(书上写的,感觉不方便啊?)

不过通过这个我们也发现,对于任意一棵树,都能找到唯一的二叉树与之对应

普通树转化成的二叉树,其根节点都没有右孩子,即普通树对应的二叉树肯定没有右子树。

这句话指的是对整个树而言,是一定没有右子树的.


森林和二叉树的转化

森林化二叉树

转化思路

根据上文发现的,普通树转化的二叉树都没有右孩子,所以把森林里所有树都转化成二叉树,然后第一棵树的根节点作为最后大二叉树的根节点,依次把后一棵二叉树的根节点作为前一棵二叉树根节点的右孩子,最后得到的就是森林转化的二叉树。

具体操作

  1. 加线,树中所有相邻兄弟节点之间加一条连线
  2. 去线,树中所有节点只保留它与第一个孩子节点之间的连线, 删去它与其他孩子节点之间的连线。
  3. 层次调整,以根节点为轴心,将树顺时针转动一定角度,使之层次分明。

二叉树化森林

  1. 加线,若某节点x是其双亲y的左孩子,则把节点x的右孩子、右孩子的右孩子、……都与节点y用线连起来
  2. 去线,删去原二叉树中所有的双亲结点与右孩子节点的连线
  3. 层次调整,整理由前两步得到的树或森林,使之层次分明。

遍历性质

  1. 前序遍历森林即为前序遍历森林中的每一棵树。
  2. 后序遍历森林即为后序遍历森林中的每一棵树。
  3. 树的前序遍历等价于二叉树的前序遍历
  4. 树的后序遍历等价于二叉树的中序遍历

注: 树显然不存在“中序”这种方式,因为不一定是二叉,根节点的遍历顺序会出现问题。

哈夫曼树及哈夫曼编码

相关概念

  • 叶子结点的权值:对叶子结点赋予的一个有意义的数值量。
  • 二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。
  • 哈夫曼树:给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
  1. 权值越大的深度越小,权值越小的深度越大。
  2. 只有度为2或0的节点,没有度为1 的节点(可反证证明这个问题)

基本思想

  1. 初始化,构造n棵只有根节点的二叉树,保存在集合F中。
  2. 选取与合并,在F中选取根结点的权值最小的两棵二叉树分别作为左、右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和;
  3. 删除和加入,在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中;
  4. 重复⑵、⑶两步,当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树。

代码实现

// 哈夫曼树结点结构
// 由于哈夫曼树的构建是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针。
typedef struct {
    int weight;//结点权重
    int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;

注:哈夫曼树最终需要2*n-1个节点,n为元素个数,一般采用一维数组的方式实现。

// n 为全局变量,指n个元素
void Select(element huffTree[ ],int *s1 ,int *s2){	// 选择为进入树的两个最小元素
    int min1, min2;
    int i = 0;
    //先找到2个还没构建树的结点
    while(HT[i].parent != 0 && i <2*n-1){
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;
    i++;
    while(HT[i].parent != 0 && i <= 2*n-1){
        i++;
    }
    //对找到的两个结点比较大小,min2为大的,min1为小的
    if(HT[i].weight < min1){
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }else{
        min2 = HT[i].weight;
        *s2 = i;
    }
    //两个结点和后续的所有未构建成树的结点做比较
    for(int j=i+1; j <2*n-1; j++){
        //如果有父结点,直接跳过,进行下一个
        if(HT[j].parent != 0){
            continue;
        }
        //如果比最小的还小,将min2=min1,min1赋值新的结点的下标
        if(HT[j].weight < min1){
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介于两者之间,min2赋值为新的结点的位置下标
        else if(HT[j].weight >= min1 && HT[j].weight < min2){
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}

void HuffmanTree(element huffTree[ ], int w[ ], int n ) {	// 构建哈夫曼树
    for (i=0; i<2*n-1; i++) {	// 初始化数组
       huffTree [i].parent= -1;
       huffTree [i].lchild= -1;
       huffTree [i].rchild= -1;   
    }
    for (i=0; i<n; i++) 	// 初始化n棵仅有根节点的子树
       huffTree [i].weight=w[i];
    for (k=n; k<2*n-1; k++) {	// 开始构建树
        Select(huffTree, i1, i2); 
        huffTree[k].weight=huffTree[i1].weight+huffTree[i2].weight;
        huffTree[i1].parent=k;     
        huffTree[i2].parent=k; 
        huffTree[k].lchild=i1;    
        huffTree[k].rchild=i2;
    }
}

//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(element HT[ ], HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    char *cd = (char *)malloc(n*sizeof(char)); // 暂时存放结点哈夫曼编码的字符串数组
    cd[n-1] = '\0';		// 字符串结束符(因为字符串最长长度是n-1)
    for(int i=0; i<n; i++){	  // 从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
        int start = n-1;	 // 当前字符哈夫曼编码数组中已完成的位置
        int c = i;			// 当前结点的父结点在数组中的位置
        int j = HT[i].parent;
        while(j != 0){		// 一直寻找到根结点
            if(HT[j].left == c) // 若是左子树,编码为0
                cd[--start] = '0';
            else
                cd[--start] = '1';
            // 以父结点为孩子结点,继续朝树根的方向遍历
            c = j;
            j = HT[j].parent;
        }
        // 跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
        (*HC)[i] = (char *)malloc((n-start)*sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    // 使用malloc申请的cd动态数组需要手动释放
    free(cd);
}

前缀编码:一组编码中任一编码都不是其它任何一个编码的前缀 。其保证了解码时不会有多种可能。

你可能感兴趣的:(数据结构,acm,树,二叉树,数据结构,哈夫曼,链)