数据结构之树-1

TREE

1. 树与树的表示

1.1 查找 Searching

查找:根据给定的关键字 K,从集合 R 中找出关键字与 K 相同的记录

  • 静态查找:集合中记录是固定的
  • 动态查找:集合中记录时动态变化的
  • 静态和动态查找的区别在于,除了查找,有没有插入和删除操作存在

1.2 静态查找

用于查找的数据结构的定义

#define MAXSIZE 100

typedef int ElementType;
typedef struct LNode // 定义一个 LNode 数据类型,用于存储元素
{
// LNode 中元素的索引取值范围为 [1, length]
	ElementType Element[MAXSIZE + 1]; // 使用数组存储元素,其中第一个位置不存储元素,第一个元素对应数组下标 1
	int length;						 // 元素的个数
} LNode;
typedef LNode *List; // List 是指向 LNode 数据类型变量的指针

顺序查找:即从最后一个元素向前开始查找

  • 顺序查找算法的时间复杂度为 O(n),效率不是很高
  • 算法实现如下
int SequentialSearch(List Tb1, ElementType K)
{ // 在 Element[1]~Element[n] 中查找关键字为 K 的数据元素
int i;
Tb1->Element[0] = K; // 建立哨兵
for (i = 0; Tb1->Element[i] != K; i--);
return i; // 查找成功返回对应下标,失败则返回 0
}

二分查找

  • 假设 n 个数据元素的关键字满足有序,并且是连续存放(数组),那么可以进行二分查找
  • 二分查找的时间复杂度为 O(logN)
  • 二分查找举例
    在这里插入图片描述 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wFCaUIUb-1668265685993)(binarysearchexample2.png)]
  • 算法实现如下
int BinarySearch(List Tbl, ElementType K)
{
int left, right, mid, NoFound = 0;

// 初始化左右边界,待查找元素如果存在,则一定在 [left, right] 范围中
left = 1;
right = Tbl->length;
while (left <= right) // 终止条件
{
	mid = (left + right) / 2; // 计算中间元素下标
	if (K < Tbl->Element[mid]) // 此时 K 存在于 [left, mid-1]
		right = mid - 1;
	else if (K > Tbl->Element[mid]) // 此时 K 存在于 [mid+1, right]
		left = mid + 1;
	else
		return mid; // 查找成功
}

return NoFound; // 查找失败
}
  • 二分查找判定树(以 11 个元素的判定树举例)
    在这里插入图片描述
  • 判定树上每个结点需要的查找次数刚好为该结点所在的层数
  • 查找成功时查找次数不会超过判定树的深度
  • n 个结点的判定树的深度是 $[log_2n]+1 $
    • 取整函数 y = [ x ] y = [x] y=[x],y 的值是不超过实数 x 的最大整数
  • 平均查找次数 A S L = Σ ( 每个结点 ∗ 对应的层数 ) 结点个数 ASL = \frac{Σ(每个结点*对应的层数)}{结点个数} ASL=结点个数Σ(每个结点对应的层数),例如上图 $ ASL = (44+43+2*2+1)/11 = 3 $

1.3 树的定义

树(Tree): n(n≥0)个结点构成的有限集合

  • 当n=0时,称为空树
  • 对于任一棵非空树(n>0),它具备以下性质
    • 树中有一个称为 “根(Root)” 的特殊结点,用 r 表示
    • 其余结点可分为 m(m>0) 个互不相交的有限集 T1,T2, … , Tm,其中每个集合本身又是一棵树,称为原来树的 “子树(SubTree)
    • 子树是不相交的
    • 除了根节点外,每个结点有且仅有一个父节点
    • 一个 N 个结点的树由 N-1 条边(因为除了根节点外,每个结点都有一个向上的边)
  • 树的举例
    • 树和子树
      在这里插入图片描述
  • 非树
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pruYgdeR-1668265685996)(nottree.png)]

树的基本术语

  1. 结点的度(Degree):结点的子树个数
  2. 树的度:树的所有结点中最大的度数
  3. 叶结点(Leaf): 度为 0 的结点
  4. 父结点(Parent):有子树的结点是其子树的根结点的父结点
  5. 子结点(Child):若 A 结点是 B 结点的父结点,则称 B 结点是 A 结点的子结点;子结点也称孩子结点
  6. 兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点。
  7. 路径和路径长度:从结点 n1 到 nk 的路径为一个结点序列 n1, n2, …, nk, 其中 ni 是 ni+1 的父结点。路径所包含边的个数为路径的长度。
  8. 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。
  9. 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙。
  10. 结点的层次(Level):规定根结点在 1 层,其它任一结点的层数是其父结点的层数加 1。
  11. 树的深度(Depth) :树中所有结点中的最大层次是这棵树的深度。

树的表示
在这里插入图片描述

  • 儿子-兄弟表示法
    在这里插入图片描述

在这里插入图片描述

  • 二叉树(儿子兄弟表示法的实现原型)
    在这里插入图片描述

1.4 问题讨论

  • 有一个 m 棵树的集合/森林,共有 k 条边,则这 m 棵树共有多少个结点? k + m
  • 树的集合称之为森林,怎么用“儿子-兄弟表示法”存储森林? 森林中各树的根节点互为兄弟,其余按照儿子-兄弟表示法表示即可

2. 二叉树及存储结构

2.1 二叉树的定义

二叉树T:一个有穷的结点集合,是一个度为 2 的树

  • 这个集合可以为空
  • 若不为空,则它是由根结点和称为其左子树 TL 和右子树 TR 的两个不相交的二叉树组成
  • 二叉树的子树有左右顺序之分

二叉树的五种基本形态:空、只有一个根节点、只有一个子树(左右)、有两个子树
在这里插入图片描述

2.2 特殊二叉树

斜二叉树 Skewed Binary Tree
在这里插入图片描述

完美二叉树 Perfect Binary Tree
满二叉树 Full Binary Tree

  • 即除了叶节点外,每个结点都有两个子树,并且按照层次标序号
    在这里插入图片描述

完全二叉树 Complete Binary Tree

  • 有 n 个结点的二叉树,对树中结点按从上至下、从左至右顺序进行编号,编号为 i(1 ≤ i ≤ n)结点与满二叉树中编号为 i 结点在二叉树中位置相同。也可以理解为,将满二叉树按编号从后向前删除任意数量的结点得到的树,就是完全二叉树
    在这里插入图片描述

2.3 二叉树的性质

  • 一个二叉树第 i 层的最大结点数为 2 i − 1 , i ≥ 1 2^{i-1},i≥1 2i1,i1
    • 简证
      N 1 = 2 0 , N 2 = 2 1 , . . . , N i = 2 × N i − 1 , s o   N i = 2 i − 1 N_1 = 2^0,N_2 = 2^1,...,N^i = 2×N^{i-1},so\ N_i=2^{i-1} N1=20,N2=21,...,Ni=2×Ni1,so Ni=2i1
  • 深度为 k 的二叉树有最大结点总数为 2 k − 1 , k ≥ 1 2^k-1, k≥1 2k1,k1
    • 简证
      有上述知,每层最大结点数呈等比数列规律,因此 有上述知,每层最大结点数呈等比数列规律,因此 有上述知,每层最大结点数呈等比数列规律,因此
      M A X _ N o d e s = N 1 × ( 1 − q k − 1 ) 1 − q = 2 k − 1 MAX\_Nodes = \frac{N_1×(1-q^{k-1})}{1-q}=2^k-1 MAX_Nodes=1qN1×(1qk1)=2k1
  • 对于任何非空二叉树 T,若 n i ,   i = 0 , 1 , 2 n_i,\ i=0,1,2 ni, i=0,1,2 表示度为 i 的结点的个数,有关系 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1,即叶节点的个数等于度为 2 的非叶节点的个数
    • 简证
      因为二叉树(自下向上看)除根节点外每个结点都有父节点,则
      s u m ( 总边数 ) = s u m ( 结点数 ) − 1 = n 0 + n 1 + n 2 − 1 sum(总边数) = sum(结点数) - 1 = n_0 + n_1 + n_2 - 1 sum(总边数)=sum(结点数)1=n0+n1+n21
      又因为二叉树中 n i n_i ni 结点有 i i i 个子节点,因此
      s u m ( 总边数 ) = n 0 × 0 + n 1 × 1 + n 2 × 2 sum(总边数) = n_0 × 0 + n_1 × 1 + n_2 × 2 sum(总边数)=n0×0+n1×1+n2×2
      结合上述两式有
      n 0 + n 1 + n 2 − 1 = n 0 × 0 + n 1 × 1 + n 2 × 2 n_0 + n_1 + n_2 - 1 = n_0 × 0 + n_1 × 1 + n_2 × 2 n0+n1+n21=n0×0+n1×1+n2×2
      = > n 0 = n 2 + 1 => n_0 = n_2 + 1 =>n0=n2+1

3.4 二叉树的抽象数据类型定义

  • 类型名称:二叉树
  • 数据对象集: 一个有穷的结点集合
    • 若不为空,则由根结点和其左、右二叉子树组成。
  • 操作集: BT ∈ BinTree, Item ∈ ElementType,重要操作有
    • Boolean IsEmpty(BinTree BT):判别 BT 是否为空
    • void Traversal(BinTree BT):遍历,按某顺序访问每个结点
    • BinTree CreateBinTree():创建一个二叉树。

3.5 二叉树常用的遍历方法

  • void PreOrderTraversal(BinTree BT):先序 => 根、左子树、右子树;
  • void InOrderTraversal(BinTree BT):中序 => 左子树、根、右子树;
  • void PostOrderTraversal(BinTree BT):后序 => 左子树、右子树、根
  • void LevelOrderTraversal(BinTree BT):层次遍历 => 从上到下、从左到右

3.6 二叉树的存储结构

顺序存储结构

  • 常用顺序存储结构存储完全二叉树,一般二叉树也可以使用顺序二叉树进行存储,但是空间浪费较大
  • 完全二叉树
    • 按从上到下,从左到右顺序存储
    • n 个结点的完全二叉树的结点父子关系对应数组序号如下
      • 非根结点(序号 i > 1)的父结点的序号是 ⌊i/2⌋(向下取整符号)
      • 结点(序号为 i)的左孩子结点的序号是 2i,(若2i <= n,否则没有左孩子)
      • 结点(序号为 i)的右孩子结点的序号是 2i+1(若2i +1<= n,否则没有右孩子)
        在这里插入图片描述
  • 一般二叉树
    • 相较于完全二叉树,为了延续完全二叉树的结点父子关系的性质,一般二叉树存储只是将对应位置没有结点的位置赋空值处理,这种结构会造成空间浪费
      在这里插入图片描述

链式存储结构

typedef int ElementType;

typedef struct TreeNode{ // 树结点的定义
	ElementType Data;
	struct TreeNode* Left;
	struct TreeNode* Right;
} TreeNode;

typedef TreeNode *BinTree; // 二叉树类型
typedef BinTree Position;

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KU4w5Ryz-1668265686024)(binarytreelinked2.png)]

3.7 问题讨论

  • 如果一个完全二叉树最底下一层为第六层(根为第一层),且该层共有 8 个叶节点,那么该完全二叉树共有多少个结点?39
    简答:每层最大结点数 2 i − 1 2^{i-1} 2i1,二叉树最大结点数 2 k − 1 2^k-1 2k1,则 N a l l = ( N 1 + N 2 + . . . + N 5 ) + N 6 = 2 5 − 1 + 8 = 39 N_{all} = (N_1+N_2+...+N_5)+N_6 = 2^{5} - 1 + 8 = 39 Nall=(N1+N2+...+N5)+N6=251+8=39

  • 若有一二叉树的总结点数为 98,只有一个儿子的结点数为 48,则概述的叶节点数是多少?这样的树不存在
    简答:由题意分析有 n a l l = 98 , n 1 = 48 n_{all} = 98, n_1 = 48 nall=98,n1=48,又根据二叉树的定义及性质有 n 0 = n 2 + 1 n_0 = n2 + 1 n0=n2+1 n a l l = Σ 0 2 n i = n 1 + 2 n 2 + 1 = 2 n 2 + 49 = 98 n_{all} = Σ_0^2n_i = n_1 + 2n_2+1 = 2n_2 + 49 = 98 nall=Σ02ni=n1+2n2+1=2n2+49=98,显然 n 2 n_2 n2 没有整数解,所以这样的树不存在。

  • 如果参照完全二叉树的表示方法用数组存储如下二叉树,那么 e 所对应的数组下标是多少(树根下标为 1)?6
    在这里插入图片描述

  • 设深度为 d(只有一个根节点时,d = 1)的二叉树只有度为 0 和 2 的结点,则此类二叉树结点数至少为? 2d - 1
    简证:考虑结点最少的情况如下,即可得到答案
    在这里插入图片描述

  • 一棵二叉树, n 2 = 15 , n 1 = 32 , 则 n 0 = ? n_2=15,n_1=32,则 n_0=? n2=15,n1=32,n0=? 16
    简证: n 0 = n 2 + 1 = 16 n_0 = n_2 + 1 = 16 n0=n2+1=16

3. 二叉树的遍历

3.1 先序遍历

遍历流程

  • 访问根结点
  • 先序遍历其左子树
  • 先序遍历其右子树

遍历举例
在这里插入图片描述

  • 遍历流程的语言描述
    • A 非空 -> 访问 A -> 输出 A -> ‘遍历 A 的左子树 B’ -> ‘遍历 A 的右子树 C’
    • 其中 ‘遍历 A 的左子树 B’ 为
      B 非空 -> 访问 A -> ‘遍历 B 的左子树 D’ -> ‘遍历 B 的右子树 F’
    • 其中 ‘遍历 B 的左子树 D’ 为
      D 非空 -> 访问 D -> ‘访问 D 的左子树 NULL’ -> ‘访问 D 的右子树 NULL’
    • 其中 ‘访问 D 的左子树 NULL’
      NULL -> 不进行访问
    • 其中 ‘访问 D 的右子树 NULL’
      NULL -> 不进行访问
    • 至此 ‘遍历 B 的左子树 D’ 流程结束
    • 遍历流程类推

递归实现先序遍历

void PreOrderTraversal(BinTree BT)
{
	if (BT) // 如果该树不为空就对该树进行先序遍历
	{
		printf("%d\t", BT->Data);
		PreOrderTraversal(BT->Left);
		PreOrderTraversal(BT->Right);
	}
}

非递归实现先序遍历

void PreOrderTraversalWithStack(BinTree BT)
{
	BinTree T = BT;				 // 这里的 T 相当于一个工作结点(类似单链表中的工作指针)
	Stack S = CreateStack(1000); // 创建并初始化堆栈 S
	while (T || !IsEmpty(S))	 // T 不空 或者 S 不空
	{
		while (T)
		{
			printf("%d\t", T->Data);
			Push(S, T);	 // 遇到一个结点,然后压栈
			T = T->Left; // 继续遍历左子树
		}
		if (!IsEmpty(S)) // S 不空
		{
			BinTree *v;
			Pop(S, v);
			T = *v;
			T = T->Right; /*转向右子树*/
		}
	}
}

3.2 中序遍历

遍历流程

  • 中序遍历其左子树
  • 访问根结点
  • 中序遍历其右子树

遍历举例
在这里插入图片描述

递归实现中序遍历

void InOrderTraversal(BinTree BT)
{
	if (BT)
	{
		InOrderTraversal(BT->Left);
		printf("%d\t", BT->Data);
		InOrderTraversal(BT->Right);
	}
}

非递归实现中序遍历
算法实现思路

  • 遇到一个结点,就把它压栈,并去遍历它的左子树
  • 当左子树遍历结束后,从栈顶弹出这个结点并访问它
  • 然后按其右指针再去中序遍历该结点的右子树
void InOrderTraversalWithStack(BinTree BT)
{
	BinTree T = BT;				 // 这里的 T 相当于一个工作结点(类似单链表中的工作指针)
	Stack S = CreateStack(1000); // 创建并初始化堆栈 S
	while (T || !IsEmpty(S))	 // T 不空 或者 S 不空
	{
		while (T) // 循环完成将结点及左子树压栈到叶节点的任务 /*一直向左并将沿途结点压入堆栈*/
		{
			Push(S, T);	 // 遇到一个结点,然后压栈
			T = T->Left; // 继续遍历左子树
		}				 
		if (!IsEmpty(S)) // S 不空(说明此时左子树到头了,先输出叶节点,然后遍历上一个结点的右子树)
		{
			BinTree *v;
			Pop(S, v);
			T = *v;

			printf("%d\t", T->Data);
			T = T->Right; /*转向右子树*/
		}
	}
}

3.3 后序遍历

遍历流程

  • 后序遍历其左子树
  • 后序遍历其右子树
  • 访问根结点

遍历举例
在这里插入图片描述

递归实现后序遍历

void PostOrderTraversal(BinTree BT)
{
	if (BT)
	{
		PostOrderTraversal(BT->Left);
		PostOrderTraversal(BT->Right);
		printf("%d\t", BT->Data);
	}
}

非递归实现后序遍历

// 后序遍历,非递归实现(注意后序遍历的非递归实现与前序中序有所差别)
// 怎么得到 左右根的遍历呢?一个思路是转换为 根右左 的逆序(具体见文档 非递归遍历思路.md)
void PostOrderTraversalWithStack(BinTree BT)
{
	BinTree T = BT;
	Stack S = CreateStack(1000);
	Stack post = CreateStack(1000);

	while (T || !IsEmpty(S)) // 终止条件 =》 T 指向空 且 栈空
	{
		while (T) //T 指向非空 =》则指向的结点进栈,T 更新为 指向结点的右儿子
		{
			Push(S, T); 
			Push(post, T);
			T = T->Right;
		}
		if (!IsEmpty(S)) // T 指向空 =》 栈顶结点出栈,T 更新为出栈结点的左儿子
		{
			BinTree *v;
			Pop(S, v);
			T = *v;

			T = T->Left; 
		}
	}

	// 输出 post 元素,就是我们需要的后序遍历的结果(也是二叉树基于右儿子优先实现的前序遍历元素的逆序)
	while(!IsEmpty(post)) // post 不为空
	{
		BinTree *v;
		Pop(post, v);
		printf("%d\t", (*v)->Data);
	}
}

3.4 前、中、后序遍历对比

  • 先序、中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同
  • 下图中在从入口到出口的遍历曲线上用 ⓧ、★、▲ 三种符号分别标记出了先序、中序和后序访问各结点的时刻
    在这里插入图片描述

3.5 层次遍历

二叉树遍历的核心问题: 二维结构的线性化

  • 问题一:怎么从结点访问其左、右儿子结点
  • 问题二:访问左儿子后,右儿子结点怎么办?
    • 需要一个存储结构保存暂时不访问的结点
    • 存储结构:堆栈、队列

层序遍历的队列实现

  1. 根节点入队
  2. then
  • 从队列中取出一个元素(二叉树结点的指针)
  • 访问该元素所指结点
  • 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队。否则继续第一步从队列取元素循环
  1. 终止条件:队列为空

代码如下(队列实现)

void LevelOrderTraversal(BinTree BT)
{
	Queue Q;
	BinTree T;

	if (!BT)
		return;		  /* 若是空树则直接返回 */
	Q = CreatQueue(); /*创建并初始化队列Q*/

	AddQ(Q, BT);

	while (!IsEmptyQ(Q))
	{
		T = DeleteQ(Q);
		printf("%d\t", T->Data); /*访问取出队列的结点*/
		if (T->Left)
			AddQ(Q, T->Left);
		if (T->Right)
			AddQ(Q, T->Right);
	}
}

如果将层序遍历中的队列改为堆栈,是否也是一种树的遍历?可以应用这种方法改造出一种前序、中序、后序的非递归遍历吗?YES、YES

3.6 二叉树遍历的应用

  • 二叉树叶节点的输出:在二叉树的遍历算法中增加检测结点的“左右子树是否都为空”

    void PreOrderPrintLeaves(BinTree BT)
    {
    	if (BT)
    	{
    		if (!BT->Left && !BT->Right)
    			printf("%d\t", BT->Data);
    		PreOrderPrintLeaves(BT->Left);
    		PreOrderPrintLeaves(BT->Right);
    	}
    }
    
  • 求二叉树的高度
    在这里插入图片描述

    int PostOrderGetHeight(BinTree BT)
    {
    	int HL, HR, MaxH;
    	if (BT)
    	{
    		HL = PostOrderGetHeight(BT->Left);	/*求左子树的深度*/
    		HR = PostOrderGetHeight(BT->Right); /*求右子树的深度*/
    		MaxH = (HL > HR) ? HL : HR;			/*取左右子树较大的深度*/
    		return (MaxH + 1);					/*返回树的深度*/
    	}
    	else
    		return 0; /* 空树深度为0 */
    }
    
  • 二元运算表达式树的遍历得到三种不同的运算表达式

    • 其中产生的中缀表达式可能不准,解决方法:两个运算数字和运算符合成时添加括号
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsPZmPa1-1668265686041)(calculating.png)]
  • 可以根据两种遍历序列确定一个二叉树,但是其中一个序列一定要是中序遍历序列,另一个可以是先序遍历序列,也可以是后序遍历序列。如下图就是一个例子,不能由一个前序遍历序列和一个后序遍历序列确定一个二叉树。
    在这里插入图片描述

  • 举例:根据先序遍历序列和中序遍历序列确定一个二叉树

    • 确定步骤

      • 根据先序遍历序列第一个结点确定根结点;
      • 根据根结点在中序遍历序列中分割出左右两个子序列;
      • 对左子树和右子树分别递归使用相同的方法继续分解。
        在这里插入图片描述
    • 实例分析
      已知先序序列 a bcde fghij (1)、中序序列 cbed a hgijf (2),则按照如下进行分析

      • 由 (1) 知,根节点为 a
      • 根据根节点 a 对 (2) 划分左右子树如上
      • 又根据 (2) 左右子树的划分对 (1) 左右子树划分如上
      • 此时先序序列左子树 b cde
      • 中序序列左右子树 c b ed
      • 依照上述步骤进行子树的进一步划分(根据 b 是 a 的 左儿子进行分析),划分结果如上
      • 进一步划分
      • b cde(=>c de=>d e)
      • c b ed(=> d e)
      • 同理按照上述分析,最后结果为
        在这里插入图片描述

3.7 问题讨论

  • 已知有棵 5 个结点的二叉树,其前序遍历序列为 a???, 中序遍历序列是 a???,可以断定什么?该树根节点是 a,且没有左子树
  • 已知三种遍历中的任意两种遍历序列,能否唯一确定一颗二叉树?不能,两种遍历序列中至少要有一种是中序遍历序列才可以
  • 对于二叉树,如果其中序遍历结果与前序遍历结果一样,那么可以断定该二叉树?所有结点都没有左儿子
  • 已知一二叉树的后序和中序遍历的结果分别是 FDEBGCA 和 FDBEACG ,那么该二叉树的前序遍历结果是什么?ABDFECG
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值