【数据结构】第5章 树与二叉树

这是本人根据王道考研数据结构课程整理的笔记,希望对您有帮助。

5.1 树

5.1.1 树的定义和基本术语

树的基本概念
在这里插入图片描述

非空树的特性:

  • 有且仅有一个根节点
  • 没有后继的结点称为“叶子结点”(或终端结点)
  • 有后继的结点称为“分支结点”(或非终端结点)
  • 除了根节点外,任何一个结点都有且仅有一个前驱
  • 每个结点可以有0个或多个后继

空树:结点数为0的树

子树
在这里插入图片描述

森林:森林是 m ( m ≥ 0 ) m(m\ge0) m(m0)棵互不相交的树的集合(森林可以和树发生转换)
在这里插入图片描述


结点之间的关系描述
在这里插入图片描述

  • 祖先结点:父亲、爷爷
  • 子孙结点:K、L
  • 父结点(双亲结点):父亲
  • 孩子结点:K、L
  • 兄弟结点:F
  • 堂兄弟结点:G、H、I、J

  • 两个结点之间的路径:只能从上往下
  • 路径长度:经过几条边
  • 结点的层次深度):从上往下数(默认从1开始)
  • 结点的高度:从下往上数
  • 树的高度深度):总共多少层
  • 结点的:有几个孩子(分支)
  • 树的:各结点的度的最大值

  • 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
  • 无序树:逻辑上看,树中结点的各子树从左至右是无次序的,不能互换
5.1.2 树的性质
  1. 结点数 = 总度数 + 1

  2. 度为m的数 和 m叉树 的区别
    在这里插入图片描述

  3. 【度为 m m m 的树 / m m m 叉树】第 i i i 层至多有 m i − 1 m^{i-1} mi1 个结点( i ≥ 1 i\ge1 i1

  4. 高度为 h h h m m m 叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1 个结点

  5. 高度为 h h h m m m 叉树至少有 h h h 个结点。

  6. 高度为 h h h ,度为 m m m 的树至少有 h + m − 1 h+m-1 h+m1 个结点
    在这里插入图片描述

  7. 具有 n n n 个结点的 m m m 叉树的最小高度为 ⌈ log ⁡ m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1) \rceil logm(n(m1)+1)(向上取整)

5.2 二叉树

5.2.1 二叉树的定义和基本术语

二叉树的特点

  1. 每个结点至多只有两颗子树
  2. 左右子树不能颠倒(二叉树是有序树
  3. 二叉树由一个根结点和两个互不相交的左子树右子树组成。左子树和右子树又分别是一颗二叉树。
    在这里插入图片描述

二叉树的五种状态
在这里插入图片描述

特殊的二叉树

满二叉树:一棵高度为 h h h,且含有 2 h − 1 2^h-1 2h1个结点的二叉树
在这里插入图片描述

特点:

  1. 只有最后一层有叶子结点
  2. 不存在度为1的结点
  3. 按层序从1开始编号,结点 i i i 的左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1;结点 i i i 的父节点为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i(如果有的话)

完全二叉树:当且仅当其每个结点都与高度为 h h h 的满二叉树中编号为 1 ∼ n 1 \sim n 1n 的结点一一对应时,称为完全二叉树。
在这里插入图片描述

特点:

  1. 只有最后两层可能有叶子结点
  2. 最多只有一个度为1的结点
  3. 按层序从1开始编号,结点 i i i 的左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1;结点 i i i 的父节点为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i(如果有的话)
  4. i ≤ ⌊ n 2 ⌋ i \le\lfloor \frac{n}{2} \rfloor i2n为分支结点, i > ⌊ n 2 ⌋ i \gt \lfloor \frac{n}{2} \rfloor i>2n为叶子结点
  5. 如果某结点只有一个孩子,那么一定是左孩子

二叉排序树(可用于元素的排序、搜索)
在这里插入图片描述

  1. 左子树上所有结点的关键字小于根节点的关键字
  2. 右子树上所有结点的关键字大于根节点的关键字
  3. 左子树和右子树又各是一颗二叉排序树

平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
在这里插入图片描述
特点:平衡二叉树能有更高的搜索效率

5.2.2 二叉树的性质

二叉树的性质

  1. 设非空二叉树中度为0、1和2的结点个数分别为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1(叶子结点比二分支结点多一个)
    在这里插入图片描述

  2. 二叉树第 i i i 层至多有 2 i − 1 2^{i-1} 2i1 个结点 ( i ≥ 1 ) (i\ge1) (i1)

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

完全二叉树的性质

  1. 具有 n n n ( n > 0 (n\gt0 (n>0) 结点的完全二叉树的高度 h h h ⌈ log ⁡ 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil log2(n+1) ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor +1 log2n+1
    在这里插入图片描述
    在这里插入图片描述

  2. 对于完全二叉树,可以由结点数 n n n 推出度为0、1和2的结点个数为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2
    在这里插入图片描述

5.2.3 二叉树的存储结构

顺序存储

#define MaxSize 100
struct TreeNode
{
    ElemType value;	//结点中的数据元素
    bool isEmpty;	//结点是否为空
};
TreeNode t[MaxSize];
//定义一个长度为MaxSize的数组t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点

基本操作:

  • i i i 的左孩子: 2 i 2i 2i
  • i i i 的右孩子: 2 i + 1 2i+1 2i+1
  • i i i 的父结点: ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i
  • i i i 所在的层次: ⌈ log ⁡ 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil log2(n+1) ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor +1 log2n+1

完全二叉树中共有 n n n 个结点,则:

  • 判断 i i i 是否有左孩子: 2 i ≤ n 2i\le n 2in
  • 判断 i i i 是否有右孩子: 2 i + 1 ≤ n 2i+1\le n 2i+1n
  • 判断 i i i 是否是叶子/分支结点: i > ⌊ n 2 ⌋ i\gt\lfloor \frac{n}{2} \rfloor i>2n

✨二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。(只适合存储完全二叉树)
在这里插入图片描述


链式存储

typedef struct BiTNode
{
    ElemType data;	//数据域
    struct BiTNode *lchild, *rchild;	//左、右孩子指针
}BiTNode, *BiTree;

在这里插入图片描述

n n n 个结点的二叉链表共有 n + 1 n+1 n+1 个空链域(可用于构造线索二叉树)

//【使用示例】
struct ElemType
{
    int value;
}
typedef struct BiTNode
{
    ElemType data;	//数据域
    struct BiTNode *lchild, *rchild;	//左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根结点
root = (BiTNode *)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;	//作为根结点的左孩子

如果在使用场景中需要寻找父结点,可以考虑使用三叉链表,方便寻找父结点:

typedef struct BiTNode
{
    ElemType data;	//数据域
    struct BiTNode *lchild, *rchild;	//左、右孩子指针
    struct BiTNode *parent;				//父结点指针
}BiTNode, *BiTree;

5.3 二叉树的遍历和线索二叉树

5.3.1 二叉树的遍历

先序遍历:左右(NLR)

void PreOrder(BiTree T)
{
    if(T != NULL)
    {
        visit(T);	//访问根结点
        PreOrder(T->lchild);	//递归遍历左子树
        PreOrder(T->rchild);	//递归遍历右子树
    }
}

中序遍历:左右(LNR)

void InOrder(BiTree T)
{
    if(T != NULL)
    {
        PreOrder(T->lchild);	//递归遍历左子树
        visit(T);	//访问根结点
        PreOrder(T->rchild);	//递归遍历右子树
    }
}

后序遍历:左右(LRN

void PostOrder(BiTree T)
{
    if(T != NULL)
    {
        PreOrder(T->lchild);	//递归遍历左子树
        PreOrder(T->rchild);	//递归遍历右子树
        visit(T);	//访问根结点
    }
}
5.3.2 二叉树的层序遍历

算法思想:

  1. 初始化一个辅助队列
  2. 根节点入队
  3. 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
  4. 重复3直至队列为空
    在这里插入图片描述
//二叉树的结点(链式存储)
typedef struct BiTNode
{
    char data;
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode
{
    BiTNode *data;	//存指针而不是结点
    struct LinkNode *next;
}LinkNode;
typedef struct
{
    LinkNode *front, *rear;	//队头队尾
}LinkQueue;
//【层序遍历】
void LevelOrder(BiTree T)
{
    LinkQueue Q;
    InitQueue(Q);		//初始化辅助序列
    BiTNode* p;
    EnQueue(Q, T);		//将根结点入队
    while(!isEmpty(Q))	//队列不空则循环
    {
        DeQueue(Q, p);	//队头结点出队
        visit(p);		//访问出队结点
        if(p->lchild != NULL)
            EnQueue(Q, p->lchild);	//左孩子入队
        if(p->rchild != NULL)
            EnQueue(Q, p->rchild);	//右孩子入队
    }
}
5.3.3 由遍历序列构造二叉树

若只给出一棵二叉树的前序/中序/后序/层序遍历序列中的一种,不能唯一确定一棵二叉树。

前序+中序/后序+中序/层序+中序可以唯一确定一棵二叉树。(找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根节点)

5.3.4 线索二叉树的概念

约定:根据遍历序列定义前驱/后继的概念

中序线索二叉树
在这里插入图片描述

typedef struct ThreadNode
{
    ElemType data;	//数据域
    struct ThreadNode *lchild, *rchild;	//左、右孩子指针/前驱、后继线索指针
    int ltag,rtag;	//左、右线索标志
    //【tag=0,表示指针指向孩子】
    //【tag=1,表示指针是“线索”】
}ThreadNode, *ThreadTree;

在这里插入图片描述


先序线索二叉树
在这里插入图片描述


后续线索二叉树
在这里插入图片描述

5.3.5 二叉树的线索化

中序线索化

//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;

//中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
    pre = NULL;		//pre初始化为NULL
    if(T != NULL)	//非空二叉树才能线索化
    {
        InThread(T);	//中序线索化二叉树
        if(pre->rchild == NULL)
            pre->rtag = 1;	//处理遍历的最后一个结点
    }
}

//线索二叉树结点
typedef struct ThreadNode
{
    ElemType data;	//数据域
    struct ThreadNode *lchild, *rchild;	//左、右孩子指针/前驱、后继线索指针
    int ltag,rtag;	//左、右线索标志
    //【tag=0,表示指针指向孩子】
    //【tag=1,表示指针是“线索”】
}ThreadNode, *ThreadTree;

//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T)
{
    if(T != NULL)
    {
        InThread(T->lchild);	//中序遍历左子树
        visit(T);	//访问根结点
        InThread(T->rchild);	//中序遍历右子树
    }
}

void visit(ThreadNode *q)
{
    if(q->lchild == NULL)	//左子树为空,建立前驱线索
    {
        q->lchild = pre;
        q->ltag = 1;
    }
    if(pre != NULL && pre->rchild == NULL)
    {
        pre->rchild = q;	//建立前驱结点的后继搜索
        pre->rtag = 1;
    }
    pre = q;
}

(先序和后序略,如果用到去视频找)

5.3.6 在线索二叉树中找前驱后继

中序线索二叉树找中序后继
在这里插入图片描述

//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p)
{
    //循环找到最左下结点(不一定是叶结点)
    while(p->ltag == 0)
        p = p->lchild;
    return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p)
{
    //右子树中最左下结点
    if(p->rtag == 0)
        return Firstnode(p->rchild);
    else
        return p->rchild;	//rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T)
{
    for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
        visit(p);
}

中序线索二叉树找中序前驱
在这里插入图片描述

//找到以p为根的子树中最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p)
{
    //循环找到最右下结点(不一定是叶结点)
    while(p->rtag == 0)
        p = p->rchild;
    return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p)
{
    //左子树中最右下结点
    if(p->ltag == 0)
        return Lastnode(p->lchild);
    else
        return p->lchild;	//ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历(利用线索实现的非递归算法)
void RevInorder(ThreadNode *T)
{
    for(ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p))
        visit(p);
}

(先序和后序略,如果用到去视频找)
在这里插入图片描述

5.4 树、森林

5.4.1 树的存储结构

双亲表示法(顺序存储):每个结点中保存指向双亲的“指针”
在这里插入图片描述

#define MAX_TREE_SIZE 100	//树中最多结点树
//树的结点定义
typedef struct
{
    ElemType data;	//数据元素
    int parent;		//双亲位置域
}PTNode;
//树的类型定义
typedef struct
{
    PTNode nodes[MAX_TREE_SIZE];	//双亲表示
    int n;	//结点数
}PTree;

缺点:查找操作特别困难,需要从头到尾遍历

孩子表示法(顺序+链式存储)

struct CTNode
{
    int child;	//孩子结点在数组中的位置
    struct CTNode *next;	//下一个孩子
};
typedef struct 
{
    ElemType data;
    struct CTNode *firstChild;	//第一个孩子
}CTBox;
typedef struct
{
    CTBox nodes[MAX_TREE_SIZE];
    int n, r;	//结点数和根的位置
}CTree;

在这里插入图片描述

特点:查找孩子很方便,但是查找双亲很困难

孩子兄弟表示法(链式存储)

typedef struct CSNode
{
    ElemType data;	//数据域
    struct CSNode *firstchild, *nextsibling;	//第一个孩子和右兄弟指针
}CSNode, *CSTree;

相当于二叉链表中的左指针指向孩子,右指针指向兄弟
在这里插入图片描述

优点:可以用熟悉的二叉树操作来处理树

森林和二叉树的转换
在这里插入图片描述

5.4.2 树和森林的遍历

先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历。——深度优先遍历

void PreOrder(TreeNode *R)
{
    if(R != NULL)
    {
        visit(R);	//访问根结点
        while(【R还有下一个子树T】)
            PreOrder(T);	//先根遍历下一棵子树
    }
}

树的先根遍历序列与这棵树相应的二叉树的先序序列相同。

后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。——深度优先遍历

void PostOrder(TreeNode *R)
{
    if(R != NULL)
    {
        while(【R还有下一个子树T】)
            PreOrder(T);	//先根遍历下一棵子树
        visit(R);	//访问根结点
    }
}

树的后根遍历序列与这棵树相应的二叉树的中序序列相同。

层次遍历:与二叉树一致(用队列实现)——广度优先遍历


森林的先序遍历:对各个子树进行先根遍历 【or】 将森林转成二叉树,对二叉树进行先序遍历

森林的中序遍历:对各个子树进行后根遍历 【or】 将森林转成二叉树,对二叉树进行中序遍历

5.5 树与二叉树的应用

5.5.1 二叉排序树(BST)

二叉排序树,又叫二叉查找树,BST, Binary Search Tree

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字
  • 左子树和右子树又各是一棵二叉排序树
  • 左子树结点值 < 根结点值 < 右子树结点值
  • 对二叉排序树进行中序遍历,可以得到一个递增的有序序列

二叉排序树的查找

//【二叉排序树结点】
typedef struct BSTNode
{
    int key;
    struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//【在二叉排序树中查找值为key的结点】
BSTNode *BST_Search(BSTree T, int key)
{
    while(T != NULL && key != T->key)	//若树空或等于根结点值,则结束循环
    {
        if(key < T->key)	//小
            T = T->lchild;
        else
            T = T->rchild;
    }
    return T;
}
//【在二叉排序树中查找值为key的结点(递归实现)】
BSTNode *BST_Search(BSTree T, int key)
{
    if(T == NULL)
        return NULL;	//查找失败
    if(key == T->key)
        return T;		//查找成功
    else if(key < T->key)
        return BSTSearch(T->lchild, key); 	//在左子树中找
    else
        return BSTSearch(T->rchild, key);	//在右子树中找
    }
    return T;
}

二叉排序树的插入
在这里插入图片描述

若原二叉排序树为空,则直接插入结点。否则,若关键字 k k k小于根结点值,则插入到左子树;若关键字 k k k 大于根结点值,则插入到右子树。

//【在二叉排序树插入关键字为k的新结点(递归实现)】
int BST_Insert(BSTree &T, int k)
{
    if(T == NULL)	//原树为空,新插入的结点为根结点
    {
        T = (BSTree)malloc(sizeof(BSTNode));
        T->key = k;
        T->lchild = T->rchild = NULL;
        return 1;	//返回1,插入成功
    }
    else if(k == T->key)	//树中存在相同关键字的结点,插入失败
        return 0;
    else if(k < T->key)		//插入到T的左子树
        return BST_Insert(T->lchild, k);
    else					//插入到T的右子树
        return BST_Insert(T->rchild, k);
}

二叉排序树的构造

//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T, int str[], int n)
{
    T = NULL;	//初始时T为空树
    int i = 0;
    while(i < n)	//依次将每个关键字插入到二叉排序树中
    {
        BST_Insert(T, str[i]);
        i++;
    }
}

二叉排序树的删除

先搜索找到目标结点:

  1. 若被删除结点 z z z叶结点,则直接删除,不会破坏二叉排序树的性质。
  2. 若结点 z z z 只有一棵左子树或右子树,则让 z z z 的子树成为 z z z 父节点的子树,替代 z z z 的位置。
  3. 若结点 z z z 有左、右两棵子树,则令 z z z 的直接后继(或直接前驱)替代 z z z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

注:

  1. z z z前驱 z z z左子树最右下结点(该结点一定没有右子树
  2. z z z后继 z z z右子树最左下结点(该结点一定没有左子树
5.5.2 平衡二叉树(AVL)

平衡二叉树:树上任一结点的左子树和右子树的高度之差不超过1

结点的平衡因子 = 左子树高 - 右子树高

//平衡二叉树结点
typedef struct AVLNode
{
    int key;		//数据域
    int balance;	//平衡因子
    struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;

最小不平衡子树:从插入点往回找到的第一个不平衡结点

每次调整的对象都是“最小不平衡子树”。
在这里插入图片描述

调整最小不平衡子树:

  • LL:在A的左孩子左子树中插入导致不平衡
  • RR:在A的右孩子右子树中插入导致不平衡
  • LR:在A的左孩子右子树中插入导致不平衡
  • RL:在A的右孩子左子树中插入导致不平衡

调整最小不平衡子树(LL)
在这里插入图片描述

调整最小不平衡子树(RR)
在这里插入图片描述

调整最小不平衡子树(LR)
在这里插入图片描述

在这里插入图片描述

调整最小不平衡子树(RL)
在这里插入图片描述
在这里插入图片描述

5.5.3 哈夫曼树

结点的:有某种现实含义的数值(如:表示结点的重要性等)

结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。

树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
WPL = ∑ i n w i l i \text{WPL}=\sum_i^n{w_il_i} WPL=inwili
在这里插入图片描述

在含有 n n n 个带权结点的二叉树中,其中带权路径长度WPL最小的二叉树称为哈夫曼树,也称最优二叉树


哈夫曼树的构造

  1. 将这 n n n 个结点分别作为 n n n 棵仅含一个结点的二叉树,构成森林 F F F
  2. 构造一个新结点,从 F F F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  3. F F F 中删除刚才选出的两棵树,同时将新得到的树加入 F F F 中。
  4. 重复步骤2和3,直至 F F F 中只剩下一棵树为止。
    在这里插入图片描述

结论:

  • 每个初始结点最终都称为叶结点,且权值越小的结点到根结点的路径长度越大
  • 哈夫曼树的结点总数为 2 n − 1 2n-1 2n1
  • 哈夫曼树中不存在度为1的结点
  • 哈夫曼树并不唯一,但WPL必然相同且为最优

应用:哈夫曼编码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Quentin_HIT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值