数据结构-树

2023-11-16

目录

知识框架

 一.树的基本概念

1.树的定义

2.基本术语

3.树的性质

二叉树

一.二叉树的概念

1.二叉树的定义

 2.特殊的二叉树

3.二叉树的一些性质

4.二叉树的存储结构

(1)顺序存储

 (2)链式存储

 二.二叉树的创建和遍历

1.先序遍历

2.中序遍历

3.后序遍历

4.层次遍历

5.递归算法和非递归算法的转换

(1)中序遍历的非递归算法

(2)先序遍历的非递归算法

(3)后序遍历的非递归算法

 6.由遍历序列构造出二叉树    

 二叉树的应用

一.二叉排序树

1.定义

 2.插入

3.查找

二.二叉平衡树

1.定义

2.平衡二叉树的插入

红黑树

三.哈夫曼树

 1.定义

 2.构造哈夫曼树

 3.哈夫曼编码


知识框架

 一.树的基本概念

1.树的定义

树是n(n>=0)个结点的有限集。当n=0时,称为空树。任意一颗非空树应满足:

1.有且仅有一个特定的称为根的结点。

2.当n>1时,其余节点可分为m个互不相交的有限集,其中每个集又是一颗新的树,称为根的子树。

很显然,对于树的定义是递归的,树是一种递归的数据结构,在往后我们对树的各种操作都要利用到递归这个重要的性质!树作为一种逻辑结构,同时也是一种分层结构,具有一下特征:

1.树的根节点没有前驱,除根节点外的所有结点有且只有一个前驱。

2.树中所有结点可以有零个或多个后继。

2.基本术语

1.度:树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。

2.高度:从根节点开始,从1开始依次往下计数,到最后一层的总层数。

3.层数:从根节点开始,从1开始依次往下计数。

3.树的性质

树具有下面一些基本性质:

1.树中的结点数等于所有结点的度加1.

2.度为m的树中第i层至多有m^(i-1)个结点(i>=1).

3.高度为h的m叉树至多有(m^h  -1)/(m-1)个结点

4.具有n个结点的m叉树的最小高度为 logm (n(m-1)+1).

二叉树

一.二叉树的概念

1.二叉树的定义

二叉树是另一种树形结构,其特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。与树的定义相似,二叉树也以递归的形式定义。二叉树是n(n>=0)个节点的集合:

1.空二叉树,n为0;

2.或由一个根节点和两个互不相交的被称为根的左子树和右子树组成,他们分别又是一颗二叉树

一般为以下几种形式

 2.特殊的二叉树

(1)满二叉树

 一棵高度为h,且含有2^h -1个结点的二叉树称为满二叉树,若根节点从1开始编号,左孩子为2i,右孩子为2i+1

(2)完全二叉树

只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上的二叉树我们称为完全二叉树

(3)二叉排序树

左子树上所有结点的关键字均小于根结点的关键字,右子树上的所有结点的关键字都大于根节点的关键字,左右子树分别又是一颗二叉排序树。

(4)平衡二叉树

首先平衡二叉树是一棵合理的二叉排序树,做上任意结点的左子树和右子树的深度之差不超过1,在构建平衡二叉树过程中,一但发现不符合特性,需要进行调整,这个我们之后再细聊。

3.二叉树的一些性质

1.任意一棵二叉树,若节点数量为n,则边的数量为n-1;

2.非空二叉树上叶子结点树总是等于度为2的结点数量加1:n0=n2+1

3.非空二叉树上第i层最多有2^(i-1)个结点。

4.高度为h的二叉树至多有(2^h)-1个结点

4.二叉树的存储结构

(1)顺序存储

二叉树的顺序存储如何实现呢?我们用一组地址连续的存储单元从上到下,从左到右存储完全二叉树的结点元素,即将完全二叉树上编号为i的结点元素存在一维数组下标为i-1的分量中,这样我们查找起来会很方便,但是也有一个巨大的问题哦!那就是为了让数组的下标和结点编号对应上,我们必须要将二叉树变成完全二叉树,不存在的结点也要补上,不然编号对应不上,这就会造成空间的巨大浪费。如下图所示

 (2)链式存储

通过观察我们不难发现,二叉树的左孩子右孩子我们完全可以通过两个左右指针来指向他们。

看看我们如何定义结点的结构类型

typedef struct linkTree{
    int data;//数据
    struct linkTree* lchild;//左孩指针
    struct linkTree* rchild;//右孩指针
}linkTree;

 二.二叉树的创建和遍历

从根节点出发,按照某种顺序依次访问所有结点,每个结点有且只能被访问一次。

1.先序遍历

先访问根节点,先序遍历左子树,先序遍历右子树

 代码如下:

//前序遍历
void preOrder(TreeNode* T)
{
    if(T==NULL)
        return ;
    else
    {
        printf("%c ",T->data);
        preOrder(T->lchild);
        preOrder(T->rchild);
    }
}

2.中序遍历

中序访问左子树,访问根节点,中序访问右子树

//中序遍历
void inOrder(TreeNode* T)
{
    if(T==NULL)
        return ;
    else
    {
        inOrder(T->lchild);
        printf("%c ",T->data);
        inOrder(T->rchild);
    }
}

3.后序遍历

后序访问左子树,后序访问右子树,访问根节点

 代码如下:

//后序遍历
void postOrder(TreeNode* T)
{
    if(T==NULL)
        return ;
    else
    {
        preOrder(T->lchild);
        preOrder(T->rchild);
        printf("%c ",T->data);
    }
    return ;
}

三种遍历我们都用到了递归的方法,且无论是哪种遍历算法,我们每个结点都只访问了一次,所以时间复杂度为O(n),在使用递归时一定要注意退出递归的条件!不然可能会陷入死循环无法退出,那么如果不使用递归如何遍历呢?

4.层次遍历

层次遍历顾名思义就是从上往下从左到右每一层遍历完成后再往下一层开始遍历,如下图所示

 那么要如何实现层次遍历呢,这里就要用到我们之前的队列了,我们先将二叉树根结点入队,然后出队,访问出队结点,若有左子树,则将左子树根节点入队,如果有右子树,将右子树根节点入队。然后出队,访问出队结点。。。如此循环,直到队列为空。

思路如下图:

//层次遍历
void levelTraverse(QueueNode* Q,TreeNode* T)
{
    enQueue(T,Q);//根节点入队
    while(!isEmpty(Q))//队列不为空则循环
    {
        QueueNode* node=deQueue(Q);接收出队结点
        printf("%c ",node->T->data);访问出队结点
        if(node->T->lchild)
        {
            enQueue(node->T->lchild,Q);//左子树不空,则左子树根结点入队
        }
        if(node->T->rchild)
        {
            enQueue(node->T->rchild,Q);//右子树不空,则右子树根节点入队
        }
    }

}

5.递归算法和非递归算法的转换

之前我们三种遍历都用了递归的思想,那么如果不用递归能否实现呢?我们以下图为例子来分析三种遍历的非递归算法吧:

(1)中序遍历的非递归算法

这里我们要借用栈先进后出的性质来帮助我们达成中序遍历的非递归算法:

1.先沿着根的左孩子依次入栈,直到左孩子为空为止,这时说明找到可以输出的结点,如果按照我们上图所示,此时我们栈内元素为ABD。

2.将栈顶元素出栈并访问他的右孩子,如果右孩子为空则继续访问,如果不为空,则入栈右孩子进行第一步操作

如图所示,你可能会更清楚:

void inOrder(TreeNode* T)
{
    initStack(S);//初始化栈
    TreeNode* p=T;
    while(p || !isempty(s))//栈不空或p不空时循环
    {
        if(p)
        {
            push(S,p);//当前节点入栈
            p=p->lchild;//左孩子不空,则一直向左走
        }
        else
        {
            pop(S,p);//栈顶元素出栈
            visit(p);
            p=p->rchild;//向右子树走
        }
    }
}

(2)先序遍历的非递归算法

先序遍历和中序遍历的思路是类似的,不过由于先序先访问的是根,所以我们要将访问操作结点放在入栈操作的前面。

(3)后序遍历的非递归算法

后序遍历的话和先序中序有一点差别,我们需要在结点中增加一个标志域,来记录是否被访问,以免成为死循环,算法思路如下图,我将思路总结成下面两句话

1.从根结点开始,寻找最左边的节点,并依次入栈

2.出栈前,判断栈顶元素是否有右子树,还要判断右子树是否被访问过,如果有且未被访问则将右子树入栈,重复1的操作。

 

typedef struct TreeNode{
    char data;
    struct TreeNode* lchild;
    struct TreeNode* rchild;
    int flag;//判断是否被访问的标志域元素
}TreeNode;
//链栈类型定义
typedef struct StackNode{
    TreeNode* data;//指向对应结点元素的指针
    struct StackNode* next;//指向下一个栈元素的指针
}StackNode;
//栈的初始化
StackNode* initStack()
{
    StackNode* S=(StackNode*)malloc(sizeof(StackNode));
    S->data=NULL;
    S->next=NULL;
    return S;
}
//入栈操作
void push(TreeNode* data,StackNode* S)
{
    StackNode* node=(StackNode*)malloc(sizeof(StackNode));
    node->data=data;
    node->next=S->next;
    S->next=node;
}
//判空
int isEmpty(StackNode* S)
{
    if(S->next==NULL)
        return 1;
    else
        return 0;
}
//出栈
StackNode* pop(StackNode* S)
{
    if(isEmpty(S))
    {
        return NULL;
    }
    else
    {
        StackNode* node=S->next;
        S->next=node->next;
        return node;
    }
}
//获取栈顶
StackNode* getTop(StackNode* S)
{
    if(isEmpty(S))
    {
        return NULL;
    }
    else
    {
        StackNode* node=S->next;
        return node;
    }
}
//后序的非递归遍历
void postOrder(TreeNode* T)
{
    TreeNode* node=T;
    StackNode* S=initStack();//初始化链栈
    while(node || !isEmpty(S))
    {
        if(node)
        {
            push(node,S);//入栈操作
            node=node->lchild;//将左孩子依次入栈
        }
        else
        {
            TreeNode* top=getTop(S)->data;//指向栈顶元素
            if(top->rchild&&top->flag==0)//如果有右孩子且未被访问
            {
                top=top->rchild;//将指针指向右孩子
                push(top,S);//将右孩子入栈
                node=top->lchild;//指向左孩子重复上述判断
            }
            else
            {
                top=pop(S)->data;
                printf("%c ",top->data);
                top->flag=1;//将标志域置1表示被访问过
            }
        }
        
    }
}

 6.由遍历序列构造出二叉树    

1.由二叉树的先序序列和中序序列可以唯一确定一棵二叉树

先序序列,第一个结点一定是二叉树的根结点,而在中序遍历,根结点必然将中序序列分割成两个子序列,前一个子序列是根节点的左子树的中序序列,后一个子序列是根节点的右子树的中序序列。再根据这两个子序列,在先序序列中找到对应的子序列,左子序列的第一个结点时左子树的根节点,右子序列的第一个结点是右子树的根节点,递归即可得到这个二叉树。

2.由二叉树的后序序列和中序序列也可以唯一确定一棵二叉树

后序序列的最后一个结点为根节点,和上面一样根据这个将中序序列分割成两个子序列 

3.由二叉树的层次序列和中序序列也可以唯一确定一棵二叉树

4.先序和后序不能唯一确定一棵二叉树

来看个例子吧:

已知先序为:ABCDEFG,中序为CDBAEFG,画出二叉树

 二叉树的应用

一.二叉排序树

1.定义

二叉排序树,左子树上的结点值均小于根节点的值,右子树上的结点值均大于根结点的值。

可以发现如果我们将一棵二叉排序树按中序遍历,可以得到一个递增的有序序列,因此如果用来查找效率会快很多,但是也会出现一个问题,由于没有其余限制条件,可能会导致某一侧子树长度过长如下图所示,那么这样查找起来效率会慢很多,所以后面会我们引入二叉平衡树!

 2.插入

二叉排序树的插入操作很简单,放到比较大小放在合适位置即可

TreeNode* bstInsert(TreeNode** T,int data)
{
    if(*T==NULL)//如果是空树则直接为根节点
    {
        *T=(TreeNode*)malloc(sizeof(TreeNode));//开辟空间
        (*T)->data=data;//赋值
        (*T)->lchild=NULL;//初始化
        (*T)->rchild=NULL;//初始化
    }
    else if(data<(*T)->data)//如果比根节点小则继续递归和左孩子比较
    {
        bstInsert(&((*T)->lchild),data);
    }
    else if(data==(*T)->data)//如果相等则不进行操作,因为不能有相同的数存在
        return ;
    else 
        bstInsert(&((*T)->rchild),data);//如果比根节点大则继续递归和右孩子比较
}

3.查找

//查找
TreeNode* bstSearch(TreeNode* T,int data)
{
    if(T)
    {
        if(T->data==data)
        {
            return T;
        }
        else if(data<T->data)
        {
            return bstSearch(T->lchild,data);
        }
        else
        {
            return bstSearch(T->rchild,data);
        }
    }
    else
        return NULL;
}

二.二叉平衡树

1.定义

平衡二叉树其实就是一棵合理的二叉排序树,每个节点的左子树和他的右子树的高度差至多等于1.

我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子,平衡二叉树上所有结点的平衡因子只会是-1,0,1,如果大于1则不是平衡二叉树,此时我们需要调整它成为平衡二叉树。

注:由于平衡二叉树是高度平衡的,所以我们在进行查找时效率很快,但是如果进行插入等操作由于要考虑平衡,效率则会很慢。平衡二叉树的查找算法时间复杂度为O(log2n)

2.平衡二叉树的插入

在进行插入删除等操作时,我们要考虑到是否会导致不平衡来进行调整。

注:每次调整的对象都是最小不平衡子树。

调整规律有以下四种情况:我会分别用一个例子来进行说明,但首先我们需要判断该子树的调整类型,这里我总结了一些规律让我们来判断:

1.首先我们要找到失衡树的根结点root.

2.找到导致树失衡的结点node,先判断node在root哪一侧,左侧为L,右侧为R

3.再判断node在root孩子child的哪一侧,左侧为L,右侧为R(比他小在左,比他大在右边)

4.通过2,3即可判断调整类型。

1.RR旋转

例:1 2 3 4 5

如果按照我们平时二叉排序树来进行插入那么会先出现下面这种情况:

此时已经发生了失衡情况根结点的平衡因子为-2,首先我们要判断调整类型,通过上图我们可以发现node在root的右侧,在child的右侧,所以我们需要用到RR平衡旋转类型对它进行调整再进行数据的插入,这里我总结了这个类型的调整方法:

取中间的结点,使它的父亲成为它的左孩子,如果它有左孩子,那么这个左孩子连接到父亲的右孩子上面(因为它的左孩子肯定比父亲大

于是通过两个操作我们将它旋转:

root为1是根结点,child为2结点

1.root->rchild=child->lchild

2.child->lchild=root

接着我们再将剩余数据插入,但是我们会发现插入后还会发生失衡问题,如下图,我们发现此时两棵树都发生了失衡状况,此时我们要先处理小树和上一步骤一样我们先判断调整类型,再进行对应操作,类型依然为RR

root为3结点,child为4结点

1.root->rchild=child->lchild

2.child->lchild=root

 于是旋转为下图:

2.LL旋转

例:5 4 3 2 1

如果按照平时的二叉排序会成为下面情况

 按照我们上述的方法我们可以判断出类型为LL,我总结了LL型的调整方法如下:

取中间的结点,使它的父亲成为它的的右孩子,如果自己有右孩子的话,那么将自己的右孩子连接到父亲的左孩子上。

通过下面两个操作来调整:

1.root->lchild=child->rchild

2.child->rchild=root

 可以看到我们调整了两次才达到效果,所以在代码中每次插入一个数据我们都需要判断左右子树高度差来决定是否需要再次调整。

3.LR旋转

例:8 7 9 5 6

按照二叉排序树,出现下面这种情况,按照规律我们先调整小树:

 调整操作如下:

取最后一个结点作为父亲,将他的父亲作为自己的左孩子,将它父亲的父亲作为自己的右孩子,如果它有左孩子或者右孩子,将它原来的左孩子连接到父亲的右孩子上,它的右孩子连接到父亲的左孩子上

结果如下:

4.RL旋转

例:1 8 6 7 10

 还是一样的操作先判断类型,明显这里是RL类型,因为node在root右侧,在child左侧,调整方法如下:

取最后一个结点,将它的父亲作为自己的右孩子,将它父亲的父亲作为自己的左孩子

如果自己有左孩子或者右孩子,自己原来的左孩子连接到父亲的父亲的右孩子上,他原来的右孩子连接到父亲的左孩子上

注意:其实RL和LR都是基于RR,LL上的旋转,RL是先LL在RR,而LR是先RR再LL。

二叉排序树还有另外的平衡算法,如红黑树,相比较下各有优势,红黑树在进行查找和增加删除等操作效率快,相比平衡二叉树在进行增删时效率没有那么快,因为还要考虑到平衡。

红黑树

1.根结点为黑色

2.每一个节点不是黑色就是红色

3.每个叶子结点是黑色的。

4.如果一个结点是红色的,那么子节点必须为黑色。

5.从一个节点到该节点的叶子节点的所有路径上包含相同数目的黑节点。

三.哈夫曼树

 1.定义

将树中结点赋值,这个值称为这个结点的权值。从树的根到任意结点的路径长度与该节点上权值的乘积,称为该节点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度。

在含有n个叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也叫做最优二叉树如下图所示:

 2.构造哈夫曼树

那么如何通过权值结点来构造一棵哈夫曼树呢,我总结了两个步骤:

1.从结点列表中选出权值第一小和第二小的节点,并组成一棵树,父亲结点的权值等于两结点权值之和。

2.将生成的新树再次放到结点列表之中重复第一步,直到列表中只剩下一个结点

我们来看一个例子就清楚了:

 3.哈夫曼编码

哈夫曼编码是一种被广泛应用且非常有效的数据压缩编码,将左右子树权值变成0,1,再来进行编码。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构-树 的相关文章

随机推荐

  • DuplexPipe二三事(五)——来自内网的呼唤

    穿越防火墙 你是否曾经尝试过去连接一台远程计算机 却因为被防火墙拦截或路由器没有转发而造成无法通信 这是主动式连接的一个弊端 它依赖服务器的状态 而对服务器有生杀大权的只有管理员 如果能让服务器主动尝试连接我们的计算机 那就没问题了 因为防
  • linux不能编译内核,linux – 内核编译时无法统计错误

    我试图按照here所述的方式在Ubuntu 14 04中编译内核版本4 10 1 它一直工作到4 9 x版本 当4 10 x出现时 我不断收到以下错误 install p o root g root m 644 CREDITS usr sr
  • OpenCV Python 系列教程4 - OpenCV 图像处理(上)

    import cv2 cv2 version 3 4 1 更改色彩空间 学习目标 改变色彩空间 B G R G r a
  • UnicodeEncodeError: 'gbk' codec can't encode character '\xa0' in position ... 问题解决办法之一

    使用Python写文件的时候 或者将网络数据流写入到本地文件的时候 大部分情况下会遇到 UnicodeEncodeError gbk codec can t encode character xa0 in position 这个问题 网络上
  • 不愧是美团内部 “接口自动化测试学习笔记” 这细节讲解,神了

    Lego 美团接口自动化测试实践 1 1接口自动化概述 众所周知 接口自动化测试有着如下特点 1 低投入 高产出 2 比较容 实现自动化 3 和UI自动化测试相比 加稳定 如何做好一个接口自动化测试项目呢 我认为 一个 好的 自动化测试项目
  • Android 13 网络 Adb相关流程深入分析研究

    Android 13 网络 Adb 分析研究 文章目录 Android 13 网络 Adb 分析研究 一 前言 二 默认adb 代码实现 关键 1 修改的目录 2 具体修改 1 在XXX device mk 添加属性 2 设置固定端口号 3
  • 高并发场景下缓存+数据库双写不一致问题分析和解决方案设计

    一 业务场景 库存系统 库存可能会修改 每次修改都要去更新这个缓存 redis 数据 每次库存的数据在缓存中一旦过期 或者是被清理掉了 前端的nginx服务都会发送请求给库存服务 去获取相应的数据 实际上的处理流程没有这么的简单 这里 其实
  • mysql 语句优化的十个经验

    mysql 语句优化的十个经验mysql 语句优化的十个经验 本文算是前一篇 查询语句优化经验总结1的后续 总结了 lt 高性能mysql gt 中与网上常见的一些优化经验中出现的案例进行总结与勘误 但是要注意本文中出现的explain结论
  • anaconda常用命令大全(保姆级别建议收藏)

    一 创建虚拟环境 conda create name env name conda create name env name python 3 6 创建指定python版本 conda create name env name python
  • jmeter分布式压测 linux

    主机master修改 jmeter properties server rmi ssl disable true server port 1099 remote hosts 192 168 36 131 1099 slave分机的ip地址
  • 【python】numpy的array数组与pandas的DataFrame表格互相转换(图文代码超详细)

    目录 0 环境 1 array数组和DataFrame表格的简单介绍 2 转换方式详解 代码 0 前提 需注意 1 array转化为DataFrame 2 DataFrame转化为array 3 完整代码 0 环境 windows jupy
  • WAMP环境隐藏PHP文件实际路径和后缀名

    有时候做客户端开发阶段得测试 需要一个模拟服务器的环境 我使用得最顺手得还是WAMP环境 后台给出的api接口的路径千奇百怪 在WAMP环境中如何模拟这些路径呢 如何将某个路径下的PHP文件映射到另一个URL路径下并隐藏PHP文件后缀呢 在
  • MySQL数据库安装实践 Part 1:单实例部署

    1 MySQL的安装方法介绍 当今的互联网企业中 MySQL数据库大多运行在linux系列操作系统 若应用场景不同 版本不同 MySQL数据库的安装方法也会有区别 下面把常见的几种方法介绍给朋友们 1 1 yum rpm方式安装 MySQL
  • 【zookeeper】raft 共识算法 动画演示 网站

    1 概述 地址 https cyberdak github io thesecretlivesofdatacn raft
  • 中国计算机大会CNCC技术论坛

    第十五届中国计算机大会 CNCC2018 将于 2018 年 10 月 25 27 日在杭州国际博览中心举行 本届大会以 大数据推动数字经济 Big Data Drives the Digital Economy 为主题 探讨计算技术领域最
  • Java-String类的常用方法

    Java String类的常用方法 1 常用方法1 int length 返回字符串的长度 return value length char charAt int index 返回某索引处的字符return value index bool
  • 数据库表字段命名规范

    本文是一篇包含了数据库命名 数据库表命名 数据库表字段命名及SQL语言编码的规范文档 针对研发中易产生的问题和常见错误做了一个整理和修改 为日后涉及到数据库相关的研发工作做好准备 一 数据库命名规范 采用26个英文字母 区分大小写 和0 9
  • 有关深度学习的文章

    https zybuluo com hanbingtao note 485480 https tigerneil gitbooks io neural networks and deep learning zh content chapte
  • OrCAD原理图绘制使用操作

    文章目录 工程的创建 原理图整体设置 调用元器件库 常用元器件库调用 key 一些元器件库介绍 key 常用元器件搜索名 自建元器件库 新建元器件库 新建元器件 绘制元器件管脚设置 key Homogeneous和Heterogeneous
  • 数据结构-树

    目录 树 知识框架 一 树的基本概念 1 树的定义 2 基本术语 3 树的性质 二叉树 一 二叉树的概念 1 二叉树的定义 2 特殊的二叉树 3 二叉树的一些性质 4 二叉树的存储结构 1 顺序存储 2 链式存储 二 二叉树的创建和遍历 1