1. 前言

树是一种很重要的数据结构,最初对数据结构的定义就是指对的研究,后来才广义化了数据结构这个概念。从而可看出在数结构这一研究领域的重要性。

重要的原因是,它让计算机能建模出现实世界中更多领域里错综复杂的信息关系,让计算机服务这些领域成为可能。

本文将和大家聊聊树的基本概念,以及树的物理存储结构以及实现。

2. 基本概念

数据结构的研究主要是从 2 点出发:

  • 洞悉数据与数据之间的逻辑关系。
  • 设计一种物理存储方案。除了存储数据本身还要存储数据之间的逻辑关系,并且能让基于此数据上的算法利用这种存储结构达到事半功倍的效果。

当数据之间存在一对多关系时,可以使用树来描述。如公司组织结构、家庭成员关系……

完整的树结构除了需要描述出数据信息,还需要描述数据与数据之间的关系。树结构中,以节点作为数据的具体形态,作为数据之间关系的具体形态。

也可以说树是由很多节点以及组成的集合。

如果一棵树没有任何节点,则称此树为空树。如果树不为空,则此树存在唯一的根节点(root),根节点是整棵树的起点,其特殊之处在于没有前驱节点。如上图值为董事长的节点。

除此之外,树中的节点与节点之间会存在如下关系:

  • 父子关系:节点的前驱节点称其为父节点,且只能有一个或没有(如根节点)。节点的后驱节点称其为子节点,子节点可以有多个。如上图的董事长节点是市场总经理节点的父节点,反之,市场总经理节点是董事长节点的子节点。
  • 兄弟关系: 如果节点之间有一个共同的前驱(父)节点,则称这些节点为兄弟节点。如上图的市场总经理节点和运维总经理节点为兄弟关系。
  • 叶节点: 叶节点是没有后驱(子)节点的节点。
  • 子树:一棵树也可以理解是由子节点为根节点的子树组成,子树又可以理解为多个子子树组成…… 所以树可以描述成是树中之树式的递归关系。

如下图所示的 T 树 。

可以理解为T1T2子树组成。

T1、T2又可以认为是由它的子节点为根节点的子子树组成,以此类推,一直到叶节点为止。

树的相关概念:

  • 节点的度: 一个节点含有子树的个数称为该节点的度。
  • 树的度:一棵树中,最大的节点的度称为树的度。
  • 节点的层次:同级的节点为一个层次。根节点为第1层,根的子节点为第2层,以此类推。
  • 树的高(深)度: 树中节点最大的层次。如上图中的树的最大层次为 4

树的类型:

  • 无序树:树中的结点之间没有顺序关系,这种树称为无序树。
  • 有序树:树中任意节点的子节点之间有左右顺序关系。如下图,任一节点的左子节点值小于右子节点值。

  • 二叉树:如果任一节点最多只有 2 个子节点,则称此树结构为二叉树。上图的有序树也是一棵二叉树。

  • 完全二叉树:一棵二叉树至多只有最下面两层的节点的子结点可以小于 2。并且最下面一层的节点都集中在该层最左边的若干位置上。

  • 满二叉树:除了叶节点,其它节点的子结点都有 2 个。如上图中的树也是满二叉树。

3. 物理存储

可以使用邻接矩阵邻接表的形式存储树。

3.1 邻接矩阵存储

邻接矩阵是顺序表存储方案。

3.1.1 思路流程

  • 给树中的每一个节点从小到大进行编号。如下图,树共有 11 个节点。

  • 创建一个11X11的名为 arrTree的矩阵 ,行和列的编号对应节点的编号,并初始矩阵的值都为 0

  • 在树结构中,编号为 1 的节点和编号为2、3的节点存在父子关系,则把矩阵的 arrTree[1][2]arrTree[1][3]的位置设置为1。也就是说,行号和列号交叉位置的值如果是 1 ,则标志着编号和行号、列号相同的节点之间有关系。

  • 找到树中所有结点之间的关系,最后矩阵中的信息如下图所示。

矩阵记录了结点之间的双向(父到子,子到父)关系,最终看到是一个对称的稀疏矩阵。可以只存储上三角或下三角区域的信息,并可以对矩阵进行压缩存储。

邻接矩阵存储优点是实现简单、查询方便。但是,如果不使用压缩算法,空间浪费较大。

3.1.2 编码实现

现采用邻接矩阵方案实现对如下树的具体存储:

  • 节点类型: 用来描述数据的信息。
  1. struct TreeNode{
  2. //节点的编号
  3. int code;
  4. //节点上的值
  5. int data;
  6. };
  • 树类型:树类型中除了存储节点(数据)信息以及节点之间的关系,还需要提供相应的数据维护算法。本文仅考虑如何对树进行存储。
  1. class Tree {
  2. private:
  3. int size=7;
  4. vector<TreeNode> treeNodes;
  5. //使用矩阵存储节点之间的关系,矩阵第一行第一列不存储信息
  6. int matrix[7][7];
  7. //节点编号,为了方便,从 1 开始
  8. int idx=1;
  9. public:
  10. Tree() {
  11. }
  12. //初始根节点
  13. Tree(char root) {
  14. cout<<3<<endl;
  15. for(int r=1; r<this->size; r++) {
  16. for(int c=1; c<this->size; c++) {
  17. this->matrix[r][c]=0;
  18. }
  19. }
  20. TreeNode node= {this->idx,root};
  21. this->treeNodes.push_back(node);
  22. //节点的编号由内部指定
  23. this->idx++;
  24. }
  25. //获取到根节点
  26. TreeNode getRoot() {
  27. return this->treeNodes[0];
  28. }
  29. //添加新节点
  30. int addVertex(char val) {
  31. if (this->idx>=this->size)
  32. return 0;
  33. TreeNode node= {this->idx,val};
  34. this->treeNodes.push_back(node);
  35. //返回节点编号
  36. return this->idx++;;
  37. }
  38. /*
  39. * 添加节点之间的关系
  40. */
  41. bool addEdge(int from,int to) {
  42. char val;
  43. //查找编号对应节点是否存在
  44. if (isExist(from,val) && isExist(to,val)) {
  45. //建立关系
  46. this->matrix[from][to]=1;
  47. //如果需要,可以打开双向关系
  48. //this->matrix[to][from]=1;
  49. }
  50. }
  51. //根据节点编号查询节点
  52. bool isExist(int code,char & val) {
  53. for(int i=0; i<this->treeNodes.size(); i++) {
  54. if (this->treeNodes[i].code==code) {
  55. val=this->treeNodes[i].data;
  56. return true;
  57. }
  58. }
  59. return false;
  60. }
  61. //输出节点信息
  62. void showAll() {
  63. cout<<"矩阵信息"<<endl;
  64. for(int r=1; r<this->size; r++) {
  65. for(int c=1; c<this->size; c++) {
  66. cout<<this->matrix[r][c]<<" ";
  67. }
  68. cout<<endl;
  69. }
  70. cout<<"所有节点信息:"<<endl;
  71. for(int i=0; i<this->treeNodes.size(); i++) {
  72. TreeNode tmp=this->treeNodes[i];
  73. cout<<"节点:"<<tmp.code<<"-"<<tmp.data<<endl;
  74. //以节点的编号为行号,在列上扫描子节点
  75. char val;
  76. for(int j=1; j<this->size; j++ ) {
  77. if(this->matrix[tmp.code][j]!=0) {
  78. isExist(j,val);
  79. cout<<"\t子节点:"<<j<<"-"<<val<<endl;
  80. }
  81. }
  82. }
  83. }
  84. };

测试代码:

  1. int main() {
  2. //通过初始化根节点创建树
  3. Tree tree('A');
  4. TreeNode root=tree.getRoot();
  5. int codeB= tree.addVertex('B');
  6. tree.addEdge(root.code,codeB);
  7. int codeC= tree.addVertex('C');
  8. tree.addEdge(root.code,codeC);
  9. int codeD= tree.addVertex('D');
  10. tree.addEdge(codeB,codeD);
  11. int codeE= tree.addVertex('E');
  12. tree.addEdge(codeC,codeE);
  13. int codeF= tree.addVertex('F');
  14. tree.addEdge(codeC,codeF);
  15. tree.showAll();
  16. }

输出结果:

邻接矩阵存储方式的优点:

  • 节点存储在线性容器中,可以很方便的遍历所有节点。
  • 使用矩阵仅存储节点之间的关系,节点的存储以及其关系的存储采用分离机制,无论是查询节点还是关系(以节点的编号定位矩阵的行,然后在此行上以列扫描就能找到所以子节点)都较方便。

缺点:

  • 矩阵空间浪费严重,虽然可以采用矩阵压缩,但是增加了代码维护量。

3.2 邻接表存储

邻接表存储和邻接矩阵的分离存储机制不同,邻接表的节点类型中除了存储数据信息,还会存储节点之间的关系信息。

可以根据节点类型中的信息不同分为如下几种具体存储方案:

3.2.1 双亲表示法

结点类型有 2 个存储域:

  • 数据域。
  • 指向父节点的指针域。

如下文所示的树结构,用双亲表示法思路存储树结构后的物理结构如下图所示。

根节点没有父结点,双亲指针域中的值为 0

双亲表示法很容易找到节点的父节点,如果要找到节点的子节点,需要对整个表进行查询,双亲表示法是一种自引用表示法。

双亲表示法无论使用顺序存储或链表存储都较容易实现。

3.2.2 孩子表示法

用顺序表存储每一个节点,然后以链表的形式为每一个节点存储其所有子结点。如下图所示,意味着每一个节点都需要维护一个链表结构,如果某个节点没有子结点,其维护的链表为空。

孩子表示法,查找节点的子节点或兄弟节点都很方便,但是查找父节点,就不怎方便了。可以综合双亲、孩子表示法。

3.2.3 双亲孩子表示法

双亲孩子表示法的存储结构,无论是查询父节点还是子节点都变得轻松。如下图所示。

双亲孩子表示法的具体实现:

  • 设计节点类型:
  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;
  4. struct TreeNode {
  5. //节点编号
  6. int code;
  7. //节点的值
  8. char val;
  9. //节点的父节点
  10. TreeNode *parent;
  11. //节点的子节点信息,以单链表的方式存储,head 指向第一个子节点的地址
  12. TreeNode *head;
  13. //兄弟结点
  14. TreeNode *next;
  15. //构造函数
  16. TreeNode(int code,char val) {
  17. this->code=code;
  18. this->val=val;
  19. this->parent=NULL;
  20. this->head=NULL;
  21. this->next=NULL;
  22. }
  23. //自我显示
  24. void show() {
  25. cout<<"结点:";
  26. cout<<this->code<<"-"<<this->val<<endl;
  27. if(this->parent) {
  28. cout<<"\t父节点:";
  29. cout<<this->parent->code<<"-"<<this->parent->val<<endl;
  30. }
  31. TreeNode *move=this->head;
  32. while(move) {
  33. cout<<"\t子节点:"<<move->code<<"-"<<move->val<<endl;
  34. move=move->next;
  35. }
  36. }
  37. };

树类型定义:

  1. class Tree {
  2. private:
  3. //一维数组容器,存储所有结点
  4. vector<TreeNode*> treeNodes;
  5. //节点的编号生成器
  6. int idx=0;
  7. public:
  8. //无参构造函数
  9. Tree() {}
  10. //有参构造函数,初始化根节点
  11. Tree(char val) {
  12. //动态创建节点
  13. TreeNode* root=new TreeNode(this->idx,val);
  14. this->idx++;
  15. this->treeNodes.push_back(root);
  16. }
  17. //返回根节点
  18. TreeNode* getRoot() {
  19. return this->treeNodes[0];
  20. }
  21. //添加新节点
  22. TreeNode* addTreeNode(char val,TreeNode *parent) {
  23. //创建节点
  24. TreeNode* newNode=new TreeNode(this->idx,val);
  25. if(!newNode)
  26. //创建失败
  27. return NULL;
  28. if(parent) {
  29. //设置父节点
  30. newNode->parent=parent;
  31. //本身成为父节点的子节点
  32. if(parent->head==NULL)
  33. parent->head=newNode;
  34. else {
  35. //原来头节点成为尾节点
  36. newNode->next=parent->head;
  37. //新子节结点成为头结点
  38. parent->head=newNode;
  39. }
  40. }
  41. //编号自增
  42. this->idx++;
  43. //添加到节点容器中
  44. this->treeNodes.push_back(newNode);
  45. return newNode;
  46. }
  47. //显示树上的所有结点,以及结点之间的关系
  48. void showAll() {
  49. for(int i=0; i<this->treeNodes.size(); i++) {
  50. TreeNode *tmp=this->treeNodes[i];
  51. tmp->show();
  52. }
  53. }
  54. };

测试代码:

  1. int main(int argc, char** argv) {
  2. Tree tree('A');
  3. //返回根节点
  4. TreeNode * root =tree.getRoot();
  5. //根节点下添加 B、C 两个子节点
  6. TreeNode * rootB= tree.addTreeNode('B',root);
  7. TreeNode * rootC= tree.addTreeNode('C',root);
  8. //B下添加D子节点
  9. TreeNode * rootD= tree.addTreeNode('D',rootB);
  10. //C下添加E、F子节点
  11. TreeNode * rootE= tree.addTreeNode('E',rootC);
  12. TreeNode * rootF= tree.addTreeNode('F',rootC);
  13. tree.showAll();
  14. return 0;
  15. }

输出结果:

3.2.4 孩子兄弟表示法

指针域中存储子节点和兄弟节点。节点类型中有 3 个信息域:

  • 数据域。
  • 指向子节点的地址域。
  • 指向兄弟节点的地址域。

孩子兄弟表示法的具体实现过程有兴趣者可以自行试一试,应该还是较简单的。

如上几种实现存储方式,可以根据实际情况进行合理选择。

4. 总结

本文先讲解了树的基本概念,然后讲解了树的几种存储方案。本文提供了邻接矩阵和双亲孩子表示法的具体实现。

本文同时也收录至"编程驿站"公众号!

C++ 不知树系列之初识树(树的邻接矩阵、双亲孩子表示法……)的更多相关文章

  1. 后缀树系列一:概念以及实现原理( the Ukkonen algorithm)

    首先说明一下后缀树系列一共会有三篇文章,本文先介绍基本概念以及如何线性时间内构件后缀树,第二篇文章会详细介绍怎么实现后缀树(包含实现代码),第三篇会着重谈一谈后缀树的应用. 本文分为三个部分, 首先介 ...

  2. 大白话5分钟带你走进人工智能-第二十六节决策树系列之Cart回归树及其参数(5)

                                                    第二十六节决策树系列之Cart回归树及其参数(5) 上一节我们讲了不同的决策树对应的计算纯度的计算方法, ...

  3. 17-看图理解数据结构与算法系列(NoSQL存储-LSM树)

    关于LSM树 LSM树,即日志结构合并树(Log-Structured Merge-Tree).其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想.大多NoSQL数据库核心思想都是基于L ...

  4. 看图轻松理解数据结构与算法系列(NoSQL存储-LSM树) - 全文

    <看图轻松理解数据结构和算法>,主要使用图片来描述常见的数据结构和算法,轻松阅读并理解掌握.本系列包括各种堆.各种队列.各种列表.各种树.各种图.各种排序等等几十篇的样子. 关于LSM树 ...

  5. 初识主席树_Prefix XOR

    主席树刚接触觉得超强,根本看不懂,看了几位dalao的代码后终于理解了主席树. 先看一道例题:传送门 题目大意: 假设我们预处理出了每个数满足条件的最右边界. 先考虑暴力做法,直接对x~y区间暴枚,求 ...

  6. NOIp 数据结构专题总结 (2):分块、树状数组、线段树

    系列索引: NOIp 数据结构专题总结 (1) NOIp 数据结构专题总结 (2) 分块 阅:<「分块」数列分块入门 1-9 by hzwer> 树状数组 Binary Indexed T ...

  7. 区块链学习1:Merkle树(默克尔树)和Merkle根

    ☞ ░ 前往老猿Python博文目录 ░ 一.简介 默克尔树(Merkle tree,MT)又翻译为梅克尔树,是一种哈希二叉树,树的根就是Merkle根. 关于Merkle树老猿推荐大家阅读<M ...

  8. BZOJ 3196 Tyvj 1730 二逼平衡树 ——树状数组套主席树

    [题目分析] 听说是树套树.(雾) 怒写树状数组套主席树,然后就Rank1了.23333 单点修改,区间查询+k大数查询=树状数组套主席树. [代码] #include <cstdio> ...

  9. BZOJ 1901 Zju2112 Dynamic Rankings ——树状数组套主席树

    [题目分析] BZOJ这个题目抄的挺霸气. 主席树是第一时间想到的,但是修改又很麻烦. 看了别人的题解,原来还是可以用均摊的思想,用树状数组套主席树. 学到了新的姿势,2333o(* ̄▽ ̄*)ブ [代 ...

随机推荐

  1. LuoguP4219 [BJOI2014]大融合(LCT)

    早上考试想用\(LCT\)维护联通块\(size\),现在才发现\(LCT\)的\(size\)有虚实之分 \(Link\)与\(Acess\)中虚实变,干他丫的 \(Splay\)中只是相对关系,没 ...

  2. Redis 04 列表

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 在 Redis ...

  3. 10种有用的Linux Bash_Completion 命令示例

    摘要:我们可以对这个 bash 补全进行加速,并使用 complete 命令将其提升到一个新的水平. 本文分享自华为云社区<有用的 Linux Bash_Completion 命令示例(Ster ...

  4. CLIP:多模态领域革命者

    CLIP:多模态领域革命者 当前的内容是梳理<Transformer视觉系列遨游>系列过程中引申出来的.目前最近在AI作画这个领域 Transformer 火的一塌糊涂,AI画画效果从18 ...

  5. net::ERR_BLOCKED_BY_CLIENT 错误导致页面加载不出来

    AdBlock 禁止广告的插件屏蔽你的网络请求,屏蔽了一些重要的文件,导致页面加载不出来. 解决方案: 1.修改资源文件的名称,把ad替换成其他字符: 2.关闭广告拦截器: 3.广告拦截器设置白名单.

  6. CSS 选择器(二):子代选择器(>)

    后代选择器 后代选择器选择的范围广,范围是当前节点的所有子节点,包括其直接子节点. <div id="app"> <div>items-1 <div& ...

  7. [CF1515F] Phoenix and Earthquake(图论推导,构造)

    题面 在紧张又忙碌地准备联合省选时,发生了大地震,把原本要参赛的 n n n 个城市之间的全部 m m m 条道路震垮了,使得原本互相都能到达的这 n n n 个城市无法交通了.现在,需要紧急恢复 n ...

  8. PI控制器的由来

    20世纪20年代初,一位名叫尼古拉斯·米诺斯基(Nicolas Minorsky)的俄裔美国工程师通过观察舵手在不同条件下如何驾驶船只,为美国海军设计了自动转向系统. 根据Wikipedia.org, ...

  9. 在 node 中使用 jquery ajax

    对于前端同学来说,ajax 请求应该不会陌生.jquery 真的ajax请求做了封装,可以通过下面的方式发送一个请求并获取相应结果: $.ajax({ url: "https://echo. ...

  10. Shell第四章《正则表达式》

    一.前言 1.1.名词解释 正则表达式(regular expression, RE)是一种字符模式,用于在查找过程中匹配指定的字符.在大多数程序里,正则表达式都被置于两个正斜杠之间:例如/l[oO] ...