1 二叉树的基本概念
1.1 二叉树的定义
二叉树是一种特殊的树形数据结构,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树在计算机科学中有着广泛的应用,包括但不限于表达式解析、文件系统、搜索算法等。以下是二叉树的一些基本定义和特性:
-
节点定义:二叉树由节点组成,每个节点包含数据和两个指向其子节点的指针(左子节点和右子节点)。
-
根节点:二叉树只有一个根节点,它是树的起始点,没有父节点。
-
叶子节点:没有子节点的节点称为叶子节点或外部节点。
-
内部节点:至少有一个子节点的节点称为内部节点或非终端节点。
-
子树:由一个节点和其所有后代组成的树称为该节点的子树。
-
深度:二叉树的深度是最大的节点层数,从根节点到最远叶子节点的最长路径。
-
高度:二叉树的高度是从根节点到最远叶子节点的边的数量。在某些定义中,根节点的高度可能被视为0或1。
-
满二叉树:如果除了叶子节点外,所有节点都恰好有两个子节点,则称为满二叉树。
-
完全二叉树:如果所有层都被完全填满,并且最后一层的节点尽可能地集中在左侧,则称为完全二叉树。
-
平衡二叉树:左右子树的高度差不超过1的二叉树,例如AVL树。
-
二叉搜索树(BST):一种特殊的二叉树,其中每个节点的值大于其左子树中的所有节点的值,小于其右子树中的所有节点的值。
1.2 二叉树的性质
二叉树常见的性质如下:
-
有序性:左子节点的值总是小于或等于其父节点的值,而右子节点的值总是大于或等于其父节点的值。这种特性使得二叉树特别适合实现二叉搜索树。
-
递归结构:二叉树的每个节点(除了根节点)都可以看作是其子树的根。
-
动态性:二叉树的大小可以动态变化,节点可以在任何时候被添加或删除。
-
存储结构:二叉树通常使用链式存储结构实现,每个节点包含数据和指向其子节点的指针。
-
遍历方式:二叉树可以通过多种方式进行遍历,包括前序、中序、后序和层序遍历。
-
平衡性:在某些类型的二叉树(如AVL树)中,保持树的平衡是重要的,以确保操作的效率。
1.3 二叉树的存储结构
二叉树的存储结构主要有两种方式:数组和链表。链式存储结构在插入和删除操作中更加灵活,而顺序存储结构则在遍历和访问节点时更加高效。
(1)链式存储结构(链表):
-
链式存储是二叉树最常用的存储方式。
-
每个节点通常包含三个字段:数据域和两个指针域(分别指向左子节点和右子节点)。
-
节点通过指针域链接起来,形成一个非线性的数据结构。
typedef struct TreeNode { ElementType data; // 数据域,存储节点的数据 TreeNode *left; // 左子节点的指针域 TreeNode *right; // 右子节点的指针域 } TreeNode;
(2)顺序存储结构(数组):
-
顺序存储通常使用数组来实现,特别是在完全二叉树的情况下。
-
节点按照从上到下、从左到右的顺序存储在数组中。
-
给定一个节点的索引i,其左子节点的索引为2i+1,右子节点的索引为2i+2(如果存在)。
-
同样地,给定一个节点的索引i,其父节点的索引可以通过(i-1)/2计算得出(如果i不为0)。
// 假设使用数组存储完全二叉树 TreeNode array[n]; // n是数组的大小,即树中节点的最大数量
(3)二叉树的数组表示:
- 在数组中,可以通过节点的索引来快速定位其父节点和子节点。
- 这种存储方式特别适用于完全二叉树,因为它们可以完美地填充数组的前n个位置。
(4)二叉树的链表表示:
- 链表表示允许二叉树不必是完全二叉树,因为它不需要额外的空间来存储不存在的节点。
- 链表表示的二叉树可以动态地增长和收缩,而不需要重新分配数组。
(5)二叉堆(特殊二叉树):
- 二叉堆是一种特殊的二叉树,通常使用数组来实现。
- 它满足堆的性质,即父节点的值总是大于(或小于)其子节点的值。
2 二叉树的基本操作及实现
- 二叉树的基本操作通常包括以下几个方面:
- 创建节点:创建一个新的二叉树节点。
- 插入节点:在二叉搜索树中根据值的顺序插入新节点。
- 删除节点:从二叉树中移除一个节点。
- 查找节点:在二叉树中查找特定值的节点。
- 遍历树:按照特定的顺序访问树中的所有节点,常见的遍历方式有前序、中序、后序和层序遍历。
- 获取树的高度:计算二叉树的高度,通常用于平衡树的调整。
- 判断树是否平衡:对于平衡二叉树,需要检查树是否满足平衡条件。
以下是C++中定义二叉树节点和实现基本操作的简单示例:
#include <iostream>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 假设这里实现了其他二叉树操作,比如插入、删除等
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);
// 这里可以调用函数
return 0;
}
2.1 创建节点
TreeNode 类或结构体定义了二叉树节点的基本结构,包括存储数据的变量以及指向子节点的指针。具体实现如下:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
2.2 插入节点
对于二叉搜索树(BST),插入新节点的步骤如下:
- 创建新节点:创建一个新的
TreeNode
实例,其值为要插入的值。 - 找到插入位置:从根节点开始,递归地遍历树,直到找到一个空位(即一个
nullptr
)。 - 插入节点:将新节点插入到找到的空位。
TreeNode* insert(TreeNode* node, int val) {
// 如果当前节点为空,返回一个新节点
if (node == nullptr) {
return new TreeNode(val);
}
// 如果值小于当前节点的值,向左子树插入
if (val < node->val) {
node->left = insert(node->left, val);
}
// 如果值大于或等于当前节点的值,向右子树插入
else {
node->right = insert(node->right, val);
}
return node;
}
2.3 删除节点
删除节点稍微复杂一些,需要考虑以下几种情况:
- 删除叶节点:如果节点没有子节点,直接删除该节点。
- 删除只有一个子节点的节点:删除该节点,并用其子节点替换它。
- 删除有两个子节点的节点:找到该节点的直接前驱或直接后继(通常是其右子树中的最小值或左子树中的最大值),用这个节点的值替换要删除的节点的值,然后删除那个前驱或后继。
以下是删除节点的示例代码:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return root;
if (key < root->val) root->left = deleteNode(root->left, key);
else if (key > root->val) root->right = deleteNode(root->right, key);
else {
if (!root->left) return root->right;
if (!root->right) return root->left;
TreeNode* temp = findMin(root->right);
root->val = temp->val;
root->right = deleteNode(root->right, temp->val);
}
return root;
}
TreeNode* findMin(TreeNode* node) {
while (node && node->left) node = node->left;
return node;
}
2.4 查找节点
在二叉树中查找节点通常意味着要找到包含特定值的节点。这个过程可以通过递归或迭代的方式实现。
TreeNode* search(TreeNode* node, int val) {
while (node != nullptr) {
if (val == node->val) return node;
node = val < node->val ? node->left : node->right;
}
return nullptr;
}
2.5 遍历树
二叉树的遍历方式主要包括:
- 前序遍历(Pre-order):先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
- 中序遍历(In-order):先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
- 后序遍历(Post-order):先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。
- 层序遍历(Breadth-first search,BFS):从根节点开始,按层次访问所有节点。
下面是这四种遍历方式的基本实现。
前序遍历(Pre-order)
前序遍历的顺序是:根节点 -> 左子树 -> 右子树。
void preOrder(TreeNode* root) {
if (root == nullptr) {
return;
}
std::cout << root->val << " "; // 访问根节点
preOrder(root->left); // 遍历左子树
preOrder(root->right); // 遍历右子树
}
中序遍历(In-order)
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
void inOrder(TreeNode* root) {
if (root == nullptr) {
return;
}
inOrder(root->left); // 遍历左子树
std::cout << root->val << " "; // 访问根节点
inOrder(root->right); // 遍历右子树
}
后序遍历(Post-order)
后序遍历的顺序是:左子树 -> 右子树 -> 根节点。
void postOrder(TreeNode* root) {
if (root == nullptr) {
return;
}
postOrder(root->left); // 遍历左子树
postOrder(root->right); // 遍历右子树
std::cout << root->val << " "; // 访问根节点
}
层序遍历(BFS)
层序遍历使用队列来实现,按照从上到下,从左到右的顺序遍历。
void levelOrder(TreeNode* root) {
if (root == nullptr) {
return;
}
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
std::cout << node->val << " ";
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
这些遍历方法可以用于不同的场景,例如树的复制、树的序列化和反序列化等。每种遍历方式都有其特定的应用场景和特点。
2.6 获取树的高度
二叉树的高度是指从根节点到最远叶子节点的最长路径上的边数。在计算二叉树的高度时,通常有两种定义方式:
- 深度定义:高度是根节点到最远叶子节点的边的数量。在这种定义下,空树的高度是 -1。
- 层级定义:高度是根节点到最远叶子节点的节点数。在这种定义下,空树的高度是 0。
int getHeight(TreeNode* node) {
if (node == nullptr) return -1; // 返回-1表示空树的高度为-1
return 1 + std::max(getHeight(node->left), getHeight(node->right));
}
2.7 判断树是否平衡
平衡二叉树(Balanced Binary Tree)是一种特殊的二叉树,其中每个节点的左子树和右子树的高度差至多为1。因此,平衡二叉树的一个简单判断方法是检查树的高度差是否不超过1。
bool isBalanced(TreeNode* node) {
if (node == nullptr) return true;
int leftHeight = getHeight(node->left);
int rightHeight = getHeight(node->right);
return std::fabs(leftHeight - rightHeight) <= 1 &&
isBalanced(node->left) &&
isBalanced(node->right);
}