1.实现链式结构二叉树
1.1结构体定义
c
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
} BTNode;
-
BTDataType:将 int 类型重命名为 BTDataType,方便以后修改节点存储的数据类型(例如改为 double 或 char),只需修改一处即可。
-
BinaryTreeNode:二叉树节点的结构体,包含三个成员:
-
left:指向左孩子节点的指针。
-
right:指向右孩子节点的指针。
-
data:节点存储的数据,类型为 BTDataType(此处为 int)。
-
BTNode:通过 typedef 将 struct BinaryTreeNode 简写为 BTNode,后续代码中可直接使用 BTNode 定义变量,更简洁。
1.2建节点的函数 BTBuy
c
BTNode* BTBuy(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->left = newnode->right = NULL;
return newnode;
}
-
功能:动态分配一个新节点,并用传入的数据 x 初始化它。
-
参数:x 是要存储的数据。
-
返回值:指向新节点的指针;如果内存分配失败,则打印错误信息并返回 NULL。
-
步骤:
- 调用 malloc 分配一块大小为 sizeof(BTNode) 的内存,用于存放节点。
- 检查分配是否成功,若失败则用 perror 输出错误原因并返回 NULL。
- 将数据 x 存入节点的 data 成员。
- 将左右孩子指针置为 NULL,表示新节点目前没有左右孩子。
- 返回新节点的指针。
1.3构建二叉树的函数 CreateTree
c
BTNode* CreateTree()
{
BTNode* n1 = BTBuy(1);
BTNode* n2 = BTBuy(2);
BTNode* n3 = BTBuy(3);
BTNode* n4 = BTBuy(4);
BTNode* n5 = BTBuy(5);
BTNode* n6 = BTBuy(6);
BTNode* n7 = BTBuy(7);
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
// n5->left = n7;
return n1;
}
大致如下图所示:

2.二叉树的前中后序遍历
2.1遍历规则
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
- 前序遍历(Preorder Traversal)亦称先序遍历):访问根结点的操作发生在遍历其左右子树之前
访问顺序为:根结点、左子树、右子树 - 中序遍历(Inorder Traversal):访问根结点的操作发生在遍历其左右子树之中(间) 访问顺序为:左子树、根结点、右子树
- 后序遍历(Postorder Traversal):访问根结点的操作发生在遍历其左右子树之后 访问顺序为:左子树、右子树、根结点
2.2前序遍历

也就是说什么意思呢,我们在这里把NULL也添上更加便于理解:

因为是跟,左子树,右子树的顺序,所以先遍历到1,然后使左子树,2,3的左空指针NULL,右空指针NULL,2的右空指针NULL,接着是根4,然后左子树的5,5的左空指针...,依次这样,大家能看懂吧。这就是前序遍历。代码如下:
c
void PreOrder(BTNode * root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
我们在这里怎么来理解这里的递归呢?最直观的方法就是画出递归展开图,在这里我们画一遍,后续就不再画了。


2.3中序遍历

同之前一样,在这里不再赘述,保证如上顺序即可。
c
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
因为要保证顺序,在这里只需遍历后打印即可。

2.4后序遍历

跟之前相同。
c
void PoOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PoOrder(root->left);
PoOrder(root->right);
printf("%d ", root->data);
}

其实递归创立的函数栈帧我认为可以这么理解,就像我们平常打开一个程序一样你可能会在这个程序内打开一个窗口,然后在这个窗口中再打开一个窗口这样去套娃,那我们打开这么些程序窗口是不是应该是为了完成什么目的,一旦目的达成,那么我们就从后往前依次关闭窗口呀。
-
程序窗口 → 函数栈帧(stack frame)
每次调用函数(包括递归调用自身),系统都会在内存的栈区分配一块空间,存放局部变量、参数和返回地址,就像打开一个新窗口。
-
在窗口内再打开新窗口 → 函数内部调用另一个函数(或自身)
递归函数在执行到调用自身时,会暂停当前函数,转而进入新的一层,就像在第一个窗口里又打开一个子窗口。
-
目的达成后关闭窗口 → 函数执行完毕返回
当最内层的函数完成任务(比如遍历到空节点)后,它就会返回,对应的栈帧被销毁,控制权交还给上一层,就像从最里面的窗口一层层退出来。
-
从后往前依次关闭 → 栈的后进先出特性
最后调用的函数最先返回,这和窗口关闭的顺序完全一致。
3.二叉树结点个数以及高度
3.1计算节点个数(TreeSize)

我们首先来思考一下,如何计算节点个数呢?
那么此时肯定有聪明的小伙伴想到了,我们可以遍历二叉树呀,遍历到一个节点,我们就让size++,那么我们来试着写一下:
c
nt TreeSize(BTNode* root)
{
int size = 0;
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
我们来看这个代码的逻辑,是不是就是root为空就返回,不为空就++size呀,那么我们来测试看看我们的size最后的值对不对呢?
c
nt TreeSize(BTNode* root)
{
int size = 0;
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
int main()
{
BTNode* root = CreateTree();
//PreOrder(root);
//InOrder(root);
//PoOrder(root);
int size = TreeSize(root);
printf("%d ", size);
return 0;
}

我们会发现,哎呦呵,这是怎么回事,按照我们预想中的不应该是6吗,怎么是1呢?
此时细心的小伙伴可能已经发现了,我们调用函数是不是每次都会创建一个size呀,因为size是在这个函数作用域里创建的对吧,一旦出了函数,那么就没有用啦!
那么此时聪明的小伙伴又会想到了,既然是因为size出了作用域就没用了,那么我们加一个static不久完了吗,让它存在静态区不就好了嘛,那我们接下来再试一试。
c
int TreeSize(BTNode* root)
{
static int size = 0;
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
int main()
{
BTNode* root = CreateTree();
//PreOrder(root);
//InOrder(root);
//PoOrder(root);
int size = TreeSize(root);
printf("%d ", size);
return 0;
}

欸!我们发现,当加了static后,size最后果然是等于了6,那么是否说明我们的代码没有了问题呢?我们再多来几次试试:
c
int TreeSize(BTNode* root)
{
static int size = 0;
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
int main()
{
BTNode* root = CreateTree();
//PreOrder(root);
//InOrder(root);
//PoOrder(root);
printf("%d ", TreeSize(root));
printf("%d ", TreeSize(root));
printf("%d ", TreeSize(root));
return 0;
}

哎不是,这是啥情况,这怎么还会递增6呢?掐指一算,是不是我们的static搞的鬼呀,正因为存在静态区了,所以我们的size值为0也无效了,导致下一次再调用时size还是6,所以会递增6,那么我们这可怎么办,其实如果还是用size的话还有方法,如下:
c
int size = 0;
int TreeSize(BTNode* root)
{
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
我们把size定义为全局变量就好了,那么还有没有别的方法呢?比如说指针?
c
int main()
{
BTNode* root = CreatBinaryTree();
printf("TreeSize:%d\n", TreeSize(root));
size = 0;
printf("TreeSize:%d\n", TreeSize(root));
return 0;
}

指针:
c
void TreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return 0;
else
++(*psize);
TreeSize(root->left, psize);
TreeSize(root->right, psize);
}
int main()
{
BTNode* root = CreatBinaryTree();
int size = 0;
TreeSize(root, &size);
printf("TreeSize:%d\n", size);
size = 0;
TreeSize(root, &size);
printf("TreeSize:%d\n", size);
return 0;
}

这样是不是也是可以的呀,但是我们会发现,这样做是不是每次都要去手动置0呀,是不是有点别扭,那么我们还可以考虑去用递归来解决问题:
c
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left) + TreeSize(root->right) + 1;
}

那么逻辑其实很简单,我们在这里举一个例子:
1->2->3->NULL,返回到3->NULL,返回3,此时再返回就是我们计算出来的1了,因为3的左右都为NULL,再计算2的右为NULL,所以算出2,...依次循环往复。
3.2计算二叉树的叶子节点个数
c
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return TreeLeafSize(root->left)
+ TreeLeafSize(root->right);
}
那么其实这些都是相同的道理,我们想要计算叶子节点的个数,我们来想一想,上一道题是不是让我们计算节点个数,这个是计算叶子节点个数对吧,那么是不是就是我们的递归终止条件改变了呀,当一个节点的左右都为NULL,那么此时是不是叶子节点,是叶子节点我们就返回1,为空就返回0,到此,此题完成。

再来举个例子吧:
- 1->2
- 2->3
- 3->NULL,return 0
- 3->NULL,return 0
- 3,此时满足左右节点均为0,3为叶子节点返回1
- 3->2
- 2->NULL,return 0
- 2返回1
- ...依次操作均可
3.3 计算树的高度
c
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return TreeHeight(root->left) > TreeHeight(root->right) ?
TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}
我们这段代码看似没问题,实则有着一些时间复杂度的相关问题,我们这里浅用ai画个图来看看:
c
1
/ \
2 4
/ / \
3 5 6
\
7
c
1#1
├─ 2#1
│ ├─ 3#1
│ │ ├─ NULL (左)
│ │ ├─ NULL (右)
│ │ └─ NULL (右第二次) // 这里实际上NULL调用多次,但省略细节
│ ├─ NULL (2右)
│ └─ 3#2
│ ├─ NULL (左)
│ ├─ NULL (右)
│ └─ NULL (右第二次)
├─ 4#1
│ ├─ 5#1
│ │ ├─ 7#1
│ │ │ ├─ NULL (左)
│ │ │ ├─ NULL (右)
│ │ │ └─ NULL (右第二次)
│ │ ├─ NULL (5右)
│ │ └─ 7#2
│ │ ├─ ...
│ ├─ 6#1
│ │ ├─ NULL (左)
│ │ ├─ NULL (右)
│ │ └─ NULL (右第二次)
│ └─ 5#2
│ ├─ 7#3
│ │ └─ ...
│ ├─ NULL
│ └─ 7#4
│ └─ ...
└─ 4#2
├─ 5#3
│ ├─ 7#5
│ │ └─ ...
│ ├─ NULL
│ └─ 7#6
│ └─ ...
├─ 6#2
│ └─ ...
└─ 5#4
├─ 7#7
│ └─ ...
├─ NULL
└─ 7#8
└─ ...
可见,每个非叶子节点被多次调用,且随着深度增加,次数爆炸。例如节点7在1#1过程中被调用了4次(来自5#1和5#2),而在4#2中又被调用多次,最终总次数远大于节点数。
为什么效率低?
因为每次比较时,左右子树高度被计算了一次,但之后为了得到最终高度,又重复计算了其中一边。这种重复是递归传递的,导致每个子树被多次计算。例如,节点5在1#1中被计算了两次,而在4#2中又被计算两次,等等。
那么正确方法应该是把我们比较的值去记录下来就可以啦:
c
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ?
leftHeight + 1 : rightHeight + 1;
}