树(Tree)是一种非常常见的数据结构,它模拟了一种层级或分支结构。
树的基本概念
- 节点(Node):树由节点组成,每个节点包含数据元素和指向其他节点的指针。
- 边(Edge):连接节点的线称为边,表示节点之间的关系。
- 根节点(Root):树的最顶端节点,没有父节点。
- 子节点(Child):由边连接并由父节点指向的节点。
- 父节点(Parent):有子节点的节点称为父节点。
- 兄弟节点(Sibling):具有相同父节点的节点互称为兄弟节点。
- 叶节点(Leaf):没有子节点的节点,也称为终端节点。
- 内部节点(Internal node):至少有一个子节点的节点。
- 节点的度(Degree):节点拥有的子节点数。
- 树的度:树中所有节点的度的最大值。
- 路径(Path):从一个节点到另一个节点经过的边序列。
- 路径长度:路径上的边的数量。
- 深度(Depth):从根节点到该节点的唯一路径上的边的数量。
- 高度(Height):节点到最远叶节点的最长路径上的边的数量。树的高度通常指根节点的高度。
- 层(Level):根节点在第1层,它的子节点在第2层,以此类推。
树的种类
- 二叉树(Binary Tree):每个节点最多有两个子节点的树。
- 满二叉树:所有非叶节点都具有两个子节点的二叉树。
- 完全二叉树:除了最后一层外,每一层都被完全填满,最后一层的节点都靠左对齐。
- 平衡二叉树(AVL Tree):任何节点的两个子树的高度差不超过1的二叉树。
- 二叉搜索树(BST):左子树的所有节点都小于根节点,右子树的所有节点都大于根节点。
- 堆(Heap):特殊的完全二叉树,用于实现优先队列。
- B树、B+树:用于数据库和文件系统的多路平衡搜索树。
树的表示
树可以用多种方式表示:
- 链接表示法:每个节点包含一个数据字段和多个指向其子节点的指针。
- 数组表示法:使用数组来表示树,特别是在完全二叉树中,可以通过数组的索引来表示节点之间的关系。
树的基本操作
以下为树的一些基本操作:
- 创建树:初始化根节点和子节点。
- 遍历树:按照一定的顺序访问树的所有节点。常见的遍历方式有:
- 前序遍历(Pre-order Traversal):根-左-右
- 中序遍历(In-order Traversal):左-根-右
- 后序遍历(Post-order Traversal):左-右-根
- 层次遍历(Level-order Traversal):从上到下,从左到右
- 添加节点:在树中插入新的节点。
- 删除节点:从树中移除节点。
- 查找节点:在树中搜索特定的节点。
- 更新节点:修改树中节点的值。
树的应用
树在计算机科学中有广泛的应用,以下是一些例子:
- 文件系统:树结构用于组织文件和目录。
- 数据库索引:B树和B+树用于数据库的索引结构。
- 决策树:在机器学习中用于分类和回归任务。
- 表达式树:用于表示算术表达式,便于计算和优化。
- 网络结构:树结构用于表示网络中的分层结构。
树的优缺点
优点:
- 树结构能够模拟具有层级关系的数据。
- 树的遍历算法可以高效地处理大量数据。
- 树结构相对灵活,可以表示多种类型的数据。
缺点:
- 树结构比线性数据结构更复杂,操作实现起来也更困难。
- 如果树不平衡,某些操作(如查找)的效率会降低。
二叉树
二叉树概念
二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0), 或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
二叉树性质
- 性质1:在二叉树的第i层上,最多有2^(i-1)个节点(i≥1)。
- 性质2:深度为k的二叉树最多有2^k - 1个节点(k≥1)。
- 性质3:对于任何非空二叉树,如果叶子数为N0,度为2的节点数为N2,则N0 = N2 + 1。
- 性质4:具有n个节点的完全二叉树的深度为floor(log2(n)) + 1。
- 性质5:如果对一棵有n个节点的完全二叉树进行层次遍历(从上到下,从左到右),则节点编号为i的左子节点编号为2i,右子节点编号为2i+1(i≥1),父节点的编号为floor(i/2)。
二叉树的分类
- 满二叉树:所有非叶节点都具有两个子节点的二叉树。
- 完全二叉树:除了最后一层外,每一层都被完全填满,最后一层的节点都靠左对齐。
- 平衡二叉树(AVL Tree):任何节点的两个子树的高度差不超过1的二叉树。
- 二叉搜索树(BST):左子树的所有节点都小于根节点,右子树的所有节点都大于根节点。
二叉树的存储结构
顺序存储结构
顺序存储结构使用数组来存储二叉树中的节点。这种存储方式适用于完全二叉树,因为完全二叉树的节点可以按照层次遍历的顺序直接存储在数组中,而不浪费空间。
特点:
- 数组的第一个位置(索引为0)通常不存储节点,或者存储特殊的哨兵节点。
- 对于任意节点i(i≥1),其左子节点存储在位置2i,右子节点存储在位置2i+1。
- 节点的父节点存储在位置floor(i/2)。
遍历
链式存储结构
链式存储结构使用链表来表示二叉树,每个节点包含一个数据域和两个指针域(分别指向左子节点和右子节点)。
节点定义:
typedef struct TreeNode {
ElementType data; // 数据域
struct TreeNode *left; // 指向左子树的指针
struct TreeNode *right; // 指向右子树的指针
} TreeNode;
特点:
- 每个节点包含数据和两个指针,分别指向左子节点和右子节点。
- 空节点由NULL指针表示。
代码示例
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data;
struct node *left;
struct node *right;
} node_t, *node_p;
// 创建二叉树
node_p create_bitree(int n, int i)
{
// 创建根节点
node_p root = (node_p)malloc(sizeof(node_t));
if (root == NULL)
{
printf("malloc error\n");
return NULL;
}
// 初始化根节点
root->data = i;
if (2 * i <= n)
root->left = create_bitree(n, 2 * i);
else
root->left = NULL;
if (2 * i + 1 <= n)
root->right = create_bitree(n, 2 * i + 1);
else
root->right = NULL;
return root;
}
// 先序遍历
void pre_order(node_p root)
{
if (NULL == root)
return;
printf("%d ", root->data);
if (root->left != NULL)
pre_order(root->left);
if (root->right != NULL)
pre_order(root->right);
}
// 中序遍历
void in_order(node_p root)
{
if (NULL == root)
return;
if (root->left != NULL)
in_order(root->left);
printf("%d ", root->data);
if (root->right != NULL)
in_order(root->right);
}
// 后序遍历
void post_order(node_p root)
{
if (NULL == root)
return;
if (root->left != NULL)
post_order(root->left);
if (root->right != NULL)
post_order(root->right);
printf("%d ", root->data);
}
int main()
{
node_p root=create_bitree(10,1);
pre_order(root);
printf("\n");
}
#include <stdio.h>
#include <stdlib.h>
// 定义树节点结构体
typedef struct TreeNode {
int data; // 数据域,可以根据需要更改为其他类型
struct TreeNode *left; // 指向左子树的指针
struct TreeNode *right; // 指向右子树的指针
} TreeNode;
// 创建新节点的函数
TreeNode* createNode(int value) {
TreeNode *newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
exit(-1); // 如果内存分配失败,则退出程序
}
newNode->data = value;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 向二叉树添加节点的函数(递归方式)
TreeNode* insertNode(TreeNode *root, int value) {
// 如果根节点为空,则创建新节点并返回
if (root == NULL) {
return createNode(value);
}
// 否则递归地向左右子树添加节点
if (value < root->data) {
root->left = insertNode(root->left, value);
} else {
root->right = insertNode(root->right, value);
}
return root;
}
// 前序遍历二叉树的函数(递归方式)
void preOrderTraversal(TreeNode *root) {
if (root != NULL) {
printf("%d ", root->data); // 访问根节点
preOrderTraversal(root->left); // 遍历左子树
preOrderTraversal(root->right); // 遍历右子树
}
}
// 中序遍历二叉树的函数(递归方式)
void inOrderTraversal(TreeNode *root) {
if (root != NULL) {
inOrderTraversal(root->left); // 遍历左子树
printf("%d ", root->data); // 访问根节点
inOrderTraversal(root->right); // 遍历右子树
}
}
// 后序遍历二叉树的函数(递归方式)
void postOrderTraversal(TreeNode *root) {
if (root != NULL) {
postOrderTraversal(root->left); // 遍历左子树
postOrderTraversal(root->right); // 遍历右子树
printf("%d ", root->data); // 访问根节点
}
}
// 释放二叉树内存的函数(递归方式)
void freeTree(TreeNode *root) {
if (root != NULL) {
freeTree(root->left); // 释放左子树
freeTree(root->right); // 释放右子树
free(root); // 释放当前节点
}
}
// 主函数,演示二叉树的操作
int main() {
TreeNode *root = NULL;
// 向二叉树添加节点
root = insertNode(root, 5);
root = insertNode(root, 3);
root = insertNode(root, 7);
root = insertNode(root, 2);
root = insertNode(root, 4);
root = insertNode(root, 6);
root = insertNode(root, 8);
// 遍历二叉树
printf("Pre-order traversal: ");
preOrderTraversal(root);
printf("\n");
printf("In-order traversal: ");
inOrderTraversal(root);
printf("\n");
printf("Post-order traversal: ");
postOrderTraversal(root);
printf("\n");
// 释放二叉树内存
freeTree(root);
return 0;
}
树与二叉树应用
最优二叉树(哈夫曼树)
哈夫曼树(Huffman Tree)是一种带权路径长度最短的二叉树,也称为最优二叉树。它是一种前缀编码树,广泛应用于数据压缩中。以下是关于哈夫曼树的详细解释:
哈夫曼树的性质
-
带权路径长度最短:在所有可能的二叉树中,哈夫曼树的带权路径长度是最短的。带权路径长度是指树中所有叶子节点的权值乘以其到根节点的路径长度之和。
-
前缀编码:在哈夫曼树中,任何节点的字符都不会是其父节点字符的前缀。这意味着从根节点到任何叶子节点的路径上不会出现重复的字符序列。
-
树形结构:哈夫曼树是一个有根的树,其中每个非叶子节点都有两个子节点。
哈夫曼编码过程
-
构建哈夫曼树:
- 首先,将所有待编码的字符按照出现的频率(即权值)进行排序。
- 选择频率最低的两个字符作为叶子节点,将它们合并为一个新节点,其权值为两个字符频率之和。
- 重复上述步骤,每次合并两个频率最低的节点,直到只剩下一个节点,这个节点就是哈夫曼树的根节点。
-
哈夫曼编码:
- 从根节点开始,对每个字符进行编码,路径上的0和1分别表示左子树和右子树。
- 编码规则:遇到左子节点,编码为0;遇到右子节点,编码为1。
- 编码完成后,每个字符都对应一个唯一的二进制序列。
哈夫曼树的优点
- 压缩率较高:由于哈夫曼树能有效降低字符的频率差异,因此可以实现较高的压缩率。
- 编码和解码简单:哈夫曼编码和解码过程相对简单,只需要按照哈夫曼树的路径进行编码和解码。
哈夫曼树的缺点
- 构建哈夫曼树的时间复杂度较高:构建哈夫曼树需要进行多次合并操作,时间复杂度较高。
- 不适用于所有类型的数据压缩:哈夫曼树适合于具有明显频率差异的字符或数据,对于频率相近的数据,压缩效果可能不明显。
应用场景
哈夫曼树主要用于数据压缩,尤其是在文本压缩和文件压缩中。它也可以用于字符编码和网络传输中,以减少传输数据量。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 哈夫曼树节点的结构
typedef struct {
int freq; // 字符的频率
char data; // 字符数据(对于非叶子节点为空)
struct MinHeapNode *left, *right;
} MinHeapNode;
// 哈夫曼树的最小堆结构
typedef struct {
int size; // 当前堆的大小
int capacity; // 堆的容量
MinHeapNode** array; // 存储堆元素的数组
} MinHeap;
// 创建新的哈夫曼树节点
MinHeapNode* newNode(char data, int freq) {
MinHeapNode* temp = (MinHeapNode*)malloc(sizeof(MinHeapNode));
temp->left = temp->right = NULL;
temp->data = data;
temp->freq = freq;
return temp;
}
// 创建最小堆
MinHeap* createMinHeap(int capacity) {
MinHeap* minHeap = (MinHeap*)malloc(sizeof(MinHeap));
minHeap->size = 0;
minHeap->capacity = capacity;
minHeap->array = (MinHeapNode**)malloc(minHeap->capacity * sizeof(MinHeapNode*));
return minHeap;
}
// 交换两个哈夫曼树节点
void swapMinHeapNode(MinHeapNode** a, MinHeapNode** b) {
MinHeapNode* t = *a;
*a = *b;
*b = t;
}
// 堆的有序化
void minHeapify(MinHeap* minHeap, int idx) {
int smallest = idx;
int left = 2 * idx + 1;
int right = 2 * idx + 2;
if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq)
smallest = left;
if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq)
smallest = right;
if (smallest != idx) {
swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]);
minHeapify(minHeap, smallest);
}
}
// 检查堆是否只有一个节点
int isSizeOne(MinHeap* minHeap) {
return (minHeap->size == 1);
}
// 从最小堆中提取最小节点
MinHeapNode* extractMin(MinHeap* minHeap) {
MinHeapNode* temp = minHeap->array[0];
minHeap->array[0] = minHeap->array[minHeap->size - 1];
--minHeap->size;
minHeapify(minHeap, 0);
return temp;
}
// 在最小堆中插入一个新的节点
void insertMinHeap(MinHeap* minHeap, MinHeapNode* minHeapNode) {
++minHeap->size;
int i = minHeap->size - 1;
while (i && minHeapNode->freq < minHeap->array[(i - 1) / 2]->freq) {
minHeap->array[i] = minHeap->array[(i - 1) / 2];
i = (i - 1) / 2;
}
minHeap->array[i] = minHeapNode;
}
// 构建哈夫曼树
MinHeapNode* buildHuffmanTree(char data[], int freq[], int size) {
MinHeapNode *left, *right, *top;
MinHeap* minHeap = createMinHeap(size);
for (int i = 0; i < size; ++i)
minHeap->array[i] = newNode(data[i], freq[i]);
minHeap->size = size;
for (int i = size / 2 - 1; i >= 0; --i)
minHeapify(minHeap, i);
while (!isSizeOne(minHeap)) {
left = extractMin(minHeap);
right = extractMin(minHeap);
top = newNode('$', left->freq + right->freq);
top->left = left;
top->right = right;
insertMinHeap(minHeap, top);
}
return extractMin(minHeap);
}
二叉排序树
二叉排序树(Binary Search Tree,BST)是一种特殊的二叉树,其每个节点的键值满足特定的排序规则。以下是二叉排序树的详细解释:
基本概念
- 节点:二叉排序树的每个节点包含三个部分:键(Key)、值(Value)和指向子节点的指针(Left和Right)。
- 键:节点存储的数据,通常是整数、字符串或其他类型。
- 左子节点:键值小于当前节点键值的子节点。
- 右子节点:键值大于当前节点键值的子节点。
- 父节点:包含当前节点的键值,并且键值大于当前节点的键值的节点。
性质
- 有序性:对于任意节点,其左子树的所有键值都小于该节点的键值,右子树的所有键值都大于该节点的键值。
- 根节点:键值最大的节点,其左子树为空。
- 叶节点:没有子节点的节点,其键值小于所有非叶节点的键值。
- 非空子树:每个节点的左子树和右子树都是二叉排序树。
操作
- 插入:将一个新的键值插入到二叉排序树中,保持树的有序性。
- 删除:从二叉排序树中删除一个键值,并重新组织树以保持有序。
- 查找:在二叉排序树中查找一个键值,返回该键值对应的节点(如果存在)。
- 中序遍历:按照键值从小到大的顺序遍历二叉排序树。
性能分析
- 查找操作:在最坏的情况下,二叉排序树的查找操作的时间复杂度为O(n),其中n是树中节点的数量。但在最好情况下,查找操作的时间复杂度为O(1),当树完全平衡时。
- 插入和删除操作:在二叉排序树中,插入和删除操作的时间复杂度在最坏情况下为O(n),但在最好情况下为O(log n),当树完全平衡时。
代码示例
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点结构体
typedef struct TreeNode {
int val; // 节点存储的值
struct TreeNode *left; // 指向左子树的指针
struct TreeNode *right; // 指向右子树的指针
} TreeNode;
// 创建一个新节点
TreeNode* createNode(int value) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode)); // 动态分配内存
newNode->val = value; // 设置节点值
newNode->left = newNode->right = NULL; // 初始化左右子树指针
return newNode;
}
// 向二叉排序树中插入一个新节点
TreeNode* insert(TreeNode* node, int value) {
// 如果节点为空,创建新节点并返回
if (node == NULL) return createNode(value);
// 如果插入值小于当前节点值,递归插入到左子树
if (value < node->val)
node->left = insert(node->left, value);
// 如果插入值大于当前节点值,递归插入到右子树
else if (value > node->val)
node->right = insert(node->right, value);
// 返回当前节点
return node;
}
// 查找给定子树中的最小值节点
TreeNode* minValueNode(TreeNode* node) {
TreeNode* current = node; // 从当前节点开始
// 循环直到找到最左边的节点
while (current && current->left != NULL)
current = current->left;
return current;
}
// 从二叉排序树中删除一个节点
TreeNode* deleteNode(TreeNode* root, int value) {
// 如果根节点为空,直接返回
if (root == NULL) return root;
// 如果删除值小于根节点值,递归删除左子树中的节点
if (value < root->val)
root->left = deleteNode(root->left, value);
// 如果删除值大于根节点值,递归删除右子树中的节点
else if (value > root->val)
root->right = deleteNode(root->right, value);
else {
// 找到要删除的节点
// 如果节点只有一个子节点或没有子节点
if (root->left == NULL) {
TreeNode* temp = root->right; // 保存右子树
free(root); // 释放当前节点内存
return temp; // 返回右子树
} else if (root->right == NULL) {
TreeNode* temp = root->left; // 保存左子树
free(root); // 释放当前节点内存
return temp; // 返回左子树
}
// 如果节点有两个子节点,找到右子树中的最小值节点
TreeNode* temp = minValueNode(root->right);
// 将最小值节点的值复制到当前节点
root->val = temp->val;
// 删除右子树中的最小值节点
root->right = deleteNode(root->right, temp->val);
}
// 返回根节点
return root;
}
// 中序遍历二叉排序树
void inorderTraversal(TreeNode* root) {
// 如果根节点不为空,递归遍历
if (root != NULL) {
inorderTraversal(root->left); // 遍历左子树
printf("%d ", root->val); // 访问当前节点
inorderTraversal(root->right); // 遍历右子树
}
}
// 在二叉排序树中查找一个值
TreeNode* search(TreeNode* root, int value) {
// 如果根节点为空或根节点值等于查找值,返回根节点
if (root == NULL || root->val == value)
return root;
// 如果查找值小于根节点值,递归查找左子树
if (root->val < value)
return search(root->right, value);
// 如果查找值大于根节点值,递归查找右子树
return search(root->left, value);
}
平衡二叉树
- 二叉搜索树特性:每个节点都满足左子树的所有节点值小于该节点值,右子树的所有节点值大于该节点值。
- 平衡性:对于每个节点,其左子树和右子树的高度差(平衡因子)不超过1。这意味着树的高度大致是对数级的,从而保证了搜索、插入和删除操作的时间复杂度为O(log n)。
平衡二叉树的几个关键操作包括:
- 插入节点:在插入节点时,需要检查从插入点到根节点路径上所有节点的平衡因子,如果发现有节点不平衡(平衡因子超过1),则需要通过旋转操作(单旋转或双旋转)来重新平衡树。
- 删除节点:删除节点后,同样需要检查从删除点到根节点路径上所有节点的平衡因子,并进行必要的旋转操作以保持平衡。
- 旋转操作:包括左旋、右旋、左右旋和右左旋,用于在插入或删除节点后恢复树的平衡。
代码示例
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
typedef struct Node {
int key;
struct Node *left;
struct Node *right;
int height;
} Node;
// 获取节点的高度
int height(Node *N) {
if (N == NULL)
return 0;
return N->height;
}
// 创建新节点
Node* newNode(int key) {
Node* node = (Node*)malloc(sizeof(Node));
node->key = key;
node->left = NULL;
node->right = NULL;
node->height = 1;
return(node);
}
// 右旋转
Node* rightRotate(Node *y) {
Node *x = y->left;
Node *T2 = x->right;
// 旋转
x->right = y;
y->left = T2;
// 更新高度
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;
// 返回新的根节点
return x;
}
// 左旋转
Node* leftRotate(Node *x) {
Node *y = x->right;
Node *T2 = y->left;
// 旋转
y->left = x;
x->right = T2;
// 更新高度
x->height = max(height(x->left), height(x->right)) + 1;
y->height = max(height(y->left), height(y->right)) + 1;
// 返回新的根节点
return y;
}
// 获取平衡因子
int getBalance(Node *N) {
if (N == NULL)
return 0;
return height(N->left) - height(N->right);
}
// 插入节点
Node* insert(Node* node, int key) {
// 1. 执行正常的BST插入
if (node == NULL)
return(newNode(key));
if (key < node->key)
node->left = insert(node->left, key);
else if (key > node->key)
node->right = insert(node->right, key);
else // 相等的键值不允许在BST中
return node;
// 2. 更新节点的高度
node->height = 1 + max(height(node->left), height(node->right));
// 3. 获取平衡因子,检查是否失衡
int balance = getBalance(node);
// 如果节点失衡,则有四种情况
// 左左
if (balance > 1 && key < node->left->key)
return rightRotate(node);
// 右右
if (balance < -1 && key > node->right->key)
return leftRotate(node);
// 左右
if (balance > 1 && key > node->left->key) {
node->left = leftRotate(node->left);
return rightRotate(node);
}
// 右左
if (balance < -1 && key < node->right->key) {
node->right = rightRotate(node->right);
return leftRotate(node);
}
// 返回未失衡的节点
return node;
}
// 中序遍历
void inorder(Node *root) {
if (root != NULL) {
inorder(root->left);
printf("%d ", root->key);
inorder(root->right);
}
}
// 主函数
int main() {
Node *root = NULL;
root = insert(root, 10);
root = insert(root, 20);
root = insert(root, 30);
root = insert(root, 40);
root = insert(root, 50);
// 打印中序遍历
printf("中序遍历: ");
inorder(root);
return 0;
}