数据结构:构建 (create) 一个二叉树

目录

问题的本质——什么信息才能唯一确定一棵树?

推导“最佳拍档”——哪两种遍历序列能行?

递归思想——如何构建一棵树?

第1步:确定整棵树的根节点

第2步:划分左右子树的成员

第3步:递归构建左右子树

将逻辑翻译为代码

第1步:搭建函数框架和递归出口

第2步:实现核心逻辑 - 创建根节点

第3步:划分中序序列并进行递归

第4步:创建主函数(包装函数)

完整代码和验证


问题的本质——什么信息才能唯一确定一棵树?

我们先思考一下,要构建一棵树,我们需要什么“原材料”?

假设我给你一些节点,比如 A, B, C。我告诉你 A 是根节点。那么 BC 在哪里呢?

  • 可能是 B 是左孩子,C 是右孩子。

  • 也可能是 B 是左孩子,CB 的左孩子。

  • 还可能是 BC 都是 A 的右孩子(形成一个链表)。

显然,只告诉我节点数据是不够的,因为它缺乏结构信息

那么,我们上一节学到的“遍历序列”是不是一种结构信息呢?我们来试试。

数据结构:二叉树的遍历 (Binary Tree Traversals)-CSDN博客

假设我告诉你,一棵树的前序遍历序列是 A B C。这棵树长什么样?

  • A 肯定是根节点,因为前序遍历第一个就是根。

  • BA 的左孩子吗?如果是,那么 C 可能是 A 的右孩子,也可能是 B 的左/右孩子。

  • BA 的右孩子吗?...

    A
   / \
  B   C
    A
   /
  B
 /
C
    A
   /
  B
   \
    C

你会发现,只靠一种遍历序列,我们无法唯一地确定一棵树的结构。 信息还是不够!

核心矛盾: 遍历序列是一个一维的、线性的数据。而树是一个二维的、非线性的结构。

从一维信息恢复二维结构,必然会丢失信息,导致不确定性。

解决方案: 那么,我们需要补充什么样的信息才能消除这种不确定性呢?

我们需要两种不同规则的遍历序列,用它们互相配合,锁定每一个节点的位置。


推导“最佳拍档”——哪两种遍历序列能行?

我们来分析一下上一节学到的三种遍历序列的“特长”:

  1. 前序遍历 (DLR: 根-左-右): 序列的第一个元素永远是当前这棵(子)树的根节点。

  2. 后序遍历 (LRD: 左-右-根):序列的最后一个元素永远是当前这棵(子)树的根节点。

  3. 中序遍历 (LDR: 左-根-右): 根节点在序列的中间,它像一根“柱子”,把序列划分成了两部分:左边的所有元素都属于左子树,右边的所有元素都属于右子树。

看到关键点了吗?

  • 前序和后序遍历能帮我们轻松地找到根

  • 中序遍历能帮我们以根为界,划分出左右子树的范围

这就是“最佳拍档”!我们可以用一个(前序或后序)来确定根,再用中序来确定左右子树的成员。

我们来推导 前序遍历 + 中序遍历 这个组合。

原材料:

  • 前序序列 (Pre-order): A B D E C F

  • 中序序列 (In-order): D B E A C F

        A
       / \
      B   C
     / \    \
    D   E    F

递归思想——如何构建一棵树?

第1步:确定整棵树的根节点

  • 看前序序列 A B D E C F,第一个元素是 A

  • 结论:A 就是整棵树的根节点。

第2步:划分左右子树的成员

  • 我们已经知道根是 A 了。现在看中序序列 D B E A C F

  • 找到 A 在中序序列中的位置。

  • A 左边的 D B E 就是 A左子树的所有成员。

  • A 右边的 C F 就是 A右子树的所有成员。

第3步:递归构建左右子树

现在问题被分解成了两个一模一样的子问题:

子问题1 (构建A的左子树):

  • 我们知道它的成员是 {D, B, E}。那么它的前序和中序序列是什么?

  • 很简单,回到原始序列中,只看这三个字母,保持它们原来的相对顺序。

  • 原始前序: A [B D E] C F -> 左子树的前序: B D E

  • 原始中序: [D B E] A C F -> 左子树的中序: D B E

  • 现在,我们对这两个新的、更短的序列,重复第1步

    B
   / \
  D   E

子问题2 (构建A的右子树):

  • 成员是 {C, F}

  • 原始前序: A B D E [C F] -> 右子树的前序: C F

  • 原始中序: D B E A [C F] -> 右子树的中序: C F

  • 同样,对这两个新序列,重复第1步

  C
   \
    F

我们来深入子问题1 (左子树 B D E):

  • 第1步 (子问题): 看它的前序 B D E,第一个是 B。所以 B 是这个子树的根。

  • 第2步 (子问题): 看它的中序 D B E,找到 BB 左边是 D,右边是 E

  • 结论:DB 的左孩子,EB 的右孩子。

  • 到这里,DE 都已经是单个节点(叶子节点),它们的左右子树都是 NULL,递归的“出口”到了。

这个过程会一直持续下去,直到处理的序列为空。这就是从第一性原理推导出的,利用两种遍历序列构建树的完整逻辑。


将逻辑翻译为代码

现在我们把这个递归逻辑变成代码。我们需要一个函数,它能根据给定的前序和中序序列,返回构建好的树的根节点指针。

我们先定义函数原型。这个函数需要什么参数?

  • char* preorder: 前序序列数组。

  • char* inorder: 中序序列数组。

  • 为了处理子问题,我们还需要告诉函数当前要处理的序列是哪一段。所以我们需要数组的边界。

  • int in_start, int in_end: 表示当前处理的中序序列在原数组中的起始和结束索引。

为什么只需要中序的边界,而不需要前序的边界?

因为前序序列的根总是在第一个位置,我们处理一个就用掉一个。我们可以用一个全局的或者传入指针的索引来依次访问前序序列,这样更简单。

第1步:搭建函数框架和递归出口

// 节点定义和创建函数(和上一节一样)
typedef struct Node {
    char data;
    struct Node* left;
    struct Node* right;
} Node;

Node* createNode(char data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 这是一个递归的辅助函数
// pre_idx_ptr 是一个指向前序序列当前索引的指针,这样在递归中修改才能生效
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {
    // 递归的出口 (Base Case)
    // 如果中序序列的起始点大于结束点,说明这是一个空子树,没有节点需要创建
    if (in_start > in_end) {
        return NULL;
    }
    
    // 后续的递归步骤将在这里填充
    // ...
}

第2步:实现核心逻辑 - 创建根节点

根据我们的推导,函数要做的第一件事就是从前序序列中取出当前的根。

Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {
    if (in_start > in_end) {
        return NULL;
    }

    // 1. 从前序序列中获取根节点的值
    // 当前要处理的根,就是前序序列中 pre_idx_ptr 指向的那个元素
    char root_val = preorder[*pre_idx_ptr];
    
    // 2. 创建根节点
    Node* root = createNode(root_val);
    
    // 3. 用掉了一个前序元素,将索引向后移动一位,为下一个递归调用做准备
    (*pre_idx_ptr)++;
    
    // ... 接下来是划分和递归
    return root; // 暂时先返回根节点
}

第3步:划分中序序列并进行递归

创建了根节点后,我们需要在中序序列里找到它,然后划分出左右子树的范围,再进行递归调用。

Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {
    if (in_start > in_end) {
        return NULL;
    }

    char root_val = preorder[*pre_idx_ptr];
    Node* root = createNode(root_val);
    (*pre_idx_ptr)++;

    // 4. 在中序序列中找到根节点的位置,以划分左右子树
    int in_root_idx = -1; // 初始化一个找不到的索引
    for (int i = in_start; i <= in_end; i++) {
        if (inorder[i] == root_val) {
            in_root_idx = i;
            break; // 找到了就退出循环
        }
    }
    
    // 如果在中序序列中找不到根(输入有误),可以加个错误处理
    // if (in_root_idx == -1) { /* error handling */ }

    // 5. 递归构建左右子树
    // 注意递归的顺序!因为我们是按前序(根->左->右)的顺序消耗元素的,
    // 所以必须先递归构建左子树,再递归构建右子树。

    // 构建左子树
    // 左子树的范围是中序序列的 in_start 到 根的前一个位置(in_root_idx - 1)
    root->left = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_start, in_root_idx - 1);

    // 构建右子树
    // 右子树的范围是中序序列的 根的后一个位置(in_root_idx + 1) 到 in_end
    root->right = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_root_idx + 1, in_end);

    return root;
}

第4步:创建主函数(包装函数)

为了让用户调用起来更方便,我们创建一个主函数,它负责初始化前序索引并调用递归函数。

// 主函数,用户调用这个
Node* buildTree(char* preorder, char* inorder, int size) {
    int pre_idx = 0; // 初始化前序序列的起始索引
    // 调用递归辅助函数,初始范围是整个中序数组
    return buildTreeRecursive(preorder, &pre_idx, inorder, 0, size - 1);
}

至此,整个从逻辑推导到代码实现的过程就完成了。

完整代码和验证

让我们把所有部分组合起来,并写一个 main 函数来验证我们的成果。

验证方法很简单:用我们创建树的函数 buildTree 来构建一棵树,然后用上一节学过的任意一种遍历(比如后序遍历)来打印这棵树,看看结果是否符合预期。

#include <stdio.h>
#include <stdlib.h>
#include <string.h> // For strlen

// --- 节点定义和创建函数 ---
typedef struct Node {
    char data;
    struct Node* left;
    struct Node* right;
} Node;

Node* createNode(char data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// --- 从前序和中序序列构建树的实现 ---

// 递归辅助函数
Node* buildTreeRecursive(char* preorder, int* pre_idx_ptr, char* inorder, int in_start, int in_end) {
    // 递归出口
    if (in_start > in_end) {
        return NULL;
    }

    // 1. 创建根节点
    char root_val = preorder[*pre_idx_ptr];
    Node* root = createNode(root_val);
    (*pre_idx_ptr)++;

    // 2. 在中序序列中找到根,以划分左右子树
    int in_root_idx = -1;
    for (int i = in_start; i <= in_end; i++) {
        if (inorder[i] == root_val) {
            in_root_idx = i;
            break;
        }
    }

    // 3. 递归构建左子树和右子树
    root->left = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_start, in_root_idx - 1);
    root->right = buildTreeRecursive(preorder, pre_idx_ptr, inorder, in_root_idx + 1, in_end);

    return root;
}

// 主构建函数
Node* buildTree(char* preorder, char* inorder, int size) {
    int pre_idx = 0;
    return buildTreeRecursive(preorder, &pre_idx, inorder, 0, size - 1);
}

// --- 验证用的遍历函数 (从上一节课拿来) ---
void postOrder(Node* root) {
    if (root == NULL) return;
    postOrder(root->left);
    postOrder(root->right);
    printf("%c ", root->data);
}


// --- Main 函数 ---
int main() {
    char preorder[] = "ABDECF";
    char inorder[] = "DBEACF";
    int size = strlen(preorder);

    printf("Input Pre-order: %s\n", preorder);
    printf("Input In-order:  %s\n", inorder);

    // 使用我们推导出的函数来构建树
    Node* root = buildTree(preorder, inorder, size);

    printf("\nVerification with Post-order traversal:\n");
    printf("Expected: D E B F C A\n");
    printf("Actual:   ");
    postOrder(root);
    printf("\n");

    // 如果 Actual 和 Expected 一致,说明我们的树构建完全正确!
    
    // 在此添加释放树内存的代码...
    
    return 0;
}

关于后序+中序的思考:

这个逻辑是完全一样的。区别在于:

  1. 后序遍历的根在序列的末尾,所以你要从后往前处理后序序列。

  2. 因为根是最后处理的,所以你的递归调用顺序应该是先构建右子树,再构建左子树,最后才把它们连接到根上。

这个推导过程清晰地展示了如何从一个根本性的问题(什么信息能唯一确定一棵树)出发,通过逻辑分析和推理,一步步设计出算法,并最终转化为简洁、高效的递归代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值