【25届秋招备战C++】数据结构篇-二叉树
一、定义
由 n≥0 个节点与节点之间的关系组成的有限集合。当 n=0 时称为空树,当 n>0 时称为非空树。
树的基础知识
树的节点:由一个数据元素和若干个指向其子树的树的分支组成。
节点的度:一个节点所含有的子树个数。
叶子节点(终端节点):度为 0 的节点。
分支节点(非终端节点):度不为 0 的节点。
树的度:树中节点的最大度数。
孩子节点(子节点):一个节点含有的子树的根节点称为该节点的子节点。
父亲节点(父节点):如果一个节点含有子节点,则这个节点称为其子节点的父节点。
兄弟节点:具有相同父节点的节点互称为兄弟节点。
节点的层次:从根节点开始定义,根为第 1 层,根的子节点为第 2 层,以此类推。
树的深度(高度):所有节点中最大的层数。
堂兄弟节点:父节点在同一层的节点互为堂兄弟。、
路径:树中两个节点之间所经过的节点序列。、
路径长度:两个节点之间路径上经过的边数。、
节点的祖先:从该节点到根节点所经过的所有节点,被称为该节点的祖先。
节点的子孙:节点的子树中所有节点被称为该节点的子孙。
有序树与无序树:如果将树中节点的各个子树看做是从左到右是依次有序的(即不能互换),则称该树为 「有序树」。反之,如果节点的各个子树可以互换位置,则成该树为 「无序树」。
二叉树(Binary Tree):树中各个节点的度不大于 2 个的有序树,称为二叉树。通常树中的分支节点被称为 「左子树」 或 「右子树」。二叉树的分支具有左右次序,不能随意互换位置。
特殊二叉树
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树
1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.它的左、右子树也分别为二叉排序树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二、二叉树的存储结构
顺序存储
二叉树的顺序存储结构使用一维数组来存储二叉树中的节点,节点存储位置则采用完全二叉树的节点层次编号,按照层次从上至下,每一层从左至右的顺序依次存放二叉树的数据元素。在进行顺序存储时,如果对应的二叉树节点不存在,则设置为「空节点」。完全二叉树或满二叉树适合采用顺序存储结构。
链式存储
二叉树采用链式存储结构时,每个链节点包含一个用于数据域 val,存储节点信息;还包含两个指针域 left 和 right,分别指向左右两个孩子节点,当左孩子或者右孩子不存在时,相应指针域值为空。
三、二叉树的遍历-深度优先搜索(DFS)和广度优先搜索
1.深度优先搜索
前序遍历:根节点-左子树-右子树
中序遍历:左子树-根节点-右子树
后序遍历:左子树-右子树-根节点
2.广度优先搜索
层序遍历:按照层次从上至下,每一层从左至右
深度优先遍历的实现-递归法
前序遍历(Preorder Traversal)
步骤:
访问根节点。
递归地对左子树进行前序遍历。
递归地对右子树进行前序遍历。
C++ 实现:
#include <iostream>
#include <vector>
struct TreeNode {
int value;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : value(x), left(nullptr), right(nullptr) {}
};
void preorder(TreeNode* root, std::vector<int>& result) {
if (root == nullptr) return;
result.push_back(root->value); // 访问根节点
preorder(root->left, result); // 遍历左子树
preorder(root->right, result); // 遍历右子树
}
void printPreorder(TreeNode* root) {
std::vector<int> result;
preorder(root, result);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
}
中序遍历(Inorder Traversal)
步骤:
递归地对左子树进行中序遍历。
访问根节点。
递归地对右子树进行中序遍历。
C++ 实现:
void inorder(TreeNode* root, std::vector<int>& result) {
if (root == nullptr) return;
inorder(root->left, result); // 遍历左子树
result.push_back(root->value); // 访问根节点
inorder(root->right, result); // 遍历右子树
}
void printInorder(TreeNode* root) {
std::vector<int> result;
inorder(root, result);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
}
后序遍历(Postorder Traversal)
步骤:
递归地对左子树进行后序遍历。
递归地对右子树进行后序遍历。
访问根节点。
C++ 实现:
void postorder(TreeNode* root, std::vector<int>& result) {
if (root == nullptr) return;
postorder(root->left, result); // 遍历左子树
postorder(root->right, result); // 遍历右子树
result.push_back(root->value); // 访问根节点
}
void printPostorder(TreeNode* root) {
std::vector<int> result;
postorder(root, result);
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
}
深度优先遍历的实现-迭代法
迭代法实现二叉树的遍历通常涉及到使用栈来模拟递归过程。以下是使用迭代法实现前序、中序和后序遍历的C++代码。
//迭代前序遍历(Preorder Traversal)
#include <iostream>
#include <stack>
#include <vector>
struct TreeNode {
int value;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : value(x), left(nullptr), right(nullptr) {}
};
void iterativePreorder(TreeNode* root) {
std::stack<TreeNode*> stack;
TreeNode* current = root;
while (current != nullptr || !stack.empty()) {
// 访问当前节点
if (current != nullptr) {
std::cout << current->value << " ";
stack.push(current);
current = current->left; // 先访问左子树
} else {
// 弹出栈顶元素
current = stack.top();
stack.pop();
current = current->right; // 访问右子树
}
}
std::cout << std::endl;
}
//迭代中序遍历(Inorder Traversal)
void iterativeInorder(TreeNode* root) {
std::stack<TreeNode*> stack;
TreeNode* current = root;
while (current != nullptr || !stack.empty()) {
if (current != nullptr) {
// 将当前节点及其所有左子节点入栈
stack.push(current);
current = current->left;
} else {
// 弹出栈顶元素并访问
current = stack.top();
stack.pop();
std::cout << current->value << " ";
current = current->right;
}
}
std::cout << std::endl;
}
//迭代后序遍历(Postorder Traversal)
void iterativePostorder(TreeNode* root) {
std::stack<TreeNode*> stack;
TreeNode* current = root;
TreeNode* lastVisited = nullptr;
while (current != nullptr || !stack.empty()) {
if (current != nullptr) {
stack.push(current);
current = current->left; // 先访问左子树
} else {
TreeNode* node = stack.top();
if (node->right != nullptr && lastVisited != node->right) {
// 如果右子树未被访问,访问右子树
current = node->right;
} else {
// 访问当前节点
std::cout << node->value << " ";
lastVisited = stack.top();
stack.pop();
}
}
}
std::cout << std::endl;
}
在这些迭代方法中,使用一个栈来跟踪访问的节点。在前序遍历中,首先访问当前节点,然后将其右子节点和左子节点(如果有的话)压入栈中。在中序遍历中,先将当前节点的所有左子节点压入栈中,然后访问当前节点,最后弹出栈顶元素并访问其右子节点。在后序遍历中,先将当前节点压入栈中,然后递归访问其左右子节点,最后访问栈顶元素。
这些迭代方法在处理大型二叉树时特别有用,因为它们不会导致递归调用栈溢出。
层序遍历的实现
#include <iostream>
#include <queue>
#include <vector>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void levelOrder(TreeNode* root) {
if (root == nullptr) return;
std::queue<TreeNode*> queue;
queue.push(root);
while (!queue.empty()) {
TreeNode* node = queue.front();
queue.pop();
std::cout << node->val << " ";
if (node->left) queue.push(node->left);
if (node->right) queue.push(node->right);
}
std::cout << std::endl;
}
四、二叉树的重建
由前序和中序重建二叉树
从前序遍历序列和中序遍历序列构造二叉树是一种常见的二叉树重建问题。在这个问题中,前序遍历序列的第一个元素总是根节点,而在中序遍历序列中,根节点将序列分为左右两部分,分别对应左子树和右子树的中序遍历序列。
由中序和后序重建二叉树
从后序遍历序列和中序遍历序列构造二叉树的关键在于后序遍历序列的最后一个元素是根节点,而在中序遍历序列中,根节点将序列分为左右两部分,分别对应左子树和右子树的中序遍历序列。右子树的中序遍历序列可能会包含根节点,而左子树的中序遍历序列则不会。
以下是C++实现的代码:
//由前序和中序重建二叉树
#include <iostream>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 辅助函数,用于从给定的前序和中序遍历序列中构建二叉树
TreeNode* buildTree(const std::vector<int>& preorder, const std::vector<int>& inorder) {
if (preorder.empty() || inorder.empty()) return nullptr;
// 前序遍历的第一个元素是根节点
int rootVal = preorder[0];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍历序列中找到根节点的位置
int rootIndex = 0;
for (int val : inorder) {
if (val == rootVal) {
rootIndex = inorder.indexOf(val);
break;
}
}
// 递归构建左子树和右子树
root->left = buildTree(preorder, inorder, 0, rootIndex);
root->right = buildTree(preorder, inorder, rootIndex + 1, inorder.size() - rootIndex - 1);
return root;
}
// 重载构建函数,接受前序和中序遍历序列的子序列
TreeNode* buildTree(const std::vector<int>& preorder, const std::vector<int>& inorder, int start, int end) {
if (start > end) return nullptr;
// 前序遍历的第一个元素是当前子树的根节点
int rootVal = preorder[start];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍历序列中找到根节点的位置
int rootIndex = 0;
for (int i = start; i <= end; ++i) {
if (inorder[i] == rootVal) {
rootIndex = i;
break;
}
}
// 计算左子树和右子树的前序遍历序列的索引
int leftSize = rootIndex - start;
int rightSize = end - rootIndex + 1;
// 递归构建左子树和右子树
root->left = buildTree(preorder, inorder, start + 1, start + leftSize);
root->right = buildTree(preorder, inorder, start + leftSize + 1, start + leftSize + rightSize);
return root;
}
//由中序和后序重建二叉树
#include <iostream>
#include <vector>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 辅助函数,用于从给定的后序和中序遍历序列中构建二叉树
TreeNode* buildTree(const std::vector<int>& postorder, const std::vector<int>& inorder) {
if (postorder.empty() || inorder.empty()) return nullptr;
// 后序遍历的最后一个元素是根节点
int rootVal = postorder.back();
TreeNode* root = new TreeNode(rootVal);
postorder.pop_back(); // 移除根节点
// 在中序遍历序列中找到根节点的位置
int rootIndex = std::find(inorder.begin(), inorder.end(), rootVal) - inorder.begin();
// 递归构建左子树和右子树
// 左子树的中序遍历序列长度为 rootIndex,右子树的中序遍历序列长度为 inorder.size() - rootIndex - 1
root->left = buildTree(postorder, inorder, 0, rootIndex);
root->right = buildTree(postorder, inorder, rootIndex + 1, inorder.size() - 1);
return root;
}
五、二叉搜索树的基础操作
二叉搜索树(BST)是一种特殊的二叉树,其中每个节点都满足以下性质:对于任意节点node,其左子树中的所有节点的值都小于node的值,其右子树中的所有节点的值都大于node的值。这使得二叉搜索树非常适合进行增删改查操作。以下是二叉搜索树的基本操作的C++实现:
1. 插入(Insert)
插入操作需要维护BST的性质。新节点将被插入到树中适当的位置。
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
TreeNode* insert(TreeNode* root, int val) {
if (root == nullptr) return new TreeNode(val);
if (val < root->val) root->left = insert(root->left, val);
else if (val > root->val) root->right = insert(root->right, val);
return root;
}
2. 删除(Delete)
删除操作稍微复杂,需要考虑三种情况:删除的节点是叶子节点、有一个子节点、有两个子节点。如果节点有两个子节点,需要找到其右子树的最小节点(或左子树的最大节点)来替换它。
TreeNode* minValueNode(TreeNode* node) {
TreeNode* current = node;
while (current && current->left != nullptr) {
current = current->left;
}
return current;
}
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) 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 == nullptr) {
TreeNode* temp = root->right;
delete root;
return temp;
} else if (root->right == nullptr) {
TreeNode* temp = root->left;
delete root;
return temp;
}
TreeNode* temp = minValueNode(root->right);
root->val = temp->val;
root->right = deleteNode(root->right, temp->val);
}
return root;
}
3. 查找(Search)
查找操作是从根节点开始,根据值的大小向左或向右遍历树。
TreeNode* search(TreeNode* root, int key) {
if (root == nullptr || root->val == key) return root;
if (key < root->val) return search(root->left, key);
return search(root->right, key);
}
4. 更新(Update)
更新操作类似于查找操作,首先找到要更新的节点,然后更改其值。
void updateNode(TreeNode* root, int key, int new_val) {
if (root == nullptr) return;
if (key == root->val) {
root->val = new_val;
return;
}
if (key < root->val) updateNode(root->left, key, new_val);
else updateNode(root->right, key, new_val);
}