树和二叉树学习笔记


一、树的逻辑结构

1.定义

  • 树: n(n>=0)个结点的有限集合,树中的数据元素称为结点。n=0时为空树。
  • 非空树的条件:
    ·有且仅有一个特定的称为根的结点;
    ·当n>1时,除根结点外其余结点被分成m(m>0)个互不相交的有限集合,每个集合又是一棵树,并称为这个根结点的子树。
  • (注意: 结点不能属于多个子树,子树之间不能有关系,否则就不是树)

2.基本术语

  • 度:
    结点的度:结点所拥有的子树的个数。
    树的度:树中各结点度的最大值。
  • 结点:
    叶子结点:度为0的结点,也叫终端结点。
    分支节点:度不为0的结点,也叫非终端结点。
    ·孩子结点:某结点子树的根结点为这个结点的孩子结点。
    ·双亲结点:某结点子树的根结点为这个结点的孩子结点,这个结点为它孩子结点的双亲结点。
    ·兄弟结点:具有同一双亲的孩子结点互称为兄弟结点。
  • 路径: 如果树的结点序列n1,n2…nk有如下关系:结点ni是ni+1的双亲,把n1,n2…nk称为一条由n1至nk的路径,路径上经过的边的个数称为路径长度
  • 祖先: 树中有一条路径能从结点x到结点y,那么x就称为y的祖先,y称为x的子孙
  • 结点所在层数: 根结点层数为1,其余任何结点,若某结点在第k层,则其孩子结点在第k+1层。
  • 树的深度: 树中所有结点的最大层数为树的深度,也称高度。
    树的宽度: 树中每一层结点的最大值称为树的宽度。
  • 如果树有n个结点,则有n-1条边。

3.树的遍历

  • 定义: 从根结点出发,按照某种次序访问树中所有结点,每个结点仅被访问一次。
  • 分类:
    ·前序遍历:根左右
    ·后序遍历:左右根
    ·层序遍历:从根结点开始,自上而下逐层遍历,同层从左到右逐个访问。

二、树的存储结构

1.双亲表示法

一维数组存储树的结点(一般层序存储),数组一个元素对应一个结点,每个数组元素记录结点数据信息和该结点双亲在数组的下标。

struct PNode
{
    
    
    char data;
    int parent;
};

该方法能方便的找到双亲,但是不好找孩子,可以在结构体中再加一个信息,即第一个孩子的下标。

2.孩子表示法

多重链表表示法。
法1:指针域的个数等于树的度。
缺点:会造成空间的浪费
法2:指针域个数等于结点的度
缺点:结点结构不一致,不好实现
法3:结点所有孩子构成一个单链表
如下图:
在这里插入图片描述
n个单链表共有n个头指针,这n个头指针又组成了一个线性表。

struct CTNode//孩子结点
{
    
    
    int child;
    CTNode *next;
};
struct CBNode//表头结点
{
    
    
    char data;
    CTNode *firstChild;
};

缺点:孩子好找,双亲不好找

3.孩子兄弟表示法

二叉链表表示法。
每个结点除数据域外,还设置了两个指针分别指向该结点的第一个孩子和右兄弟。

4.总结

顺序存储:双亲表示法,双亲、孩子表示法
链式存储:多重链表表示法,孩子链表表示法,孩子兄弟表示法。


二叉树

二叉树及二叉搜索树BST
Treap树
Splay树
线段树(一)
线段树(二)

一、二叉树的逻辑结构

1.定义

  • 二叉树: n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树构成。
    特点:
    ·每个结点最多有两棵子树
    ·二叉树是有序的,次序不能任意颠倒
    注意: 二叉树不等于度为2的树,二叉树必须分左右子树,树没有这个要求。
  • 特殊二叉树:
    ·斜树: 所有结点都只有左子树的二叉树称为左子树,所有结点都只有右子树的二叉树称为右子树,左子树与右子树统称为斜树。
    斜树特点:每层只有一个结点,斜树的结点个数等于深度。
    ·满二叉树: 所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树。
    满二叉树特点:叶子只能出现在最下面一层,只有度为0和度为2的结点。
    满二叉树在同样深度的二叉树中结点个数最多,满二叉树在同样深度的二叉树中叶子结点个数最多。
    ·完全二叉树: 满二叉树最后一个结点开始,连续去掉任意个结点(可以不去)。
    完全二叉树特点:深度为k的完全二叉树在k-1层是满二叉树;叶子结点只能出现在最下两层,且最下层的叶子结点都集中在左侧连续的位置;度为一的结点最多有1个,只能有左孩子。

2.基本性质

  • 性质1:二叉树中,若叶子结点的个数为n0,度为2的结点个数为n2,则n0=n2+1。
  • 性质2:二叉树第i层上最多有2i-1个结点(i>=1)。
  • 性质3:一棵深度为k的二叉树中最多有2k-1个结点。
  • 性质4:具有n个结点的完全二叉树的深度为⌊log2n⌋+1。
  • 性质5:对一棵具有n个结点的完全二叉树从1开始按层序编号,对于编号为i(1<=i<=n)的结点(简称结点i),
    如果i>1,则结点i的双亲的编号为⌊i/2⌋,否则是根结点无双亲。
    如果2i<=n,则结点i的左孩子的编号为2i,否则无左孩子。
    如果2i+1<=n,则结点i的右孩子的编号为2i+1,否则结点i无右孩子。

3.遍历

  • 前序遍历: 根左右
  • 中序遍历: 左根右
  • 后序遍历: 左右根
  • 层序遍历: 从根结点开始,自上而下逐层遍历,同层从左到右逐个访问。
  • ·如何确定唯一二叉树? 前序+中序 或 后序+中序。
    在这里插入图片描述
    上图发现,叶子结点为GEF,在前中后序遍历的相对次序不变。

二、二叉树的存储结构

1.顺序存储

用一维数组存储二叉树中的结点,并且结点的存储位置即下标应能体现结点之间的逻辑关系即父子关系。

void PreOrder(int root,char data[])//前序遍历
{
    
    
    if(data[root]!='\0')
    {
    
    
        cout<<data[root];
        PreOrder(2*root,data);
        PreOrder(2*root+1,data);
    }
}
void InOrder(int root,char data[])//中序遍历
{
    
    
    if(data[root]!='\0')
    {
    
    
        InOrder(2*root,data);
        cout<<data[root];
        InOrder(2*root+1,data);
    }
}
void PostOrder(int root,char data[])//后序遍历
{
    
    
    if(data[root]='\0')
    {
    
    
        PostOrder(2*root,data);
        PostOrder(2*root+1,data);
        cout<<data[root];
    }
}

二叉树的顺序存储结构一般仅存储完全二叉树。

2.二叉链表的基本操作

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

struct BiNode
{
    
    
    char data;
    BiNode *lchild,*rchild;
};

前序遍历:(递归算法)
在这里插入图片描述

void BiTree::PreOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else {
    
    
        cout<<bt->data<<"\t";
        PreOrder(bt->lchild);
        PreOrder(bt->rchild);
        }
}

非递归算法:用栈实现,栈是实现递归最常用的结构。即遇到一个结点,就访问该结点并把该结点推入栈中,然后遍历它的左子树。遍历完它的左子树后,从栈顶托出该结点,遍历右子树。

void BiTree::PreOrder(BiNode *root)
{
    
    
    Stack<BiNode*>s;
    while(root!=NULL||s.empty())
    {
    
    
        while(root!=NULL)
        {
    
    
            cout<<root->data;
            s.push(root);
            root=root->lchild;
        }
        if(!s.empty())
        {
    
    
            root=s.top();
            s.pop();
            root=root->rchild;
        }
    }
}

中序遍历:(递归)

void BiTree::InOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else {
    
    
        InOrder(bt->lchild);
        cout<<bt->data<<"\t";
        InOrder(bt->rchild);
        }
} 

非递归:用栈实现,遇到一个结点,把该结点推入栈中,然后遍历它的左子树。遍历完它的左子树后,从栈顶托出该结点并访问该结点,遍历右子树。

void BiTree::InOrder(BiNode *root)
{
    
    
    Stack<BiNode*>s;
    while(root!=NULL||s.empty())
    {
    
    
        while(root!=NULL)
        {
    
    
            s.push(root);
            root=root->lchild;
        }
        if(!s.empty())
        {
    
    
            root=s.top();
            s.pop();
            cout<<root->data;
            root=root->rchild;
        }
    }
}

后序遍历:(递归)

void BiTree::PostOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else {
    
    
        PostOrder(bt->lchild);
        PostOrder(bt->rchild);
        cout<<bt->data<<"\t";
        }
}  

非递归:用栈实现,遇到一个结点,把该结点推入栈中,然后遍历它的左子树。遍历完它的左子树后,遍历右子树,最后从栈顶托出该结点并访问该结点。
这里的每个结点进栈两次,第二次出栈时访问结点。而前序和中序只进栈一次。
这里比较复杂,需要给栈中每个元素加一个特征位,区别是从栈顶元素左边回来还是右边回来的,如果从左边回来,需要继续遍历右子树,如果从右边回来的,说明左右子树均已遍历。
法1:

enum Tags//特征标识符定义
{
    
    
    Left,Right;
};
class StackElement//栈元素的定义
{
    
    
public:
    BiNode *pointer;//指向二叉树结点的指针
    Tags tag;//特征标识符申明
};
void BiTree::PostOrder(BiNode *root)
{
    
    
    StackElement element;
    Stack<StackElement>s;
    BiNode *pointer;
    if(root==NULL)
        return;
    else pointer=root;
    while(1)
    {
    
    
        while(pointer!=NULL)
        {
    
    
            element.pointer=pointer;
            element.tag=Left;
            s.push(element);
            pointer=pointer->lchild;
        }
        element=s.pop();
        pointer=element.pointer;
        while(element.tag==Right)
        {
    
    
            cout<<pointer->data;
            if(s.empty())
                return;
            else{
    
    
                element=s.pop();
                pointer=element.pointer;
            }
        }
        element.tag=Right;
        s.push(element);
        pointer=pointer->rchild;
    }
}

法2:依次将根结点的右儿子,左儿子入栈,当结点出栈时再进行访问。

void tree::T_print(BiNode *bt)
{
    
    
    stack<BiNode*>s;
    BiNode *cur,*pre=NULL;//cur指向栈顶,pre记录刚刚访问的结点。
    if(root==NULL)
        return;
    s.push(bt);
    while(!s.empty())
    {
    
    
        cur=s.top();
        if((cur->lchild==NULL&&cur->rchild==NULL)||(pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
        //如果栈顶是叶子结点或刚出栈元素和栈顶元素之间有“儿子-双亲”关系,出栈
        {
    
    
            cout<<cur->data;
            s.pop();
            pre=cur;
        }
        else {
    
    
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)
                s.push(cur->lchild);
        }
    }
}

层序遍历:队列
法1:顺序队列

void BiTree::LeverOrder( )
{
    
    
    BiNode *Q[100],*q=NULL;//顺序队列最多100个结点
    int front=-1,rear=-1; //队列初始化
    if(root==NULL)
        return;
    Q[++rear]=root;//根指针入队
    while(front!=rear)//当队列非空时
    {
    
    
        q=Q[++front];//出队
        cout<<q->data;
        if(q->lchild!=NULL)
            Q[++rear]=q->lchild;
        if(q->rchild!=NULL)
            Q[++rear]=q->rchild;
    }
}

法2:队列

void BiTree::LevelOrder(BiNode *root)
{
    
    
    queue<BiNode*>a;
    if(root)
        a.push(root);
    while(!a.empty())
    {
    
    
        root=a.front();
        a.pop();
        cout<<root->data;
        if(root->lchild)
            a.push(root->lchild);
        if(root->rchild)
            a.push(root->rchild);
        
    }
}

二叉树的建立
原二叉树->扩展二叉树,如下图:
在这里插入图片描述
这里的二叉树的建立是按扩展前序遍历序列输入每个结点的值。如果输入#,建一棵空的子树,否则申请空间建新的结点。

BiNode* BiTree::creat(BiNode *bt)
{
    
    
    BiNode *bt;
    char ch;
    cin>>ch;
    if(ch=='#')
        bt=NULL;
    else
    {
    
    
        bt=new BiNode;
        bt->data=ch;
        bt->lchild=creat(bt->lchild);
        bt->rchild=creat(bt->rchild);
    }
    return bt;
}

在这里插入图片描述
销毁二叉树
防止根结点先删导致找不到孩子结点,考虑后序的方法:

void BiTree::Release(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else
    {
    
    
        Release(bt->lchild);
        Release(bt->rchild);
        delete bt;
    }
}

3.二叉链表的完整代码:

#include<iostream>
using namespace std;

struct BiNode
{
    
    
    char data;
    BiNode *lchild, *rchild;
};
class BiTree
{
    
    
public:
    BiTree( )
    {
    
    
        root=Creat(root);   //构造函数,建立一棵二叉树
    }
    ~BiTree( )
    {
    
    
        Release(root);   //析构函数,释放各结点的存储空间
    }
    void PreOrder( )
    {
    
    
        PreOrder(root);   //前序遍历二叉树
    }
    void InOrder( )
    {
    
    
        InOrder(root);   //中序遍历二叉树
    }
    void PostOrder( )
    {
    
    
        PostOrder(root);   //后序遍历二叉树
    }
    void LeverOrder( ); //层序遍历二叉树
private:
    BiNode *Creat(BiNode *bt); //构造函数调用
    void Release(BiNode *bt); //析构函数调用
    void PreOrder(BiNode *bt); //前序遍历函数调用
    void InOrder(BiNode *bt); //中序遍历函数调用
    void PostOrder(BiNode *bt); //后序遍历函数调用
    BiNode *root; //指向根结点的头指针
};
void BiTree::PreOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else
    {
    
    
        cout<<bt->data<<"\t";
        PreOrder(bt->lchild);
        PreOrder(bt->rchild);
    }
}
void BiTree::InOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else
    {
    
    
        InOrder(bt->lchild);
        cout<<bt->data<<"\t";
        InOrder(bt->rchild);
    }
}
void BiTree::PostOrder(BiNode *bt)
{
    
    
    if(bt==NULL)
        return;
    else
    {
    
    
        PostOrder(bt->lchild);
        PostOrder(bt->rchild);
        cout<<bt->data<<"\t";
    }
}
void BiTree::LeverOrder( )
{
    
    
    BiNode *Q[100],*q=NULL;
    int front=-1,rear=-1;
    if(root==NULL)
        return;
    Q[++rear]=root;
    while(front!=rear)
    {
    
    
        q=Q[++front];
        cout<<q->data<<'\t';
        if(q->lchild!=NULL)
            Q[++rear]=q->lchild;
        if(q->rchild!=NULL)
            Q[++rear]=q->rchild;
    }
}
BiNode *BiTree::Creat(BiNode *bt)
{
    
    
    char ch;
    cout<<"请输入扩展二叉树的前序遍历序列,每次输入一个字符:";
    cin>>ch;
    if(ch=='#') bt=NULL;
    else
    {
    
    
        bt=new BiNode;
        bt->data=ch;
        bt->lchild=Creat(bt->lchild);
        bt->rchild=Creat(bt->rchild);
    }
    return bt;
}
void BiTree::Release(BiNode *bt)
{
    
    
    if(bt==NULL) return;
    else
    {
    
    
        Release(bt->lchild);
        Release(bt->rchild);
        delete bt;
    }
}
int main( )
{
    
    
    BiTree T;
    cout << "该二叉树的前序遍历序列是:";
    T.PreOrder( );
    cout << "\n该二叉树的中序遍历序列是:";
    T.InOrder( );
    cout << "\n该二叉树的后序遍历序列是:";
    T.PostOrder( );
    cout << "\n该二叉树的层序遍历序列是:";
    T.LeverOrder( );
    return 0;
}

运行结果图:在这里插入图片描述

4.二叉链表的其它操作

求二叉树的结点个数: 前序,中序,后序都可

void Count(BiNode *root)
{
    
    
    if(root)
    {
    
    
        Count(root->lchild);
        number++;
        Count(root->rchild);
    }
}

求叶子结点个数:

void constleaf(BiNode *root)
{
    
    
    int leaf=0;
    if(root)
    {
    
    
        if(root->lchild==NULL&&root->rchild==NULL)
            leaf++;
        else
        {
    
    
            constleaf(root->lchild);
            constleaf(root->rchild);
        }
    }
}

求树的高度

int BiTree::cal_height(BiNode *root)
{
    
    
    int lheight=0,rheight=0;
    if(root==NULL)
        return 0;
    lheight=cal_height(root->lchild);
    rheight=cal_height(root->rchild);
    if(lheight>rheight)
        return lheight+1;
    else return rheight+1;
    
}

三、最优二叉树

1.概念

叶子结点的权值: 叶子结点的数值
二叉树的带权路径长度: 从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和
哈夫曼树: 给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
哈夫曼树的特点: 权值越大的叶子结点越靠近根结点,权值越小的叶子结点越远离根结点。
只有度为0或2的结点,不存在度为1的结点。
哈夫曼树的构造: 已知二叉树集合,每次选根结点权值最小的两棵二叉树分别作为左右子树构造一棵新的二叉树,新根结点的权值为其左右子树根结点的权值之和。新根结点替代原来集合里的那两个结点,继续合并。
哈夫曼树共有2n-1个结点。

2.存储结构

在这里插入图片描述

struct element
{
    
    
    int weight;
    int lchild,rchild,parent;
};
void HuffmanTree(element huffTree[],int w[],int n)
{
    
    
    int i,k,i1,i2;
    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++)
        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].lchild=i2;
    }
}

3.哈弗曼编码

等长编码: 所有编码都等长,表示n个不同的字符需要[log2n]位。
不等长编码: 让频率高的字符采用尽可能短的编码。
前缀编码: 一组编码中任意一编码都不是其他任何一个编码的前缀,保证在解码时不会有多种可能。
哈弗曼编码: 用于构造最短不等长编码,先构造哈夫曼树,树的左分子为0,右分支为1,从根结点到叶子结点所经过的路径组成的0和1的序列为该叶子结点对应字符的编码,称为哈夫曼编码。(因为叶子结点不在同一个路径上,不会出现一个编码是另一个编码的前缀的情况)。
叶子结点的平均深度即平均编码长度,树的带权路径长度是各个字符的码长与其出现次数的乘积之和,即编码总长度。

四、线索二叉树

在2n个指针域中只有n-1个指针域用来存储孩子结点的地址,存在n+1个空指针,利用这些空指针存某种遍历的前序和后序结点,指向前驱和后继结点的指针称为线索,加上线索的二叉链表称为线索链表,加上线索的二叉树称为线索二叉树
前序线索二叉树:
若左指针为空,指向前序遍历的前驱;若右指针为空,指向前序遍历的后继。
在这里插入图片描述
中序,后序同理。
在这里插入图片描述

森林

一、概念

定义: 森林是m(m>=0)棵互不相交的树的集合。
遍历森林: 用前序或后序的方式遍历森林里的每一棵树。

二、树、二叉树、森林的相互转换

1.树转二叉树

就相当于孩子兄弟表示法,结点的第一个孩子为其左孩子,结店的右兄弟为其右孩子。
如下图:
在这里插入图片描述
树的前序遍历等价于二叉树的前序遍历;树的后序遍历等价于二叉树的中序遍历。

2.森林转二叉树

先把森林里的每棵树先转换为二叉树,然后每棵二叉树的根结点依次挂到前一棵二叉树根结点的右孩子上。
转换后的二叉树根结点右分支上的结点个数就是原来森林中树的个数。

3.二叉树转为树或森林

二叉树根结点右分支上的每一个结点都是一棵树的根结点,先拆分出每一棵二叉树,再根据孩子兄弟表示法倒推出树。

猜你喜欢

转载自blog.csdn.net/weixin_51443397/article/details/120976824