二叉树 binary tree
定义
二叉树是一种树状数据结构,非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,二叉树的每个节点包含三个主要部分:
- 节点值:存储该节点的数据。
- 左子节点(left-child node):指向该节点左侧的子树或为空。
- 右子节点(right-child node):指向该节点右侧的子树或为空。
二叉树节点结构体如下所示:
/* 二叉树节点结构体 */
struct TreeNode {int val; // 节点值TreeNode *left; // 左子节点指针TreeNode *right; // 右子节点指针TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
当给定一个二叉树的节点时,将该节点的左子节点及其以下节点形成的树称为该节点的 左子树(left subtree),同理可得 右子树(right subtree)。
举个例子,下面是一棵简单的二叉树:
在这棵二叉树中:
- 节点 1 是根节点,它有两个子节点 2 和 3。
- 节点 2 是节点 1 的左子节点,它有两个子节点 4 和 5。
- 节点 3 是节点 1 的右子节点,且没有子节点。
- 节点 4 和节点 5 没有子节点,称为叶节点。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。如上图所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
常见术语
二叉树的常用术语如图所示:
- 根节点 root node :位于二叉树顶层的节点,没有父节点。
- 叶节点 leaf node :没有子节点的节点,其两个指针均指向
None
。 - 边 edge :连接两个节点的线段,即节点引用(指针)。
- 节点所在的 层 level :从顶至底递增,根节点所在层为 1 。
- 节点的 度 degree :节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 二叉树的 高度 height :从根节点到最远叶节点所经过的边的数量。
- 节点的 深度 depth :从根节点到该节点所经过的边的数量。
- 节点的 高度 height :从距离该节点最远的叶节点到该节点所经过的边的数量。
二叉树的先序遍历
二叉树的遍历方式主要有以下几种:
- 先序遍历(Preorder Traversal):根节点 -> 左子树 -> 右子树。
- 中序遍历(Inorder Traversal):左子树 -> 根节点 -> 右子树。
- 后序遍历(Postorder Traversal):左子树 -> 右子树 -> 根节点。
- 层次遍历(Level Order Traversal):按照树的层级逐层访问节点。
先序遍历是一种访问二叉树节点的顺序,按如下规则遍历每个节点:
- 首先访问根节点。
- 递归地先序遍历根节点的左子树。
- 然后递归地先序遍历根节点的右子树。
举个例子,下面是一棵简单的二叉树:
1/ \2 3/ \4 5
遍历顺序为:1 -> 2 -> 4 -> 5 -> 3
- 访问根节点
1
。- 递归遍历左子树,访问
2
,再递归到左子树访问4
,4
没有子节点返回上一层。- 回到节点
2
,然后访问5
,5
没有子节点返回。- 左子树遍历完成,回到根节点
1
,接着访问右子树节点3
。
题目要求
给定一个二叉树的根节点,编写程序实现先序遍历并打印每个节点的值。要求如下:
- 请实现递归和非递归(使用栈)两种方法(本文中,C使用递归方法,C++使用非递归)。
- 输出每个节点的值。
做题思路
先序遍历是一种从根节点开始,依次遍历左子树和右子树的树遍历方法。可以用递归和迭代来实现:
- 递归方法:根据先序遍历的定义,先访问当前节点,然后递归地访问左子树和右子树。
- 非递归方法:使用栈来模拟递归过程。先将根节点压入栈中,之后在每次迭代中取出栈顶节点并访问其值,接着将右子节点和左子节点分别入栈(保证左节点先出栈)。
运用到的知识点
- 递归:函数调用自身以简化复杂问题,是二叉树遍历的典型方法。
- 栈(Stack):一种后进先出的数据结构,用于实现非递归的先序遍历。
- 二叉树数据结构:二叉树中每个节点最多有两个子节点,即左子节点和右子节点。
示例代码
C 实现(递归实现)
#include <stdio.h> // 引入标准输入输出库,用于printf函数
#include <stdlib.h> // 引入标准库,用于malloc函数 // 定义二叉树节点结构体
struct TreeNode { int val; // 节点的值 struct TreeNode *left; // 指向左子节点的指针 struct TreeNode *right; // 指向右子节点的指针
}; // 创建新节点的函数
// 参数:val - 新节点的值
// 返回值:指向新创建的节点的指针
struct TreeNode* createNode(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); // 分配内存给新节点 node->val = val; // 设置节点的值 node->left = NULL; // 初始化左子节点指针为空 node->right = NULL; // 初始化右子节点指针为空 return node; // 返回新节点的指针
} // 先序遍历递归函数
// 参数:root - 要遍历的二叉树的根节点
void preorderTraversal(struct TreeNode* root) { if (root == NULL) return; // 如果当前节点为空,则直接返回,不继续遍历 // 访问根节点 printf("%d ", root->val); // 打印当前节点的值 // 递归遍历左子树 preorderTraversal(root->left); // 递归调用先序遍历函数,遍历左子树 // 递归遍历右子树 preorderTraversal(root->right); // 递归调用先序遍历函数,遍历右子树
} int main()
{ // 构建示例二叉树 // 创建根节点 struct TreeNode* root = createNode(1); // 创建左子节点,并连接到根节点 root->left = createNode(2); // 创建右子节点,并连接到根节点 root->right = createNode(3); // 创建左子节点的左子节点,并连接到左子节点 root->left->left = createNode(4); // 创建左子节点的右子节点,并连接到左子节点 root->left->right = createNode(5); // 打印先序遍历的提示信息 printf("递归先序遍历结果:"); // 调用先序遍历函数,遍历整棵树 preorderTraversal(root); // 注意:此代码示例中未包含释放已分配内存的代码,实际使用时应注意内存管理 return 0; // 程序正常结束
}
C++实现(递归实现)
#include <iostream> // 引入输入输出流库,用于cout和endl
#include <stack> // 引入栈容器库,用于实现非递归遍历 using namespace std; // 使用标准命名空间,避免每次调用标准库时都要加std::前缀 // 定义二叉树节点结构体
struct TreeNode { int val; // 节点的值 TreeNode *left; // 指向左子节点的指针 TreeNode *right; // 指向右子节点的指针 // 构造函数,用于创建新节点并初始化其值和子节点指针 TreeNode(int x) : val(x), left(NULL), right(NULL) {}
}; // 非递归先序遍历函数
// 参数:root - 要遍历的二叉树的根节点
void preorderTraversal(TreeNode* root) { if (root == nullptr) return; // 如果根节点为空,则直接返回 stack<TreeNode*> s; // 创建一个栈,用于存储待访问的节点指针 s.push(root); // 将根节点指针入栈 // 当栈不为空时,继续循环 while (!s.empty()) { // 从栈顶获取节点指针,并访问其值(即先序访问) TreeNode* node = s.top(); // 获取栈顶节点指针 s.pop(); // 将栈顶节点指针出栈 cout << node->val << " "; // 输出节点的值 // 注意:这里先将右子节点入栈,再将左子节点入栈 // 这样做是为了保证在出栈时左子节点先于右子节点被访问 // 因为栈是后进先出(LIFO)的数据结构 if (node->right) s.push(node->right); // 如果右子节点存在,则入栈 if (node->left) s.push(node->left); // 如果左子节点存在,则入栈 }
} int main()
{ // 构建示例二叉树 // 创建根节点并初始化 TreeNode* root = new TreeNode(1); // 创建左子节点并连接到根节点 root->left = new TreeNode(2); // 创建右子节点并连接到根节点 root->right = new TreeNode(3); // 创建左子节点的左子节点并连接到左子节点 root->left->left = new TreeNode(4); // 创建左子节点的右子节点并连接到左子节点 root->left->right = new TreeNode(5); // 输出非递归先序遍历的提示信息 cout << "非递归先序遍历结果:"; // 调用非递归先序遍历函数,遍历整棵树 preorderTraversal(root); // 注意:此代码示例中未包含释放已分配内存的代码,实际使用时应注意内存管理 // 在实际应用中,应在遍历完成后释放所有动态分配的内存,以避免内存泄漏 return 0; // 程序正常结束
}