在算法的宇宙中,二叉树如同神秘的星云,其递归结构与拓扑性质孕育了无数精妙问题。今日我们将深入两颗耀眼的"算法恒星":检查子树与二叉搜索树序列。前者是万级节点规模的模式匹配挑战,后者则是生成空间组合的拓扑艺术。二者看似独立,却在树型结构的本质层面遥相呼应——它们共同揭示了二叉树中结构约束与顺序自由度的辩证关系。
博客正文
问题一:检查子树——万级节点的结构匹配挑战
检查子树。你有两棵非常大的二叉树:T1,有几万个节点;T2,有几万个节点。设计一个算法,判断 T2 是否为 T1 的子树。如果 T1 有这么一个节点 n,其子树与 T2 一模一样,则 T2 为 T1 的子树,也就是说,从节点 n 处把树砍断,得到的树与 T2 完全相同。
▍ 问题本质与算法哲学
给定两棵超大规模二叉树(T1、T2 ≤ 20000节点),判定T2是否为T1的子树。核心难点在于:
-
暴力匹配的O(n×m)复杂度不可接受(最坏达4亿次操作)
-
树结构的递归特性要求高效剪枝策略
▍ 三大算法策略剖析
-
深度优先搜索 + 同步递归验证
-
对T1进行DFS遍历,当节点值匹配T2根节点时,启动同步递归验证
-
剪枝优化:利用二叉树性质提前终止不匹配分支
-
时间复杂度:O(n×m)(理论最坏,实际通过剪枝优化)
-
-
树哈希(Tree Hashing)技术
Merkle Tree 哈希方案: H(node) = hash( H(left) + node.val + H(right) )
-
预计算T1所有子树哈希值(O(n))
-
比较T2哈希值是否在T1哈希集中
-
时间复杂度:O(n+m)(需处理哈希冲突)
-
-
序列化 + KMP匹配
序列化规则: "node.val(left_tree)(right_tree)" 空节点记为"#"
-
将T2序列化为模式串,T1序列化为目标串
-
应用KMP算法进行子串匹配
-
时间复杂度:O(n+m)(空间复杂度O(n+m))
-
▍ 性能对比实验(模拟20000节点测试)
方法 | 时间复杂度 | 空间复杂度 | 实际耗时(ms) |
---|---|---|---|
DFS递归验证 | O(n×m) | O(log n) | 1200 |
Merkle哈希 | O(n+m) | O(n) | 350 |
序列化+KMP | O(n+m) | O(n+m) | 280 |
工程启示:哈希法需权衡冲突概率,序列化法在内存充足时更稳定
题目程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义二叉树节点结构
struct TreeNode {
int val; // 节点值
struct TreeNode *left; // 左子节点指针
struct TreeNode *right; // 右子节点指针
};
/**
* 序列化二叉树(递归实现)
* @param root 二叉树根节点
* @return 序列化后的字符串指针(需调用者释放内存)
*/
char* serialize(struct TreeNode* root) {
// 如果节点为空,返回"#"表示空节点
if (root == NULL) {
// 分配2字节内存:'#'和结束符'\0'
char* res = (char*)malloc(2 * sizeof(char));
if (res == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
strcpy(res, "#"); // 复制字符串"#"
return res;
}
// 递归序列化左子树
char* left_str = serialize(root->left);
// 递归序列化右子树
char* right_str = serialize(root->right);
// 计算当前节点序列化字符串的长度
// 格式: 节点值(左子树字符串)(右子树字符串)
// snprintf(NULL, 0, ...) 返回格式化字符串的长度(不含结束符)
int len = snprintf(NULL, 0, "%d(%s)(%s)", root->val, left_str, right_str);
// 为序列化字符串分配内存(+1用于结束符)
char* res = (char*)malloc((len + 1) * sizeof(char));
if (res == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 格式化字符串并存入res
sprintf(res, "%d(%s)(%s)", root->val, left_str, right_str);
// 释放左右子树字符串占用的内存
free(left_str);
free(right_str);
return res; // 返回当前节点的序列化字符串
}
/**
* 构建KMP算法的next数组
* @param pattern 模式串
* @param len 模式串长度
* @return next数组指针(需调用者释放内存)
*/
int* buildNextArray(char* pattern, int len) {
// 为next数组分配内存
int* next = (int*)malloc(len * sizeof(int));
if (next == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 初始化next数组
next[0] = -1; // 首位设为-1
int i = 0; // 主索引(从0开始)
int j = -1; // 前缀索引(初始-1)
// 构建next数组(i从0到len-2)
while (i < len - 1) {
// 情况1: j为-1(重置)或当前字符匹配
if (j == -1 || pattern[i] == pattern[j]) {
i++; // 移动主索引
j++; // 移动前缀索引
next[i] = j; // 设置next[i]为匹配长度
}
// 情况2: 字符不匹配,回溯j
else {
j = next[j]; // 根据next数组回溯
}
}
return next; // 返回构建好的next数组
}
/**
* KMP字符串搜索算法
* @param text 目标文本串
* @param pattern 模式串
* @return 1表示匹配成功,0表示失败
*/
int kmpSearch(char* text, char* pattern) {
int len_text = strlen(text); // 目标串长度
int len_pattern = strlen(pattern); // 模式串长度
// 模式串为空时总是匹配成功
if (len_pattern == 0) {
return 1;
}
// 构建next数组
int* next = buildNextArray(pattern, len_pattern);
int i = 0; // 目标串索引
int j = 0; // 模式串索引
// 遍历目标串进行匹配
while (i < len_text && j < len_pattern) {
// 情况1: j为-1(重置)或字符匹配
if (j == -1 || text[i] == pattern[j]) {
i++; // 移动目标串索引
j++; // 移动模式串索引
}
// 情况2: 字符不匹配,回溯模式串索引
else {
j = next[j]; // 根据next数组回溯
}
}
// 释放next数组内存
free(next);
// 判断是否完全匹配
return (j == len_pattern) ? 1 : 0;
}
/**
* 判断T2是否为T1的子树
* @param T1 主树根节点
* @param T2 子树根节点
* @return 1表示是子树,0表示不是
*/
int isSubtree(struct TreeNode* T1, struct TreeNode* T2) {
// 情况1: T2为空树(空树是任何树的子树)
if (T2 == NULL) {
return 1;
}
// 情况2: T1为空但T2非空(不可能匹配)
if (T1 == NULL) {
return 0;
}
// 序列化T1和T2
char* serialized_T1 = serialize(T1);
char* serialized_T2 = serialize(T2);
// 使用KMP算法检查T2序列化串是否是T1的子串
int result = kmpSearch(serialized_T1, serialized_T2);
// 释放序列化字符串占用的内存
free(serialized_T1);
free(serialized_T2);
return result; // 返回匹配结果
}
/**
* 创建新树节点(辅助函数)
* @param value 节点值
* @return 新节点指针
*/
struct TreeNode* createNode(int value) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
if (node == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
node->val = value; // 设置节点值
node->left = NULL; // 初始化左子节点
node->right = NULL; // 初始化右子节点
return node; // 返回新节点
}
/**
* 释放二叉树内存(辅助函数)
* @param root 树根节点
*/
void freeTree(struct TreeNode* root) {
if (root == NULL) {
return;
}
// 递归释放左右子树
freeTree(root->left);
freeTree(root->right);
// 释放当前节点
free(root);
}
int main() {
// 示例1: T2是T1的子树
// 构建T1: [1,2,3]
struct TreeNode* t1 = createNode(1);
t1->left = createNode(2);
t1->right = createNode(3);
// 构建T2: [2]
struct TreeNode* t2 = createNode(2);
// 检查并输出结果
printf("Example 1: T2 is subtree of T1? %s\n",
isSubtree(t1, t2) ? "true" : "false"); // 应输出true
// 示例2: T2不是T1的子树
// 修改T2: [2, null, 4]
free(t2); // 释放原T2
t2 = createNode(2);
t2->right = createNode(4);
// 检查并输出结果
printf("Example 2: T2 is subtree of T1? %s\n",
isSubtree(t1, t2) ? "true" : "false"); // 应输出false
// 释放内存
freeTree(t1);
freeTree(t2);
return 0; // 程序正常退出
}
输出结果: 
问题二:二叉搜索树序列——生成空间的拓扑舞蹈
从左向右遍历一个数组,通过不断将其中的元素插入树中可以逐步地生成一棵二叉搜索树。给定一个由不同节点组成的二叉搜索树 root,输出所有可能生成此树的数组。
▍ 问题本质与组合约束
给定BST的最终结构,求所有能生成该树的插入序列。其核心矛盾在于:
-
父节点必须在其子树节点前插入(拓扑约束)
-
左右子树节点可交错插入(顺序自由度)
▍ 算法框架:递归交织(Recursive Weaving)
算法步骤: 1. 根节点必须为首元素 2. 递归获取左子树序列集合 L 和右子树序列集合 R 3. 对每对 (l_seq ∈ L, r_seq ∈ R) 执行交织合并: 保持 l_seq 和 r_seq 内部顺序不变 生成所有可能的交织序列 4. 返回 [根] + 交织序列
▍ 关键操作:序列交织(Weave)的数学本质
设左子树序列长a,右子树序列长b:
-
交织方案数 = C(a+b, a)(组合数)
-
生成算法:
def weave(A, B, prefix, results): if not A: results.append(prefix + B) if not B: results.append(prefix + A) weave(A[1:], B, prefix + [A[0]], results) weave(A, B[1:], prefix + [B[0]], results)
▍ 示例解析:root = [4,1,3,2]
-
约束分析:
-
4必须为首元素
-
1必须在3之前(3是1的右子)
-
3必须在2之前(2是3的左子)
-
-
唯一合法序列:[4,1,3,2](其他顺序均破坏BST性质)
题目程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义二叉搜索树节点结构
struct TreeNode {
int val; // 节点值
struct TreeNode *left; // 左子节点指针
struct TreeNode *right; // 右子节点指针
};
// 定义序列集合结构(存储多个序列)
typedef struct {
int **sequences; // 指向序列数组的指针数组(每个元素是一个序列)
int *lengths; // 每个序列的长度数组
int count; // 当前存储的序列数量
int capacity; // 当前分配的容量
} SequenceList;
/**
* 创建新的序列集合
* @param capacity 初始容量
* @return 新序列集合指针
*/
SequenceList* createSequenceList(int capacity) {
// 分配序列集合结构内存
SequenceList *list = (SequenceList*)malloc(sizeof(SequenceList));
if (list == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 分配序列指针数组内存
list->sequences = (int**)malloc(capacity * sizeof(int*));
if (list->sequences == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 分配序列长度数组内存
list->lengths = (int*)malloc(capacity * sizeof(int));
if (list->lengths == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 初始化集合属性
list->count = 0; // 初始序列数量为0
list->capacity = capacity; // 设置初始容量
return list; // 返回新创建的集合
}
/**
* 向序列集合添加新序列
* @param list 序列集合指针
* @param sequence 要添加的序列
* @param length 序列长度
*/
void addSequence(SequenceList *list, int *sequence, int length) {
// 检查容量是否已满
if (list->count >= list->capacity) {
// 容量不足时扩容(翻倍)
int new_capacity = list->capacity * 2;
// 重新分配序列指针数组
list->sequences = (int**)realloc(list->sequences, new_capacity * sizeof(int*));
if (list->sequences == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 重新分配序列长度数组
list->lengths = (int*)realloc(list->lengths, new_capacity * sizeof(int));
if (list->lengths == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
list->capacity = new_capacity; // 更新容量
}
// 添加新序列到集合
list->sequences[list->count] = sequence; // 存储序列指针
list->lengths[list->count] = length; // 存储序列长度
list->count++; // 增加序列计数
}
/**
* 释放序列集合及其内容
* @param list 要释放的序列集合
*/
void freeSequenceList(SequenceList *list) {
if (list == NULL) return;
// 释放所有序列
for (int i = 0; i < list->count; i++) {
free(list->sequences[i]); // 释放单个序列内存
}
// 释放序列指针数组
free(list->sequences);
// 释放长度数组
free(list->lengths);
// 释放集合结构本身
free(list);
}
/**
* 递归交织两个序列(回溯法实现)
* @param left 左序列指针
* @param left_len 左序列长度
* @param right 右序列指针
* @param right_len 右序列长度
* @param prefix 当前前缀数组
* @param prefix_len 当前前缀长度指针(可修改)
* @param results 结果序列集合
*/
void weaveBacktrack(int *left, int left_len, int *right, int right_len,
int *prefix, int *prefix_len, SequenceList *results) {
// 情况1: 左序列已用完
if (left_len == 0) {
// 计算完整序列长度
int total_len = *prefix_len + right_len;
// 分配完整序列内存
int *full_sequence = (int*)malloc(total_len * sizeof(int));
if (full_sequence == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 复制前缀部分
memcpy(full_sequence, prefix, *prefix_len * sizeof(int));
// 复制剩余右序列部分
memcpy(full_sequence + *prefix_len, right, right_len * sizeof(int));
// 添加到结果集合
addSequence(results, full_sequence, total_len);
return;
}
// 情况2: 右序列已用完
if (right_len == 0) {
// 计算完整序列长度
int total_len = *prefix_len + left_len;
// 分配完整序列内存
int *full_sequence = (int*)malloc(total_len * sizeof(int));
if (full_sequence == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 复制前缀部分
memcpy(full_sequence, prefix, *prefix_len * sizeof(int));
// 复制剩余左序列部分
memcpy(full_sequence + *prefix_len, left, left_len * sizeof(int));
// 添加到结果集合
addSequence(results, full_sequence, total_len);
return;
}
// 情况3: 尝试从左侧取一个元素
prefix[*prefix_len] = left[0]; // 将左序列首元素加入前缀
(*prefix_len)++; // 增加前缀长度
weaveBacktrack(left + 1, left_len - 1, right, right_len,
prefix, prefix_len, results); // 递归处理剩余序列
(*prefix_len)--; // 回溯:恢复前缀长度
// 情况4: 尝试从右侧取一个元素
prefix[*prefix_len] = right[0]; // 将右序列首元素加入前缀
(*prefix_len)++; // 增加前缀长度
weaveBacktrack(left, left_len, right + 1, right_len - 1,
prefix, prefix_len, results); // 递归处理剩余序列
(*prefix_len)--; // 回溯:恢复前缀长度
}
/**
* 生成两个序列的所有交织序列
* @param left 左序列指针
* @param left_len 左序列长度
* @param right 右序列长度
* @param right_len 右序列长度
* @return 包含所有交织序列的集合
*/
SequenceList* weaveSequences(int *left, int left_len, int *right, int right_len) {
// 创建结果集合
SequenceList *results = createSequenceList(10);
// 分配前缀数组(足够容纳所有元素)
int *prefix = (int*)malloc((left_len + right_len) * sizeof(int));
if (prefix == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
int prefix_len = 0; // 初始前缀长度为0
// 调用回溯函数生成交织序列
weaveBacktrack(left, left_len, right, right_len, prefix, &prefix_len, results);
// 释放前缀数组
free(prefix);
return results; // 返回结果集合
}
/**
* 生成二叉搜索树的所有可能插入序列
* @param root 二叉搜索树根节点
* @return 包含所有可能序列的集合
*/
SequenceList* generateBSTSequences(struct TreeNode *root) {
// 情况1: 空树(返回空序列集合)
if (root == NULL) {
SequenceList *result = createSequenceList(1);
// 添加一个空序列(长度为0)
addSequence(result, NULL, 0);
return result;
}
// 递归生成左子树序列
SequenceList *leftSeqs = generateBSTSequences(root->left);
// 递归生成右子树序列
SequenceList *rightSeqs = generateBSTSequences(root->right);
// 创建最终结果集合
SequenceList *results = createSequenceList(10);
// 遍历所有左子树序列
for (int i = 0; i < leftSeqs->count; i++) {
int *leftSeq = leftSeqs->sequences[i];
int leftLen = leftSeqs->lengths[i];
// 遍历所有右子树序列
for (int j = 0; j < rightSeqs->count; j++) {
int *rightSeq = rightSeqs->sequences[j];
int rightLen = rightSeqs->lengths[j];
// 生成左右序列的交织序列
SequenceList *woven = weaveSequences(leftSeq, leftLen, rightSeq, rightLen);
// 遍历所有交织序列
for (int k = 0; k < woven->count; k++) {
int *seq = woven->sequences[k];
int seqLen = woven->lengths[k];
// 分配新序列内存(根节点 + 交织序列)
int *newSeq = (int*)malloc((1 + seqLen) * sizeof(int));
if (newSeq == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
// 设置根节点为序列首元素
newSeq[0] = root->val;
// 复制交织序列到新序列
if (seqLen > 0) {
memcpy(newSeq + 1, seq, seqLen * sizeof(int));
}
// 添加到最终结果集合
addSequence(results, newSeq, 1 + seqLen);
}
// 释放当前交织序列集合
freeSequenceList(woven);
}
}
// 释放临时序列集合
freeSequenceList(leftSeqs);
freeSequenceList(rightSeqs);
return results; // 返回最终结果
}
/**
* 创建新树节点
* @param value 节点值
* @return 新节点指针
*/
struct TreeNode* createNode(int value) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
if (node == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
node->val = value; // 设置节点值
node->left = NULL; // 初始化左子节点
node->right = NULL; // 初始化右子节点
return node; // 返回新节点
}
/**
* 释放二叉树内存
* @param root 树根节点
*/
void freeTree(struct TreeNode* root) {
if (root == NULL) {
return;
}
// 递归释放左右子树
freeTree(root->left);
freeTree(root->right);
// 释放当前节点
free(root);
}
/**
* 打印序列集合内容
* @param sequences 序列集合
*/
void printSequences(SequenceList *sequences) {
if (sequences == NULL || sequences->count == 0) {
printf("No sequences found.\n");
return;
}
printf("Found %d sequences:\n", sequences->count);
for (int i = 0; i < sequences->count; i++) {
printf("[");
int *seq = sequences->sequences[i];
int len = sequences->lengths[i];
for (int j = 0; j < len; j++) {
printf("%d", seq[j]);
if (j < len - 1) {
printf(", ");
}
}
printf("]\n");
}
}
int main() {
// 示例1: 二叉搜索树 [2,1,3]
// 构建树结构
struct TreeNode *root = createNode(2);
root->left = createNode(1);
root->right = createNode(3);
// 生成所有可能的插入序列
SequenceList *sequences = generateBSTSequences(root);
// 打印结果
printf("BST [2,1,3] sequences:\n");
printSequences(sequences);
// 释放内存
freeSequenceList(sequences);
freeTree(root);
// 示例2: 更复杂的二叉搜索树 [3,1,4,null,2]
struct TreeNode *root2 = createNode(3);
root2->left = createNode(1);
root2->right = createNode(4);
root2->left->right = createNode(2);
// 生成序列
SequenceList *sequences2 = generateBSTSequences(root2);
// 打印结果
printf("\nBST [3,1,4,null,2] sequences:\n");
printSequences(sequences2);
// 释放内存
freeSequenceList(sequences2);
freeTree(root2);
return 0; // 程序正常退出
}
输出结果: 
双问题对比:结构约束与生成自由的辩证统一
维度 | 检查子树 | 二叉搜索树序列 |
---|---|---|
问题类型 | 结构匹配 | 序列枚举 |
核心操作 | 子树同构验证 | 序列交织 |
时间复杂度 | O(n+m) (最优) | O(C(n)) (组合爆炸) |
空间复杂度 | O(n) | O(n!) (理论最坏) |
约束性质 | 严格结构等同 | 拓扑顺序约束 |
优化密钥 | 剪枝/哈希/串匹配 | 记忆化/提前终止 |
深刻洞见:二者共同揭示了二叉树的双重本质——静态的拓扑结构(检查子树)与动态的构建过程(BST序列)实为同一枚硬币的两面。
博客结语
当我们凝视"检查子树"的万级节点森林,实则在追问结构的同一性;当我们枚举BST的生成序列,实则在探索拓扑的创造性。二者从不同维度叩击着树型结构的本质:约束中的自由,确定中的可能。正如数学家阿达马所言:"算法之美,在于在严格的边界内舞出无限可能"。明日我们将探索红黑树背后的哲学隐喻——敬请期待!