一、相关概念
(一)、树的基本概念
- 定义:树是n(n≥0)个节点的有限集。当n=0时,称为空树;在任意一颗非空树中,有且仅有一个特定的称为根(Root)的节点;当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1、T2、...、Tm,其中每一个集合本身又是一棵树,并且成为根的子树(SubTree)。
- 节点分类:
- 根节点:树中唯一的没有父节点的节点。
- 子节点:一个节点含有的子树的根节点称为该节点的子节点。
- 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。
- 叶子节点:度为0的节点,即没有子节点的节点。
- 非叶子节点或分支节点:度不为0的节点。
- 兄弟节点:具有相同父节点的节点互称为兄弟节点。
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟。
- 树的度:一棵树中,最大的节点的度称为树的度。
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
- 树的高度或深度:树中节点的最大层次。
(二)、树的性质
- 树中的节点数等于所有节点的度数加1。
- 度为m的树中第i层上至多有m^(i-1)个节点(i≥1)。
- 高度为h的m叉树至多有(m^h)/(m-1)个节点。
- 在二叉树的第i层上最多有2^(i-1)个结点 i>=1
- 深度为k的二叉树至多有2^k -1 个结点 k>=1
- 任意一个二叉树T,如果其叶子结点的个数是n0,度数为2的结点数为n2, n0 = n2 +1;
- 有n个结点的完全二叉树深度为(logn/log 2) +1;
(三)、树的类型
- 有序树:树中各棵子树的排列顺序是有先后次序的,则称该树为有序树。
- 无序树:树中各棵子树的排列顺序没有先后次序的,则称该树为无序树。
(四)、树的应用
树结构在数据结构和算法设计中有着广泛的应用,例如:
- 二叉搜索树:用于数据的查找、插入、删除等操作,保持数据的排序状态
二叉树,binary tree
n个结点的有限集合,集合要么为空树,要么由一个根结点和两棵互不相交,分别称谓根结点的左子树和右子树的二叉树组成。。特点,
1,每个结点最多两个子树。
2,左子树和右子树是有顺序的,次序不能颠倒。
3,如果某个结点只有一个子树,也要区分左,右子树。特殊的二叉树
1,斜树,所有的结点都只有左子树,左斜树,所有结点都只有右子树,右树。
2,满二叉树,所有的分支结点都存在左右子树,并且叶子都在同一层上。
3,完全二叉树,对于一颗有n个结点的二叉树按层序编号,如果编号i(1<=i<=n)的结点于同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这可树为完全二叉树。
- 平衡搜索树:如AVL树、红黑树等,通过自平衡机制保持树的深度尽可能小,从而提高操作效率。
- Huffman树:用于数据压缩中的编码,通过构建具有最小加权外部路径长度的二叉树,实现对字符的不等长编码,减少存储空间。
(五)、读写访问顺序
层序,
前序,根左右,先访问根,然后访问左,访问右。
中序,左根右,先从根开始(不是先访问根),从左开始访问,在访问根,在访问右结点。
后序,左右根,先从根开始(不是先访问根),先访问左,在访问右。在访问根。
二、顺序结构树
按照前序的方法,层层递归:
#include <stdio.h>
#include <stdlib.h>
typedef char DATATYPE;
typedef struct BiTNode /* 结点结构 */
{
DATATYPE data; /* 结点数据 */
struct BiTNode *lchild,*rchild; /* 左右孩子指针 */
}BiTNode;
char data[]="Abdg##h##e##c#fi###";
int ind = 0 ;
void CreateTree(BiTNode**root)
{
char c = data[ind++];
if('#'==c)
{
*root = NULL;
}
else
{
*root = (BiTNode*)malloc(sizeof(BiTNode));
if(NULL == *root)
{
perror("malloc");
return ;
}
(*root)->data = c;
CreateTree(&(*root)->lchild);
CreateTree(&(*root)->rchild);
}
return;
}
void PreOrderTraverse(BiTNode* root)
{
if(NULL == root)
{
return ;
}
else
{
printf("%c",root->data);
PreOrderTraverse(root->lchild);
PreOrderTraverse(root->rchild);
}
}
void InOrderTraverse(BiTNode* root)
{
if(NULL == root)
{
return ;
}
else
{
InOrderTraverse(root->lchild);
printf("%c",root->data);
InOrderTraverse(root->rchild);
}
}
void PostOrderTraverse(BiTNode* root)
{
if(NULL == root)
{
return ;
}
else
{
PostOrderTraverse(root->lchild);
PostOrderTraverse(root->rchild);
printf("%c",root->data);
}
}
void DestroyTree(BiTNode* root)
{
if(NULL == root)
{
return ;
}
DestroyTree(root->lchild);
DestroyTree(root->rchild);
free(root);
}
int main()
{
BiTNode* root=NULL;
CreateTree(&root);
PreOrderTraverse(root);
printf("\n");
InOrderTraverse(root);
printf("\n");
PostOrderTraverse(root);
printf("\n");
DestroyTree(root);
printf("Hello World!\n");
return 0;
}
三、树的其中一种应用(哈夫曼树)
哈夫曼树(Huffman Tree),又称最优二叉树,是一种特殊的二叉树结构,其特点在于树的带权路径长度(Weighted Path Length,简称WPL)达到最小。以下是对哈夫曼树的详细解释:
一、定义与性质
- 定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则这样的二叉树被称为哈夫曼树。
- 性质:
- 哈夫曼树的带权路径长度是最小的。
- 权值较大的结点离根较近,而权值较小的结点则离根较远。
- 哈夫曼树的总结点数是2n-1(n是叶子节点数)。
二、基本概念
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
- 结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
- 带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积称为结点的带权路径长度。树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
三、构造方法
哈夫曼树的构造通常采用自底向上的方法,具体步骤如下:
- 将给定的n个权值{w1, w2, ..., wn}看成是n棵只有根结点的二叉树,这n棵二叉树构成了一个森林。
- 在森林中选出两个根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树根结点权值之和。
- 在森林中删除这两棵树,同时将新得到的二叉树加入森林中。
- 重复步骤2和3,直到森林中只剩下一棵树为止,这棵树即为所求得的哈夫曼树。
首先找到两个最小的合成一个根节点,消掉两个小的;
紧接着选择小的,遵循左大右小的原则 继续合并。
具体要怎么使用:
节省内存空间,如果是网络传输则节省流量之类的
四、应用
哈夫曼树在计算机科学中有着广泛的应用,尤其是在数据压缩领域。通过哈夫曼编码,可以使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过评估来源符号出现机率的方法得到的。出现机率高的字母使用较短的编码,反之则使用较长的编码,从而使编码之后的字符串的平均长度、期望值降低,达到无损压缩数据的目的
四、哈希表
哈希表(Hash table),也称为散列表,是一种根据关键码值(Key value)而直接进行访问的数据结构。它通过散列函数(Hash function)将关键码值映射到表中一个位置来访问记录,以加快查找的速度。以下是对哈希表的详细解释:
一、定义与性质
- 定义:哈希表是一种通过哈希函数组织数据,以支持快速插入和搜索的数据结构。
- 性质:
- 提供快速的插入和查找功能,理论上可以在O(1)时间复杂度内完成。
- 基于数组存储数据,通过哈希函数将关键码值映射为数组下标。
- 数组创建后容量固定,如果数据较多需要不断扩展其长度。
二、工作原理
- 散列函数:哈希表的关键在于散列函数,该函数将输入的关键码值转换成数组的一个索引值。理想的散列函数应尽可能减少冲突(即不同的关键码值映射到同一索引值的情况)。
- 冲突解决:当不同的关键码值映射到同一索引值时,需要采用冲突解决方法。常见的冲突解决方法有开放定址法(如线性探测、二次探测等)、再哈希法、链地址法(将冲突的关键码值存储在链表中)和建立公共溢出区等。
- 存储与查找:通过将关键码值通过散列函数映射到数组的一个位置,可以直接在该位置找到对应的值或进行插入操作。查找时,同样使用散列函数定位到可能的索引位置,然后通过冲突解决方法找到确切的值。
三、构造方法
哈希表的构造方法主要依赖于散列函数的设计。常见的散列函数构造方法包括:
- 数字分析法:从关键字中选出分布比较均匀的若干位,构成哈希地址。
- 平方取中法:先求出关键字的平方值的中间几位作为哈希地址。
- 分段叠加法:将关键字分成位数相等的几部分(最后一部分较短),然后将这几部分相加,舍弃最高位后的结果就是该关键字的哈希地址。
- 除留余数法:假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为H(k) = k % p。
! - 伪随机数法:采用一个伪随机函数作为哈希函数,即H(key) = random(key)。
四、应用场景
哈希表因其快速查找和插入的特性,在多个领域有着广泛的应用,包括但不限于:
- 快速查找:在需要快速查找未排序的数据的场景中,哈希表可以显著提高查找效率。
- 数据去重:由于哈希表中每个键必须是唯一的,因此它经常用于去重数据。
- 缓存:将经常访问的数据存储在哈希表中,可以加快访问速度,减少数据库或磁盘访问次数。
- 计数:哈希表可以用于计数数据的出现次数,如统计单词出现次数等。
- 图形算法:在图形算法中,哈希表可以用于快速检索和操作有关节点的信息。
五、实际应用
下面实现了一个简单的哈希表,使用线性探测法解决冲突。在InsertHsTab
和SearchHs
函数中,通过计算哈希索引并在冲突时通过线性探测法找到下一个空槽位或匹配的数据。CreateHsTable
函数用于创建哈希表并初始化其所有槽位为-1,表示空。main
函数中演示了如何使用这些函数来插入和搜索数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // 通常用于POSIX操作系统API,这里可能不需要
#include <string.h> // 用于内存操作函数,但在这个程序中并未直接使用
// 定义数据类型和哈希表结构体
typedef int DATATYPE;
typedef struct
{
DATATYPE* head; // 指向存储数据的数组的指针
int tlen; // 哈希表的总长度
} HS_TAB;
// 创建一个哈希表
HS_TAB* CreateHsTable(int len)
{
HS_TAB* hs = malloc(sizeof(HS_TAB)); // 分配哈希表结构体的内存
if(NULL == hs) // 检查内存分配是否成功
{
perror("CreateHsTable");
return NULL;
}
hs->head = (DATATYPE*)malloc(sizeof(DATATYPE)*len); // 分配存储数据的数组的内存
if(NULL == hs->head)
{
perror("CreateHsTable");
free(hs); // 如果数据数组内存分配失败,释放已分配的哈希表结构体内存
return NULL;
}
int i = 0;
for(i=0;i<len;i++)
{
hs->head[i]=-1; // 初始化哈希表为-1,表示空槽位
}
hs->tlen = len; // 设置哈希表的总长度
return hs;
}
// 哈希函数,简单使用取模运算
int HsFun(HS_TAB* hs, DATATYPE* data)
{
return (*data) % hs->tlen;
}
// 向哈希表中插入数据
int InsertHsTab(HS_TAB* hs, DATATYPE* data)
{
int ind = HsFun(hs, data); // 计算哈希索引
while(hs->head[ind]!=-1) // 如果该位置已有数据,则处理冲突
{
printf("collision ind %d val:%d\n", ind, *data);
ind =(ind+1) % hs->tlen; // 线性探测法解决冲突
}
hs->head[ind] = *data; // 直接赋值,不需要memcpy,因为DATATYPE是int类型
return 0;
}
// 在哈希表中搜索数据
int SearchHs(HS_TAB* hs, DATATYPE* data)
{
int ind = HsFun(hs, data); // 计算哈希索引
int old_ind = ind;
while(hs->head[ind]!= *data) // 如果数据不匹配,继续搜索
{
ind = (ind+1) % hs->tlen; // 线性探测法
if(old_ind == ind) // 如果回到原点,说明表中没有该数据
{
return -1;
}
}
return ind; // 返回找到的数据的索引
}
int main()
{
int array[12]={12,67,56,16,25,37,22,29,15,47,48,34};
HS_TAB* hs = CreateHsTable(12); // 创建一个长度为12的哈希表
int i = 0;
for(i = 0; i < 12; i++)
{
InsertHsTab(hs, &array[i]); // 插入数据到哈希表
}
int want_num = 37;
int ret = SearchHs(hs, &want_num); // 搜索数据
if(-1 == ret)
{
printf("can't find\n");
}
else
{
printf("find, ind %d\n", ret);
}
printf("Hello World!\n");
return 0;
}