【算法与数据结构】:链式表中单链表的实现

177 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

1、写在前面

大家好,我是翼同学。今天文章的内容是:

  • 单链表

2、内容

2.1、链式表的介绍

(1) 简介

链表的含义:

链式表,就是以链式结构存储的线性表,简称链表。

在线性表中,顺序存储结构的最大缺点就是插入删除运算时需要通过移动大量数据元素,效率较低,需要耗费时间较多,而链式表则克服了顺序表的缺点,链表中的结点是在运行时动态申请和释放的,也就是说,链表在插入元素时会申请新的存储空间,而在删除元素时会释放其占有的内存空间。

所以,链表可以充分的利用计算机的内存空间,实现灵活的动态内存管理,而不像顺序表一样需要预先确定表长。

(2) 结点

链表是有一个概念叫结点。在链表中,每个结点都包含两部分内容,即数据域和指针域。

其中:

  • 数据域:用于存储每个数据元素的信息;
  • 指针域:用于存储数据原数之间的链接关系;

每个结点在运行时会动态生成,然后存放在一个独立的存储单元中。而结点间的逻辑关系则由指针域中的指针来给出。

(3) 分类

链表的常见分类有三种,分别是:

  1. 单链表:每个结点都只有一个地址域;
  2. 双链表:每个结点都有两个地址域;
  3. 循环链表:首尾相连的链表。

如图所示:

image.png

(4) 小结

在C++中,我们通过设置结构体来创建结点概念,其中结构体的设置可以开辟内存空间作为指针,进而利用指针来指向下一个结点,每个结点都可以通过指针域来相互联系。因此我们说链表是自适应内存大小的。

接下来我们来看看链式表中单链表的简单实现。

2.2、什么是单链表?

  • 结点

单链表是一种链式存取的数据结构。

单链表中的数据元素是以结点来表示的,每个结点都由数据域和指针域构成:

image.png

  • 数据域:数据元素本身的信息;
  • 指针域:包含一个指针,用于指向后继元素存储的位置

也就是说,对于单链表中的每一个数据元素aia_i,除了存放数据元素本身,还需要存放其后继元素ai+1a_{i+1}所在的存储单元地址,这两部分构成了一个结点。

我们将单链表中第一个结点成为首元结点

因此单链表的每个操作都必须从第一个结点开始,根据指针域中的指向的方向找到第二个结点,再从第二个结点出发找到第三个结点,以此类推。直到最后一个结点,其地址域为空,则表明到达了表尾。看到这我们也能发现,对于单链表的定位运算来说,时间复杂度是O(n)O(n),这要比顺序表慢。但是对于单链表的插入操作和删除操作来说,则比顺序表方便许多。


  • 头指针

在单链表的设计中,我们需要定义一个指针变量,用于存放单链表第一个结点的地址,该指针被称为头指针。

另外,头指针的另一个作用就是可以标识一个单链表。比如一个单链表m_pHead既可以表示单链表第一个结点的地址存储在指针变量m_pHead中,又可以表示单链表的名字是m_pHead.

当单链表为空时,头指针的值为NULL,用于表示空表。


  • 头结点

为了消除“空表”和“非空表”的操作差异,我们会引入头结点,使得运算更加方便,更加统一。

为什么这么说呢?

举一个插入运算的例子:

image.png

可以看到,当我们想要在aia_iai+1a_{i+1}之间插入一个元素axa_x,就必须申请一个空间作为新结点,然后存放数据元素axa_x,需要注意,此时应修改aia_i的指针域,使其指向axa_x,并且将新结点的指针域指向ai+1a_{i+1}。这就是单链表的插入运算。

但如果,我们在单链表首结点的前面插入一个新的结点,使其成为新的首元结点,此时的插入算法则是这样:

image.png

这就修改了头指针,和一般插入运算区分开了。

为了统一运算,使得操作一致,我们通常会引入头结点的概念。

头结点,就是在整个单链表的第一个结点的前面加一个结点,该结点的数据域可以不用存储任何信息,其指针域则指向首元结点。

有了头结点的好处是消除插入算法和删除算法的差异,使得在首结点处的处理方式和一般情况相同。

2.3、单链表的简单实现

(1) 类型定义

首先是单链表类型的定义,如下所示:

carbon (15).png

这样,一个单链表类型的类就创建好了,来看看示意图:

image.png


(2) 构造函数

对于单链表的构造函数,就是创建一个带头结点的空链表。申请一个新的结点作为头结点,将指针域设置为空,而数据域则不用设置,具体如下:

image.png


(3) 析构函数

当局部变量所在的函数调用完毕时,系统会自动执行析构函数来释放单链表的空间。

析构函数如下所示:

image.png


(4) 清空单链表

clear()函数用于清空单链表,主要操作是引入一个工作指针,并利用该工作指针来完成单链表的遍历操作,使其从表中头结点一直移动到表尾,边移动边释放结点。

image.png


(5) 求表长

如果类中没有设置变量length用于存储表长,则需要利用一个工作指针以及计数器来遍历单链表,最后得到表长。

方法如下:

image.png


(6) 遍历单链表

遍历单链表就是从头结点开始,从头到尾访问单链表中的每一个结点的数据域,并输出。

如下所示:

image.png


(7) 查找位序为 i 的结点的地址

设定一个函数getPosition()用于查找对应位序处的结点地址。当i-1表示查找头结点,i0表示查找首元结点,如果i的值为非法的,则返回NULL,否则返回位序为i的结点的地址。

image.png


(8) 查找值为value的结点的位序

定义一个函数search(),用于查找值为value的数据元素,并返回其第一次出现的位置。从单链表的第一个结点开始,判断当前结点的数据域中的值是否等于查找值value,如果是则返回结点位序i,否则继续查找。如果查到表尾仍没有找到,则返回-1表示查找失败。

image.png


(9)查找值为value的结点的前驱的位序

定义一个函数prior用于查找值为value的结点的前驱的位序,如果找到值为value的结点,且该结点不是首元结点,则返回其前驱的位序i.

image.png


(10) 插入结点

定义一个函数insert()用于在位序为i处插入值为value的新结点。由于单链表中的结点只有一个指向后继的指针.

image.png


(11) 删除结点

当我们要删除位序为i的结点,应该注意删除的范围。

对于有length个结点的单链表,合法的删除范围应是:[0,length1][0, length-1]

其中0代表删除首元结点,length-1表示删除尾结点。

因此,代码如下:

image.png


(12) 逆置单链表

我们利用头插法来逆置单链表元素。每次插入的新结点都是插在头结点之后,首元结点之前,也就是说,从空表开始,每读入一个数据元素就会插在单链表的头部,读入的顺序和单链表中的逻辑顺序是相反的。

由于每访问一个结点,就将它插在头结点的后面,然后向后移动工作指针p,直到所有结点都重新插入到单链表中,这就实现了单链表的逆置操作。

image.png

2.4、总结

单链表的特点:

  1. 单链表不要求使用连续地址的存储空间来存储元素,并且每个结点都在运行中动态生成。
  2. 单链表的插入操作和删除操作都不需要移动结点,只需要修改指针。
  3. 单链表满足经常插入和删除结点的需求,但空间开销较大。
  4. 单链表不具备顺序表随机存储的特点。

3、写在最后

好了,今天文章的内容就到这里。