实现自定义的数据结构 —— 自然树
作者:互联网
最近看了一些公司的面试题,发现涉及 IT技术,包括 AI 行业的题目都会涉及到最基本的 C/C++、数据结构和算法之类的,突发奇想写了一个貌似没啥用的数据结构,就当是复习了 [手工哭笑表情]
1 结构
该自定义结构不同于二叉树及其他数据结构,每个节点的子节点个数不受限制,最大限度保留了数据的原始结构,并实现了其前序和后序遍历的方法。优点是节省了内存,但缺点则是基于链表结构查询的时间开销会相对较大。
举个例子,图1.1所示,即是该自定义树的一种结构
图1.1左边是一种直观的结构,Root为树的根节点,其子节点包括 A、B、C,节点 A 的子节点为 D,节点 B 的子节点为 E、F
【注】:Root 用来表示自然树的头节点,其 data 不储存任何有意义的数据,T是指向Root节点的指针,也就是头指针。
而图1.1右边则是程序中的结构,为此设计了两种子结构 TNode 和 Node,TNode 用于表示树的节点信息,而 Node 则用于构成链表,组成某节点的子节点集合
1.1 节点结构 TNode
如图1.2所示包括4种数据类型:(1)节点的名称 name、(2)数据 data、(3)双亲节点指针 parent、(4)子节点指针 child。data用于储存该节点的数据,parent 节点指针用于向上层查找,child 节点用于向下层查找。(图中白色块表示普通变量,橙色块表示指向TNode类型的指针变量,蓝色块表示指向Node类型的指针变量)
程序定义如下:
struct TNode //结点结构
{
std::string name; //结点名称
TElemType data; //结点数据
TNode *parent; //双亲节点指针
Node *child; //子节点指针
};
typedef TNode * Tree;
【注】:name为节点的标识符(等同于 ID 标记),根据名称长度分配不同大小的动态内存,不可重复,TElemType为节点包含的数据类型,作者定义为 int,根据实际需要修改。Tree 用来表示整个树(树的头节点数据类型为TNode,故用指向头节点的指针来表示树)
1.2 链表的节点结构 Node
如图1.3所示包括4种数据类型:(1)指向TNode节点的指针 tNode、(2)前一个链表节点的指针 pre、(3)下一个链表节点的指针 next。
程序定义如下:
struct Node // 链表结构
{
TNode *tNode; //指向TNode节点
Node *pre; //指向前一个Node节点
Node *next; //指向后一个Node节点
};
typedef Node * LinkList;
此处有个特殊的节点:链表的头节点(head)
如图1.4所示,该头节点中 tNode 指向的 TNode 节点储存其双亲点的子节点的数量信息(由 data存储,图中举例该双亲节点的子节点共有3个,也就是说该head节点后还有3个后继节点)。而其 name 会自动命名为 “双亲名称” + “_head”,标记其为头节点(图中举例双亲名称为 “A”,故其头节点名称为“A_head”)。
【注】:因为链表的头节点没有前继节点,故 pre 始终为 NULL,而且也禁止为头节点添加子节点,所有 child 也始终为 NULL。
2 遍历
对于一种自定义的数据结构来说,最核心需要实现的功能就是遍历,程序中对于不同功能设计了两种遍历算法:前序遍历 和 后序遍历。对于节点的插入操作,可以通过前序遍历或后序遍历来实现,而节点的删除操作因为涉及由后往前的动态内存的释放,则需使用后序遍历来实现。
2.1 前序遍历
由于head节点的存在,故可进一步设计出两种遍历方式:(1)遍历所有节点,包括head(2)跳过所有head。
由于兄弟节点插入设计为后插入(具体见第3章),故需要遍历head节点,即需要使用第(1)种遍历方式,而插入子节点(具体见第3章)或是单纯的输出显示所有节点信息,则第(2)种就可以。
如图2.1所示为树的第(1)种:前序遍历所有节点,包括所有head
具体到结构则如图2.2所示
核心逻辑如下,先递归遍历,再移动指针
for(int i=0; i< len+1; i++)
{
_ShowAll_Pre(P->tNode);
P = P->next;
}
控制台输出如图2.3所示,格式为 (双亲节点name) <—— (该节点的name: 该节点的data)。
【注】:Root 节点无双亲,故仅显示name和data。
如图2.4所示为树的第(2)种:前序遍历,跳过所有head
具体到结构如图2.4所示
核心逻辑如下,先移动指针,再递归遍历,同时由于先移动指针跳过了head节点,故需要循环的长度为 len
for(int i=0; i< len; i++)
{
P = P->next;
_ShowAll_Pre(P->tNode);
}
控制台输出如图2.5所示
2.2 后序遍历
对于节点删除操作,因为要从该节点的最后一个子节点开始倒序释放(避免指针失效),故需使用后序遍历。同理,分为上述两种遍历方式:(1)遍历输出所有节点(2)跳过所有head
如图2.6所示为树的第(1)种:后序遍历输出所有节点
如图2.7所示为树的第(2)种:后序遍历输出所有节点,但跳过所有head
控制台输出如图2.7所示:
3 插入
笔者实现了两种插入:(1)为该节点插入子节点(禁止为head节点插入子节点),该子节点会插入到最后一个位置;(2)在该节点的后面插入兄弟节点(可以 head 后插入)。
3.1 插入子节点
如图3.1所示,为插入子节点的示意图,首先遍历所有节点(可跳过head),找到需要插入子节的节点(作为双亲节点)后,逐步检索其子节点至最后一个,然后生成新的节点,并令其 parent 指针指向其双亲节点,最后令新生成的节点与最后一个子节点相连接(原最后一个节点的 next 指针指向新生成的节点,新生成节点的 pre 指针指向原最后一个节点),同时此条链表的长度+1(head 节点的 data+1)。
【注】:若该节点没有子节点,则先生成一个 head 节点再进行插入
3.2 在某一节点后插入兄弟节点
如图3.2所示,遍历检索到该节点,然后生成新的节点,并令新生成节点的 parent 节点指向该节点,然后断开该节点与后续节点之间的指针连接,将新生成节点的指针连接至两节点之间,同时此条链表的长度+1(head 节点的 data+1)。
【注】:如果该节点没有后继节点,则只需将新生成节点的后继指针 next 指向 NULL 即可。
4 删除
删除某一节点如图4.1所示,首先找到该节点和其后后继节点,然后断开后继节点与该节点和后后继节点之间的指针连接,并释放后继节点的内存,最后将该节点与后后继节点相连,同时 head 节点的 data-1。
如果该节点有子节点呢,则需要通过后序遍历找到该节点的子节点的最后一个节点(子节点可能仍存在子节点,则需要层层遍历),倒序依次释放所有节点。
如图4.2所示,假如需要删除图1.1中的 B 节点,由于 B 节点存在子节点,则需要将其及其所有子节点全部删除,并释放内存,删除顺序同后序遍历顺序,不过在删除链表最后一个子节点后需要回过来删除该链表的 head 节点。
删除节点B后的控制台输出为(前序遍历所有):
删除树则是删除所有节点(包括Root节点),并释放所有内存。
结语
未来有时间会加入与二叉树的相互转换功能,目前完成了第一版,可能仍有Bug,如果发现的话,还望指正 (∩_∩)
编写了两版:C语言和C++版本
OS:windows10;开发环境:Dev C++;编译器:MinGW GCC 4.8.1 32-bit Release(不过看控制台输出貌似使用的 G++ 编译器),所以如果编译失败的话建议换G++编译器试试
代码链接(C):https://download.csdn.net/download/jack__linux/12494399
代码链接(C++):https://download.csdn.net/download/jack__linux/14992267
标签:head,遍历,自定义,自然,链表,所示,数据结构,节点,指针 来源: https://blog.csdn.net/jack__linux/article/details/113576127