4、树(中篇)

前言:前节二叉树只能适用于静态查找,不能实现动态插入、删除等。如何解决以下两个问题:

  1. 静态查找与动态查找
  2. 针对动态查找,数据如何组织?

4.1 二叉搜索树

4.1.1 什么是二叉搜索树

二叉搜索树(BST,Binary Search Tree),也称二叉排序树二叉查找树

二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质,

  • 非空左子树的所有键值小于其根结点的键值。
  • 非空右子树的所有键值大于其根结点的键值。
  • 左、右子树都是二叉搜索树
    在这里插入图片描述
    二叉搜索树操作的特别函数:
  • Position Find( ElementType X, BinTree BST ):从二叉搜索树BST中查找元素X,返回其所在结点的地址;
  • Position FindMin( BinTree BST ):从二叉搜索树BST中查找并返回最小元素所在结点的地址;
  • Position FindMax( BinTree BST ) :从二叉搜索树BST中查找并返回最大元素所在结点的地址。
  • BinTree Insert( ElementType X, BinTree BST ):插入
  • BinTree Delete( ElementType X, BinTree BST ):删除

4.1.2 二叉搜索树的查找

二叉搜索树的查找操作:Find
查找从根结点开始,如果树为空,返回NULL;
若搜索树非空,则根结点关键字和X进行比较,并进行不同处理:

  • (1) 若X小于根结点键值,只需在左子树中继续搜索;
  • (2) 如果X大于根结点的键值,在右子树中进行继续搜索;
  • (3)若两者比较结果是相等,搜索完成,返回指向此结点的指针。
    在这里插入图片描述
    代码:
Position Find( ElementType X, BinTree BST )
{
	if( !BST ) return NULL; 			/*查找失败*/
	if( X > BST->Data )
 		return Find( X, BST->Right ); 	/*在右子树中继续查找*/
	Else if( X < BST->Data )
 		return Find( X, BST->Left ); 	/*在左子树中继续查找*/
	else /* X == BST->Data */
 		return BST; 					/*查找成功,返回结点的找到结点的地址*/
}

但采用递归效率低,非递归函数的执行效率高,可将“尾递归”函数改为迭代函数,查找的效率决定于树的高度。

Position IterFind( ElementType X, BinTree BST )
{
 	while( BST ) 
 	{
 		if( X > BST->Data )
 			BST = BST->Right; 	/*向右子树中移动,继续查找*/
 		else if( X < BST->Data )
 			BST = BST->Left; 	/*向左子树中移动,继续查找*/
 		else 					/* X == BST->Data */
 			return BST; 		/*查找成功,返回结点的找到结点的地址*/
 	}
 	return NULL; 				/*查找失败*/
}

查找最大和最小元素:

  • 最大元素一定是在树的最右分枝的端结点上
  • 最小元素一定是在树的最左分枝的端结点上
    在这里插入图片描述
    代码:
//查找最小元素的递归函数
Position FindMin( BinTree BST )
{
 	if( !BST ) return NULL; 			/*空的二叉搜索树,返回NULL*/
 	else if( !BST->Left )
 		return BST; 					/*找到最左叶结点并返回*/
 	else
 		return FindMin( BST->Left ); 	/*沿左分支继续查找*/
}
//查找最大元素的迭代函数
Position FindMax( BinTree BST )
{
 	if(BST )
 	while( BST->Right ) BST = BST->Right;
 		/*沿右分支继续查找,直到最右叶结点*/
 	return BST;
} 

4.1.3 二叉搜索树的插入

〖分析〗关键是要找到元素应该插入的位置,可以采用与Find类似的方法
在这里插入图片描述
代码:

//二叉搜索树的插入算法

BinTree Insert( ElementType X, BinTree BST )
{
 	if( !BST )
 	{
 		/*若原树为空,生成并返回一个结点的二叉搜索树*/
 		BST = malloc(sizeof(struct TreeNode));
 		BST->Data = X;
 		BST->Left = BST->Right = NULL;
 	}
 	else /*开始找要插入元素的位置*/
 	{
 		if( X < BST->Data )
 			BST->Left = Insert( X, BST->Left);
 				/*递归插入左子树*/
 		else if( X > BST->Data )
 			BST->Right = Insert( X, BST->Right);
 				/*递归插入右子树*/
 	}
 	
 	/* else X已经存在,什么都不做 */
 	return BST;
}

【例】 以一年十二个月的英文缩写为键值,按从一月到十二月顺序输入,即输入序列为(Jan, Feb, Mar, Apr, May, Jun, July, Aug, Sep, Oct, Nov, Dec)
在这里插入图片描述

4.1.4 二叉搜索树的删除

考虑三种情况:
(1)要删除的是叶结点:直接删除,并再修改其父结点指针—置为NULL
****〖例〗:删除 35
在这里插入图片描述
(2)要删除的结点只有一个孩子结点:将其父结点的指针指向要删除结点的孩子结点
〖例〗:删除 33
在这里插入图片描述
(3)要删除的结点有左、右两棵子树:用另一结点替代被删除结点:右子树的最小元素 或者 左子树的最大元素
〖例〗:删除 41
在这里插入图片描述
代码:

// 二叉搜索树的删除算法

BinTree Delete( ElementType X, BinTree BST ) 
{
	Position Tmp; 
 	if( !BST ) printf("要删除的元素未找到"); 
 	else if( X < BST->Data ) 
 		BST->Left = Delete( X, BST->Left); 		/* 左子树递归删除 */
 	else if( X > BST->Data ) 
 		BST->Right = Delete( X, BST->Right); 	/* 右子树递归删除 */
 	else /*找到要删除的结点 */ 
 		if( BST->Left && BST->Right ) 			/*被删除结点有左右两个子结点 */
 		{  
 			Tmp = FindMin( BST->Right ); 		/*在右子树中找最小的元素填充删除结点*/
 			BST->Data = Tmp->Data; 
 			BST->Right = Delete( BST->Data, BST->Right);/*在删除结点的右子树中删除最小元素*/
 		} 
 		else /*被删除结点有一个或无子结点*/
 		{ 
 			Tmp = BST; 
 			if( !BST->Left ) 					/* 有右孩子或无子结点*/
 				BST = BST->Right; 
 			else if( !BST->Right )				/*有左孩子或无子结点*/
 				BST = BST->Left;
 			free( Tmp );
 		}
 
 	return BST;
}

4.2 平衡二叉树

4.2.1 什么是平衡二叉树

〖例〗搜索树结点不同插入次序,将导致不同的深度和平均查找长度ASL
在这里插入图片描述
平衡因子(Balance Factor,简称BF): BF(T) = hL-hR,其中hL和hR分别为T的左、右子树的高度。
平衡二叉树(Balanced Binary Tree)(AVL树)空树,或者任一结点左、右子树高度差的绝对值不超过1,即|BF(T) |≤ 1
在这里插入图片描述
平衡二叉树的高度能达到log2n吗?
设 nh 高度为h的平衡二叉树的最少结点数。结点数最少时:
在这里插入图片描述
在这里插入图片描述

4.2.2 平衡二叉树的调整

无论怎么调整,一定要保证调整之后仍然是搜索树(即每棵树,左节点小于根节点,右节点大于根节点)。

右旋在这里插入图片描述
例题:13是麻烦节点,5是发现者,10提上去,5右旋下,8比10小比5大,挂在5的右节点。
在这里插入图片描述
左旋
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

C语言代码:AVL树的旋转与插入

typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
struct AVLNode{
    ElementType Data; /* 结点数据 */
    AVLTree Left;     /* 指向左子树 */
    AVLTree Right;    /* 指向右子树 */
    int Height;       /* 树高 */
};

int Max ( int a, int b )
{
    return a > b ? a : b;
}

AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B */
  /* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */     

    AVLTree B = A->Left;
    A->Left = B->Right;
    B->Right = A;
    A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
    B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
 
    return B;
}

AVLTree DoubleLeftRightRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B,且B必须有一个右子结点C */
  /* 将A、B与C做两次单旋,返回新的根结点C */
    
    /* 将B与C做右单旋,C被返回 */
    A->Left = SingleRightRotation(A->Left);
    /* 将A与C做左单旋,C被返回 */
    return SingleLeftRotation(A);
}

/*************************************/
/* 对称的右单旋与右-左双旋请自己实现 */
/*************************************/

AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
    if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
        T = (AVLTree)malloc(sizeof(struct AVLNode));
        T->Data = X;
        T->Height = 0;
        T->Left = T->Right = NULL;
    } /* if (插入空树) 结束 */

    else if ( X < T->Data ) {
        /* 插入T的左子树 */
        T->Left = Insert( T->Left, X);
        /* 如果需要左旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
            if ( X < T->Left->Data ) 
               T = SingleLeftRotation(T);      /* 左单旋 */
            else 
               T = DoubleLeftRightRotation(T); /* 左-右双旋 */
    } /* else if (插入左子树) 结束 */
    
    else if ( X > T->Data ) {
        /* 插入T的右子树 */
        T->Right = Insert( T->Right, X );
        /* 如果需要右旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
            if ( X > T->Right->Data ) 
               T = SingleRightRotation(T);     /* 右单旋 */
            else 
               T = DoubleRightLeftRotation(T); /* 右-左双旋 */
    } /* else if (插入右子树) 结束 */

    /* else X == T->Data,无须插入 */

    /* 别忘了更新树高 */
    T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
    
    return T;
}

4.3 小白专场:是否同一棵二叉搜索树- C实现

给定一个插入序列就可以唯一确定一棵二叉搜索树。然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到。
例如,按照序列{2, 1, 3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果。
在这里插入图片描述

问题:对于输入的各种插入序列,需要判断它们是否能生成一样的二叉搜索树。

在这里插入图片描述
3 1 4 2 和 3 4 1 2对应的二叉树是一样的,3 2 4 1对应的二叉树不一样。

求解思路
两个序列是否对应相同搜索树的判别
1.分别建两棵搜索树的判别方法
// 根据两个序列分别建树,再判别树是否一样
2.不建树的判别方法
// 以3为根节点,把比3小的放在左侧(左子树),比3大的放在右侧(右子树),然后对比左右侧是否相同。
在这里插入图片描述3. 建一棵树,再判别其他序列是否与该树一致 (详细讲解)

两个序列是否对应相同搜索树的判别

思路: 建一棵树,再判别其他序列是否与该树一致

  1. 搜索树表示
  2. 建搜索树T
  3. 判别一序列是否与搜索树T一致

搜索树表示

typedef struct TreeNode *Tree;
struct TreeNode {
int v;
Tree Left, Right;
int flag;
};

程序框架搭建

int main() 
{ 对每组数据
	 读入N和L (N代表搜索树节点个数,L代表有多少个序列需要比较)
	 根据第一行序列建树T
	 依据树T分别判别后面的L个序列是否能与T形成同一搜索树并输出结果 
	return 0; 
}

int main()
{ int N, L, i;
  Tree T;
  
  scanf("%d", &N);
  while (N) 
  {
    scanf("%d", &L);
    T = MakeTree(N);
    for (i=0; i<L; i++) 
    {
		if (Judge(T, N)) printf("Yes\n");
		else printf("No\n");
		ResetT(T); /*清除T中的标记flag*/
	}
	FreeTree(T);
	scanf("%d", &N);
  }
  return 0;
}

需要设计的主要函数:

  • 读数据建搜索树T
  • 判别一序列是否 与T构成一样的搜索树

如何建搜索树

Tree MakeTree( int N )
{ 	Tree T;
	int i, V;

	scanf("%d", &V);
	T = NewNode(V);
	for (i=1; i<N; i++) {
		scanf("%d", &V);
		T = Insert(T, V);
	}
	return T;
}
Tree NewNode( int V )
{ 	Tree T = (Tree)malloc(sizeof(struct TreeNode));
	T->v = V;
	T->Left = T->Right = NULL;
	T->flag = 0;
	return T;
}
Tree Insert( Tree T, int V )
{
	if ( !T ) T = NewNode(V);
	else 
	{
		if ( V>T->v )
			T->Right = Insert( T->Right, V );
		else
			T->Left = Insert( T->Left, V );
	}
	return T;
}

如何判别
通过3 1 4 2构造的T
在这里插入图片描述
3 2 4 1对应的树
在这里插入图片描述

如何判别一序列是否与树T一致

如何判别序列3 2 4 1是否 与树T一致?
方法:在树T中按顺序搜索序列3 2 4 1中的每个数

  • 如果每次搜索所经过的结点在前面均出现过,则一致
  • 否则(某次搜索中遇到前面未出现的结点),则不一致
int check ( Tree T, int V )
{
	if ( T->flag ) 
	{
		if ( V<T->v ) return check(T->Left, V);
		else if ( V>T->v ) return check(T->Right, V);
		else return 0;
	}
	else 
	{
		if ( V==T->v ) 
		{
			T->flag = 1;
			return 1;
		}
		else return 0;
	}
}
int Judge( Tree T, int N ) 
{
	int i, V, flag = 0; /* flag: 0代表目前还一致,1代表已经不一致*/

	scanf("%d", &V);
	if ( V!=T->v ) flag = 1;
	else T->flag = 1;

	for (i=1; i<N; i++) 
	{
		scanf("%d", &V);
		if ((!flag) && (!check(T, V)) ) flag = 1;
	}
	if (flag) return 0;
	else return 1;
}

3 2 4 1 当发现序列中的某个数与T不一致时,必须把序列后面的数都读完!

void ResetT ( Tree T ) /* 清除T中各结点的flag标记 */
{
	if (T->Left) ResetT(T->Left);
	if (T->Right) ResetT(T->Right);
	T->flag = 0;
}

void FreeTree ( Tree T ) /* 释放T的空间 */
{
	if (T->Left) FreeTree(T->Left);
	if (T->Right) FreeTree(T->Right);
	free(T);
}

4.4 线性结构之习题选讲

Reversing Linked List:题目,给一个单链表的头和节点数量,将该链表逆转并返回逆转后的链表头。

重申什么是链表(C/C++有指针,JAVA无指针是否链表无意义)。

什么是抽象的链表
有块地方存数据;
有块地方存指针—— 下一个结点的地址。

//如下图,给定元素个数k,链表头地址00100,链表并不是连续的内存。三列:第一列表示本元素地址,中间是元素值,最后是指针指向下一个元素。在内存中,可以将地址看作是超级大数组的“下标”。
在这里插入图片描述

单链表的逆转

new指针表示逆转好的节点,old表示还没有逆转的节点,要交换两者指针,还需要一个tmp指针记录old后的节点。
在这里插入图片描述
在这里插入图片描述

取巧:用顺序表存储,先排序,再直接逆序输出。
在“内存”里多加几个没用的结点,让你偷懒!-- 该怎么办?

//输入参数:默认传入head指空的头节点
Ptr Reverse( Ptr head, int K )
{ 	cnt = 1;
	new = head->next; //初始化一个new指针,一开始指向1
	old = new->next;  //old指向也即是还没有逆转的节点2
	while ( cnt < K ) //需要逆转的节点个数K,这里K=4,逆转1-2-3-4-5-6为4-3-2-1-5-6
	{
		tmp = old->next; //tmp先记录节点3,也就是old/2节点的下一个节点
		old->next = new; //交换,old/节点2的下一个节点指向节点1/new
		new = old; old = tmp; //指针依次前移,new此时前移1个,old前移到节点3
		cnt++; //计数,循环
	}
	head->next->next = old; //头节点指向的原来头节点,变为现在的“尾巴/逆转的最后一个节点,也就是当前新的头节点”
	return new;
}

测试数据
 有尾巴不反转
 地址取到上下界
 正好全反转
 K=N全反转
 K=1不用反转
 最大(最后剩K-1不反转)、最小N
//边界测试
 有多余结点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值