数据结构——树

文章目录

1. 树

  • 树是一个递归的定义,树是由子树组成的,子树又是由子树的子树组成这样嵌套下去

  • 根节点(root):第一个节点,没有前驱结点。

  • 度:结点拥有子树的个数(每个结点有两个:二叉树;每个结点有三个:三叉树)

  • 树的度:树内结点度的最大值

  • 树的深度:树的层数

  • 有序树:树中节点的各子树从左到右有次序

  • 森林:由m棵不相交的树的集合
    树一定是森林,森林不一定是树

  • 树: 只有一个前趋结点,0个或多个后趋结点,根结点没有前趋结点

  • 父节点:有一个节点的上一个节点。根节点没有父节点。

  • 兄弟节点:同层的节点。

  • 中间节点:有父节点且有子节点的节点。

  • 叶子节点:最后一层节点没有后面的。

  • 树逻辑的作用:
    用来描述或存储具有层级的数据,即非线性逻辑的数据。

2. 二叉树

  • 每个结点最多有两个子树(左子树,右子树)
  • 二叉树的子树要严格区分左右,即使只有一个子树也要区分左子树还是右子树,其顺序颠到之后是另外一个二叉树
  • 整个树只有两个节点的话,就不需要分左右子树

2.2 二叉树的性质

  1. 二叉树的第i层上有2^(i-1)个结点
  2. 深度为k的二叉树,最多有(2^k)-1个结点;最少有K个结点(每层只有一个)
  3. 任何一棵二叉树T,如果叶子树为n0,度为2的结点数为n2,则n0=n2+1
  4. 具有n个节点的完全二叉树深度为[log_2n]+1
  5. 如果对一棵有n个节点的完全二叉树的节点按程序编号,则对任意节点i有:
    1. 如果i=1则节点i是二叉树的根无双亲;如果a>1则其双亲是节点[i/2]
      2.如果2i>n,则节点a为子,节点无左孩子,否则其左孩子是节点时孩子2i
    2. 如果2i+1>n,则节点无右孩子,否则其右孩子是结点2i+1。

满二叉树

每一层上的结点都是满的。

完全二叉树

在满二叉树中,从最后一个节点开始连续去掉任意一个相连的节点,既是一颗完全二叉树。

  • 满二叉树也是一个完全二叉树,但是完全二叉树不是满二叉树

2.3二叉树的遍历

二叉树的遍历有四种:前序遍历,中序遍历,后序遍历,层序遍历

  • 调用的方法是递归调用

前序遍历

  • 输出顺序是:根左右,先输出根,然后先左后右,如果左边也是一个小根的话,也是先把左边小根的输出出完(左侧小根也是根左右的顺序),然后再输出右侧的小根

中序遍历

  • 输出顺序是:左根右,先输出左边的,然后再根,最后后右。

后序遍历

  • 输出顺序是:左右根,先左后右,最后输出根。

  • 已知一个二叉树,其各节点值不一样,得到的前中后序序列都是唯一的,
  • 已知(前序+中序),(后序+中序)可以确定一个唯一的二叉树

二叉树的建立

struct CreateRiTree(BiTree &T)
{
    cin >> ch ;
    if(ch=="#")
    {
        T=NULL;
    }
    else
    {
        if(!(T=(BiTNode*)malloc(sizeof(BiTNode))))
        {
            T = new BiTNode ;
        }
        T->data = ch;  //生成根节点
        CreateRiTree(T->LChild);  //创建左子树
        CreateRiTree(T->RChild);  //创建右子树
    }
    return OK;                     
}

递归遍历算法

先序遍历算法

struct PerOrderTraverse(BiTree T)
{
    if(T = NULL)
    {
        return ok;
    }
    else
    {
        cout << T->data;  //先输出根结点
        PerOrderTraverse(T->LChild);
        PerOrderTraverse(T->RChild);
    }
}

中序遍历算法

struct PerOrderTraverse(BiTree T)
{
    if(T = NULL)
    {
        return ok;
    }
    else
    {
        PerOrderTraverse(T->LChild);
         cout << T->data; 
        PerOrderTraverse(T->RChild);
    }
}

后序遍历算法

struct PerOrderTraverse(BiTree T)
{
    if(T = NULL)
    {
        return ok;
    }
    else
    {
        PerOrderTraverse(T->LChild);
        PerOrderTraverse(T->RChild);
         cout << T->data;  
    }
}

遍历算法的分析

时间效率:O(n) 每个结点都要访问一次
空间效率:O(n) 栈占用的最大辅助空间

非递归遍历算法

  • 用栈来实现
    1. 建立一个栈
    2. 根结点进栈,遍历左子树
    3. 根结点出栈,输出根结点,遍历右子树

复制二叉树

int Copy(BiTree T, BiTree &NewT)
{
    //如果树为空,递归结束
    if(T==NULL)
    {
        NewT = NULL;
        retrun 0;
    }
    //否则申请新空间,复制根结点
    else
    {
        // 申请新空间给要被复制的新量
        NewT = new BiTNode;
        // 把原数据赋值给新量
        NewT->data = T->data;
        // 同样递归的方法,把原来左子树分的值赋值给新的左子树
        Copy(T->LChild, NewT->LChild);
        Copy(T->RChild, NewT->RChild);

    }
}

计算二叉树的深度

  1. 如果是空树,深度为0;
  2. 否则,递归计算左子树深度记作m,递归计算右子树深度记作n
  3. 深度为取n,m中较大的那个数+1,
int Depth(BiTree T)
{
    // 判断树是否为空
    if(T==NULL)
    {
        return 0;
    }
    // 不为空的情况下,同样求左右子树的深度
    else 
    {
        m= Depth(T->LChild);
        n= Depth(T->RChild);
        // +1 是因为深度要在左右子树的基础上在加上根结点
        return (n>=m?n:m)+1;
    }
}

计算二叉树的结点数

  1. 判断如果空树结点数为0
  2. 结点数=左节点数+右节点数+1
int NodeCount(BiTree T)
{
    // 如果空树结点数为0
    if(T==NULL)
    {
        return 0;
    }
    else
    {
        // 结点数=左节点数+右节点数+1
        return NodeCount(T->LChild)+NodeCount(T->RChild)+1;
    }
}

计算二叉树的叶子结点数

  1. 判断如果空树结点数为0
  2. 叶子结点数=左节点叶子结点数+右节点叶子结点数
int LeafCount(BiTree T)
{
     // 如果空树叶子结点数为0
    if(T==NULL)
    {
        return 0;
    }
    // 只要一个根结点,叶子数为1
    if(T->LChild==NULL&&T->RChild==NULL)
    {
        return 1;
    }
    else
    {
        return LeafCount(T->LChild)+LeafCount(T->RChild); 
    }
}

2.4 线索二叉树

二叉树在寻找她的左右孩子很方便,但是无法直接寻找该节点在某种序列中的前驱结点和后继节点

  • 解决方法:利用二叉链表中的空指针域
    每个接结点又有左右两个指针域,N个的二叉树有2N个指针域,但是最多只有N-1个子节点,多余除了N+1个指针域

    • 如果结点无左孩子,就将空着的左孩子的指针域改为指向其前驱
    • 如果结点无右孩子,就将空着的右孩子的指针域改为指向其后继
    • 这种改变指针指向的指针称为线索
    • 注:如果一个结点左右孩子都有,那么就不可以改变指针的指向,也就没有线索
  • 为了区分LChild和RChild指针是指向孩子结点,还是前后趋结点,在二叉链表中增加两个标志域,ltag和rtag

    • ltag=0:LChild指向结点的左孩子
    • ltag=1:LChild指向结点的前驱
    • rtag=0:RChild指向结点的右孩子
    • rtag=1:RChild指向结点的后驱

3. 森林

是N(N>=0)棵互不相交的树的集合

3.1 树的存储结构

双亲表示法

数组下标 数据 双亲下标
0 R -1
1 A 0
2 B 0
3 C 0
4 D 1
5 E 1
6 F 3
7 G 6
8 H 6
9 H 6
typedef struct PTNode
{
    TElemType data;
    int parent  // 记录双亲节点的位置
}
  • 特点:找双亲容易,找孩子结点难

孩子链表表示法

把每个节点的孩子孩子排列起来,看成是一个线性表,用单链表存起来,N个结点有N个孩子链表。每个孩子链表并不存放具体数据,而是存放该孩子结点的地址,也为孩子结点数据已经存储了,而且该孩子结点可能有自己的孩子结点,在存储她的数据就会造成浪费

孩子结点的结构:child next

typedef struct CTNode
{
    int child;
    struct CTNode *next;
}*childPtr;

双亲结点的结构 data firstchild

typedef Struct 
{
    TELemType data;
    ChildPtr firstchild; // 孩子链表的标头指针
}CTBox;
  • 找孩子容易,找双亲比较困难

孩子兄弟表示法

有二叉树的形式来存储,链表中每个结点有两个指针,左边指针指向第一个孩子结点,右边指针指向下一个兄弟结点

typedef struct CSNode
{
    ElemType data;
    struct CSNode *firstChild *nextSibling;
}CSNode,*CSTree;

把树树转成二叉树

树的每个结点可能有多个子树,要想转化为二叉树,需要转化,把一个结点的左节点指向她的第一个子节点,右节点指向他的下一个兄弟结点

森林转化为二叉树

  1. 将森林中的每棵树转化成二叉树
  2. 将每棵树的根节点用线连起来
  3. 第一棵树的很结点作为整个二叉树的根,再以根节点为轴心,顺时针旋转,构成二叉树型结构

二叉树转换回森林

去掉全部右孩线,孤立二叉再还原

3.2 树的遍历

  • 二叉树的遍历有四种:前序,中序,后序,层次
  • 树的遍历有三种:前序,后序,层次
    • 先序遍历:树不为空,先访问根结点,然后依次遍历各子树。
    • 后序遍历:树不为空,线依次遍历各课子树,然后访问根结点
    • 层次遍历:自上而下,从左到右,访问书中的每个节点。

4. 哈斯曼树

哈斯曼树是用来构造最优二叉树的

  • 路径:从树中一个结点到另一个结点之间要经过的分支,这些分支构成两个结点间的路径

  • 路径长度:连接点之间分支的个数

  • 树的路径长度:从树根到每个节点的路径长度之和,记作:LT

    • 结点数相同的二叉树中,完全二叉树路径长度最短,但是路径长度最短的二叉树不一定都是完全二叉树
  • 权:给树的结点辅一个有着某种意义的值,这个值成为该节点的权。(意义根据二叉树使用的场合而定

  • 结点的带权路径长度:(从根结点到该结点之间的路径长度)×(该节点的权)

  • 树的的带权路径长度:树的所有叶子结点带权路径长度之和

哈斯曼树:带权路径长度最短的树
是在度相同的树中比较而得的结果,因此有最优二叉树、最优三叉树之称

4.1 哈斯曼树的构造

  1. 把所有数据都设置成根结点
  2. 在在所根结点中,选出两个最小的结点a、b,构成一个二叉树,a和b的根结点为新生成的c,c的权=a权+b权,同时在原来所有根结点中删除a、b。
  3. 在从现有根结点中在挑选出两个最小的生成二叉树,这里包含上一步a、b生成的新结点c
  4. 不断重复两个小树生成一个新树,直到构成一个二叉树位置
  • 总结
    • 哈斯曼算法中,初始有N棵二叉树,要经过N-1次合并,且最终形成2N-1个新结点,并且最终形成的新节点又具有2个子树分支。
    • 哈斯曼树的结点度数为0或2,没有度为1的结点
    • 哈斯曼树具有N(叶子)+N-1(新添加的根)=2N-1(共有结点数),且所有结点的度都不为1

哈斯曼树的算法实现

// 结点类型定义
typedef struct 
{
    int weight;   //结点的权值
    int parent,Lch,Rch;   
}HTNode,*HuffmanTree;

// 初始化哈斯曼树
void CreatHuffmanTree(HuffManTree HT, int n)
{
    if(n<1) // 结点数为0,退出
    {
        return ;
    }
    int m=2*n-1;  // n个结点组成二叉树结点共2n-1个元素
    HT = new HtNode[m+1]  // 0号单元不使用,HT[m]就便是根结点。
    for(int i=1;i<=m;++i)  // 将2n-1个元素的parent,Lch,Rch都设置为0
    {
        HT[i].Lch=0;
        HT[i].Rch=0;
        HT[i].parent=0;
    }
    for(int i=1;i<n;++i) // 输入前n个元素的weight值
    {
        cin >> HT[i].weight;
    } 
}

// 构造哈斯曼树
for(i=n+1;i<=m;i++)  // 合并产生n-1个新结点——构造哈斯曼树(1-n为原有结点,n+1-2n-1为新生成的结点)
{
    Select(HT,i-1,s1,s2);   // 在原有1-n个结点中选择处2个双亲为0,
                            // 并且权重最小的结点
                            // 并返回他们在HT表中的序号s1,s2
    HT[s1].parent=i;  // 表示从表中删除s1,s2
    HT[s2].parent=i;
    HT[i].Lch=s1;   // s1,s2分别作为i的左右孩子
    HT[i].Rch=s2;
    HT[i].weight=HT[s1].weight+HT[s2].weight;   // i的权值为左右孩子的权值之和
}

哈斯曼编码

为每个字符设置二进制编码的时候,每一位都要有相同的位数,为一些出现次数多的数据设置段的编码,可以节省存储空间,但是设计编码的时候,这些特殊的编码,不能是其它正常编码的前缀。

设计前缀码,同时让电文总长最短——哈斯曼编码

  1. 统计字符集中每个字符出现的概率。
  2. 利用哈斯曼树的特点,字符的概率值为其权值,构造哈斯曼树,权值越大,路径越短
  3. 哈斯曼树每个分支:左分支记为0,右分支记为1
  4. 每个分支都有1或0,每个结点的编号就是从根结点到这个结点的所有分支上的数连起来,从根结点的数开始大头

哈斯曼编码的代码实现

// 从子叶向根逆向求每个字符的哈斯曼编码,存储在编码表HC中
void CreatHuffmanCode(HuffmanTreee HT,HuffmanCode &HC ,int n)
{
    HC = new char*[n+1];    // 分配n个字符编码的指针矢量
    cd = new char[n];       // 分配临时存放编码的动态数组空间
    cd[n-1] = '0';          // 编码结束标志符
    for(i=1;i<n;i++)        // 逐个字符求哈斯曼编码
    {
        start = n+1;
        c=i;
        f=HT[i].parent;
        while(f!=0)         // 从子叶节点开始向上回溯,直达根结点
        {
            --start;        // 回溯一次start向前指一个位置
            if(HT[f].lchild==c) // 结点C是f的左孩子,则生成代码0
            {
                cd[start]='0';
            }
            else                // 结点C是f的右孩子,则生成代码1
            {
                cd[start]='1';
            }
            c = f;          // 继续向上回溯
            f = HT[f].parent;
        }                   // 求出第i个字符编码
        HC[i] = new char[n-start]; // 为第i个字符串编码分配空间 
        strcpy(HC[i],&cd[start]);  // 讲求的的编码从临时空间cd复制到HC的当前行中

        delete cd;          // 释放临时空间
    }
}
上一篇:unordered_map和unordered_set的模拟实现


下一篇:智慧党建云展厅三维可视化