万节点丛林寻踪:解构检查子树与二叉搜索树序列的算法美学

#王者杯·14天创作挑战营·第4期#

在算法的宇宙中,二叉树如同神秘的星云,其递归结构与拓扑性质孕育了无数精妙问题。今日我们将深入两颗耀眼的"算法恒星":检查子树二叉搜索树序列。前者是万级节点规模的模式匹配挑战,后者则是生成空间组合的拓扑艺术。二者看似独立,却在树型结构的本质层面遥相呼应——它们共同揭示了二叉树中结构约束顺序自由度的辩证关系。


博客正文

问题一:检查子树——万级节点的结构匹配挑战
检查子树。你有两棵非常大的二叉树:T1,有几万个节点;T2,有几万个节点。设计一个算法,判断 T2 是否为 T1 的子树。如果 T1 有这么一个节点 n,其子树与 T2 一模一样,则 T2 为 T1 的子树,也就是说,从节点 n 处把树砍断,得到的树与 T2 完全相同。

▍ 问题本质与算法哲学
给定两棵超大规模二叉树(T1、T2 ≤ 20000节点),判定T2是否为T1的子树。核心难点在于:

  • 暴力匹配的O(n×m)复杂度不可接受(最坏达4亿次操作)

  • 树结构的递归特性要求高效剪枝策略

▍ 三大算法策略剖析

  1. 深度优先搜索 + 同步递归验证

    • 对T1进行DFS遍历,当节点值匹配T2根节点时,启动同步递归验证

    • 剪枝优化:利用二叉树性质提前终止不匹配分支

    • 时间复杂度:O(n×m)(理论最坏,实际通过剪枝优化)

  2. 树哈希(Tree Hashing)技术

    Merkle Tree 哈希方案:
      H(node) = hash( 
         H(left) + 
         node.val + 
         H(right) 
      )
    • 预计算T1所有子树哈希值(O(n))

    • 比较T2哈希值是否在T1哈希集中

    • 时间复杂度:O(n+m)(需处理哈希冲突)

  3. 序列化 + 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
序列化+KMPO(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的生成序列,实则在探索拓扑的创造性。二者从不同维度叩击着树型结构的本质:约束中的自由,确定中的可能。正如数学家阿达马所言:"算法之美,在于在严格的边界内舞出无限可能"。明日我们将探索红黑树背后的哲学隐喻——敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司铭鸿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值