数据结构 - 链表

news/2024/10/12 2:22:40

今天我们将开始第二个数据类型-链表的学习,同样我们还是用最原始的方式,自己申请内存管理内存来实现一个链表。

01、01、定义

什么是链表?链表在物理存储结构上表现为非顺序性和非连续性,因此链表的数据元素物理存储位置是随机的,动态分配的;而在逻辑结构上表现为线性结构的特点,即元素一个连着一个元素串起来像一条线 。

节点:其中链表元素又叫节点,一个节点主要包含数据域和指针域,其中数据域主要存放数据元素,而指针域主要存放下一个节点存储位置地址。

头指针:一个表示链表第一个节点位置的普通指针,并且永远指向第一个节点位置,方便后面使用链表。

头节点:通常表示链表的第一个节点,并且节点内数据域为空,因此也叫空节点,其作用主要用于解决一些特殊问题,因此也可以省略。

首元节点:由于头节点数据域为空,因此链表的第一个数据域不为空的节点叫首元节点,只是一个名称,并没有什么实际意义。

02、02、分类

链表有两种分类方法,其一可以分为静态链表和动态链表,其二可以分为单向链表、双向链表以及循环链表。

单链表只有一个方向,每个节点包含数据域和指向下一个节点的指针域。

双向链表有两个方向,即每个节点包含数据域以及同时指向上一个节点和下一个节点的指针域。

循环链表指链表首尾相连,即最后一个节点的指针域指向第一个节点。循环链表也分单向循环链表和双向循环链表,原理都一样。

03、03、实现

下面我们一起使用最原始的方式,自己申请内存空间,自己维护,完成链表的实现。

1、ADT定义

我们首先来定义链表的ADT(单链表)。

ADT LinkedList{

数据对象:D 是一个非空的元素集合,D = {a1, a2, ..., an},其中 ai 表示一个元素即节点,一个节点存储着数据和指向下一个节点的指针。数据关系:D中的节点通过指针进行连接,每一个节点都包含一个指向下一个节点的指针。基本操作:[Init(n) :初始化一个空链表,即声明一个头指针,如有必要也可以声明一个头节点。Length:返回链表长度。HeadNode:返回头节点。Find(v):返回数据域v对应的节点。Update(n,v):更新n节点的数据域。InsertAfter(n,v):在n节点后面添加数据域为v的新节点。Remove(n):移除n节点。Destroy():销毁链表。

]

}

定义好链表ADT,下面我们就可以开始自己实现一个数据域为string类型的链表。

2、定义类

首先我们需要定义节点,其中包含两个字段一个是存放数据、一个是存放指针,代码如下。

public struct MyselfLinkedListNode
{//数据域public string Data { get; set; }//指针域public IntPtr Next { get; set; }
}

然后再定义链表实现类MyselfLinkedList,用来实现链表的相关操作。

因为我们直接管理内存,所以需要一个维护内存的指针字段;

因为我们直接获取链表长度,所以需要一个存储链表长度字段;

因此我们的MyselfLinkedList类初步是这样的:

public sealed class MyselfLinkedList : IDisposable
{//申请内存起始位置指针private IntPtr _head;//链表长度private int _length;
}

3、初始化Init

初始化结构主要做几件事。

a.分配内存空间;

b.什么头指针;

c.创建头节点;

d.维护链表长度属性;

具体实现代码如下:

//初始化链表,声明头指针,并创建头节点
public MyselfLinkedListNode Init()
{//计算节点的大小var size = Marshal.SizeOf(typeof(MyselfLinkedListNode));//分配指定字节数的内存空间_head = Marshal.AllocHGlobal(size);//创建头节点var node = new MyselfLinkedListNode{Data = null,Next = IntPtr.Zero};//将节点实例写入分配的内存Marshal.StructureToPtr(node, _head, false);//链表长度加1_length++;//返回头节点return node;
}

4、获取链表长度 Length

这个比较简单直接把链表长度私有字段返回即可。

//链表长度
public int Length
{get{return _length;}
}

5、获取头节点 HeadNode

获取头节点主要是为了方便数据处理,可以通过头指针直接读取内存地址获取。具体代码如下:

//头节点
public MyselfLinkedListNode? HeadNode
{get{if (_head == IntPtr.Zero){return null;}return GetNode(_head);}
}
//获取节点
private MyselfLinkedListNode GetNode(IntPtr pointer)
{// 从分配的内存读取实例return Marshal.PtrToStructure<MyselfLinkedListNode>(pointer);
}

同样我们也可以定义一个尾节点属性,可以方便使用,原理都差不多,这里就不赘述了。

6、在指定节点后插入节点 InsertAfter

通过前面对链表结构的了解,要想再两个节点之间加入一个新节点,只需要把两者之间的线剪断,即前一个节点的指针域需要重新指向新节点,并且新节点的指针域要指向后一个节点,其他保持不变,如下图:

业务逻辑清楚了,我们再来梳理代码逻辑,要想实现这个功能我们大致需要一下几步:

a.获取指定节点的指针;

b.创建一个新的节点;

c.重新调整指定节点及新节点指针域;

d.把指定节点和新节点指针调整后数据更新到内存中;

e.更新链表长度属性;

具体实现如下:

//在指定节点后插入新节点
public MyselfLinkedListNode InsertAfter(MyselfLinkedListNode node, string value)
{//获指定取节点对应指针var pointer = GetPointer(node);//如果指针不为空才处理if (pointer != IntPtr.Zero){//以新值创建一个节点var (newPointer, newNode) = CreateNode(value);//把新节点的下一个节点指针指向指定节点的下一个节点newNode.Next = node.Next;//把指定节点的下一个节点指针指向新节点node.Next = newPointer;//更新修改后的节点Marshal.StructureToPtr(newNode, newPointer, false);Marshal.StructureToPtr(node, pointer, false);//链表长度加1_length++;return newNode;}return default;
}
//获取节点对应指针
private IntPtr GetPointer(MyselfLinkedListNode node)
{//从头指针开始查找var currentPointer = _head;//如果当前指针为空则停止查找while (currentPointer != IntPtr.Zero){//获取当前指针对应的节点var currentNode = GetNode(currentPointer);//如果当前节点数据域和指针域与要查找的节点相同则返回当前节点指针if (currentNode.Data == node.Data && currentNode.Next == node.Next){return currentPointer;}//否则查找下一个节点currentPointer = currentNode.Next;}return IntPtr.Zero;
}
//创建节点
private (IntPtr Pointer, MyselfLinkedListNode Node) CreateNode(string value)
{//计算大小var size = Marshal.SizeOf(typeof(MyselfLinkedListNode));//分配指定字节数的内存空间var pointer = Marshal.AllocHGlobal(size);//创建实例并设置值var node = new MyselfLinkedListNode{Data = value,Next = IntPtr.Zero};//将实例写入分配的内存Marshal.StructureToPtr(node, pointer, false);//返回节点指针和节点return (pointer, node);
}

这里只实现了一个在指定节点后插入节点,我们还可以实现在指定节点前插入,在首元节点前插入,在尾节点后添加,都是可以的,感兴趣的可以自己实现试试。

7、根据数据域查找节点 Find

在链表中对查找是不友好的,因为查找一个值,需要从链表头一个一个往后查找,实现逻辑到不复杂,具体实现代码如下:

//根据数据查找节点
public MyselfLinkedListNode Find(string value)
{//从头指针开始查找var pointer = _head;//如果当前指针为空则停止查找while (pointer != IntPtr.Zero){//获取当前指针对应的节点var node = GetNode(pointer);//如果当前节点数据域和要查找值相同则返回当前节点if (node.Data == value){return node;}//否则查找下一个节点pointer = node.Next;}return default;
}

8、更新指定节点数据域 Update

这个方法逻辑也比较简单,只需要找到节点指针,然后把节点更新,最后把更新后的数据写入内存即可。

//更新节点数据
public void Update(MyselfLinkedListNode node, string value)
{//获取节点对应指针var pointer = GetPointer(node);//当指针不为空,则更新节点数据if (pointer != IntPtr.Zero){//修改数据node.Data = value;//将数据写入分配的内存,完成数据更新Marshal.StructureToPtr(node, pointer, false);}
}

9、移除指定节点 Remove

如果要想移除一个节点,则需要把指定节点与前后节点的连接删除,然后把前后两个节点建立起连接,同时需要手动释放被删除节点内存。如下图。

具体代码实现如下:

//移除节点
public void Remove(MyselfLinkedListNode node)
{//从头指针开始查找var currentPointer = _head;//获取当前节点var currentNode = GetNode(_head);//查找节点对应的指针var pointer = GetPointer(node);while (true){if (currentNode.Next == IntPtr.Zero){//指针为空则返回return;}else if (currentNode.Next == pointer){//把要删除节点的上一个节点对应的下一个节点指向要删除节点的下一个节点currentNode.Next = node.Next;//手动释放被删除节点对应的内存Marshal.FreeHGlobal(pointer);//更新要删除节点的上一个节点Marshal.StructureToPtr(currentNode, currentPointer, false);//链表长度减1_length--;break;}else{//查找下一个节点currentPointer = currentNode.Next;currentNode = GetNode(currentPointer);}}
}

10、销毁链表 Destroy

销毁链表主要是使用因为是我们自己手动管理内存,用完后要及时清理,放在内存泄漏等意外情况出现。代码也很简单,循环把每个节点内存释放即可,如下代码:

//销毁链表
public void Destroy()
{var pointer = _head;while (pointer != IntPtr.Zero){var value = GetNode(pointer);Marshal.FreeHGlobal(pointer);_length--;pointer = value.Next;}_head = IntPtr.Zero;
}

11、释放内存 Dispose

因为我们实现了IDisposable接口,所有需要实现Dispose方法,只需要在Dispose方法中调用上面销毁链表Destroy方法即可。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/70467.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Design Compiler多时钟约束

这里的资料来源于《Synopsys Timing Constraints and Optimization User Guide, Version P-2019.03-SP4, September 2019》 下面图中这几种情况都是我在实际项目中碰到过的,因此有必要单独做个说明。 第一个是同步派生时钟,即CK2是通过CK1的分频来产生的,我们之前的一个实际…

uniapp创建小程序

uniapp创建小程序https://www.dcloud.io/一、安装Hbuilder和对应基本操作 ​ 安装Hbuilder这里就不在赘述。 (一)、插件安装: ​ 如果考虑到后续需要使用Scss,可以前往插件市场进行搜索安装,浏览器会提示我们是否需要打开对应的HbuilderX,然后进入应用进行安装。(二)…

labelme使用方法

labelme是一款在实例分割、语义分割、目标检测等任务中的一个常用工具,本文将介绍如何使用labelme。 labelme有各种版本,包括ubuntu、windows、macOS等。关于windows版本,也可以下载其相关的exe文件https://github.com/wkentaro/labelme/releases来使用标注 一、安装labelme…

《综合与Design Compiler》笔记

《综合与Design Compiler》笔记 一直没系统的整理过DC这块的东西,这里借助一个挺好的文档《综合与Deisgn Compiler》以及我自己的经验和理解来归总一下。 1. 综合是什么 综合是使用软件的方法来设计硬件,然后将门级电路实现与优化的工作留给综合工具的一种设计方法。它是根据…

C# unsafe 快速复制数组

/// <summary>/// 复制内存/// </summary>/// <param name="dest">目标指针位置</param>/// <param name="src">源指针位置</param>/// <param name="count">字节长度</param>/// <returns&…

11.Java集合框架_Set接口

Set接口和常用方法 基本介绍无序(添加和取出的顺序不一致),没有索引。 不允许重复元素,所以最多包含一个null。 JDK API中Set接口的实现类有HashSet、LinkedHashSet和TreeSet。set接口常用方法 和List接口一样,set接口也是Collection的子接口,因此,常用方法和Collection…