目录
前言
实验要求:
1.按先序序列构造一棵二叉链表表示的二叉树T;
2.对这棵二叉树进行递归遍历:先序、中序、后序以及层次遍历遍历序列,分别输出结点的遍历序列;
3. 对这棵树用非递归方式进行遍历:先序、中序以及后序遍历序列,分别输出结点的遍历序列;
4.求二叉树的深度/结点数目/叶结点数目;
5.将二叉树每个结点的左右子树交换位置;
6. 设计二叉树的双序遍历算法(双序遍历是指对于二叉树的每一个结点来说,先访问这个结点,再按双序遍历它的左子树,然后再一次访问这个结点,接下来按双序遍历它的右子树);
7. 计算二叉树最大宽度(二叉树的最大宽度是指二叉树所有层中结点个数的最大值);
8. 求二叉树中第一条最长的路径长度,并输出此路径上各结点。
功能介绍:
我将上述题目分成两个部分:递归和非递归。
递归部分实现:创建,先、中、后、双序、层次遍历,每个结点左右子树交换。完成1、2、5、6题。
非递归部分实现:先、中序遍历,后序遍历并求二叉树中第一条最长的路径及长度,层次遍历并求最长路径长度并输出最长路径。完成3、4、7、8题。
栈和队列:
实验需要用到栈和队列,栈和队列的定义和需要用到的基本方法如下:
#define stasize 20
#define quesize 21 //队列需多一个空间用于判满
typedef enum { true = 1, false = 0} bool;
//定义栈
typedef struct {
BTNode *data[stasize];
int top;
} Stack;
//定义循环队列
typedef struct {
BTNode *data[quesize];
int front;
int rear;
} Queue;
//栈基本操作:
Stack *InitStack(); //栈初始化
bool StackFull(Stack *s); //栈判满
bool StackEmpty(Stack *s); //栈判空
bool Push(Stack *s, BTNode *T); //入栈
bool Pop(Stack *s, BTNode **T); //出栈
bool GetTop(Stack *s, BTNode **T); //获取栈顶元素
int GetLength(Stack *s); //获取栈元素数量
//队列基本操作:
Queue *InitQueue(); //队初始化
bool QueueFull(Queue *q); //队判满
bool QueueEmpty(Queue *q); //队判空
bool EnQueue(Queue *q, BTNode *c); //入队
bool DeQueue(Queue *q, BTNode **c); //出队
为了不频繁初始化队和栈,我申请了两个全局变量:
Stack *S = InitStack();
Queue *Q = InitQueue();
代码实现及思路讲解:
递归实现
先序创建二叉树:
BTNode *PreCreateBTree(char *str, int *loca) {
BTNode *T;
if (str[*loca] == '#') { //如果当前字符是'#',表示空树
(*loca)++; //移动到下一个字符
return NULL; //返回空指针
}
else { //否则,表示有数据
T = (BTNode *)malloc(sizeof(BTNode));
if (!T) {
exit(0);
}
T->data = str[(*loca)++];
T->lChild = PreCreateBTree(str, loca); //递归创建左子树
T->rChild = PreCreateBTree(str, loca); //递归创建右子树
return T; //返回根结点指针
}
}
先、中、后、双序遍历:
代码相似度极高,复制粘贴即可轻松完成。
//先序遍历二叉树T,输出结点数据
void PreOrder(BTNode *T) {
if (T) { //如果T不为空
printf("%c ", T->data); //输出根结点数据
PreOrder(T->lChild); //递归遍历左子树
PreOrder(T->rChild); //递归遍历右子树
}
}
//中序遍历二叉树T,输出结点数据
void InOrder(BTNode *T) {
if (T) { //如果T不为空
InOrder(T->lChild); //递归遍历左子树
printf("%c ", T->data); //输出根结点数据
InOrder(T->rChild); //递归遍历右子树
}
}
//后序遍历二叉树T,输出结点数据
void PostOrder(BTNode *T) {
if (T) { //如果T不为空
PostOrder(T->lChild); //递归遍历左子树
PostOrder(T->rChild); //递归遍历右子树
printf("%c ", T->data); //输出根结点数据
}
}
//使用递归进行双序遍历
void DoubelOrder(BTNode *T) {
if (T) { //如果T不为空
printf("%c ", T->data); //输出根结点数据
DoubelOrder(T->lChild); //递归遍历左子树
printf("%c ", T->data);
DoubelOrder(T->rChild); //递归遍历右子树
}
}
层次遍历:
思路很简单:访问当前结点,然后依次判断左右子树存在否,存在则入队,然后出队一个递归即可。不必担心兄弟,兄弟会被父母结点访问到并入队的,如果有弟弟出队的就是弟弟,没有弟弟,就是长子的孩子。所以出队顺序就是层次遍历。
void LevelOrder(BTNode *T) {
if (!T) return;
BTNode *p = T;
printf("%c ", p->data); //输出根结点数据
if (p->lChild) {
EnQueue(Q, p->lChild);
}
if (p->rChild) {
EnQueue(Q, p->rChild);
}
if(QueueEmpty(Q)) return;
DeQueue(Q, &p);
LevelOrder(p);
}
交换左右子树:
采用先序遍历非常简单,就把替换功能替代原本的printf。
//将二叉树T每个结点的左右子树交换位置
void Swap(BTNode *T){
BTNode *temp;
if (T) {
temp = T->lChild;
T->lChild = T->rChild;
T->rChild = temp;
Swap(T->lChild); //递归交换左子树
Swap(T->rChild); //递归交换右子树
}
}
非递归实现
先、中序遍历:
思路也很简单:靠while循环遍历到最左树,然后有右结点访问右结点,依然还是循环到最左,先,中序区别就是printf放的位置。
//非递归先序遍历二叉树T,输出结点数据,假设栈已经开好(全局变量)
void PreOrderNonRec(BTNode *T) {
BTNode *p = T; //创建一个结点指针
while (p || !StackEmpty(S)) {
if (p) { //如果p不为空
printf("%c ", p->data);
Push(S, p); //将p入栈
p = p->lChild; //令p指向其左孩子
}
else{ //如果p为空
Pop(S, &p); //出栈一个结点指针p
p = p->rChild; //令p指向其右孩子
}
}
}
//非递归中序遍历二叉树T,输出结点数据,假设栈已经开好(全局变量)
void InOrderNonRec(BTNode *T) {
BTNode *p = T; //创建一个结点指针
while (p || !StackEmpty(S)) {
if (p) { //如果p不为空
Push(S, p); //将p入栈
p = p->lChild; //令p指向其左孩子
}
else { //如果p为空
Pop(S, &p); //出栈一个结点指针p
printf("%c ", p->data);
p = p->rChild; //令p指向其右孩子
}
}
}
后序遍历:
后序遍历需要加一个标志flag,判断左子树是否处理完成,完成为true;r用来备份上一次访问的结点,如果r指向p的右子树,说明p上一次访问完右子树回溯到左子树。也就是在中序的基础上加了一个获取栈顶元素判断是否访问过右子树没有访问右子树,访问过了就访问根结点。
void PostOrderNonRec(BTNode *T) {
BTNode *p = T, *r; //创建一个结点指针
bool flag; //是否处理完左子树
do {
while (p) { //指向最左
Push(S, p);
p = p->lChild;
}
r = NULL;
flag = true;
while (!StackEmpty(S) && flag) {
GetTop(S, &p);
if (p->rChild == r) { //处理完右子树
printf("%c ", p->data);
Pop(S, &p);
r = p; //r备份p
}
else { //右子树未处理
p = p->rChild;
flag = false;
}
}
} while(!StackEmpty(S));
}
求最长路径长度并输出最长路径的思路:
后序遍历是访问完左右子树再访问根节点,所以非递归后序遍历栈中的数据从底到顶,即为根节点到当前结点的路径,所以我们需要在遍历到叶子结点的时候判断,根节点到当前结点是不是最长路径,用GetLength获取栈元素数量即可。
如果是我们需要将栈中的元素取出并倒序再放回去,那么我使用一个缓冲栈,将栈中元素挨个取出放入缓冲栈,完成倒序,再从缓冲栈中取出,记录字符串记录元素,并放回去即可,原本的相当于栈原封不动,所以我只需要在原本非递归后序遍历的基础上把printf语句改成上述语句便。
求最长路径长度并输出最长路径的代码:
//非递归后序遍历二叉树T,求最长路径长度并输出最长路径
void GetLongestPath(BTNode *T) {
Stack *bufferS = InitStack(); //建一个栈用于缓冲
char path[stasize+1]; //存路径
BTNode *p = T, *r; //创建一个结点指针
bool flag; //是否处理完左子树
int longest = 0;
do {
while (p) { //指向最左
Push(S, p);
p = p->lChild;
}
r = NULL;
flag = true;
while (!StackEmpty(S) && flag) {
GetTop(S, &p);
if (p->rChild == r) { //处理完右子树
//替换printf语句部分:
if (longest < GetLength(S)) {
longest = GetLength(S);
while (!StackEmpty(S)) {
Pop(S, &p);
Push(bufferS, p);
}
while (!StackEmpty(bufferS)) {
Pop(bufferS, &p);
path[GetLength(S)] = p->data;
Push(S, p);
}
path[GetLength(S)] = '\0';
}
Pop(S, &p);
r = p;
}
else { //右子树未处理
p = p->rChild;
flag = false;
}
}
} while(!StackEmpty(S));
printf("longest = %d Path:", longest);
int i;
for (i = 0; i < longest; i++) printf("%c ", path[i]);
free(bufferS); //释放缓冲栈
}
层次遍历:
我将第4题和第7题一到给求了,理由如下:
1.结点数和叶结点数只要是递归都能轻易求出来;
2.最大宽度深度遍历不太好操作;
3.求深度遍历需要比较各子树的深度取大,层次遍历访问完一层就加1即可;
4.如果是递归算法,返回参数要频繁用指针传参很麻烦
思路:
定义用两个width交替记录正在处理的这一层剩余结点数量和下一层入队的结点数量,当width交替时说明访问完一层;那么结点数量只需出队时记录即可,顺便判断是否为叶子结点;层次交替时可以记录深度,可以用最大宽度和当前宽度比较取大。
代码实现:
//非递归层次遍历,并求二叉树的深度/结点数目/叶结点数目/最大宽度
void LevelOrderNonRec(BTNode *T, int *depth, int *count, int *leafCount, int *maxWidth) {
if (!T) return; //空树直接返回
EnQueue(Q, T); //将根节点入队
int width[2], loca = 2; //loca负责切换两个width
*depth = *maxWidth = width[0] = 1;
width[1] = 0; //下一层结点数量目前为0
BTNode *p;
while (!QueueEmpty(Q)) {
DeQueue(Q, &p); //出队一个节点
if (!width[loca%2]) { //如果本层结点出完,计算下一层结点数
(*depth)++; //一层结束深度+1
loca++;
if (width[loca%2] > *maxWidth) *maxWidth = width[loca%2];
}
width[loca%2]--;
(*count)++; //结点数+1
if (!p->lChild && !p->rChild) {
(*leafCount)++; //叶结点数量+1
}
if (p->lChild) {
EnQueue(Q, p->lChild);
width[(loca+1)%2]++;
}
if (p->rChild) {
EnQueue(Q, p->rChild);
width[(loca+1)%2]++;
}
}
}
结果测试:
我们用一个二叉树测试一下,如图:
测试结果
总结:
掌握二叉树的递归和非递归遍历,其他的都是在基础上功能的替换,主要需要理解递归的原理和非递归的原理和应用。递归针对于单个结点的操作会让代码非常简洁,也很简单。非递归则是在一些针对过程的需求非常好用。