树的算法
1.基于后序遍历
1.求i,j最近的公共祖先结点(顺序存储)
2.树的后序遍历(非递归算法)
3.求i,j最近的公共祖先结点(链式存储)
13.求值为x的结点的所有祖先
2.基于层次遍历
4.求树的高度(非递归+递归),求某节点所在层次也用这个非递归就行
5.求树的宽度
6.判断是否是完全二叉树
3.基于递归构建二叉链表
7.给先序和中序(数组),构建二叉链表
8.给满二叉树的先序,求后序
4.前、中、后根遍历都可的算法
9.求双分支节点个数,叶子节点个数,单分支节点个数
10.交换所有结点的左右子树
11.求先序遍历的第k个结点
12.找到所有值为x的结点,并删除它和其子树
14.把所有叶子节点从左到右串起来,变成一个链表
15.判断两个二叉树相似
17.所有叶子的带权路径WPL之和
18.把二叉树转化为中缀表达式(带括号)
5.线索二叉树相关算法
6.孩子兄弟链表表示树
19.孩子兄弟链表存储的森林的叶子节点个数
20.孩子兄弟链表存储的树的深度
21.已知一棵树的层次序列以及每个节点的度,构造孩子兄弟链表
7.二叉排序树相关算法
22.判断是否是二叉排序树
23.二叉排序树的查找,插入,构造
24.从大到小输出BST中所有值不小于k的关键字
25.求BST中第k小的数,返回结点指针
Question 1:
任意两个结点必然存在最近公共祖先,最坏情况是根节点。且从最近公共祖先结点到根节点所经过的结点都是公共的。
这是顺序存储,双亲和孩子的位置可以互相表示,所以算法简单
若链式存储,孩子无法找双亲,就稍微难点,在后面的树的遍历中会涉及到该算法。
//查找i,j的最近公共祖先
ElemType Same_Ancestor(SqTree T,int i,int j){ (非递归)
if(T[i]!='#'&&T[j]='#'){ //结点存在
while(i!=j){ //未找到公共祖先,继续循环,当i==j,即找到公共祖先
if(i>j) i=i/2; //谁编号大,说明它的层数可能更低,谁编号大谁往上找
else j=j/2;
}
return T[i]; //最终肯定有返回值,最坏情况是根
}
}
ElemType Same_Ancestor(SqTree T,int &i,int &j){ (递归)
if(i==j) return T[i];
if(i>j)
Same_Ancestor( T,i/2,j);
else
Same_Ancestor( T,i,j/2);
}
Question 2:
给栈元素加一个属性,普通的栈只是存树节点,现加入flag标记(0表示访问了左子树,1表示访问了左右),即只有flag=1时才能进行visit访问操作。
struct elem{ //定义栈元素,包括树结点和标记
BiNode *data;
int flag;
}elem;
void PostTraverse(BiTree bt){
//1.准备工作,栈,工作指针
stack s;
BiNode *p=bt;
//2.开始循环,先访问左子树
while(!s.empty()||p){
if(p){
elem.data=p;
elem.flag=0; //构造一个完整的栈元素,把访问到的树结点信息存入栈
s.push(elem);
p=p->lchild;
}
elem=s.pop();
p=elem.data; //先出栈,访问一下信息
if(elem.flag==0){ //需要再次入栈,且修改flag为1
elem.flag=1;
s.push(elem);
p=p->rchild; //左子树访问完了,该右子树了
}
else{ //即flag==1,右子树访问过了,可以访问当前节点了
visit(p);
p=NULL;//重置p是为了,确保下一次循环的出栈
}
}
}
Question 3:
采用后序遍历的非递归算法,
当访问到某结点时,栈中所有元素均为该节点的祖先。
1.假设 i 在 j 的左边,后序遍历必定先遍历到结点 i,此时栈中都是 i 的祖先。
2.再将栈复制到另一个辅助栈中。
3.继续遍历到结点 j 时,将栈中元素从栈顶开始逐个到辅助栈去匹配。第一个匹配成功的元素就是最近公共祖先。(根结点在栈底)
typedef struct{
BiTree t;
int tag;
}stack;
stack s[],s1[]; //整了个顺序栈
//下面开始算法,返回值为最小公共祖先结点。
BiTree Same_Ancestor(BiTree bt,BiNode *i,BiNode *j){
top=0;
BiNode *p=bt;
while(top>0||p){ //这是总的大循环,工作指针p进行遍历
if(p){
top++; //这三行表示入栈
s[top].t=p;
s[top].flag=0;
p=p->lchid; //一路向左
}
//这是一个内部小循环,进行统一退栈
while(top!=0&&s[top].tag==1){ //相当于修改visit操作
//此时找到了 i 结点
if(s[top].t==i){ //假设i在j左边,即i先找到,把此时的栈复制一份,
for循环,数组赋值我就不写了;
}
//此时找到了 j 结点
if(s[top].t==j){
for(int k=top;k>0;k--)
for(int m=top1;m>0;m--)
if(s1[m].t==s[k].t) //字符串匹配。最简单的二重循环暴力法
return s[k].t; //匹配成功,返回
}
top--; //统一退栈操作,若是1就退栈,若是0 根本不循环
//当前结点不是 i也不是 j,前面两个if不走,只退栈(空遍历)
} //到这统一退栈的whle结束
//栈不空,又不能访问,只能修改以下flag,向右边遍历
if(top!=0){ //栈不空
s[top].tag=1;
p=s[top].t->rchild;
(//为什么没有入栈操作,不是二次入栈吗?
//top--;然后再top++;这不是费事吗,顺序栈可以直接访问,不需要取出来再放进去)
}
}
}
Question 4
非递归:采用层次遍历的算法,
设置level记录当前所在层数;
设置last保存当前层的最右边的结点
每次层次遍历出队时与last比较,相等则层数加1,并让last变成下一层的最右系结点。
int Depth(BiTree T){
if(T==null)
return 0; //树空
int front=-1,rear=-1; //层次遍历需要队列,用顺序队列,用下标方便表示结点的关系
int last=0,level=0;
BiTree Q[Maxsize];
BiTree p; //以上声明必要的变量,p为工作指针,接收出队的结点
Q[++rear]=T; //根节点入队
while(front<rear){ //队不空则循环
p=Q[++front]; //出队。正要访问 p
if(p->lchild)
Q[++rear]=p->lchild;
if(p->rchild)
Q[++rear]=p->rchild;
//以上是层序遍历的代码
//以下是扣题代码,(核心)
if(front==last){ //当前要出队的结点下标等于last,即当前最右边结点要被访问
level++;
last=rear; //为什么把队尾赋给last??
//若当前结点为该层最右,他的右孩子(若存在)必为下一层的最右,队尾就是下一层的最后一个结点。
} //if()
//可以在while循环中加一个判断,如果遍历到了所求结点,立即返回level(当前level),而不需要全部遍历完(整个树的高度)
} //while()
return level;
}
递归算法
int Depth(BiTree T){
if(T==null)
return 0;
else{
int left=Depth(T->lchild);
int right=Depth(T->rchild);
return left>right?left+1:right+1;
}
}
求宽度和求高度的非递归思想一致。
利用层序遍历,last,level-----变成width,记录所以层中的最大的宽度
int Width(BiTree T){
//代码完全一致,只是最后的核心代码不同!
//1.声明变量
2.while队不空,循环层序遍历
3.遍历每一个结点时进行判断是否到头==last,更新width(核心)
if(T==null)return 0;
int front=-1,rear=-1;
int width=1.last=0; //width初始化为1,不然只有一个结点时,核心函数运行不是正确答案
BiTree Q[Maxsize];
BiTree p;
Q[++rear]=T;
while(front<rear){
p=Q[++front];
if(p->lchild)
Q[++rear]=p->lchild;
if(p->rchild)
Q[++rear]=p->rchild;
if(front==last){
if(rear-last>width) //width的赋值要先于last,因为用到了老last
width=rear-last;
last=rear; //指向下一层最右
}
}
}
Question 6:
基于层次遍历,将所有结点加入队列,入队时无需判断左右孩子是否不空,空的也加入;
当出队时(遍历到当前结点)遇到了空,查看其后是否有非空结点,若有则不是完全二叉树。
bool IsComplete(BiTree T){
if(!T) return 1; //空树是满二叉树
Queue Q;
InitQueue(Q);
BiTree p=T;
EnQueue(Q,T);
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p){
EnQueue(Q,p->lchild);
EnQueue(Q,p->rchild);
}
else{
while(!IsEmpty(Q)){ //在这里加入了判断,若p为空就会跳到这里执行while
DeQueue(Q,p) //不断遍历空结点后面的结点,一旦发现有非空结点,则不是
if(p)
return 0;
}
}
} //大while
return 1; //所有的都遍历完了,还没说明不是完全二叉树,那就是完全的呗
}
Question 7:
这个题若是画图题就不难,算法题有些抽象。
核心在于划分子树,这就要用到变量来划清界限,例如用前序确定根节点后,在中序划分为左子树——根——右子树,需要标记左子树的头和尾,右子树的头和尾,这就需要4个整数变量。
l1,h1为先序的第一个和最后一个结点下标
l2,h2为中序的第一个和最后一个结点下标
BiTree PreInCreate(ElemType A[],ElemType B[],int l1,int h1,int l2,int h2){
//传入表示边界的参数,为了往下递归
//初始调用时,l1=l2=1; h1=h2=n;分别为整体的头和尾
BiTree root=(BiTree)malloc(sizeof(BiTNode)); //动态申请一个根,每调用一次都会申请一个新结点,作为当前子树的根,不断递归。
root->data=A[l1]; //先序定根
int i,llen.rlen; //定义循环计数 i , 左子树长度 llen,右子树长度 rlen
for( i=l2;B[i]!=root->data;i++); //中序定左右,从第一个开始直到找到根,这都是左子树
//这个for循环是个空循环,只用来计数 i,当前 i 为根节点下标
llen=i-l2; //左子树长度,根节点下表减去当前序列中的第一个,以后还会在分,第一个结点可能就不是此时的l2了
rlen=h2-i; //右子树长度
if(llen) //左子树不空,就继续创建左子树,修改边界,为此时的左子树序列
root->lchild=PreInCreate(A,B,l1+1,l1+llen,l2,l2_llen-1);
else
root->rchild=null;
//边界参数怎么修改的?
//l1+1为下一个根的下标;l1+llen由原来的h1变来,先序的最后一个。划分给了左子树多少结点,先序的最后一个就到哪。
//l2没变,中序第一个结点还是第一个结点,l2+llen-1中序最后一个结点变了,
//例如 初始时 l2=1,若左子树长度为1,l2+llen=2,而左子树只有1个就是1,位置对应应该是 l2+llen-1。前序为什么都后移一位,因为根节点已经被创建过了,中序为什么不后移,因为他的根在中间,中序数组中最左边的不是根;所以在边界变量上体现在先序比中序的总体要大1位。
//继续代码
if(rlen)
root->rchild=PreInCreate(A,B,h1-rlen+1,h1,h2-rlen+1,h2);
else
root->rchild=null;
//右子树都是从右边算的,最后往前减的(n-len),而不是1+len
//h2-rlen是根的位置,已经创建完了,所以他的右边一位
return root;
}
Question 8:
对于满二叉树,
前序:根——左——右
后续:左——右——根
1.左右子树结点数相等;
2.先序的第一个结点 = 后序的最后一个结点;
这就可以想到,把pre[]的最后一个结点赋给post[]的第一个结点,剩下的可分成左、右子树(从中间分),分别递归
void PreToPost(ElemType pre[],int l1,int h1,ElemType post[],int l2,int h2){
int half; //中间划分
if(h1>=l1){ //说明这一段有结点
post[h2]=pre[l1]; //后序序列的第一个点=前序最后一个点
half=(h1-l1)/2; //找到划分点
PreToPost(pre,l1+1,h1+half,post,l2,l2+half-1); //转换左子树
PreToPost(pre,l1+half+1,h1,post,l2+half,h2-1);
}
}
//递归完成后 post[]存的就是后序序列
Question 9:
任选树的遍历算法,修改visit函数(本来是访问功能,修改为计数,若度为2,则计数++)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
int num=0; //全局变量
void Double_indegree(BiTree T){ //无返回值类型,用全局变量带回
if(T){
if(T->lchild&&T->rchild)
num++;
Double_indegree(T->lchild);
Double_indegree(T->rchild);
}
}
或者另一种写法有返回值
int Double_indegree(BiTree T){
if(!T) return 0;
if(T->lchild&&T->rchild)
return Double_indegree(T->lchild)+Double_indegree(T->rchild)+1;
else
return Double_indegree(T->lchild)+Double_indegree(T->rchild);
}
Question 10:
自下而上交换,利用递归先从顶递归到最底部,交换。
void swap(BiTree &T){
swap(T->lchild);
swap(T->rchild);
//进行左右递归到底,下面是交换
BiTree temp=T->lchild;
T->lchild=T->rchild;
t->rchild=temp;
}
Question 11:
就进行先序遍历,每遍历一个结点就 计数++,到计数器=k时,返回data即可
int i=0; //全局的计数器
int Pre_k(BiTree T,int k){
if(T==null)
return -1; //表示查找失败
if(i==k)
return T->data;
i++;
int p; //定义一个整数,临时存储一下结果
p=Pre_k(T->lchild,k); //左子树递归的返回值
if(p!=-1) //返回值若不是-1 则只可能为 T->data就是所需值直接返回
return p;
p=Pre_k(T->rchild,k); //左子树没找到,才会走到这一步,且无需判断
return p; //没在左,肯定在右!
}
}
Question 12:
1.先找到x;2.删除并释放空间;这是两个基本操作,合并在一起。
void Delete(BiTree &T){ //删除操作
if(T){
Delete(T->lchild);
Delete(T->rchild);
free(T);
}
}
void Search_Del(BiTree T,int x){
if(T){
if(T->data==x)
Delete(T);
Search(T->lchild,x);
Search(T->rchild,x);
}
}
非递归后序遍历,找到值为x的点后,输出栈中的元素即可
void Ancestor(BiTree T,int x){
stack s;//假设已经定义好了
top=0;
BiTree p=T;
while(p||top>0){
if(p){
top++;
s[top].data=p;
s[top].flag=0;
p=p->lchild
}
while(top!=0&&s[top].flag==1) { //修改visit
if(p->data==x){
for(int i=1;i<=top;i++)
printf("%d",s[i].data->data);
}
top--;
}
if(top!=0){
s[top].flag=1;
p=s[top].data->rchild;
}
}
}
Question 14:
前、中、后遍历都是对于叶子都是先左后右,任取一个遍历方法。
设置前驱结点指针pre,初始为空;
第一个叶子用指针head指向,遍历到叶子时,将其前驱的rchild指向他,最后一个叶子的rchild为空。
//采用中序,修改visit中的代码即可
LinkList head,pre=NULL;
LinkList InOrder(BiTree T){
if(T){
InOrder(T->lchild);
//visit
if(T->lchild==NULL&&T->rchild==NULL){
if(pre==NULL){ //处理第一个叶子时单独操作
head-=T;
pre=T;
}
else{
pre->rchild=T;
pre=T;
}
}
InOrder(T->rchild);
pre->rchild=NULL; //表尾置空
}
return head; //返回head就等于整个链表
}
Question 15:
递归思想求解,
若T1和T2都是空树,则相似;
若一个为空一个不空,则不相似;
否则递归比较他们的左、右子树是否相似。
bool similar(BiTree T1,BiTree T2){
bool left,right; //接收左右子树的返回值,方便比较是否相似
if(T1==NULL&&T2==NULL)
return true;
else if(T1==NULL||T2==NULL)
return false;
else{
left=similar(T1->lchild,T1->rchild);
right=similar(T2->lchild,T2->rchild);
return left&&right;
}
}
Question 16:
找关系,在后序遍历中,某结点的前驱是:
1.有右孩子,则前驱为右孩子;
2.无右孩子,有左孩子,则前驱为左孩子;
3.无左无右,比较复杂。设 f 是当前结点 p 的中序线索前驱,f的左孩子就是p的前驱,若f无左孩子,继续找其前驱是否有左孩子,直到有左孩子,那个左孩子就是p的后序前驱。
4.若p是中序遍历的第一个结点(左下角),也是后序的第一个,则无前驱
ThreadTree InPostPre(ThreadTree T,ThreadTree p){
//在线索二叉树T中,找到p的后序前驱,设为q
ThreadTree q;
if(p->rtag==0) //情况1
q=p->rchild;
else if(p->ltag==0) //情况2
q=p->lchild;
else if(p->lchild==NULL) //情况4
q=NULL;
else{ //情况3最复杂,代码多 ,写在最后多美观
//顺着左线索找到p的祖先,
//再找该祖先的左孩子
while(p->ltag==1&&p->lchild!=NULL) //有左线索。跳出循环只有两种情况:1.有左孩子了,ltag=0; 2.到头了,左线索为空,到第一个节点了;
p=p->lchild;
if(p->ltag==0) //跳出循环后,判断是哪种情况,有左孩子,则左子是前驱
q=p->lchild;
else //只能是第二种情况,即为第一个结点了,无前驱
q=NULL;
}
return q;
}
Question 17:
二叉树的带权路径长度为每个叶子的深度与权值乘积的总和。
利用static 变量记录wpl
int wpl_pre(BiTree T,int deep){
static int wpl=0; //存储总和
if(T->lchild==NULL&&T->rchild==NULL) //若为叶子节点,则积累wpl。
wpl=wpl+deep*T->weight; //weight属性在定义结构体时给出,就是data
if(T->lchild)
wpl_pre(T,deep+1);
if(T->rchild)
wpl_pre(T,deep+1);
return wpl;
}
int WPL(BiTree T){
return wpl_pre(T,0);
}
Question 18:
基于中序遍历,得到所需表达式;
除了根结点和叶子结点,遍历到其他结点时在其左子树之前加" ( ",遍历完右子树之后加上 " ) "。
void BiTreeToExp(BiTree T,int deep){
if(T==NULL) return; //1.空树
else if(T->lchild==NULL&&T->rchild==NULL) //若为叶子结点
printf("%s",T->data); //直接输出数,而不要加括号
else{ //分支节点(单、双分支)情况
if(deep>1)
printf("("); //deep>1,表示不是根节点,在遍历左子树之前,需要加(
BiTreeToExp(T->lchild,deep+1); //左子树
printf("%s",T->data); //输出操作符 //输出当前结点
BiTreeToExp(T->rchild,deep+1);
if(deep>1) //遍历完右子树,加上右括号
printf(")");
}
}
void Print(BiTree T){
BiTreeToExp(T,1); //输出函数。调用该算法
}
明确叶子节点满足什么条件:
若结点没有孩子,则比为叶子。即在孩子兄弟表示法中,firstchild为空就没孩子。
所有的叶子就是 孩子子树的叶子加上兄弟子树的叶子,(即二叉树的左右子树)
typedef struct node{
ElemType data;
struct node *firstchild,*sibling;
}*Tree;
int Leaves(Tree T){
if(T==NULL) return 0;
if(T->firstchild==NULL) //无孩子
return 1+Leaves(T->sibling); //该节点为叶子,计数+1,再加节点右兄弟们的叶子一起返回。
else
return Leaves(T->firstchild)+Leaves(T->sibling);
}
若树为空,则高度为0;
否则高度为第一个孩子的高度+1和兄弟子树高度 的大者;
类比二叉树的高度,左子树和右子树高度的大者,这里存储时,我的右兄弟变成了我的右孩子,比我多了一层。所有我要+1 再与你比较,取大者。
int Height(CSTree bt){
int height_c,height_s; //分别表示第一孩子的高度,和兄弟的高度
if(bt==NULL) return 0;
else{
height_c=Height(bt->firstchild); //第一个孩子的高度
height_s=Height(bt->sibling); //右兄弟的高度
if(height_c+1>height_s)
return height_c+1; //每次返回的都是当前这个结点的最大高度,这个点可能是孩子可能是兄弟
else
return height_s;
}
}
与层次序列有关,设立一个数组,pointer[]用来存储新建树的各结点地址。
再根据层次序列与每个结点的度,逐个链接。
void ceateCSTree(CSTree &T,DataType e[],int degree[],int n){
//参数需要 层次序列 e[],各节点的度degree[],结点个数n
CSNode *pointer=new CSNode[maxsize];//申请动态数组
int i,j,d,k=0;
for(i=0;i<n;i++){ //pointer全部初始化为指针为空,只保存data,表示孤立的各节点。
pointer[i]->data=e[i];
pointer[i]->lchild=pointer[i]->rsibling=NULL;
}
for(i=0;i<n;i++){ //每一个节点都链接其左儿子和右兄弟
d=degree[i]; //d暂存当前结点的度,
if(d){
k++; //k为左儿子的编号,因为初始k=0,为根,他的左儿子就是k=1,没错,当i=2时,k怎么等于左儿子编号的?
pointer[i]->lchild=pointer[k]; //连左孩子
//d=1时不执行下面的循环,只连左儿子,没有右兄弟
for(j=2;j<=d;j++){
k++; //循环执行完毕后k=d,即为最右边的兄弟;当下一次大循环i=2时,k++,就变成了下一行的第一个结点,即左孩子编号。
pointer[k-1]->rsiblng=pointer[k]; //左兄弟连右兄弟
}
}
T=pointer[0];
delete [] pointer;
}
}
二叉排序树,中序遍历为有序序列:利用中序遍历,后一个元素一定比前一个元素大。
int pre=-1; //全局变量,保存当前结点的前一个结点的值
int IsBST(BiTree T){
int b1,b2;
if(T==NULL) //空树
return 1; //也算
else{
b1=IsBST(T->lchild);
//visit(),中序遍历,修改visit函数,把访问变成判断
if(b1==0||pre>=T->data) //失败的条件,1.左子树已经不是了;2.当前点不是
return 0;
pre=T->data; //若没进入上面的if,则意味着当前结点符合排序规则,pre记住当前结点,比较下一结点。
b2=IsBST(T->rchild);
}
}
查找
int Search(BSTree T,int key){\
while(T||key!=T->data){
if(key<T->data)
T=T->lchild;
else
T=T->rchild;
}
return T;
}
插入
int Insert(BSTree T,int key){
if(T==NULL){ //情况1:找到空地位置,要插入的位置,进行插入
BSTree p=(BSTree)malloc(sizeof(BSTree));
p->data=keyl
p->lchild=p->rchild=NULL;
return 1; //找到空地,插入完成
}
else if(T->data==key) //情况2
return 0; //有相同的元素,插入失败
else if(key<T->data) //情况3:元素应插入其左子树上
return Insert(T->lchild);
else //情况4:应插入在右子树上
return Insert(T->rchild);
}
构造:循环调用插入算法,即可
for(int i=0;i<n;i++){
Insert(T,str[i]);
}
Question 24:
首先明确顺序:从大到小,即右——根——左方式遍历
然后按这个方法进行遍历输出就行了;代码我都懒得写(中序递归算法的左右子树递归交换一下位置)
Question 25:
count为结点的一个属性,表示子树有多少个结点(包括自己)
BSTNode *Search_k(BSTree T,int k,int n){
if(k<1||k>T->count) return NULL; //k非法,小于1,大于节点个数
if(T->lchild==NULL){ //如果没有左子树。1.k在根;2.k在右子树上
if(k==1) return T;
else return Search_k(T->rchild,k-1); //往右子树中找 应该是k-1小的
}
else{ //存在左子树,看看左子树有多少个结点
if(T->lchild->count==k-1) //左子树有k-1个结点,第k个就是根
return T;
else if(T->lchild->count>k-1) //左子树的结点超级多,k一定在左子树上
return Search_k(T->lchild,k);
else
return Search_k(T->rchild,k-(T->lchild->count+1));
}
}