本文案例提取于严蔚敏《数据结构c语言》
目录
二叉排序树
简述
二叉排序树是基础,简单说一下,不难。
基本定义简言之就是根节点的值大于左孩子小于右孩子。
重要性质:一直往左就是最小,一直往右就是最大。
插入:
就是递归,小就往左插,大就往右插,等于就不插。
删除:
两种操作,分别理解一下,书里说的太学术化。
第一种。直接把p摘掉,把p的leftchild作为f的leftchild,那么问题来了,p的rightchild呢?分析一下,p的rightchild比p大,所以一定比p左边的左右东西都大,而没了p后,p左边的就是f左边的;但是比f小,所以只能放在f左边。综上,p的rightchild是新树左边最大的,很自然就把他作为s的右孩子。
第二种。把s放在p的位置,因为s曾经是老二,现在老大没了,就可以直接上位,那么问题来了,s的左孩子怎么安排?(请注意,s是最大的,所以s没有右孩子)。分析一下,s的左孩子,应该是比q大,所以可以直接插在q的右孩子位置上。
平衡二叉树(ALV树)
简述-平衡二叉树图形的直观化
现在有一个问题就是,即使是同一个集合,以不同序列进入二叉排序树,可以形成不同结果,会造成二分查找效率下降。现在有一种方法虽然不一定构造唯一的二叉排序树,但是至少可以保证深度是最小的。
基本定义我就不说了,我们着重理解插入操作。首先看四个图,然后从中提取出一个核心模型,便于后续理解。
这张图原书上有,用来说明四种情况,然而这四个图其实是用一种方法画的,我们找到这种方法,后面对插入操作的理解和记忆就会快很多。图中,节点我只标了bf,没有标data
结合图来看,上面四个图其实都是由下面的图演变而来。首先是一颗即将不平衡的树,bf为1和0,我们可以根据需要来继续分出一个根节点和他的左右孩子来细节化描述一个情况。
然后就可以方便地解释插入过程的各种判断了。
先放插入,有两个子函数在后面,我们先了解了大方向。
宏定义:
#include<cstdio>
#include<cstdlib>
#include<cstdbool>
#define WIDTH 4
#define LH 1 //H high
#define EH 0
#define RH -1
typedef struct AVLNode {//最简单的双子定义
char data;//看情况改
int bf;//平衡系数
struct AVLNode* leftchild, * rightchild;//左孩子右孩子
}AVLNode, * AVLTree;
插入函数:
又是递归老朋友。这是个首递归,递归调用后面的都是从最后一步反着走的。
整个函数的思路就是,一直往下找,直到为空,就新建节连上,至于平不平衡,再说。
从插入之后,就开始逆向走,因为taller是引用,所以所有函数公用一个taller,从下往上,taller一直在改变,同时也在根据上一层影响后的taller改变当前层的bf。这个改变有三种情况,一种是由即将不平衡变成平衡,不仅没长高,bf也没了,另一种是由平衡变成不平衡,那就继续长高,bf也变成1或者-1,第三种情况就是从即将不平衡变成不平衡,直接调用平衡函数,自然长不高,但是bf还需要复杂的判断,这个就有平衡函数搞定了。
bool AVLInsert(AVLTree& T, char input_data, bool& taller)
{
//终止条件:找到叶节点位置
if (!T)
{
T = (AVLNode*)malloc(sizeof(AVLNode));
if (!T) exit(0);
T->data = input_data;
T->bf = EH;
T->leftchild = T->rightchild = NULL;
taller = true;//开始回传taller
return true;//成功
}
//终止条件:相等,开始连续回退
if (input_data == T->data)
{
taller = false;
return false;//失败
}
//非终止
if (input_data < T->data)//小,左插后判断平衡
{
if (!AVLInsert(T->leftchild, input_data, taller))//失败(相等)
{
return 0;//taller已经是false,只需要return0就一直回退
}
if (taller)//一旦有一层taller不变,上面的所有taller都不用变
{
switch (T->bf)
{
case LH://左上加左
{
left_balance(T);
taller = false;//平衡后就不再变高
break;
}
case EH://平上加左
{
T->bf = LH;
taller = true;
break;
}
case RH://右上加左
{
T->bf = EH;
taller = false;
break;
}
default:
{
puts("error");
}
}
}
}
else //大,右插
{
if (!AVLInsert(T->rightchild, input_data, taller))
{
return 0;
}
if (taller)
{
switch (T->bf)
{
case LH:
{
T->bf = EH;
taller = false;
break;
}
case EH:
{
T->bf = RH;
taller = true;
break;
}
case RH:
{
right_balance(T);
taller = false;
break;
}
default:
{
puts("error");
}
}
}
}
return true;//这一步最好有,虽然在大多数情况下没问题
}
旋转函数:
void r_rotate(AVLTree& T)//左重,右旋
{
AVLNode* lc = T->leftchild;//记录
T->leftchild = lc->rightchild;//转移子树
lc->rightchild = T;//改变父子关系
T = lc;//根节点变化
}
void l_rotate(AVLTree& T)//右重,左旋
{
AVLNode* rc = T->rightchild;//记录
T->rightchild = rc->leftchild;//转移子树
rc->leftchild = T;//改变父子关系
T = rc;//根节点变化
}
平衡函数:
void left_balance(AVLTree& T)
{
AVLNode* lc = T->leftchild;
//分情况平衡
//改变bf这一步一定放前面,不然旋转后,T就是lc了,都变了
switch (lc->bf)
{
case LH://单转
{
//改变bf
lc->bf = T->bf = EH;
//旋转
r_rotate(T);
break;
}
case RH://双转
{
AVLNode* rd = lc->rightchild;//rd大概是rightgrandson的意思
//根据情况改变bf
switch (rd->bf)
{
case LH:
{
lc->bf = EH;
T->bf = RH;
rd->bf = EH;//其实rd总是为0的,不然怎么叫平衡树呢
break;
}
case EH://我觉得这种情况不可能出现,怎么可能同时有两个崩溃点
{//打脸了,在简单情况下(三个点的不平衡),是这样的
rd->bf = T->bf = lc->bf = EH;
break;
}
case RH:
{
lc->bf = LH;
T->bf = EH;
rd->bf = EH;
break;
}
default:
{
puts("error");
}
}
//旋转
//l_rotate(lc);
/*注意,这么干就错了,因为我们是要去改变传入的指针的
如果你传入lc,那就改变lc,但是lc变了T->leftchild变了没?
没有,所以就错了*/
/*这里就提醒我们,如果不是引用参数,那你随便代替
但是在含有引用参数的函数里,你要改变什么就传入什么
否则传入另一个变量会导致修改失败*/
/*柳暗花明又一村,既然不能传变量,我们传引用总可以了吧
说干就干,真行!!!!*/
//AVLNode*& quote_lc = T->leftchild;//这是正确的方法
//l_rotate(quote_lc);
l_rotate(T->leftchild);
r_rotate(T);
break;
}
default:
{
puts("error");
}
}
}
void right_balance(AVLTree& T)
{
AVLNode* rc = T->rightchild;
//分情况平衡
switch (rc->bf)
{
case RH://单转
{
//修改bf
rc->bf = T->bf = EH;
//旋转
l_rotate(T);
break;
}
case LH://双转
{
AVLNode* ld = rc->leftchild;
//根据情况修改bf
switch (ld->bf)
{
case LH:
{
T->bf = EH;
rc->bf = RH;
ld->bf = EH;
break;
}
case EH:
{
T->bf = rc->bf = ld->bf = EH;
break;
}
case RH:
{
T->bf = LH;
rc->bf = EH;
ld->bf = EH;
break;
}
default:
{
puts("error");
}
}
//旋转
r_rotate(T->rightchild);
l_rotate(T);
break;
}
default:
{
puts("error");
}
}
}
前中后序遍历+旋转输出
void pre_print(AVLTree T)
{
if (!T)
{
return;
}
putchar(T->data);
pre_print(T->leftchild);
pre_print(T->rightchild);
}
void in_print(AVLTree T)
{
if (!T)
{
return;
}
in_print(T->leftchild);
putchar(T->data);
in_print(T->rightchild);
}
void post_print(AVLTree T)
{
if (!T)
{
return;
}
post_print(T->leftchild);
post_print(T->rightchild);
putchar(T->data);
}
void init_chs_depths(AVLTree T, int depth, int& p, char* chs, int* depths)
{
if (!T)
{
return;
}
init_chs_depths(T->leftchild, depth + 1, p, chs, depths);
*(chs + p) = T->data;
*(depths + p) = depth;
p++;
init_chs_depths(T->rightchild, depth + 1, p, chs, depths);
}
int main(void)
{
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
char data;
AVLTree T = NULL;
bool taller;
while ((data = getchar()) != '\n')
{
AVLInsert(T, data, taller);
}
printf("Preorder: ");
pre_print(T);
putchar('\n');
printf("Inorder: ");
in_print(T);
putchar('\n');
printf("Postorder ");
post_print(T);
putchar('\n');
//中序逆序凹入输出
char chs[30];//储存打印顺序,后面倒序打印
int depths[30];//储存对应字母的深度
int p = 0;
init_chs_depths(T, 1, p, chs, depths);
*(chs + p) = '\0';//封尾
puts("Tree:");
for (int i = p - 1; i >= 0; i--)//倒序输出
{
for (int j = 0; j < (depths[i] - 1) * WIDTH; j++)
{
putchar(' ');
}
putchar(chs[i]);
putchar('\n');
}
return 0;
}
总结和吐槽:
总结就从我的注解里抽出来。
//改变bf这一步一定放前面,不然旋转后,T就是lc了,都变了
//旋转
//l_rotate(lc);
/*注意,这么干就错了,因为我们是要去改变传入的指针的
如果你传入lc,那就改变lc,但是lc变了T->leftchild变了没?
没有,所以就错了*/
/*这里就提醒我们,如果不是引用参数,那你随便代替
但是在含有引用参数的函数里,你要改变什么就传入什么
否则传入另一个变量会导致修改失败*/
/*柳暗花明又一村,既然不能传变量,我们传引用总可以了吧
说干就干,真行!!!!*/
//AVLNode*& quote_lc = T->leftchild;//这是正确的方法
//l_rotate(quote_lc);
吐槽的话,就喷一下学校的oj。
这个oj系统的编译器是gcc4.8,有点老,不够智能,比如一个函数,一些返回是完全不可能用到的,但是他强制你必须要有这个返回,而在4.9以上的gcc里这种问题就不会再出现了,老的就会有control reaches end of non-void function [-Wreturn-type]这样的报错。