文章目录
二叉树
二叉树的概述
- 概述:二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 二叉树可以为空,或者包含一个根节点和多个子树
- 每个子树也是一个二叉树。
- 二叉树种类:二叉树有多种特殊类型,包括满二叉树、完全二叉树、平衡二叉树等。
- 满二叉树是一种每个非叶节点都有两个子节点的二叉树
- 完全二叉树是一种除了最后一层外所有层都被填满,并且最后一层的节点都靠左对齐的二叉树
- 平衡二叉树是一种左右子树高度差不超过1的二叉树
- 二叉树的应用
- 二叉树常用于实现搜索算法、排序算法和表达式求值等。
二叉链式结构体
c
/*二叉链表存储*/
typedef struct BTNode{
char data;
struct BTNode *lChild; // 左孩子指针
struct BTNode *rChild; // 右孩子指针
}BTNode;
遍历算法
问题:
为什么非递归遍历效率高于递归遍历效率?
答:
因为非递归遍历使用的是自定义栈,而递归遍历使用的是系统栈,系统栈是所有递归函数都通用的栈,所以效率会低。
先序遍历(根左右)
递归
c
void preOrder(BTNode *p){
if(p != NULL){
cout<<p->data<<" ";
preOrder(p->lChild); // 先序遍历左子树
preOrder(p->rChild); // 先序遍历右子树
}
}
非递归
c
分析:
1)非递归遍历用到了栈(自定义栈)
2)首先访问根结点,并让其入栈,再出栈(并输出根节点值)
3)出栈后,访问当前结点 ,让其右孩子先入栈,左孩子再入栈(因为栈先进后出,而我们先访问的是左孩子)
4)依次重复执行 2)和 3) 步骤,直到栈空为止
c
void preOrder(BTNode *bt){
/*根结点不空,执行遍历*/
if(bt != NULL){
// 1)
BTNode *stack[maxSize]; // 定义一个栈(存的都是BTNode类型指针)
int top = -1;
BTNode *p;
// 2)
stack[++top] = bt; // 根节点入栈
while(top != -1){ // 栈不空
p = stack[top--]; // p 指向根结点,并执行出栈
visit(p); // visit 访问结点
if(p->rChild != NULL){
stack[++top] = p->rChild;
}
if(p->lChild != NULL){
stack[++top] = p->lChild;
}
}
}
}
中序遍历(左根右)
递归
c
void inOrder(BTNode *p){
if(p != NULL){
inOrder(p->lChild); // 中序遍历左子树
cout<<p->data<<" ";
inOrder(p->rChild); // 中序遍历右子树
}
}
非递归
c
分析:
1)自定义栈,栈存放结点指针
2)左孩子结点一直入栈,直到无左孩子为止
3)然后判断栈是否空,不空,执行左孩子出栈、并输出,再将右孩子执行入栈
4)直到栈空且当前指针为空,结束
c
void inOrder(BTNode *bt){
if(bt){
/*创建栈,存储结点指针*/
BTNode *stack[maxSize]; // maxSize 已定义常量
int top = -1;
BTNode *p = bt; // p 指针指向根结点
while(top != -1 || p != NULL){
/*左孩子结点一直入栈*/
while(p != NULL){
stack[++top] = p;
p = p->lChild;
}
/*此时栈顶元素就是最左端结点*/
if(top != -1){
p = stack[top--];
visit(p); // visit 是访问函数,假设已定义
p = p->rChild;
}
}
}
}
后序遍历(左右根)
递归
c
void postOrder(BTNode *p){
if(p != NULL){
postOrder(p->lChild); // 后序遍历左子树
postOrder(p->rChild); // 后序遍历右子树
cout<<p->data<<" ";
}
}
非递归
c
分析:
1)先序遍历: 1 2 3 5 4 后序遍历:3 5 2 4 1 逆后序遍历:1 4 2 5 3
2)我们可以看到 逆后序遍历其实就是先序遍历改变左右子树遍历顺序,先右再左
3)所以我们可以定义两个栈,stack1存储逆后序遍历,stack2存储从stack1中的数据,然后再执行stack2出栈即可得到后序遍历
4)循环结束还是栈空的时候为止
c
void posOrder(BTNode *bt){
if(bt != NULL){
BTNode *stack1[maxSize];
BTNode *stack2[maxSize];
int top1 = -1;
int top2 = -1;
BTNode *p;
/*根结点入栈*/
stack1[++top1] = bt;
while(top1 != -1){
/*结点出栈,并访问*/
p = stack1[top1--];
/*stack1结点出栈后,进入stakc2中*/
stack2[++top2] = p;
/*先存入左子树结点再存入右子树结点*/
if(p->lChild != NULL){
stack1[++top1] = p->lChild;
}
if(p->rChild != NULL){
stack1[++top1] = p->rChild;
}
}
/*遍历输出stack2*/
while(top2 != -1){
p = stack2[top2--];
visit(p);
}
}
}
层次遍历
c
分析:
1)层次遍历要用到 队列,在这里我们使用循环队列,定义队列
2)根结点入队列,然后根节点出队列(并输出访问),然后检查当前结点的左右子树,左非空,左子树先入队列,右非空,右子树再入队列
3)然后当队列不空的时候一直循环,将队头元素弹出,重复 2)
c
void level(BTNode *bt){
/*定义队列,并初始化*/
BTNode *que[maxSize];
int front , rear;
front = rear = 0;
BTNode *p;
if(bt != NULL){
rear = (rear + 1)%maxSize;
que[rear] = bt;
while(front != rear){ // 队列不空
front = (front + 1)%maxSize;
p = que[front];
visit(p); // 访问 p
if(p->lChild != NULL){ // 左子书进入队列
que[(rear + 1)%maxSize] = p->lChild;
}
if(p->rChild != NULL){ // 右子树进入队列
que[(rear + 1)%maxSize] = p->rChild;
}
}
}
}
树的应用算法
-
表达式 (a-(b+c))*(d/e) 存储在一棵以二叉链表为存储结构的二叉树中(二叉树结点的data域为字符型),编写程序求出该表达式的值(表达式中的操作数都是一位的整数)。
c分析: 1) (a-(b+c))*(d/e) 可以看作 A * B 2)然后我们 可以先去计算 表达式 A ,再计算表达式 B,最后 A * B ,对应着先左 再右 再后,我们二叉树的后序遍历 3) 对于表达式,度要么为2 都要么为 0 ,没有度为 1 的
cint comp(BTNode *p){ int A,B; if(p != NULL){ // 根结点不空时 if(p->lchild != NULL && p->rchild !=NULL){ // 非叶子结点进行计算 A = comp(p->lchild); B = comp(p->rchild); return op(A, B,p->data); // 根据求得的 A,B 进行计算 }else{ // 叶子结点,是数值 return p->data - '0'; } }else{ return 0; // 空树,表达式值为 0 } }
op 是返回 A p->data B 计算得来的函数值
-
写一个算法求一棵二叉树的深度,二叉树以二叉链表为存储方式。
c分析: 1)求二叉树的深度,我们可以去分别计算出 左子树 和 右子树的高度,取其中的最大值 加1 就是二叉树的深度 2)计算左子树深度 计算右子树深度 根,可以采用后序遍历
cint high(BTNode *p){ int Lh,Rh; if(p == NULL){ return 0 }else{ Lh = high(p->lchild); Rh = high(p->rchild); return (Lh>Rh?Lh:Rh) + 1; // 返回左右子树深度最大值 加1 } }
-
在一棵以二叉链表为存储结构的二叉树中,查找data 域值等于 key 的结点是否存在(找到任何一个满足要求的结点即可),如果存在,则将 q 指向该结点,否则 q 赋值为 NULL ,假设 data 为 int 型。
c分析: 1)随意一种遍历方式都可以 2)为了提高效率,假设我们在左子树中找到了,我们就不在右子树找了
c/*先序遍历*/ void search(BTNode *p , BTNode *&q ,int key){ // q 改变用引用型 if(p != NULL){ // p 为空,则 q 保持NULL if(p->data == key){ q = p; }else{ search(p->lchild,q,key); search(p->rchild,q,key); } } }
c/*先序遍历高效*/ void search(BTNode *p , BTNode *&q , int key){ if(p != NULL){ if(p->data == key){ q = p; }else{ search(p->lchild,q,key); if(q == NULL){ // 左子树没找到,再去右子树找 search(p->rchild,q,key); } } } }
-
假设二叉树采用二叉链表存储结构存储,编写一个程序,输出先序遍历中 第 k 个结点的值,假设 k 不大于总的结点数(结点 data 域类型为 char 型)。
c分析: 1)要输出第 k 个结点的值,所以要采用计数器 num,全局变量 2)题目要求先序遍历,先访问根结点 再左子树,再访问右子树
cint num = 0; // 定义计数器并初始化,全局变量 void trave(BTNode *p , int k){ if(p != NULL){ ++num; if(num == k){ cout<<p->data<<endl; return; // 找到了,就无需再遍历了 } trave(p->lchild,k); trave(p->rchild,k); } }
-
假设二叉树采用二叉链表存储结构存储,设计一个算法,求出该二叉树的宽度(具有结点数最多的那一层上的结点个数)。
c分析: 1)求二叉树宽度,可以定义队列,进行层次遍历 2)可以根据双亲结点找到其左右孩子结点的层号,这样知道所有结点层号 3)通过遍历找到宽度最大的那个,即是二叉树的宽度 4)用到非循环队列,设置 maxSize足够大
ctypedef struct St{ BTNode *p; int lno; // 记录每个结点的层号 }St; int maxNode(BTNode *bt){ /*创建一个队列,并初始化*/ St qu[maxSize]; // 非循环队列 int front,rear; front=rear=0; // 队列置空 int Lno = 0; // 层号 int i,j,n,max = 0; // i,j循环变量,n max 找最大值 BTNode* q; if(bt != NULL){ qu[++rear].p = bt; // 结点入队列 qu[rear].lno = 1; // 根结点在第一层 while(front != rear){ q = qu[++front].p; Lno = qu[front].lno; if(q->lchild != NULL){ qu[++rear].p = q->lchild; qu[rear].lno = Lno+1; // 根据当前层号推测 子树层号 } if(q->rchild != NULL){ qu[++rear].p = q->rchild; qu[rear].lno = Lno + 1; } } /*循环结束后,Lno 中保存着最大层号,因为我们是将其存入一个足够大的队列中,rear 是二叉树结点的个数*/ for(i=1;i<=Lno;++i){ // 从第一层开始遍历到最后一层,i层号 n=0; for(j=0;j<rear;++j){ // j 相当于是从第一个元素到最后一个 if(qu[j].lno == i){ // qu[j].lno 存当前结点的层号 ++n; } if(max < n){ max = n; } } } return max; }else{ return 0; // 空树 返回 0 } }
-
假设二叉树采用二叉链表存储结构,设计一个算法,计算一棵给定二叉树的所有结点数。
c分析: 1)方法一:创建计数器,通过遍历,没遇到一个结点,计数器加1,这样就可以得到所有结点数 2)方法二:分冶,先去计算左子树个数,再去计算右子树个数,最后,左子树个数 + 右子树个数 + 1
cint num = 0; // 定义全局变量,用来计数 void count(BTNode *p){ if(p != NULL){ ++num; count(p->lchild); count(p->rchild); } }
cint count(BTNode* p){ int A,B; if(p == NULL){ return 0; }else{ A = count(p->lchild); // 查左子树结点个数 B = count(p->rchild); // 查右子树结点个数 return A + B + 1; // 左子树 + 右子树 + 根 } }
-
假设二叉树采用二叉链表存储结构,设计一个算法,计算一棵给定二叉树的所有叶子结点数。
c分析: 1)方法一:计数器方式,条件是左右子树都为NULL ,计数器加1 2)方法二:分冶,先计算左子树叶子结点个数 再计算右子树叶子结点个数,两个相加
cint num = 0; void treeCount(BTNode *p){ if(p != NULL){ if(!p->lchild && !p->rchild){ // 左右子树均空 ++num; } treeCount(p->lchild); treeCount(p->rchild); } }
cvoid treeCount(BTNode *p){ int A,B; if(p == NULL){ return 0; }else if(p->lchild == NULL && p->rchild == NULL){ // 是叶子结点返回 1 return 1; }else{ A = treeCount(p->lchild); B = treeCount(p->rchild); return A + B; // 计算叶子结点个数 } }
-
假设二叉树采用二叉链表存储结构,设计一个算法,利用结点的右孩子指针 rchild 将一棵二叉树的叶子结点按照从左往右的顺序串成一个单链表(在题目中定义两个指针 head 和 tail ,其中 head 指向第一个叶子结点,head 的初值为 NULL ,tail 指向最后一个叶子结点)。
c分析: 1)想要链接起所有叶子结点,我们第一步要去遍历二叉树,遇见叶子结点链入链表中 2)让head 指向第一个叶子结点,tail 一直指向最后一个叶子结点,遇见叶子结点修改 右孩子指针 3)尾插法
cvoid link(BTNode *p,BTNode *&head , BTNode *&tail){ if(p != NULL){ if(!p->lchild && !p->rchild){ // 遇见叶子结点 if(head == NULL){ // 判断是否是第一个叶子结点 head = p; tail = p; }else{ tail->rchild = p; // 上一个叶子结点链接当前叶子结点 tail = p; // 让 tail 一直指向最后一个叶子结点 } } link(p->lchild , head , tail); link(p->rchild , head , tail); } }
-
在二叉树的二叉链式存储结构中,增加一个指向双亲结点的 parent 指针,设计一个算法,给这个指针赋值,并输出所有结点到根节点的路径。
c分析: 1)首先定义新的二叉链式存储结构,增加一个 parent 双亲指针 2)去遍历二叉树,走到某个结点,即可通过 parent 指针向上找到根结点 3)直到 parent 双亲指针为空,为止,过程中,输出 路径
c/*结构体*/ typedef struct BTNode{ char data; struct BTNode *lchild; struct BTNode *rchild; struct BTNode *parent; // 双亲指针 }BTNode; /*(1)给各个结点的 parent 双亲指针赋值*/ void pTree(BTNode *p , BTNode *q){ // q 指向当前结点的双亲结点 if(p != NULL){ p->parent = q; // q 刚开始为 NULL q = p; // q 指向根结点 pTree(p->lchild,q); pTree(p->rchild,q); } } /*(2)打印单个结点到根节点的路径*/ void print(BTNode *p){ while(p != NULL){ // 最终当指向根节点上的 NULL 时结束 cout<<p->data<<" "<<endl; p = p->parent; } } /*(3)打印所有结点*/ void printAll(BTNode *p){ if(p != NULL){ print(p); // 每到一个结点,都打印一下路径 printAll(p->lchild); printAll(p->rchild); } }
-
假设满二叉树 b 的先序遍历序列已经存在于数组 A 中,长度为 n ,设计一个算法,将其转换为后序遍历序列。
c分析: 1)满二叉树,所以先序遍历第一个结点是根结点,后边等分,是左子树和右子树 2)先序遍历中第一结点是后序遍历中最后一结点 3)用分冶方法,一层一层递归的处理,重复 1)和 2)
cvoid change(char pre[] , int L1 , int R1 , char post[] , int L2 , int R2){ if(L1 <= R1){ /*将pre中第一个结点(先序遍历根结点)放入post最后位置,即后序遍历根结点*/ post[R2] = pre[L1]; /*递归处理 pre 前一半序列,将其存在 post 对应的前一半位置*/ change(pre,L1+1,(L1+1+R1)/2,post,L2,(L2+R2-1)/2); /*递归处理 pre 后一半序列,将其存在 post 对应的后一半位置*/ change(pre,(L1+1+R1)/2+1,R1,post,(L2+R2-1)/2+1,R2-1); } }
-
假设二叉树采用二叉链式存储结构,设计一个算法,求二叉树 b 中值为 x 的结点的层号。
c分析: 1)根据双亲结点可以找到子结点的层号 2)技巧图,我们定义一个层号变量,让其随着遍历 增或者减
cint L = 1; // 代表着第一层 void leno(BTNode *p , char x){ if(p != NULL){ if(p->data == x){ cout<<L<<endl; // 输出 x 所在层数 } ++L; leno(p->lchild,x); leno(p->rchild,x); --L; } }
-
假设二叉树采用二叉链式存储结构,设计一个算法,输出根节点到每个叶子结点的路径。
cint i; int top = 0; char path[maxSize]; void allPath(BTNode *p){ if(p != NULL){ path[top] = p->data; // 入栈,也是 p 自上向下走的过程 ++top; if(p->lchild == NULL && p->rchild == NULL){ for(i=0;i<top;++i){ cout<<path[i]; } } allPath(p->lchild); allPath(p->rchild); --top; // 出栈,也是 p 自下向上走的过程 } }