《从零搭建二叉树体系:从节点定义到子树判断的实战指南(含源码可直接运行)》
目录
- 二叉树基础概念
- 环境准备与目录结构
- 二叉树的构建
- 二叉树的遍历
- 二叉树的基本属性
- 经典算法题解析
- 易错点与注意事项
1. 二叉树基础概念
二叉树是每个节点最多有两个子树的树结构,通常子树被称为“左子树”和“右子树”。
基本术语
- 根节点:树的起始节点(无父节点)
- 叶子节点:无左右子树的节点
- 节点深度:从根到该节点的边数
- 节点高度:从该节点到最深叶子节点的边数
- 树的高度:根节点的高度
特殊二叉树
- 满二叉树:所有叶子节点都在同一层,且非叶子节点都有两个子节点
- 完全二叉树:除最后一层外,其余层全满,最后一层节点靠左排列
- 平衡二叉树:左右子树高度差不超过1的二叉树
2. 环境准备与目录结构
为了系统学习二叉树操作,建议使用以下目录结构:
binary_tree/
├── basic/ # 基础操作
│ ├── tree_node.h # 节点定义
│ ├── tree_construct.c # 构建二叉树
│ └── tree_traversal.c # 遍历操作
├── problems/ # 算法题目
│ ├── invert_tree.c # 翻转二叉树
│ ├── is_balanced.c # 判断平衡二叉树
│ ├── is_same_tree.c # 检查两棵树是否相同
│ ├── is_subtree.c # 另一棵树的子树
│ └── is_symmetric.c # 对称二叉树
└── test/ # 测试代码└── main.c
3. 二叉树的构建
3.1 节点定义
首先定义二叉树节点结构(tree_node.h
):
#ifndef TREE_NODE_H
#define TREE_NODE_Htypedef char TreeDataType; // 可根据需要修改数据类型typedef struct TreeNode {TreeDataType data;struct TreeNode* left; // 左子树struct TreeNode* right; // 右子树
} TreeNode;#endif
3.2 手动构建二叉树
通过手动创建节点并连接(tree_construct.c
):
#include "tree_node.h"
#include <stdlib.h>
#include <stdio.h>// 创建单个节点
TreeNode* createNode(TreeDataType data) {TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));if (node == NULL) {perror("malloc failed");return NULL;}node->data = data;node->left = NULL;node->right = NULL;return node;
}// 手动构建示例二叉树
TreeNode* buildManualTree() {// 创建节点TreeNode* node1 = createNode('1');TreeNode* node2 = createNode('2');TreeNode* node3 = createNode('3');TreeNode* node4 = createNode('4');TreeNode* node5 = createNode('5');TreeNode* node6 = createNode('6');// 连接节点node1->left = node2;node1->right = node4;node2->left = node3;node4->left = node5;node4->right = node6;return node1;
}
3.3 先序遍历字符串构建二叉树
通过先序遍历序列(含空节点标记)构建二叉树:
#include "tree_node.h"
#include <stdlib.h>// 先序遍历字符串创建二叉树('#'表示空节点)
TreeNode* buildTreeFromPreorder(const char* preStr, int* index) {// 终止条件:遇到空节点或字符串结束if (preStr[*index] == '#' || preStr[*index] == '\0') {(*index)++;return NULL;}// 创建当前节点TreeNode* node = createNode(preStr[*index]);(*index)++;// 递归构建左右子树node->left = buildTreeFromPreorder(preStr, index);node->right = buildTreeFromPreorder(preStr, index);return node;
}
重点解析:
- 使用
index
指针跟踪字符串遍历位置,确保递归过程中共享同一个计数器- 空节点用
#
标记,是重构二叉树的关键(仅靠先序遍历无法唯一确定二叉树)- 递归顺序严格遵循"根->左->右"的先序规则
4. 二叉树的遍历
遍历是二叉树最基本的操作,分为深度优先(DFS)和广度优先(BFS)两大类。
4.1 深度优先遍历(DFS)
先序遍历(根->左->右)
void preorderTraversal(TreeNode* root) {if (root == NULL) {printf("# "); // 打印空节点标记return;}printf("%c ", root->data); // 访问根节点preorderTraversal(root->left); // 遍历左子树preorderTraversal(root->right); // 遍历右子树
}
中序遍历(左->根->右)
void inorderTraversal(TreeNode* root) {if (root == NULL) {printf("# ");return;}inorderTraversal(root->left); // 遍历左子树printf("%c ", root->data); // 访问根节点inorderTraversal(root->right); // 遍历右子树
}
后序遍历(左->右->根)
void postorderTraversal(TreeNode* root) {if (root == NULL) {printf("# ");return;}postorderTraversal(root->left); // 遍历左子树postorderTraversal(root->right); // 遍历右子树printf("%c ", root->data); // 访问根节点
}
4.2 遍历示例
对于如下二叉树:
1/ \2 4/ / \3 5 6
三种遍历结果:
- 先序:
1 2 3 # # # 4 5 # # 6 # #
- 中序:
# 3 # 2 # 1 # 5 # 4 # 6 #
- 后序:
# # 3 # 2 # # 5 # # 6 # 1
5. 二叉树的基本属性
5.1 节点数量
int getNodeCount(TreeNode* root) {if (root == NULL) return 0;// 根节点数 + 左子树节点数 + 右子树节点数return 1 + getNodeCount(root->left) + getNodeCount(root->right);
}
5.2 树的高度
int getTreeHeight(TreeNode* root) {if (root == NULL) return 0;// 左子树高度与右子树高度的最大值 + 1(当前节点)int leftHeight = getTreeHeight(root->left);int rightHeight = getTreeHeight(root->right);return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}
5.3 查找节点
TreeNode* findNode(TreeNode* root, TreeDataType target) {if (root == NULL) return NULL;if (root->data == target) return root;// 先在左子树查找,找到则返回TreeNode* leftResult = findNode(root->left, target);if (leftResult != NULL) return leftResult;// 左子树未找到,在右子树查找return findNode(root->right, target);
}
易错点:
查找节点时容易遗漏左子树的返回结果判断,直接写成:findNode(root->left, target); // 错误:未处理返回值 return findNode(root->right, target);
正确做法是先判断左子树是否找到,再查找右子树
6. 经典算法题解析
6.1 翻转二叉树
题目:翻转一棵二叉树(左右子树交换)
TreeNode* invertTree(TreeNode* root) {if (root == NULL) return NULL;// 交换左右子树TreeNode* temp = root->left;root->left = root->right;root->right = temp;// 递归翻转左右子树invertTree(root->left);invertTree(root->right);return root;
}
解析:
采用后序遍历思想,先翻转左右子树,再交换当前节点的左右指针,时间复杂度O(n),空间复杂度O(h)(h为树高)。
6.2 判断平衡二叉树
题目:判断一棵二叉树是否为平衡二叉树(任意节点的左右子树高度差不超过1)
// 辅助函数:计算树的高度
int getHeight(TreeNode* root) {if (root == NULL) return 0;int left = getHeight(root->left);int right = getHeight(root->right);return 1 + (left > right ? left : right);
}// 判断是否为平衡二叉树
bool isBalanced(TreeNode* root) {if (root == NULL) return true;// 计算左右子树高度差int leftHeight = getHeight(root->left);int rightHeight = getHeight(root->right);if (abs(leftHeight - rightHeight) > 1) return false;// 递归判断左右子树return isBalanced(root->left) && isBalanced(root->right);
}
优化版本(避免重复计算):
// -1表示不平衡
int checkHeight(TreeNode* root) {if (root == NULL) return 0;int left = checkHeight(root->left);if (left == -1) return -1; // 左子树不平衡int right = checkHeight(root->right);if (right == -1) return -1; // 右子树不平衡if (abs(left - right) > 1) return -1;return 1 + (left > right ? left : right);
}bool isBalanced(TreeNode* root) {return checkHeight(root) != -1;
}
6.3 检查两棵树是否相同
题目:判断两棵二叉树是否结构相同且节点值相等
bool isSameTree(TreeNode* p, TreeNode* q) {// 都为空则相同if (p == NULL && q == NULL) return true;// 一个为空一个不为空则不同if (p == NULL || q == NULL) return false;// 节点值不同则不同if (p->data != q->data) return false;// 递归判断左右子树return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
解析:
采用同步遍历思想,同时检查两个树的节点,时间复杂度O(min(m,n)),空间复杂度O(min(h1,h2))。
6.4 对称二叉树
题目:判断一棵二叉树是否是镜像对称的
// 辅助函数:判断两个子树是否对称
bool isSymmetricHelper(TreeNode* left, TreeNode* right) {if (left == NULL && right == NULL) return true;if (left == NULL || right == NULL) return false;if (left->data != right->data) return false;// 左子树的左与右子树的右对称,左子树的右与右子树的左对称return isSymmetricHelper(left->left, right->right) && isSymmetricHelper(left->right, right->left);
}bool isSymmetric(TreeNode* root) {if (root == NULL) return true;return isSymmetricHelper(root->left, root->right);
}
6.5 另一棵树的子树
题目:判断s是否为t的子树(结构和节点值完全一致)
// 检查两棵树是否相同(复用前面的函数)
bool isSameTree(TreeNode* p, TreeNode* q);bool isSubtree(TreeNode* root, TreeNode* subRoot) {if (root == NULL) return false;// 检查当前节点为根的树是否与subRoot相同if (isSameTree(root, subRoot)) return true;// 检查左子树或右子树是否包含subRootreturn isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
7. 易错点与注意事项
-
空指针处理
所有递归函数必须先判断root == NULL
的情况,否则会导致访问空指针崩溃。 -
内存泄漏
动态分配的节点需要手动释放,释放顺序应为:先释放左右子树,再释放当前节点:void freeTree(TreeNode* root) {if (root == NULL) return;freeTree(root->left);freeTree(root->right);free(root); // 最后释放当前节点 }
-
递归终止条件
忘记设置递归终止条件会导致栈溢出,如计算树高时必须处理root == NULL
的情况。 -
参数传递
构建树时的index
必须通过指针传递,否则递归过程中无法正确共享计数器。 -
二叉树唯一性
仅通过先序/中序/后序遍历中的一种无法唯一确定二叉树,必须结合空节点标记或两种遍历序列。
总结
本文从二叉树的基础概念出发,讲解了二叉树的构建、遍历方法和基本属性计算,并通过6道经典算法题展示了二叉树的常见操作。掌握这些内容可以为后续学习更复杂的树结构(如二叉搜索树、红黑树)打下坚实基础。