《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树

树和二叉树


树的基本概念

树的定义

树是 n ( n ≥ 0 ) n(n \ge 0) n(n0) 个结点的有限集合, n = 0 n = 0 n=0 时称为空树。

任意一个非空树都应该满足:

  • 有且仅有一个特定的结点,称为根;
  • n > 1 n > 1 n>1 时,其余结点可分为 m ( m ≥ 0 ) m(m\ge0) m(m0) 个互不相交的有限集合 T 1 , T 2 , . . . , T m T_{1}, T_{2}, ... , T_{m} T1,T2,...,Tm,这些子集合本身也是树,称为根结点的子树。

树的定义是具有递归特性的,是一种递归的数据结构。在作为一种逻辑结构的同时,树也是一种分层结构,并具有两个特点:

  • 树的根结点没有前驱结点,除根结点之外的所有结点有且仅有一个前驱结点;
  • 树中所有结点可以有零个或者无穷个后继结点。

树的基本术语

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第1张图片
  • 对于结点 G G G,根结点 A A A 到结点 G G G 的唯一路径上的任意结点,都称为结点 G G G祖先结点。其中,结点 B B B 是结点 G G G 的祖先结点,而结点 G G G 是结点 B B B子孙结点;路径上最接近结点 G G G 的结点 D D D G G G双亲结点,而 G G G D D D孩子结点;根结点 A A A 是树中唯一没有双亲的结点;具有相同双亲的结点称为兄弟结点,如结点 G G G H H H 是兄弟结点。
  • 树中一个结点的子结点个数称为该结点的,树中结点的最大度数为树的度
  • 度大于 0 的结点称为分支结点(或非终端结点),度为 0 (没有孩子结点)的结点称为叶子结点(或终端结点)。在分支结点中,结点的分支数为该结点的度。
  • 结点的层次:从树根开始定义,根结点为第一层,子结点以此类推。
  • 结点的深度:从根结点开始自顶向下逐层累加得到。
  • 结点的高度:从叶子结点开始自底向上逐层累加得到。
  • 树的高度(或深度)是树中结点的最大层次数。
  • 有序树和无序树:树中结点的子树从左到右存在次序,并且不能交换,这样的树称为有序树,在有序树中,一个结点的子结点按照从左到右的顺序出现存在关联性。反之子结点之间不存在次序则称为无序树
  • 路径和路径长度: 树中两个结点之间的路径是由着两个结点之间所经过的结点序列组成的,而路径长度是路径上经过的的个数。
  • **森林:**是 m ( m ≥ 0 ) m(m\ge0) m(m0) 个互不相交的树的集合。

树的基本特性

  • 树中的结点数等于所有结点的度数加1;
  • 度为 m m m 的树中第 i i i 层上至多 m i − 1 ( i ≥ 0 ) m^{i-1}(i\ge0) mi1(i0) 个结点;
  • 高度为 h h h m m m 叉树至多 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m1mh1 个结点;
  • 具有 h h h 个结点的 m m m 叉树的最小高度 ⌈ log ⁡ m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1) \rceil logm(n(m1)+1)

二叉树的概念

定义及特性

二叉树定义

二叉树的一种特殊的树形结构,其特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能交换颠倒(即二叉树是有序树)。

与一般的树相似,二叉树也有递归的形式定义,二叉树是 n ( n ≥ 0 ) n(n\ge0) n(n0) 个结点的有限集合,为空树时 n = 0 n=0 n=0 。由一个根结点和连个互不相交的根的左子树和右子树组成,其中左、右子树均是二叉树。

二叉树的五种基本形态:

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第2张图片

二叉树与度为 2 的树的区别:

  • 度为 2 的树至少有三个结点,而二叉树可以为空;
  • 度为 2 的有序树的孩子结点的左右次序是相对于另一个孩子结点而言的,若某结点只有一个孩子结点,那么这个孩子结点就没有左右次序之分。而二叉树内无论有无孩子结点,孩子结点多少,均需确定其左右次序,即二叉树的结点次序不是对于另一个结点而言,是结点本身的属性。

特殊的二叉树

满二叉树
一棵高度为 h h h ,且含有 2 h − 1 2^{h}-1 2h1 个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点的度数都为2。

对满二叉树按层序编号,约定从根结点开始编号,根结点编号为 1,自上而下,从左往右,每个结点对应一个编号。对于编号为 i i i 的结点,若有双亲结点,在则其双亲编号为 ⌊ i 2 ⌋ \lfloor \frac{i}{2}\rfloor 2i;若有左孩子,则左孩子为 2 i 2i 2i ;若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1

完全二叉树
设高度为 h h h,有 n n n 个结点的二叉树,当且仅当其每个结点都与高度为 h h h 的满二叉树中编号
1 ∼ n 1\sim n 1n 的结点一一对应时,称为完全二叉树。

  • i ≤ ⌊ n 2 ⌋ i \le \lfloor \frac{n}{2} \rfloor i2n,则结点 i i i 为分支结点,否则为叶子结点;
  • 叶子结点只可能在层次最大的两层出现(即最后一层和倒数第二层)。对于最大层次中的叶子结点,都依次排列在该层最左边的位置;
  • 若有度为 1 的结点,则只可能存在一个,且该结点只有左孩子;
  • 按层次编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子,则编号大于 i i i 的结点均为叶子结点;
  • n n n 为奇数,则每个分支结点都有左、右孩子;若 n n n 为偶数,则编号最大的分支结点
    (编号为 n 2 \frac{n}{2} 2n) 只有左孩子。

二叉排序树
一棵二叉树或者空二叉树,或是具有以下性质的二叉树:左子树上所有的结点的关键字均小于根结点的关键字,右子树的上所有结点的关键字均大于根结点的关键字,左右子树又各是二叉排序树。

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

二叉树的性质

  • 非空二叉树上的叶子结点数等于度为 2 的结点数加 1,即 n 0 = n 2 + 1 n_{0} = n_{2}+1 n0=n2+1
     推导:
        结点总数 = 分支数 + 1
        结点总数 n = n 0 + n 1 + n 2 n = n_{0}+n_{1}+n_{2} n=n0+n1+n2
        分支数 B = n 1 + 2 ∗ n 2 B = n_{1} + 2*n_{2} B=n1+2n2
        故得: n 0 + n 1 + n 2 = n 1 + 2 ∗ n 2 + 1 n_{0}+n_{1}+n_{2} = n_{1} + 2*n_{2} +1 n0+n1+n2=n1+2n2+1
             ⟶ \longrightarrow n 0 = n 2 + 1 n_{0} = n_{2}+1 n0=n2+1
  • 非空二叉树上第 k k k 层上至多 2 k − 1 ( k ≥ 1 ) 2^{k-1}(k \ge 1) 2k1(k1) 个结点;
  • 高度为 h h h 的二叉树至多 2 h − 1 ( h ≥ 1 ) 2^{h}-1(h \ge 1) 2h1(h1) 个结点;
  • 对完全二叉树按从上到下,从左往右的顺序依次编号,则有:
      - 当 i > 1 i >1 i>1 时,结点 i i i 的双亲结点编号为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i ,即当 i i i 为偶数时,其双亲结点编号为 i 2 \frac{i}{2} 2i ,并且该结点是其双亲结点的左孩子;当 i i i 为奇数时,其双亲结点编号为 i − 1 2 \frac{i-1}{2} 2i1 ,并且该结点是其双亲结点的右孩子。
      - 当 2 i ≤ n 2i \le n 2in 时,结点 i i i 的左孩子编号为 2 i 2i 2i ,否则无左孩子;
      - 当 2 i + 1 ≤ n 2i+1 \le n 2i+1n 时,结点 i i i 的左孩子编号为 2 i + 1 2i+1 2i+1 ,否则无右孩子;
      - 结点 i i i 所在层数(深度)为 ⌊ log ⁡ 2 i ⌋ + 1 \lfloor \log_{2}{i} \rfloor +1 log2i+1
  • 具有 n ( n > 0 ) n(n>0) n(n>0) 个结点的完全二叉树的高度为 ⌈ 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


二叉树的存储结构

顺序存储结构

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第3张图片

二叉树的顺序存储结构是指用一组地址连续的存储单元依次自上而下、从左到右存储完全二叉树上的结点,即将完全二叉树上编号为 i i i 的结点存储在数组下标为 i − 1 i-1 i1 的数组分量中,然后通过一定方式确定结点在逻辑上的父子或兄弟关系。
   

链式存储结构

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第4张图片

链式结构是指用一个链表来存储二叉树,二叉树中的每个结点用链表中的一个链结点来存储。在二叉树中,结点结构通常包括若干数据域和若干指针域,则二叉链表至少包含三个域:数据域 d a t a data data、左指针域 l c h i l d lchild lchild、右指针域 r c h i l d rchild rchild

二叉树的链式存储结构描述:

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

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

二叉树的遍历

遍历,是指按照某条搜索路径访问树中的每一个结点,使得每个结点均会被访问一次,而且仅被访问一次。

由二叉树的递归定义可知,遍历一棵二叉树首先需要确定对根结点、左子树和右子树的访问顺序。常见的遍历次序有先序遍历 (NLR) 、中序遍历 (LNR) 和后序遍历 (LRN) 三种。

先序遍历
若二叉树为空,则不采取操作,否则:

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

对应的递归算法:

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

中序遍历
若二叉树为空,则不进行操作,否则:

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

对应的递归算法:

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

后序遍历
若二叉树为空,则不进行操作,否则:

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

对应的递归算法:

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

递归算法和非递归算法的转换
借助栈,可将二叉树的三种递归遍历算法转换为非递归算法。

以中序遍历为例:先扫描(非访问操作)根结点的所有左结点并将其一一入栈,然后出栈一个结点 ∗ p *p p(该结点没有左孩子结点或左孩子结点已经全被访问过)并访问。然后扫描该结点的右孩子结点并将其入栈,再扫描该右孩子结点的所有左结点并入栈。重复上述操作直到栈空为止。

中序遍历的非递归算法:

void InOrder_NonRrecurrence(BiTree T){
	InitStack(s);			//创建栈并初始化
	BiTNode p = T;			//创建遍历指针 p
	while(p || !IsEmpty(s)){	//若指针 p 不空或者栈不空时循环
		if(p){			//若指针 p 不空(非空左子树),则遍历左子树入栈
			Push(s, p);	//p 指向的结点入栈
			p = p->lchild;	//指针 p 指向左子树
		}
		else{			//根结点的所有左结点均入栈
			Pop(s, p);	//一个结点出栈
			visit(p);	//访问该结点
			p = p->rchild;	
			//指针 p 指向该结点的右子树,下一个循环遍历该右子树的所有左结点
		}
	}
}

层次遍历

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第5张图片

如图所示为二叉树的层次遍历,即按照箭头所指方向,按照层次1,2,3,4的顺序,横向遍历对二叉树中的结点进行访问。

进行层次遍历需要借助队列:先将二叉树根结点入队,然后出队并访问该结点,若有左子树,则将左子树根结点入队;若有右子树,则将右子树根结点入队。然后出队并对出队结点进行访问,重复上述操作直到队列为空。

二叉树的层次遍历算法:

void LevelOrder(BiTree T){
	InitQueue(Q);				//创建空队列 Q
	BiTree p;				//创建遍历指针 p
	EnQueue(Q, T);				//根结点入队
	while(! IsEmpty Q){			//队列非空则继续循环进行遍历
		DeQueue(Q, p);			//队头元素出队
		visit(p);			//访问出队结点
		if(p->lchild != NULL)		//若左子树存在
			EnQueue(Q, p->Lchild);	//左子树根结点入队
		if(p->child != NULL)		//若右子树存在
			EnQueue(Q, p->rchild);	//右子树根结点入队
	}
}

由遍历序列构造二叉树

  • 由二叉树的先序序列和中序序列可以唯一的确定一棵二叉树;
  • 由二叉树的后序序列和中序序列可以唯一的确定一棵二叉树;
  • 由二叉树的层次遍历和中序序列可以唯一的确定一棵二叉树;
  • 若只知道二叉树的先序序列和后序序列。无法确定一棵二叉树。

线索二叉树

基本概念
在有 n n n 个结点的二叉树中,有 n + 1 n+1 n+1 个空指针(每个叶子结点有 2 个空指针,度为 1 的结点有 1 个空指针,空指针总 n 1 + 2 ∗ n 0 n_{1} + 2*n_{0} n1+2n0,又有 n 0 = n 2 + 1 n_{0} = n_{2} +1 n0=n2+1,所以空指针总数为 n 0 + n 1 + n 2 + 1 = n + 1 n_{0} +n_{1}+n_{2}+1 = n +1 n0+n1+n2+1=n+1)。

在二叉树线索化时,通常规定:若无左子树,则令 l c h i l d lchild lchild 指向其前驱结点;若无右子树,则令 r c h l i d rchlid rchlid 指向其后继结点。增加两个标志域表明当前指针域所指向对象是前、后驱结点还是左、右子树结点。

其中,标志域的含义为:

l t a g { 0 l c h i l d 域 指 向 结 点 的 左 孩 子 1 l c h i l d 域 指 向 结 点 的 前 驱 ltag\left\{ \begin{array}{rcl} 0 & lchild域指向结点的左孩子\\ 1 & lchild域指向结点的前驱\\ \end{array} \right. ltag{01lchildlchild

r t a g { 0 r c h i l d 域 指 向 结 点 的 右 孩 子 1 r c h i l d 域 指 向 结 点 的 后 继 rtag\left\{ \begin{array}{rcl} 0 & rchild域指向结点的右孩子\\ 1 & rchild域指向结点的后继\\ \end{array} \right. rtag{01rchildrchild

线索二叉树的存储结构描述:

typedef struct ThreadNode{
	ElemTyped data;				//数据域
	struct ThreadNode *lchild, *rchild;	//左、右指针域
	int ltag, rtag;				//左、右线索标志
}ThreadNode, *ThreadTree;			//类型名申明

以上述结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针称为线索。具有线索的二叉树称为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化

线索二叉树的构造
对二叉树的线索化,实质上就是遍历一次二叉树,在遍历的过程中,检查当前结点的左、右指针域是否为空,若为空,将其改为指向前驱结点或后继结点的线索。

以中序遍历为例:
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第6张图片
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第7张图片

通过中序遍历对二叉树线索化的递归算法:

void InThread(ThreadTree &p, ThreadTree &pre) { 
//指针 p 指向二叉树根结点,指针 pre 指向中序遍历时上一个刚访问过的结点
	if(p != NULL){				//若二叉树不为空
		InThread(p->lchild, pre); 	//递归线索化左子树
		if(p->lchild == NULL){ 	//若左子树为空,建立前驱线索
			p->lchild = pre;	//lchild指针指向上一个刚访问过的结点
			p->ltag = 1;		//线索标志表明此时lchild指向前驱结点
		}
		if(pre != NULL && pre->rchild == NULL){	
		//不存在上一个访问的结点 或 上一个访问的结点的右子树不存在
			pre->rchild = p;	//rchild指向父亲结点
			pre->rtag = 1;		//线索标志表明此时rchild指向后继结点
		}
		pre = p;			//pre 指针指向父亲结点
		InThread(p->rchild, pre);	//递归线索化右子树
	}
}

通过中序遍历建立线索二叉树的主过程算法:

void CreatInThread(ThreadTree T){
	ThreadTree pre = NULL;		//创建线索指针 pre
	if(T != NULL){			
		InThread(T, pre);	//线索化二叉树
		pre->rchild = NULL;	//处理遍历到的最后一个结点
		pre->rtag = 1;		//线索标志表明此时rchild	指向后继结点
	}
}

为了操作方便,在二叉树的线索链表上添加头结点,并让头结点的 l c h i l d lchild lchild 域的指针指向二叉树的根结点,让 r c h i l d rchild rchild 域的指针指向中序遍历时访问到的最后一个结点。反之,让中序序列的第一个结点和最后一个结点的 l c h i l d lchild lchild 域的指针都指向头结点。

线索二叉树的遍历
中序线索化的二叉树主要为访问运算服务,因为结点中包含了线索二叉树的前驱和后继信息,所以遍历不需要借助栈。利用线索二叉树,可以实现二叉树遍历的非递归算法。

  • 求中序线索二叉树的中序序列下的第一个结点:
ThreadNode *FirstNode(ThreadNode *p){
 	while(p->ltag == 0)		//若父亲结点的child指针指向左孩子,则继续遍历
 		p = p->lchild;		//直到遍历到最左下的结点
 	return p;
}
  • 求中序线索二叉树中结点 p p p 在中序序列下的后继结点:
ThreadNode *NextNode(ThreadNode *p){
	if(p->rtag == 0)			//若结点 p 的 rchild 指向右孩子
		return FirstNode(p->rchild);	//递归遍历右子树
	else					//rtag == 1
		return p->rchild;		//直接返回后继线索
}
  • 不含头结点的中序线索二叉树的中序遍历算法:
void InOrder_NonRecurrence(ThreadTree T){
	for(ThreadNode *p = FirstNode(T); p != NULL; p = NextNode(p))
	//初始条件:中序序列第一个结点
	//循环条件:父亲结点不为空
	//步长条件:父亲结点为中序序列下一个结点
		visit(p);		//访问结点 p
}

树、森林

树的存储结构

双亲表示法
采用一组连续空间存储每个结点,同时在每个结点中增设一个伪指针用来指示其双亲结点在数组中的位置。其中,根结点下标为 0,其伪指针域为 -1。

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第8张图片

双亲表示法的存储结构描述:

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

双亲表示法的存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但搜索孩子结点时需要遍历整个树。

孩子表示法
将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时 n n n 个结点就有 n n n 个孩子链表(叶子结点的孩子链表为空表)。

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第9张图片

该存储方式寻找孩子结点非常快捷,但需要双亲结点则需要遍历 n n n 个结点中孩子链表指针域所指向的 n n n 个孩子链表。

孩子兄弟表示法
孩子兄弟表示法又称为二叉树表示法,是以二叉链表作为树的存储结构,每个结点包括三部分:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第10张图片

孩子兄弟表示法的存储结构描述:

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

该存储方式最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子结点。但从当前结点查找其双亲结点较为复杂,时间开销较大。

树、森林与二叉树的转换

由于二叉树和树都可以用二叉链表作为存储结构,因此以二叉链表作为媒介可以导出树与二叉树的对应关系,即给定一棵树,可以找到唯一的一棵二叉树与之对应。

  • 树转换为二叉树的规则:
    每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,即“左孩子右兄弟”。其中,由于根结点没有兄弟,所以由树转换成的二叉树没有右子树。
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第11张图片
  • 森林转换为二叉树的规则:
    先将森林的每棵树转换为二叉树,再将第一棵树的根作为转换后的二叉树的根,将第一棵树的左子树作为转换后的二叉树的左子树,将第二棵树作为转换后的二叉树的右子树,将第三棵树作为转换后的二叉树的右子树的右子树,以此类推。
  • 二叉树转换为森林的规则:
    若二叉树非空,则二叉树的根及其左子树转换为第一棵树的二叉树形式,二叉树根的右子树转换为第二棵树的二叉树形式,以此类推直到最后产生一棵没有右子树的二叉树为止。
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第12张图片     

树和森林的遍历

树的遍历操作:是以某种方式访问树中的每个结点且仅访问一次。

  • 先根遍历: 若树非空,先访问根结点,再按从左到右的顺序遍历根结点的每棵子树了,其访问顺序与这棵树对应二叉树的先序遍历顺序相同;
  • 后根遍历: 若树非空,按从左到右的顺序遍历根结点的每棵子树,之后再访问根结点,其访问顺序与这棵树对应二叉树的后序遍历顺序相同;
  • 层次遍历: 按层序依次访问各结点,与对应二叉树的层次遍历相同。

森林的遍历操作:

  • 先序遍历森林: 若森林非空,
      - 访问森林中第一棵树的根结点;
      - 先序遍历第一棵树中根结点的子树;
      - 先序遍历除去第一棵树后的森林。
  • 中序遍历森林:
      - 中序遍历森林中第一棵树的根结点的子树;
      - 访问第一棵树的根结点;
      - 中序遍历除去第一棵树后的森林。
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第13张图片

树与二叉树的应用

二叉排序树

定义

二叉排序树 (BST),又称为二叉查找树。二叉排序树或是空树,或是具有下列特性的非空二叉树:

  • 若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值;
  • 若右子树非空,则右子树上所有结点关键字值均大于根结点的关键字值;
  • 左右子树本身也是二叉排序树。

由上可知, 二叉排序树是一个递归的数据结构;
      左子树结点值 ≤ \le 根节点值 ≤ \le 右子树结点值。

查找

二叉排序树的查找从根结点开始,沿某个分支逐层向下进行比较的过程。若非空,则将给定值与根结点值进行比较,若相等则查找成功;若不等,当给定值比根结点值小则在根结点的左子树中进行查找,否则在根结点的右子树中进行查找。

二叉排序树的非递归查找算法:

BSTNode *BST_Search(BiTree T, ElemType key, BSTNode *&p){		
	p = NULL;		//p 指针指向被查找结点的双亲结点,用于插入和删除
	while(T != NULL && key != T->data){	//树为空或查找成功,退出循环
		p = T;			
		if(key < T->data)	//给定值比根结点值小
			T = T->lchild;	//则在根结点的左子树中进行查找
		else
			T = T->rchild;	//否则在根结点的右子树中进行查找
	}
	return T;
}

插入

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

int BST_Insert(BiTree T, keyType k){
	if(T == NULL){					//树为空
		T = (BiTree)malloc(sizeof(BSTNode)));	//创建新结点
		T->key = k;				
		T->lchild = NULL;
		T->rchild = NULL;
		return 1;
	}
	else if(k == T->key)				//树中已存在关键字值
		return 0;
	else if(k < T->key)				//关键字值小于根结点值
		return BST_Insert(T->lchild, k);	//插入 T 的左子树
	else						//关键字值大于根结点值
		return BST_Insert(T->rchild, k);	//插入 T 的右子树
}
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第14张图片

构造

每读入一个元素,就建立一个新结点,若二叉排序树非空,则将新结点的值与根结点的值进行比较,若小于根结点的值则插入左子树,否则插入插入右子树;若二叉排序树为空,则将新结点作为二叉排序树的根结点。

构造算法:

void Creat_BST(BiTree &T, KeyType str[], int n){
	T = NULL;			//初始时 T 为空树
	int i = 0;				
	while(i < n){			//依次创建给定数量的结点
		BST_Insert(T, str[i]);	//用关键字数组 str[] 插入创建二叉排序树
		i++;
	}
}

删除

删除操作的实现需要按照三种情况来处理:

  • 若被删除结点是叶子结点,则可以直接删除,不会破坏二叉排序树的性质;
  • 若被删除结点只有一个左或右子树,则让该结点的子树成为其父结点的子树以代替该结点;
  • 若被删除结点有左、右两棵子树,则令该结点的直接后继(或直接前驱)代替该结点,然后再删除这个直接后继(或直接前驱),转换为第一种或第二种情况。
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第15张图片      

平衡二叉树

定义

在插入和删除二叉树结点时,保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),简称平衡树(AVL)。定义结点左、右子树的高度差为该结点的平衡因子,平衡二叉树结点的平衡因子的值只可能为 -1,0,1。

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第16张图片

插入

每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为该结点的插入而导致不平衡,若不平衡则先找到插入路径上最小不平衡子树(离插入结点最近的、平衡因子绝对值大于 1 的结点,将以该结点为根的子树),保证二叉排序树特性的前提下,调整各结点的位置关系以重新达到新的平衡。

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第17张图片

调整规律

  • LL平衡旋转
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第18张图片
  • RR平衡旋转
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第19张图片
  • LR平衡旋转
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第20张图片
  • RL平衡旋转
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第21张图片

查找

平衡二叉树的查找与二叉排序树的查找过程相同。
在查找过程中,与给定值进行比较的关键字个数不超过树的深度。含有 n n n 个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_{2}{n}) O(log2n),因此平衡二叉树的平均查找长度为 O ( l o g 2 n ) O(log_{2}{n}) O(log2n)

哈夫曼树和哈夫曼编码

定义

  • 树中结点被赋予的数值,称为该结点的
  • 从根结点到任意结点的路径长度(经过的边数),与该结点的权值的乘积,称为该结点的带权路径长度
  • 树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,记为
                    W P L = ∑ n i = 1 w i l i WPL = \sum_{n}^{i=1}w_{i}l_{i} WPL=ni=1wili
    式中, w i w_{i} wi 是第 i i i 个叶子结点的权值, l i l_{i} li 是根结点到第 i i i 个叶子结点的路径长度。

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

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第22张图片
其中,(c)树的 WPL 最小,并且为哈弗曼树。

构造

构造算法描述:

  • 给定 n n n 个权值分别为 w 1 , w 2 , . . . , w n w_{1}, w_{2}, ..., w_{n} w1,w2,...,wn 的结点;
  • 将上述结点分别作为 n n n 棵仅含有一个结点的二叉树,构成森林 F F F
  • 构造一个新结点,从 F F F 中选取两颗权值最小的树作为新结点的左、右子树构造一棵新树,并将新结点的权值置为左、右子树权值之和;
  • F F F 中删除选出的两棵树,并将新树加入森林 F F F 中;
  • 重复第 3、4 步骤直到森林 F F F 中只剩下一棵树。

由构造过程可得,哈夫曼树具有以下特点

  • 每个初试结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大;
  • 构造过程共新建结点 n − 1 n-1 n1 个,因此哈夫曼树的结点总数为 2 n − 1 2n-1 2n1 个;
  • 每次构造新树都选择两棵树作为左右子树,因此哈夫曼树中不存在度为 1 的结点。
《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第23张图片

哈夫曼编码

《2020王道》| 数据结构 | 学习笔记 | 第四章 | 树和二叉树_第24张图片

0 和 1 表示左子树还是右子树没有明确规定。因此,左、右结点的顺序是任意的, 所以构造出的哈夫曼树并不唯一, 但是各哈夫曼树的带权路径长度相同且为最优。

你可能感兴趣的:(GCT学习笔记,数据结构与算法)