在计算机科学领域,二叉树是最基础且重要的非线性数据结构之一。它由节点组成,每个节点最多有两个子节点,分别称为左子节点和右子节点。这种结构天然适合用C语言的指针来实现,因为每个节点可以看作一个结构体,包含数据域和两个指针域。
用C语言手动实现二叉树具有特殊意义:
我见过太多初学者在二叉树实现上踩坑,特别是在递归终止条件和指针操作方面。下面这个基础结构体定义,就是一切实现的起点:
c复制typedef struct TreeNode {
int data; // 节点数据域
struct TreeNode* left; // 左子树指针
struct TreeNode* right; // 右子树指针
} TreeNode;
创建单个节点是构建二叉树的基础操作。需要注意三点:
c复制TreeNode* createNode(int value) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (!newNode) {
perror("Memory allocation failed");
exit(EXIT_FAILURE);
}
newNode->data = value;
newNode->left = newNode->right = NULL;
return newNode;
}
构建完整二叉树时,可以采用递归方式。以下是一个手动构建示例:
c复制TreeNode* buildSampleTree() {
TreeNode* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
return root;
}
重要提示:实际应用中应该通过更智能的方式构建树,比如根据输入数据动态构建,而非硬编码。这里只是为了演示基本结构。
遍历是二叉树最基础的操作,主要有三种递归实现方式:
c复制void preOrderTraversal(TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->data); // 先访问根节点
preOrderTraversal(root->left);
preOrderTraversal(root->right);
}
c复制void inOrderTraversal(TreeNode* root) {
if (root == NULL) return;
inOrderTraversal(root->left);
printf("%d ", root->data); // 中间访问根节点
inOrderTraversal(root->right);
}
c复制void postOrderTraversal(TreeNode* root) {
if (root == NULL) return;
postOrderTraversal(root->left);
postOrderTraversal(root->right);
printf("%d ", root->data); // 最后访问根节点
}
遍历时的常见错误:
层次遍历需要借助队列数据结构。以下是使用简单队列的实现:
c复制void levelOrderTraversal(TreeNode* root) {
if (root == NULL) return;
// 简易队列实现
TreeNode* queue[100]; // 假设树节点不超过100
int front = 0, rear = 0;
queue[rear++] = root;
while (front < rear) {
TreeNode* current = queue[front++];
printf("%d ", current->data);
if (current->left != NULL)
queue[rear++] = current->left;
if (current->right != NULL)
queue[rear++] = current->right;
}
}
对于更健壮的实现,应该使用动态扩容的队列或者链式队列。这里为了简洁使用了固定大小数组。
计算二叉树节点总数是常见的面试题。递归解法非常简洁:
c复制int countNodes(TreeNode* root) {
if (root == NULL) return 0;
return 1 + countNodes(root->left) + countNodes(root->right);
}
这个实现的时间复杂度是O(n),因为需要访问每个节点一次。空间复杂度是O(h),h是树的高度,由递归栈深度决定。
性能提示:对于非常大规模的树,递归可能导致栈溢出。这时可以用迭代方式+显式栈来实现。
树的高度(最大深度)定义为从根节点到最远叶子节点的最长路径上的节点数:
c复制int treeHeight(TreeNode* root) {
if (root == NULL) return 0;
int leftHeight = treeHeight(root->left);
int rightHeight = treeHeight(root->right);
return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}
常见错误包括:
查找二叉树中是否包含某个值的节点:
c复制TreeNode* findNode(TreeNode* root, int value) {
if (root == NULL) return NULL;
if (root->data == value) return root;
TreeNode* leftResult = findNode(root->left, value);
if (leftResult != NULL) return leftResult;
return findNode(root->right, value);
}
这个实现会返回第一个找到的匹配节点。如果需要所有匹配节点,需要修改实现方式。
由于二叉树节点是动态分配的,使用完毕后需要正确释放:
c复制void freeTree(TreeNode* root) {
if (root == NULL) return;
freeTree(root->left);
freeTree(root->right);
free(root);
}
注意释放顺序必须是后序(先释放子树再释放根),否则会导致访问已释放内存。
完全二叉树是指除了最后一层外,其他层节点都达到最大数量,且最后一层节点都集中在左侧。判断算法:
c复制bool isCompleteTree(TreeNode* root) {
if (root == NULL) return true;
TreeNode* queue[100];
int front = 0, rear = 0;
bool hasNull = false;
queue[rear++] = root;
while (front < rear) {
TreeNode* current = queue[front++];
if (current == NULL) {
hasNull = true;
} else {
if (hasNull) return false;
queue[rear++] = current->left;
queue[rear++] = current->right;
}
}
return true;
}
将二叉树转换为字符串表示(序列化),以及从字符串重建二叉树(反序列化):
c复制// 序列化:使用前序遍历,空节点用'#'表示
void serialize(TreeNode* root, char* str, int* index) {
if (root == NULL) {
str[(*index)++] = '#';
str[(*index)++] = ',';
return;
}
char numStr[20];
sprintf(numStr, "%d", root->data);
strcpy(str + *index, numStr);
*index += strlen(numStr);
str[(*index)++] = ',';
serialize(root->left, str, index);
serialize(root->right, str, index);
}
// 反序列化
TreeNode* deserialize(char* str, int* index) {
if (str[*index] == '#') {
*index += 2; // 跳过'#'和','
return NULL;
}
int num = 0;
while (str[*index] != ',') {
num = num * 10 + (str[*index] - '0');
(*index)++;
}
(*index)++; // 跳过','
TreeNode* root = createNode(num);
root->left = deserialize(str, index);
root->right = deserialize(str, index);
return root;
}
虽然递归实现简洁,但在极端情况下(如极度不平衡的树)可能导致栈溢出。关键操作的迭代实现示例:
c复制// 迭代方式的中序遍历
void inOrderIterative(TreeNode* root) {
TreeNode* stack[100];
int top = -1;
TreeNode* current = root;
while (current != NULL || top != -1) {
while (current != NULL) {
stack[++top] = current;
current = current->left;
}
current = stack[top--];
printf("%d ", current->data);
current = current->right;
}
}
对于频繁遍历的场景,可以考虑实现线程二叉树(Threaded Binary Tree),它利用空指针域存储遍历线索,可以不用栈或递归实现遍历。
对于需要频繁创建/销毁节点的场景,可以实现简单的内存池来提升性能:
c复制#define POOL_SIZE 1000
typedef struct {
TreeNode nodes[POOL_SIZE];
int nextAvailable;
} TreeNodePool;
TreeNode* allocateNode(TreeNodePool* pool) {
if (pool->nextAvailable >= POOL_SIZE) return NULL;
return &pool->nodes[pool->nextAvailable++];
}
void initPool(TreeNodePool* pool) {
pool->nextAvailable = 0;
}
内存管理:
递归深度:
错误处理:
性能考量:
测试建议:
c复制// 示例测试代码框架
void testTreeFunctions() {
TreeNode* root = NULL;
assert(countNodes(root) == 0);
assert(treeHeight(root) == 0);
root = buildSampleTree();
assert(countNodes(root) == 5);
assert(treeHeight(root) == 3);
TreeNode* found = findNode(root, 3);
assert(found != NULL && found->data == 3);
freeTree(root);
}