数据结构中的树:从基础概念到C++实现
文章目录
引言
在计算机科学的广阔领域中,数据结构是构建高效算法和程序的基石。而在众多数据结构中,树(Tree)以其独特的层次结构和灵活的组织方式,成为了解决各类复杂问题的强大工具。无论是操作系统的文件管理、数据库的索引设计,还是编译器的语法分析,树结构都扮演着不可替代的角色。
本教程将深入探讨树这一重要的数据结构,从基本概念出发,逐步介绍各种类型的树及其实现方法,并通过丰富的C++代码示例,帮助读者全面理解树结构的原理和应用。无论你是计算机科学的初学者,还是希望巩固知识的专业人士,这篇教程都将为你提供系统而详尽的指导。
在接下来的内容中,我们将首先介绍树的基本概念和术语,然后探讨不同类型的树结构(如二叉树、二叉搜索树、AVL树、红黑树、B树等),最后通过C++代码实现这些树结构的常见操作(如创建、插入、删除、遍历等)。每个部分都会配以详细的解释和丰富的示例,确保读者能够透彻理解每个概念和操作。
让我们开始这段探索树结构奥秘的旅程吧!
树的基本概念
树的定义
树(Tree)是一种非线性的数据结构,它是由n(n≥0)个有限节点组成的一个具有层次关系的集合。当n=0时,称为空树。当n>0时,树具有以下特点:
- 有且仅有一个特定的称为根(Root)的节点
- 其余节点可分为m(m≥0)个互不相交的有限集T1、T2、…、Tm,其中每个集合本身又是一棵树,称为原树的子树(SubTree)
树的结构之所以称为"树",是因为它看起来像一棵倒置的树,根在上,叶在下。下图展示了一个典型的树结构:
树的基本术语
为了更好地理解和讨论树结构,我们需要熟悉以下基本术语:
-
节点(Node):树中的每个元素称为节点,它包含数据元素和指向子树的链接。
-
根节点(Root Node):树中最顶层的节点,它没有父节点。在上图中,A是根节点。
-
子节点(Child Node):如果节点A是节点B的父节点,那么节点B就是节点A的子节点。在上图中,B、C、D是A的子节点。
-
父节点(Parent Node):如果节点A是节点B的子节点,那么节点B就是节点A的父节点。在上图中,A是B、C、D的父节点。
-
兄弟节点(Sibling Node):具有相同父节点的节点互称为兄弟节点。在上图中,B、C、D互为兄弟节点。
-
叶节点(Leaf Node):没有子节点的节点称为叶节点或终端节点。在上图中,E、F、G、H、I、J、K、L、M、N、O、P是叶节点。
-
非叶节点(Non-leaf Node):有子节点的节点称为非叶节点或分支节点。在上图中,A、B、C、D是非叶节点。
-
节点的度(Degree):一个节点拥有的子树数量称为该节点的度。在上图中,A的度为3,B的度为2。
-
树的度(Degree of Tree):树中所有节点的度的最大值称为树的度。在上图中,树的度为3。
-
节点的层次(Level):从根开始定义,根为第1层,根的子节点为第2层,以此类推。
-
树的深度(Depth):树中所有节点的层次的最大值称为树的深度或高度。在上图中,树的深度为4。
-
森林(Forest):m(m≥0)棵互不相交的树的集合称为森林。
树与其他数据结构的比较
树结构与线性结构(如数组、链表)有着本质的区别:
-
线性结构:
- 第一个元素:无前驱
- 最后一个元素:无后继
- 中间元素:一个前驱一个后继
- 一对一的关系
-
树结构:
- 根节点:无父节点,唯一
- 叶节点:无子节点,可以有多个
- 中间节点:一个父节点多个子节点
- 一对多的关系
这种一对多的关系使得树结构特别适合表示具有层次关系的数据,如文件系统、组织结构等。
树的分类
根据节点的子树数量和排列方式,树可以分为多种类型:
-
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树。
-
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树。
-
二叉树:每个节点最多有两个子树的树称为二叉树。二叉树是最常见和最重要的树类型之一,我们将在后续章节中详细讨论。
-
满二叉树:所有叶节点都在同一层,且每个非叶节点都有两个子节点的二叉树称为满二叉树。
-
完全二叉树:除了最后一层外,其他层的节点数都达到最大值,且最后一层的节点都集中在左侧的二叉树称为完全二叉树。
-
平衡二叉树:任意节点的左右子树的高度差不超过1的二叉树称为平衡二叉树,如AVL树。
-
二叉搜索树:左子树上所有节点的值均小于根节点的值,右子树上所有节点的值均大于根节点的值的二叉树称为二叉搜索树。
-
B树和B+树:多路搜索树,常用于数据库和文件系统中。
在接下来的章节中,我们将详细介绍这些不同类型的树结构及其实现方法。
树的存储结构
树的存储结构主要有两种:
-
双亲表示法:每个节点除了存储数据外,还存储其父节点的位置。
-
孩子表示法:每个节点除了存储数据外,还存储其所有子节点的位置。
在C++中,我们通常使用指针来实现树的节点,例如:
// 二叉树节点的基本结构
struct TreeNode {
int data; // 节点存储的数据
TreeNode* left; // 指向左子节点的指针
TreeNode* right; // 指向右子节点的指针
// 构造函数
TreeNode(int val) : data(val), left(nullptr), right(nullptr) {
}
};
对于一般的树(非二叉树),我们可以使用动态数组或链表来存储子节点:
// 普通树节点的基本结构
struct TreeNode {
int data; // 节点存储的数据
vector<TreeNode*> children; // 存储所有子节点的指针
// 构造函数
TreeNode(int val) : data(val) {
}
};
不同类型的树结构及其C++实现
二叉树(Binary Tree)
二叉树的定义与特性
二叉树是最常见和最重要的树类型之一,它的每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树具有以下特性:
- 每个节点最多有两个子节点
- 左子节点和右子节点是有序的,不能互换
- 第i层的节点数最多为2^(i-1)
- 深度为k的二叉树最多有2^k-1个节点
- 对于任何一棵二叉树,如果叶节点数为n0,度为2的节点数为n2,则n0 = n2 + 1
二叉树的C++实现
在C++中,我们通常使用指针来实现二叉树的节点:
// 二叉树节点的基本结构
struct TreeNode {
int data; // 节点存储的数据
TreeNode* left; // 指向左子节点的指针
TreeNode* right; // 指向右子节点的指针
// 构造函数
TreeNode(int val) : data(val), left(nullptr), right(nullptr) {
}
};
二叉树的类型
1. 满二叉树(Full Binary Tree)
满二叉树是指所有非叶节点都有两个子节点,所有叶节点都在同一层的二叉树。
2. 完全二叉树(Complete Binary Tree)
完全二叉树是指除了最后一层外,其他层的节点数都达到最大值,且最后一层的节点都集中在左侧的二叉树。
完全二叉树的特点使其非常适合用数组来存储,因为对于数组中的任意位置i,其左子节点的位置是2i+1,右子节点的位置是2i+2,父节点的位置是(i-1)/2(整数除法)。
3. 平衡二叉树(Balanced Binary Tree)
平衡二叉树是指任意节点的左右子树的高度差不超过1的二叉树。平衡二叉树的目的是确保树的深度尽可能小,从而提高查找、插入和删除操作的效率。
4. 二叉搜索树(Binary Search Tree)
二叉搜索树是一种特殊的二叉树,它具有以下性质:
- 左子树上所有节点的值均小于根节点的值
- 右子树上所有节点的值均大于根节点的值
- 左子树和右子树也分别为二叉搜索树
这种特性使得二叉搜索树非常适合进行查找、插入和删除操作,这些操作的平均时间复杂度都是O(log n)。
下面是一个二叉搜索树的示例:
二叉搜索树(Binary Search Tree)
二叉搜索树的定义与特性
二叉搜索树(BST)是一种特殊的二叉树,它的每个节点都满足以下条件:
- 节点的左子树中的所有节点的值都小于该节点的值
- 节点的右子树中的所有节点的值都大于该节点的值
- 左子树和右子树也都是二叉搜索树
这种特性使得二叉搜索树非常适合进行查找、插入和删除操作,这些操作的平均时间复杂度都是O(log n)。
二叉搜索树的C++实现
// 二叉搜索树的节点结构
struct BSTNode {
int data; // 节点存储的数据
BSTNode* left; // 指向左子节点的指针
BSTNode* right; // 指向右子节点的指针
// 构造函数
BSTNode(int val) : data(val), left(nullptr), right(nullptr) {
}
};
// 二叉搜索树类
class BST {
private:
BSTNode* root; // 根节点指针
// 递归插入节点
BSTNode* insertRecursive(BSTNode* node, int val) {
// 如果当前节点为空,创建新节点
if (node == nullptr) {
return new BSTNode(val);
}
// 如果值小于当前节点的值,插入到左子树
if (val < node->data) {
node->left = insertRecursive(node->left, val);
}
// 如果值大于当前节点的值,插入到右子树
else if (val > node->data) {
node->right = insertRecursive(node->right, val);
}
// 返回更新后的节点指针
return node;
}
// 递归查找节点
BSTNode* searchRecursive(BSTNode* node, int val) {
// 如果当前节点为空或者找到了目标值,返回当前节点
if (node == nullptr || node->data == val) {
return node;
}
// 如果值小于当前节点的值,在左子树中查找
if (val < node->data) {
return searchRecursive(node->left, val);
}
// 如果值大于当前节点的值,在右子树中查找
return searchRecursive(node->right, val);
}
// 查找最小值节点
BSTNode* findMin(BSTNode* node) {
// 最小值节点是最左边的节点
while (node->left != nullptr) {
node = node->left;
}
return node;
}
// 递归删除节点
BSTNode* deleteRecursive(BSTNode* node, int val) {
// 基本情况:如果树为空
if (node == nullptr) {
return nullptr;
}
// 如果值小于当前节点的值,在左子树中删除
if (val < node->data) {
node->left = deleteRecursive(node->left, val);
}
// 如果值大于当前节点的值,在右子树中删除
else if (val > node->data) {
node->right = deleteRecursive(node->right, val);
}
// 找到了要删除的节点
else {
// 情况1:叶节点(没有子节点)
if (node->left == nullptr && node->right == nullptr) {
delete node;
return nullptr;
}
// 情况2:只有一个子节点
else if (node->left == nullptr) {
BSTNode* temp = node->right;
delete node;
return temp;
}
else if (node->right == nullptr) {
BSTNode* temp = node->left;
delete node;
return temp;
}
// 情况3:有两个子节点
// 找到右子树中的最小值节点(中序后继)
BSTNode* temp = findMin(node->right);
// 将当前节点的值替换为后继节点的值
node->data = temp->data;
// 删除后继节点
node->right = deleteRecursive(node->right, temp->data);
}
return node;
}
// 递归中序遍历
void inorderRecursive(BSTNode* node) {
if (node != nullptr) {
inorderRecursive(node->left);
std::cout << node->data << " ";
inorderRecursive(node->right);
}
}
// 递归销毁树
void destroyRecursive(BSTNode* node) {
if (node != nullptr) {
destroyRecursive(node->left);
destroyRecursive(node->right);
delete node;
}
}
public:
// 构造函数
BST() : root(nullptr) {
}
// 析构函数
~BST() {
destroyRecursive(root);
}
// 插入节点
void insert(int val) {
root = insertRecursive(root, val);
}
// 查找节点
bool search(int val) {
return searchRecursive(root, val) != nullptr;
}
// 删除节点
void remove(int val) {
root = deleteRecursive(root, val);
}
// 中序遍历
void inorder() {
inorderRecursive(root);
std::cout << std::endl;
}
};
二叉搜索树的局限性
虽然二叉搜索树在平均情况下具有良好的性能,但在最坏情况下(例如,当输入数据已经排序时),二叉搜索树会退化成一个链表,此时查找、插入和删除操作的时间复杂度会退化为O(n)。为了解决这个问题,人们发明了各种平衡二叉搜索树,如AVL树和红黑树。
AVL树
AVL树的定义与特性
AVL树是一种自平衡的二叉搜索树,它在插入和删除操作后会自动调整树的结构,以保持树的平衡。在AVL树中,任意节点的左右子树的高度差不超过1。这种平衡性保证了AVL树的操作(查找、插入、删除)的时间复杂度始终保持在O(log n)。
AVL树的平衡因子
AVL树的每个节点都有一个平衡因子(Balance Factor),定义为左子树的高度减去右子树的高度。在AVL树中,每个节点的平衡因子只能是-1、0或1。
AVL树的旋转操作
当插入或删除节点导致AVL树失去平衡时,需要通过旋转操作来恢复平衡。旋转操作分为四种:
- 左旋(Left Rotation):当右子树的高度比左子树的高度大2时,进行左旋。
- 右旋(Right Rotation):当左子树的高度比右子树的高度大2时,进行右旋。
- 左右旋(Left-Right Rotation):先对左子节点进行左旋,再对当前节点进行右旋。
- 右左旋(Right-Left Rotation):先对右子节点进行右旋,再对当前节点进行左旋。
AVL树的C++实现
// AVL树节点结构
struct AVLNode {
int data; // 节点存储的数据
AVLNode* left; // 指向左子节点的指针
AVLNode* right; // 指向右子节点的指针
int height; // 节点的高度
// 构造函数
AVLNode(int val) : data(val), left(nullptr), right(nullptr), height(1) {
}
};
// AVL树类
class AVLTree {
private:
AVLNode* root; // 根节点指针
// 获取节点的高度
int getHeight(AVLNode* node) {
if (node == nullptr) {
return 0;
}
return node->height;
}
// 获取节点的平衡因子
int getBalanceFactor(AVLNode* node) {
if (node == nullptr) {
return 0;
}
return getHeight(node->left) - getHeight(node->right);
}
// 更新节点的高度
void updateHeight(AVLNode* node) {
if (node == nullptr) {
return;
}
node->height = 1 + std::max(getHeight(node->left), getHeight(node->right));
}
// 右旋操作
AVLNode* rightRotate(AVLNode* y) {
AVLNode* x = y->left;
AVLNode* T2 = x->right;
// 执行旋转
x->right = y;
y->left = T2;
// 更新高度
updateHeight(y);
updateHeight(x);
// 返回新的根节点
return x;
}
// 左旋操作
AVLNode* leftRotate(AVLNode* x) {
AVLNode* y = x->right;
AVLNode* T2 = y->left;
// 执行旋转
y->left = x;
x->right = T2;
// 更新高度
updateHeight(x);
updateHeight(y);
// 返回新的根节点
return y;
}
// 插入节点
AVLNode