前言:前节二叉树只能适用于静态查找,不能实现动态插入、删除等。如何解决以下两个问题:
- 静态查找与动态查找
- 针对动态查找,数据如何组织?
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. 建一棵树,再判别其他序列是否与该树一致 (详细讲解)
两个序列是否对应相同搜索树的判别
思路: 建一棵树,再判别其他序列是否与该树一致
- 搜索树表示
- 建搜索树T
- 判别一序列是否与搜索树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 //边界测试
有多余结点