最近想把之前学的一些东西稍微总结一下,用C语言写一下基本的数据结构,先从比较难理解但是容易写的二叉树开始。
目录
一、学习二叉树不得不说的知识
1.1二叉树的定义
二叉树(binary tree)是一个有限的结点集合,这个集合或者为空,或者由一个根节点和两根互不相交的称为左子树和右子树的二叉树组成。
这是官方给出的定义,我们可以从中知道这么几个点:
(1)二叉树是可以为一个空树的,也就是一个节点都没有,成为空二叉树
(2) 二叉树的度最多是2,意味着左右结点可以为空,但是不要求度只能是2。当所有分支节点都有左右孩子节点的时候,并且叶子节点都集中在二叉树最下一层时,这种二叉树成为满二叉树。
两个基本概念:
(1)结点的度:某个结点的子数的个数
(2)叶子结点:度为0的结点,也就是没有左右孩子的结点
满二叉树如图所示(超级整齐是不是)
(3)最重要的一点,每一个根节点的左右孩子子树也是一个二叉树,这也就是我们下列使用递归算法的前提。正是因为他的左右孩子结点是一颗全新二叉树的根节点,我们对此类问题才能得心应手,但是这也是比较难理解的点。
1.2其他的一些基础知识
(1)完全二叉树:若二叉树最多只有最下面两层结点度数可以小于2,并且最下面一层的叶子节点都依次排列在该层最左边的位置上,这样的树成为完全二叉树(complete binary tree)
满二叉树其实就是完全二叉树的一种特例
抓住这两个关键字”最下面两层“、”最左边“,很关键,这就好像让二叉树赋予了某种线性特征一般,结点与结点之间更加有序了,从上到下,从左到右,一个接着一个标号,只要有了结点数目我们就可以构造出一个唯一的完全二叉树,下列所说的顺序存储结构与之有很大的联系。结合下图看看:
(2)二叉树效率更高(那必须),任何m次树都i可以转换为二叉树结构(这很关键),具体转换过程大家可以自行搜索。
二、二叉树的存储结构
2.1顺序存储结构
记住几个比较重要的点
(1)对于一般的二叉树,如果按照顺序结构存储,很难得到二叉树中结点与结点之间的逻辑关系,所以我们可以将一般二叉树进行改造,通过虚构一些空节点,从而将一般二叉树转换为完全二叉树,只要将虚构的空节点用特殊的标号(比如‘@’)就可以
(2)完全二叉树和满二叉树适合采用顺序存储结构,因为不会造成空间的大量浪费,而对于一般二叉树,当树的深度提高,空节点数目过多会造成空间的大量浪费。这是顺序结构的缺陷
(3)顺序结构存储有利于我们快速找到一个结点的孩子和双亲。结合上述完全二叉树的结点来看,如果我们将二叉树每个结点的编号放入了一个数组中
typedef Elemtype sqBinTree[MaxSize]
那么对于编号为i的结点,他的双亲结点的编号就是【i/2】,如果他有左孩子,他的左孩子的结点编号就是【2*i】,如果他有右孩子,他的右孩子的结点编号就是【2*i+1】。
总之呢,他虽然可以快速查找双亲结点和孩子结点,但是他由于浪费大量空间,并且这也导致了插入删除等操作会比较麻烦,正常不用。
2.2 链式存储结构
为了节省空间,我们想到用链式存储结构保存树的结点,这对于已经学过链表、栈等数据结构的我们来说并不陌生,他之中包含所保存的数据和指针域,其中对于二叉树,他的指针域就比较特殊了,包含两个指向树节点的指针,一个指向左孩子结点,另一个指向右孩子结点。
typedef struct node
{
Elemtype data;
struct node* lchild;
struct node* rchild;
}BTNode;
优势不仅在于可以节省空间,而且可以快速找到孩子节点
缺陷在于很难找到双亲结点,比较麻烦
三、基础功能及代码实现
3.1 构造二叉树
构造二叉树之前先说两个结论
(1)任何n个结点的二叉树都可以由他的中序序列和先序序列唯一地确定
(2)任何n个结点的二叉树都可以由他的中序序列和后序序列唯一地确定
用这两个结论,我们很容易用两种方法构造出同一个二叉树
两种思路操作方法类似,我们就第一种思路展开说说:
由先序序列和中序序列构造二叉树:先序序列提供了二叉树的根节点的信息(任何一棵二叉树的先序序列的第一个节点为根节点),而中序序列提供了由根节点将整个序列分为左、右子树的信息。
- 确定树的根节点:先序遍历的第一个节点
- 求解树的子树:找出根节点在中序遍历中的位置,根左边的是左子树,右边的是右子树。
- 递归求解树:将左子树和右子树看成一棵二叉树,重复上面步骤
如图,说说分析步骤
1 根据先序可以知道根节点是A,此时用中序序列发现A的位置,此时我们得到结论:以A为根节点的左子树由DGB组成,右子树由ECF组成
2 将DGB作为一个独立的子树,在先序中继续遍历三个结点,顺序为BDG,由于先序序列的第一个结点肯定是根节点,所以此处B为根节点,此时找到DGB(中序),就可以发现DG肯定位于B的左子树上
3 在B的左子树上,先序序列为DG,中序序列为DG,可以由先序序列找到D是根节点,G可以在中序序列是在D后面,由此判断为右孩子
4对于A的右子树结点ECF,与上述步骤类似
个人认为,最重要的落脚在于递归,根据根节点的位置关系一步一步找到左子树,右子树,左子树和右子树又是独立的子树,所以他们也有各自的根节点和左右孩子,由此可以一步一步深入。
先上代码:
//构造二叉树1
//任何n个结点的二叉树都可以由他的中序序列和先序序列唯一地确定
//先序序列的第一个值一定是根节点
/*
*
* 返回值:根节点指针
* 参数:pre,先序序列,mid:中序序列,n:二叉树节点数目
*/
BTNode* create_tree_method1(Elemtype* pre, Elemtype* mid, int n)
{
int len = 0;
Elemtype* ptr = NULL;//用于寻找mid中保存根节点的坐标
if (n <= 0)
return NULL;
BTNode* tree = (BTNode*)malloc(sizeof(BTNode));
tree->data = *pre;//确认根节点
//测试printf("%c\n", tree->data);
for (ptr = mid; ptr < mid + n; ptr++,len++)
{
if (*ptr == *pre)
{
break;// 找到根节点了
}
}
tree->lchild = create_tree_method1(pre + 1, mid, len);
tree->rchild = create_tree_method1(pre + len + 1, ptr + 1, n - len - 1);
return tree;
}
对于第二种思路,就直接上代码了,大家可以尝试先写一下
//构造二叉树2
//任何n个结点的二叉树都可以由他的中序序列和后序序列唯一地确定
//后序序列的最后一个值一定是根节点
/*
*
* 返回值:根节点指针
* 参数:post,后序序列,mid:中序序列,n:二叉树节点数目
*/
BTNode* create_tree_method2(Elemtype* post, Elemtype* mid, int n)
{
if (n <= 0)
return NULL;
int len = 0;
Elemtype dat = *(post + n - 1);//后序序列的最后一个值一定是根节点
BTNode* b = (BTNode*)malloc(sizeof(BTNode));
b->data = dat;
Elemtype* ptr;
for (ptr = mid; ptr < mid + n; ptr++,len++)
{
if (*ptr == dat)//查找根节点,从而确定左子树的长度
break;
}
b->lchild = create_tree_method2(post, mid, len);//构造左子树
b->rchild = create_tree_method2(post + len, ptr + 1, n - len - 1);// 构造右子树
return b;
}
3.2 销毁二叉树
简单简单,对于每个二叉树(我可没说是你传入参数为根节点的二叉树,而是每一个只有根节点、可能没有左节点、可能没有右节点的二叉树,唯独那个空二叉树我觉得可以不去销毁啊,毕竟也毁不掉啊)
那我们的思路就是下面的代码
/*
* 销毁二叉树
* 传入参数:二叉树根节点的地址
*/
void destroy_tree(BTNode** tree)
{
if (*tree != NULL)
{
destroy_tree(&(*tree)->lchild);//回收左子树
destroy_tree(&(*tree)->rchild);//回收右子树
free(*tree);//回收根节点,一定要最后回收根节点,否则就找不到左右了
(*tree)->lchild = NULL;
(*tree)->rchild = NULL;
}
}
3.3 输出二叉树
用括号表示法输出二叉树,其实步骤很简单,就是先输出根节点数据,然后输出一个括号,输出左孩子结点数据,如果有右孩子,就输出右孩子节点数据。
但是每一个左孩子又是某个节点的双亲结点,于是我们不能简简单单的只把孩子看作孩子,可能在下一层,他就当爹了(也就是根节点),于是我们避免不了使用迭代,但其实也简单直接把他当作根节点传入函数就可以。
/*
* 输出二叉树,括号表述法输出
* 无返回值
* 参数:根节点指针
*/
void display_tree(BTNode* tree)
{
if (tree != NULL)
{
fprintf(stdout,"%c", tree->data);//只输出根节点
if (tree->lchild != NULL || tree->rchild != NULL)
{
//此时已经到了下一层,一定要加左括号
fprintf(stdout, "(");
//其他的交给左子树,左子树此时也是一个根节点(相对的)
display_tree(tree->lchild);
if (tree->rchild != NULL)
fprintf(stdout, ",");
display_tree(tree->rchild);
fprintf(stdout, ")");
}
}
}
3.4 二叉树节点数和深度
求节点数,也就是将左孩子结点和右孩子结点树都加起来再加1(别忘了根节点)
递归一定要有一个出去的机会,也就是当我们递归到了空二叉树,那空二叉树的节点数也就是0咯,这个一定别忘!
/*
* 功能:求二叉树的节点数
* 传入根节点,返回节点数
*/
int getNum_tree(BTNode* tree)
{
//如果是叶子节点的孩子节点,那么就返回节点数目为0
if (tree == NULL)
return 0;
return getNum_tree(tree->lchild) + getNum_tree(tree->rchild) + 1;
}
求深度,也就是看左孩子深度和右孩子深度哪个更高,再加上我们自己的根节点的这一层,再加一个1嘛,同样的,别忘了递归出去的条件
/*
* 功能:求二叉树的深度
* 传入根节点,返回深度
*/
int getHeight_tree(BTNode* tree)
{
//如果已经到了树的树叶节点的孩子节点,可以认为那个子树的深度为0
if (tree == NULL)
return 0;
int num_lchild = getHeight_tree(tree->lchild);
int num_rchild = getHeight_tree(tree->rchild);
return (num_lchild > num_rchild) ? (num_lchild + 1) :(num_rchild + 1);
}
3.5 二叉树最左的结点和最右的结点
这个交给你们写啦,我放一下参考代码
/*
* 返回最左边的那个节点
* 参数:二叉树根节点
*/
static BTNode* getMostLeftNode_tree(BTNode* tree)
{
if (tree->lchild == NULL)//如果左子树是空,说明根节点就是最左节点
return tree;
else
return getMostLeftNode_tree(tree->lchild);//否则就去左子树找,就是把左孩子节点当作根节点
}
/*
* 返回最右边的那个节点
* 参数:二叉树根节点
*/
static BTNode* getMostRightNode_tree(BTNode* tree)
{
if (tree->rchild == NULL)//如果左子树是空,说明根节点就是最左节点
return tree;
else
return getMostRightNode_tree(tree->rchild);//否则就去左子树找,就是把左孩子节点当作根节点
}
3.6 查找结点
查找结点需要分析一下步骤
1 先从根节点开始找,如果数据对了,返回;如果数据不对,就把这个数据交给左子树
2 左子树先从根节点开始,类似1的步骤
3 此时如果一直这么找肯定不行,需要一个递归的返回条件,很简单,类似之前的,如果到了空二叉树还没有找到,那么直接返回NULL,告知没有,但是这个返回的NULL并不是最后的结果,而是返回给他的双亲结点看的
4 当他的双亲结点看到了NULL,也就明白了左子树没有他想要的数据,开始向右子树查找
5 右子树重复之前的寻找步骤,因为他此时也是一个二叉树的根节点,他将结果返回给他的双亲结点
6 如果返回不是NULL,那么说明找到了最终结果,可以一路返回,如果还是NULL,那也没有办法(因为此时已经遍历过左子树和右子树了)只能返回NULL表示并没有发现要查的数据。
我们上代码
/*
* 功能:查找节点
* 参数:tree:根节点指针 x:要查找的数据
* 返回值:查找结点对象的指针
*/
BTNode* findNode_tree(BTNode* tree, Elemtype x)
{
BTNode* p;
if (tree == NULL)
{
return NULL;//根节点是空节点表示该子树不存在所要查找的节点
}
else if (tree->data == x)
{
return tree;//如果根节点满足条件,返回根节点
}
else
{
p = findNode_tree(tree->lchild, x);
if (p != NULL)
{
return p;//如果根节点左孩子节点作为根节点的子树有满足条件的节点,返回
}
else
return findNode_tree(tree->rchild, x);//否则肯定在右孩子节点
}
}
四 写在最后
个人以为,二叉树最重要的是知道如何递归的这个过程,最重要的思想是把每一个结点都看作一个二叉树的根节点,而不能把二叉树只看成是一个二叉树。
我本人使用的是VS2019写下的这些代码,如果当你遇到问题,要积极使用调试,观察他是如何递归,从一个函数到一个函数,从一个树到另一个树,这些都很关键
两个方法我大力推荐
1、可通过调试----窗口----调用堆栈或者Ctrl+Alt+c来打开调用堆栈的窗口
用这个方法可以观察到函数栈帧,可以看出如何一步一步调用子树,帮助理解
2、调试--快速监视 可以进行对于变量的监视
这个对于我们解决此类问题来说也很好用,大家也可以多用用
还有其他的调试功能比如看内存、反汇编等等都十分好用,会了调试,妈妈再也不担心我找不到bug啦
五 完整代码
/*
*title: 二叉树基本函数实现
*author: 且听风铃
*data: 2023-4-26
*
*/
#include <stdio.h>
#include <stdlib.h>
typedef char Elemtype;//先假设输入值是char
typedef struct node
{
Elemtype data;
struct node* lchild;
struct node* rchild;
}BTNode;
BTNode* init_tree(Elemtype x); //初始化二叉树
BTNode* create_tree_method1(Elemtype* pre, Elemtype* mid, int n);
void display_tree(BTNode* tree);
int getNum_tree(BTNode* tree);
int getHeight_tree(BTNode* tree);
static BTNode* getMostLeftNode_tree(BTNode* tree);
static BTNode* getMostRightNode_tree(BTNode* tree);
BTNode* findNode_tree(BTNode* tree, Elemtype x);
void destroy_tree(BTNode** tree);
// 初始化二叉树,仅仅创建树根节点
/*
* 返回值:根节点指针
* 参 数:x,根节点数据
*/
BTNode* init_tree(Elemtype x)
{
BTNode* tree = (BTNode*)malloc(sizeof(BTNode));
tree->data = x;
tree->lchild = NULL;
tree->rchild = NULL;
return tree;
}
//构造二叉树1
//任何n个结点的二叉树都可以由他的中序序列和先序序列唯一地确定
//先序序列的第一个值一定是根节点
/*
*
* 返回值:根节点指针
* 参数:pre,先序序列,mid:中序序列,n:二叉树节点数目
*/
BTNode* create_tree_method1(Elemtype* pre, Elemtype* mid, int n)
{
int len = 0;
Elemtype* ptr = NULL;//用于寻找mid中保存根节点的坐标
if (n <= 0)
return NULL;
BTNode* tree = (BTNode*)malloc(sizeof(BTNode));
tree->data = *pre;//确认根节点
//测试printf("%c\n", tree->data);
for (ptr = mid; ptr < mid + n; ptr++,len++)
{
if (*ptr == *pre)
{
break;// 找到根节点了
}
}
tree->lchild = create_tree_method1(pre + 1, mid, len);
tree->rchild = create_tree_method1(pre + len + 1, ptr + 1, n - len - 1);
return tree;
}
//构造二叉树2
//任何n个结点的二叉树都可以由他的中序序列和后序序列唯一地确定
//后序序列的最后一个值一定是根节点
/*
*
* 返回值:根节点指针
* 参数:post,后序序列,mid:中序序列,n:二叉树节点数目
*/
BTNode* create_tree_method2(Elemtype* post, Elemtype* mid, int n)
{
if (n <= 0)
return NULL;
int len = 0;
Elemtype dat = *(post + n - 1);//后序序列的最后一个值一定是根节点
BTNode* b = (BTNode*)malloc(sizeof(BTNode));
b->data = dat;
Elemtype* ptr;
for (ptr = mid; ptr < mid + n; ptr++,len++)
{
if (*ptr == dat)//查找根节点,从而确定左子树的长度
break;
}
b->lchild = create_tree_method2(post, mid, len);//构造左子树
b->rchild = create_tree_method2(post + len, ptr + 1, n - len - 1);// 构造右子树
return b;
}
/*
* 输出二叉树,括号表述法输出
* 无返回值
* 参数:根节点指针
*/
void display_tree(BTNode* tree)
{
if (tree != NULL)
{
fprintf(stdout,"%c", tree->data);//只输出根节点
if (tree->lchild != NULL || tree->rchild != NULL)
{
//此时已经到了下一层,一定要加左括号
fprintf(stdout, "(");
//其他的交给左子树,左子树此时也是一个根节点(相对的)
display_tree(tree->lchild);
if (tree->rchild != NULL)
fprintf(stdout, ",");
display_tree(tree->rchild);
fprintf(stdout, ")");
}
}
}
/*
* 功能:求二叉树的节点数
* 传入根节点,返回节点数
*/
int getNum_tree(BTNode* tree)
{
//如果是叶子节点的孩子节点,那么就返回节点数目为0
if (tree == NULL)
return 0;
return getNum_tree(tree->lchild) + getNum_tree(tree->rchild) + 1;
}
/*
* 功能:求二叉树的深度
* 传入根节点,返回深度
*/
int getHeight_tree(BTNode* tree)
{
//如果已经到了树的树叶节点的孩子节点,可以认为那个子树的深度为0
if (tree == NULL)
return 0;
int num_lchild = getHeight_tree(tree->lchild);
int num_rchild = getHeight_tree(tree->rchild);
return (num_lchild > num_rchild) ? (num_lchild + 1) :(num_rchild + 1);
}
/*
* 返回最左边的那个节点
* 参数:二叉树根节点
*/
static BTNode* getMostLeftNode_tree(BTNode* tree)
{
if (tree->lchild == NULL)//如果左子树是空,说明根节点就是最左节点
return tree;
else
return getMostLeftNode_tree(tree->lchild);//否则就去左子树找,就是把左孩子节点当作根节点
}
/*
* 返回最右边的那个节点
* 参数:二叉树根节点
*/
static BTNode* getMostRightNode_tree(BTNode* tree)
{
if (tree->rchild == NULL)//如果左子树是空,说明根节点就是最左节点
return tree;
else
return getMostRightNode_tree(tree->rchild);//否则就去左子树找,就是把左孩子节点当作根节点
}
/*
* 功能:查找节点
* 参数:tree:根节点指针 x:要查找的数据
* 返回值:查找结点对象的指针
*/
BTNode* findNode_tree(BTNode* tree, Elemtype x)
{
BTNode* p;
if (tree == NULL)
{
return NULL;//根节点是空节点表示该子树不存在所要查找的节点
}
else if (tree->data == x)
{
return tree;//如果根节点满足条件,返回根节点
}
else
{
p = findNode_tree(tree->lchild, x);
if (p != NULL)
{
return p;//如果根节点左孩子节点作为根节点的子树有满足条件的节点,返回
}
else
return findNode_tree(tree->rchild, x);//否则肯定在右孩子节点
}
}
/*
* 销毁二叉树
* 传入参数:二叉树根节点的地址
*/
void destroy_tree(BTNode** tree)
{
if (*tree != NULL)
{
destroy_tree(&(*tree)->lchild);//回收左子树
destroy_tree(&(*tree)->rchild);//回收右子树
free(*tree);//回收根节点,一定要最后回收根节点,否则就找不到左右了
(*tree)->lchild = NULL;
(*tree)->rchild = NULL;
}
}
void test()
{
char str1[] = "ABDGCEF";//先序序列
char str2[] = "DGBAECF";//中序序列
char str3[] = "GDBEFCA";//后序序列
BTNode* test = create_tree_method1(str1,str2,7);//先序/中序构造树
if (test == NULL)
{
fprintf(stderr, "create error ...");
exit(1);
}
BTNode* test2 = create_tree_method2(str3, str2, 7);//中序/后续构造树
if (test2 == NULL)
{
fprintf(stderr, "create error ...");
exit(1);
}
display_tree(test);
printf("\n==========================\n");
display_tree(test2);
fprintf(stdout, "\n二叉树节点个数是%d\n", getNum_tree(test));
fprintf(stdout, "二叉树深度是%d\n", getHeight_tree(test));
//测试查找节点
BTNode* tmp = findNode_tree(test, 'D');
if (tmp == NULL)
fprintf(stdout, "未找到");
else
fprintf(stdout, "查找到的节点值为%c\n", tmp->data);
destroy_tree(&test);
destroy_tree(&test2);
//测试是否销毁完全
tmp = findNode_tree(test, 'A');
if (tmp == NULL)
fprintf(stdout, "未找到");
else
fprintf(stdout, "查找到的节点值为%c\n", tmp->data);
}
int main()
{
test();
return 0;
}