《数据结构》学习系列——树(中)

系列文章目录

目录

  • 线索二叉树
    • 定义
    • 结构
    • 链接表示
    • 问题与改进
    • 优点
    • 基本操作
      • 搜索线索二叉树中根序列的第一个结点
      • 搜索线索二叉树中根序列的最后一个结点
      • 查找结点 p 的中根前驱结点
      • 查找结点 p 的中根后继结点
  • 树和森林
    • 定义
    • 树转化为二叉树
    • 二叉树转化为树
    • 森林转化为二叉树
    • 二叉树转化为森林
  • 树的顺序存储结构
    • 双亲表示法
    • 孩子表示法
    • 先根序列及结点次数表示法
    • 树的后根序列及结点次数表示法
    • 层次序列表示法
  • 树的链式存储结构
    • 父亲连接结构
    • 孩子链表结构
    • 父亲—孩子链表结构
    • 孩子—兄弟链接存储结构
  • 孩子顺序表示法
      • 基本算法
        • 搜索父节点
        • 搜索指定数据域的结点
        • 搜索大儿子结点和大兄弟结点
        • 删除子树


线索二叉树

定义

通过 遍历二叉树 可得到一个结点的一个线性序列,在线性序列中,除第一个结点外, 每个结点有且仅有一个前驱,除最后一个结点外,每个结点有且仅有一个后继,但是在二叉树中只能找到结点的左孩子、右孩子,结点在线性序列中的前驱和后继只有在遍历过程中才能得到

为了与结点在二叉树中所具有的前驱(即父结点)和后继(即子结点)区别开来,通常把某种序列中结点的前驱或后继冠以某种遍历的名称,如把中根序列中结点的前驱称作中根前驱,结点的后继称作中根后继

设 T 是由增加某种遍历顺序的线索域的结点所构成的一棵二叉树,在 T 中可直接查找任一结点在这种遍历顺序下的前驱和后继结点,则称 T*为线索二叉树(Threaded Binary Tree)


结构

指示在某种遍历次序下的 前驱和后继的指针称为线索

  • 加上线索的二叉树称为线索二叉树
  • 对二叉树以某种次序进行遍历使其变为线索二叉树的过程称为 线索化
  • 按中序遍历得到的线索二叉树称为中序线索二叉树,按先序遍历得到的线索二叉树称为先序线索二叉树,按后序遍历得到的线索二叉树称为后序线索二叉树

img


链接表示


问题与改进

  • 问题:
    • 线索二叉树的结点中仍有很多空指针,就是说存储空间的浪费问题还未得到有效解决
  • 分析:
    • 指针域需占用较多的存储空间,若将指针域中的 Pred 和 Succ 换成 1 个二进制位则会节省许多空间
  • 改进:
    • 将原指针域 Pred 和 Succ 分别改成存储一个二进制位的域 LThread 和 RThread
    • 若结点 t 有左孩子,则 Left 指向 t 的左孩子,且 LThread 值为 0;若 t 没有左孩子,则 Left 指向 t 的前驱结点,且 LThread 值为 1,此时称 Left 为线索
    • 若结点 t 有右孩子,则 Right 指向 t 的右孩子,且 RThread 值为 0;若 t 没有右孩子,则 Right 指向 t 的后继结点,且 RThread 值为 1,此时称 Right 为线索

改进后结构

img

img

img


优点

  • 中序线索二叉树 中不需要对二叉树进行遍历就可以方便地找到给定结点的中序前驱和中序后继结点, 并且不需要太多额外的空间
  • 线索二叉树中一个结点是叶结点的充要条件为: 左、右标志(LThread、RThread)均是 1

基本操作

搜索线索二叉树中根序列的第一个结点

思想

  • 若 t 有左子树, 中根序列的第一个结点就是其左子树中最左下的结点
  • 若 t 无左子树, 中根序列的第一个结点就是 t

伪代码

FIO(root,q)
{
    q = root;  // 初始化
    while LThread(q) = 0   //找二叉树在中根序列下第一个被访问的结点
    {
        q = Left(q)
    }
} 

搜索线索二叉树中根序列的最后一个结点

思想

  • 若 t 有右子树,中根序列的最后一个结点就是其右子树中最右下的结点
  • 若 t 无右子树,中根序列的最后一个结点就是 t

伪代码

LIO(root,q)
{
    q = root;
    while RThread(q) = 0
    {
        q = Right(q)
    }
}

查找结点 p 的中根前驱结点

思想

  • 若结点 p 是中根序列的第一个结点,则 p 无中根前驱结点;(情况 1)
  • 若结点 p 非中根序列的第一个结点:
    • 若 LThread§= 1,则 Left§为左线索,直接指向 p 的中根前驱结点;(情况 2)
    • 若 LThread§= 0,p 的中序前驱结点是 p 之左子树中根序列的末结点。(情况 3)

伪代码

PIO(root,p,q) // q 为目标结点
{
    FIO(root,first); // 求线索二叉树中根序列的首结点
    if p = first  { q = NULL;  // 首结点无前驱 }
    if LThread = 1 { p = Left(p); // LThread = 1 时,左指针指向前驱结点 }
    else { LIO(Left(p),q); // p的左子树中根序列末结点为前驱}   
}

查找结点 p 的中根后继结点

思想

  • 若结点 p 是中序序列的末结点,则其无后继结点;(情况 1)
  • 若结点 p 非中序序列的末结点:
    • 若 RThread§= 1,则 Right§指向 p 之中序后继;(情况 2,Right§为右线索)
    • 若 RThread§= 0,则 p 之中序后继为 p 的右子树的中根序列的首结点。(情况 3)

伪代码

NIO(root,p,q)
{
    LIO(root,last); // 求中根序列末结点
    if p = last { q = NULL }; // 末结点无后继
    if RThread(p) = 1 { p = Right(p) } // RThread(p) = 1时,右指针指向后继结点
    else { FIO( Right(p),q ) }
}

树和森林

定义

树是由 n(n > 0)个结点组成的有限集合。

  • 有一个特定的称之为根(root)的结点,它只有直接后继,但没有直接前驱

  • 除根以外的其它结点划分为 m(m≥0)个互不相交的有限集合 T 0 T_0 T0, T 1 T_1 T1,…, T m − 1 T_{m-1} Tm1, 每个集合又是一棵树, 并且称之为根的子树(subTree)

  • 每棵子树的根结点有且仅有一个直接前驱,但可以有 0 个或多个直接后继


树转化为二叉树

  • 树或森林与二叉树之间有一种自然的对应关系。任一个森林或一棵树都可对应到一棵二叉树; 反之, 任一棵二叉树也能唯一对应一个森林或一棵树

  • 按如下步骤可将任一棵树 T 自然地转换成相应的二叉树:

    • 在所有兄弟结点间加一条连线
    • 对 T 中每个结点 p, 除保留与其大孩子和大兄弟结点的连线之外,去掉 p 与其它孩子结点间的连线
    • 调整部分连线方向、长短使之成为符合二叉树规范的图形

img


二叉树转化为树

  • 对每个右孩子结点,找其大兄弟结点的父结点,并在两者间加一连线
  • 去掉所有父结点和右孩子之间的连线
  • 调整部分连线方向、长短使之成规范图形

img


森林转化为二叉树

定义

设F=( T 1 T_1 T1, T 2 T_2 T2,…, T n T_n Tn)表示由树 T 1 T_1 T1, T 2 T_2 T2,…, T n T_n Tn组成的森林,森林F自然对应下的二叉树B(F)递归定义如下:

  • 若n = 0,则B(F)为空
  • 若n > 0,则B(F)的根是Root( T 1 T1 T1),B(F)的右子树是B(( T 2 T_2 T2, T 3 T_3 T3…, T n T_n Tn)),左子
    树是B(( T 11 T_{11} T11, T 12 T_{12} T12,…, T 1 m T_{1m} T1m)) ,其中 T 11 T_{11} T11, T 12 T_{12} T12,…, T 1 m T_{1m} T1m是Root( T 1 T_1 T1)的诸子树

方法一

  • 把森林看成一棵树,森林中所有树的根结点彼此看成兄弟结点
  • 为此在形式上引进一个虚拟总根 R,作为所有树根的父结点,当转换成二叉树时,虚拟总根R不起任何作用

img

img

方法二

  • 将森林中每一棵树转换成二叉树,因为每棵二叉树之根结点的右子树均为空。故可将第一棵二叉树的根结点视为总根,将其他二叉树的根结点彼此视为兄弟,依次从左到右连接在一起

img


二叉树转化为森林

定义

设二叉树T的根是Root(T),T的左子树是L,T的右子树是R,二叉树T在自然对应下的森林F(T)递归定义如下

  • 若T为空,则F(T)为空的森林;
  • 若T非空,则F(T)由第一棵树 T 1 T_1 T1和森林F®组成,其中T1是以Root(T)为根的树, T 1 T_1 T1的诸子树由森林F(L)组成

方法

若根结点的右子树非空,则可将二叉树转换成相应的森林。具体转换办法如下:

  • 从根结点出发,断开其与右孩子的连线,得到多个二叉树
  • 将每个二叉树,转换成树形,就得到对应的森林

img


树的顺序存储结构

双亲表示法

结构:层次顺序+父节点下标

img


孩子表示法

结构层次顺序+子结点下标

img


先根序列及结点次数表示法

  • 定理:如果已知一棵树的先根序列和每个结点相应的次数(孩子个数),则能唯一确定该树的结构
  • 证明:用数学归纳法
    • 若树中只有一个结点,定理显然成立
    • 假设树中结点个数小于n(n≥2)时定理成立
    • 当树中有n个结点时,由树的先根序列可知,第一个结点是根结点,设该结点的次数为,,k≥1,因此根结点有k个子树
    • 第一个子树排在最前面,第k个子树排在最后面,并且每个子树的结点个数小于n,由归纳假设可知,每个子树可以唯一确定,从而整棵树的树形可以确定

例子

img

可从右向左来确定树T的结构,由先根序遍历的定义可知,l 是 J 和 K 的父结点;同样,H 是 I 和 L 的父结点;C 是 D 、E 和 F 的父结点;A 是 B 、 C 、 G 和 H 的父结点


树的后根序列及结点次数表示法

树的后根遍历递归定义

  • 从左向右依次后根遍历根结点的诸子树(若存在)
  • 访问树的根结点

例子

img


层次序列表示法

已知一个树的层次序列和每个结点相应的次数,则能唯一确定该树的结构

例子

img


树的链式存储结构

父亲连接结构

img

  • 不易遍历,不推荐使用

孩子链表结构

img

  • 便于涉及孩子的操作;但不易遍历
  • 求结点的双亲时不方便

父亲—孩子链表结构

img

  • 父亲数组表与孩子链表表示相结合的存储结构

孩子—兄弟链接存储结构

最大优点:它与二叉树的链接表示完全一样,可用二叉树的算法来实现对树的操作

img!


孩子顺序表示法

像二叉树一样直接存储每个孩子的指针,最常用

结构

img

typedef struct Node{
    int data;
    struct Node** child;
    int count;
}Node;

例子

img

基本算法

搜索父节点

伪代码

FindFather(root,target,result)
{
	if root = NULL or target = NULL 
	{ 
		result = NULL;
   		return;
	} // 树为空或者目标为空
	else {  q = FirstChild(root); }	// 找到根节点第一个孩子
	while q != NULL and q != p
	{
		FindFather(q,target,result); // 在孩子中递归查找
		if result = NULL { q = NextBrother; } // 如果在目前孩子中找不到,去兄弟中查找
		else return;  // 找到了,退出
	}
	if q = target // 找到了
	{ 
		result = root;  // 父亲为目前子树的根
	}
	else return NULL;
}

搜索指定数据域的结点

算法思想

  • 若t的数据域为target,则指针result指向t;否则p指向t的大儿子结点,在以p为根的树中进行搜索(递归调用)
  • 若在以p为根的树中搜索失败,令p指向其右兄弟结点,继续进行搜索,直到找到数据域等于target的结点或p为空

伪代码

FindTarget(t,target,result)
{
	if t = NULL { return result = NULL; } // 树为空
	if Data(t) = target  // 找到目标
	{
    	result = t;
    	return;
	}
	p = FirstChild(t)  // 从第一颗子树开始搜索
	while p != NULL;
	{
		FindTarget(p,target,reslut) // 递归查询
		if result != NULL return; // 找到了退出
		p = NextBrother(p); // 去兄弟树查询
	}
	// 没有找到
	result = NULL;
	return;
}

搜索大儿子结点和大兄弟结点

伪代码

GNB(p,q)
{
	// p所指结点存在,并且在下一个兄弟结点
	if p != NULL and NextBrother(p) != NULL
	{
		q = NextBorther(p);	
	}
	q = NULL; // 下一个兄弟节点不存在
	return;
}

删除子树

三种情况

  • 删除以根结点R为根的树
  • 删除以大儿子结点A为根的子树
  • 删除以结点B或C(非大儿子结点)为根的子树

伪代码

DS(root, p) // 在root为根的树中删除以p为根的子树
{
	if root = NULL or p = NULL { return; }
	// 确定p所指结点的父亲是否存在
	result = new*;
	FindFather(root,p,result);
	if result = NULL
	{
		Del(p);
		return;
	}
	// 若p所指结点的父节点存在,并且p是其父亲结点的大儿子结点
	if FirstChild(result) = p;
	{
		FindChild(result) = NestBrother(p);
		Del(p);
		return;
	}
	// 若p所指结点的父亲结点存在,并且p不是其父大儿子结点
	q = Firstchild(result)
	while NextBrother(q) != p 
	{
		q = NextBrother;
	}
	NextBrother(q) = NextBrother(p);
	Del(p);
	return;
}
Del(p) // 删除根为p的子树
{
	// 指针p所指结点不存在则返回
	if p = NULL return;
	// 从左到右删除p的子树
	q = FirstChild(q);
	while q != NULL
	{
		next = NextBrother(q);
		Del(q);
		q = next;
	}
	p = NULL;
}
上一篇:【Java知识】java基础-开发一个自定义注解


下一篇:Redis学习笔记(六)--Redis底层数据结构之集合的实现原理