算法与数据结构(一) 线性表的顺序存储与链式存储(Swift版)

温故而知新,在接下来的几篇博客中,将会系统的对数据结构的相关内容进行回顾并总结。数据结构乃编程的基础呢,还是要不时拿出来翻一翻回顾一下。当然数据结构相关博客中我们以Swift语言来实现。因为Swift语言是面向对象语言,所以在相关示例实现的时候与之前在大学学数据结构时C语言的实现有些出入,不过数据结构还是要注重思想,至于实现语言是面向对象的还是面向过程的影响不大。

接触过数据结构的小伙伴应该都知道程序 = 数据结构 + 算法。数据结构乃组织组织数据的结构,算法就是对这些结构中的数据进行操作,可见数据结构的重要性,就连算法也是依赖于数据结构的。

在博客的开头,我们先简单的聊些数据结构整体的东西。数据结构整体可以分为物理结构和逻辑结构,物理结构指的是数据在磁盘、内存等硬件上的存储结构,主要包括顺序结构和链式结构。而逻辑结构是数据本身所形成的结构,包括集合结构、线性结构、树形结构以及图形结构。针对不同的数据结构我们可以依据算法解决不同的问题,如我们在以后博客中要介绍的最小生成树,就是根据算法将带权的连通图转换成最小生成树,在转换这个过程中我们就用到了Prim算法和克鲁斯卡尔算法。当然各种排序算法,最短路径等等也是算法与数据结构的结晶体。

一、线性表综述

本篇博客我们主要介绍的是逻辑结构中的线性表,也就是线性结构。线性结构的特点就好比一串珠子,其特点是第一个节点只有一个后继,没有前驱,最后一个节点是只有一个前驱,没有后继。而其余的节点只有一个前驱和一个后继。说吧了线性表就是一串。下方这个图就是线性表的示例图。中间蓝色的节点前方的是就是改点对应的前驱,后边就是改点对应的后继。从下方可以明确看出head没有前驱只有后继,而tail只有前驱没有后继。

上面这个是线性表的逻辑结构,接下来我们来聊一下线性表的物理结构,也就是存储结构。线性表的物理结构可分为顺序存储结构和链式存储结构。顺序存储结构之所以称之为顺序存储结构因为每个线性表中节点的内存地址是连续的,而链式存储结构中线性表的节点的内存地址可以是不连续的。这也就是在C语言实现顺序存储线性表时先Malloc一块连续的区域,然后用来顺序的存储线性表。而链表中就可以不是连续的了,前驱与后继间的关系由指针连接。

下方这个指示图中,上面这个就是链式存储,下方这个就是顺序存储。可见链式存储是有指针域的,也就是前驱和后继的关系有指针来链接。下方这个链式存储就是单向链表,因为只有前驱到后继的指针,而没有后继到前驱的指针。关于双向链表下方会具体给出详细的说明。而下面第二个图就是顺序存储,前驱与后继的关系是由紧挨的内存地址所关联。

上面这两种存储方式我们可以换另一种更为形象的方式来进行说明,如下所示。顺序存储的内存区块的内存地址是紧挨的,线性关系有相邻的内存地址所保存。当然下方我们是模拟的内存地址,就使用了0x1, 0x2等等来模拟。而链式存储的线性表,在物理存储结构中是不相邻的,仅仅靠内存地址无法去维持这种线性关系。所以就需要一个指针域来指向后继或者前驱节点的内存地址,从而将节点之间进行关联。在单向链式存储中,一个节点不仅仅需要存储数据,而且还要存储该节点下一个节点的内存地址,以便保持这种线性关系。具体请看下图。

原理性质的东西先就聊这么多,下面我们会给出具体实现。主要包括线性表的顺序存储及其操作,以及线性表的单链以及双链存储及其操作。下方的实例依然采用Swift面向对象语言实现,思想理解后,用什么语言都是可以的呢。

二、线性表的顺序存储

关于线性表的顺序存储,我们就使用NSMutableArray来实现,也就是使用OC中的可变数组类型来实现。我们就假设其存储的内存地址是连续的,当然数组中存储对象时要复杂的多,我们暂且假设其中的地址是连续的。下方的list就是我们的顺序线性表,count存储的是已经存入到线性表中的元素个数,而capacity则是记录线性表整个容量的大小。当然上述三个属性都是private的,而下方的计算属性length是internal类型的,供外界访问,返回线性表元素的个数。

下方是整体结构,我们下方会给出线性表具体的关键操作,并分析其时间复杂度。

1.往顺序线性表中插入数据

有时候我们会给据特定的算法往线性表中指定的位置插入数据,比如我们常见的插入排序算法,如果你的数据是顺序存储的话,那么就需要将数据插入到顺序表中。接下来我们就将数据插入到指定的位置。

下方该图中是往顺序表中插入一个元素的原理图。在下图中,我们往AB与CD之间插入一个M。在插入M之前我们需要做的事情就是将CD整体往后移动一个位置,为M腾出一个位置,然后再讲M这个元素进行插入。

顺序表的插入还是比较简单的,也是非常好理解的,那么用代码实现起来也是用不了几行代码的。下方截图就是是顺序线性表的元素插入的具体实现的代码。首先检查index的合法性,检查index是否在合理范围内,然后将index后的元素依次往后移动,然后将元素插入即可。

2. 顺序线性表的元素移除

上面介绍完元素的插入后,接下来要聊一下元素的移除。也就是移除指定索引中的元素。该过程恰好与上述插入的过程相反,上述在插入之前是相应的元素往后移,腾出index位置。而移除特定索引的元素时,是相应的元素左移,覆盖掉要删除的元素,然后将最后一个元素进行移除掉。下方的原理图对此过程进行了说明。

该部分比较简单,下方的代码段就是将指定索引的元素进行移除。在线性表的顺序存储中,前驱和后继的关系由内存地址的先后顺序所关联,所以插入和删除元素会相对麻烦一些,而查找和修改元素就比较简单了,直接由index可以找到相应的元素,再次就不做过多赘述了。github上所分享的Demo中会有完整示例。

三、线性表的单链存储

介绍完线性表的顺序存储,接下来我们来介绍线性表的链式存储。当然,本部分是对单向链表的介绍,下部分将会对双向链表的介绍。下方截图就是我们单向链表相关示例的运行结果,首先我们先正向的创建链表,也就是后来的元素插入到链表的后方。然后在给出逆向创建链表,与正向创建链表相反,后来的元素掺入到头结点的后方。创建链表完毕后,我们会给出链表元素的插入和移除的解决方案。

1.单向链表的创建

在链表创建之前,我们得先创建节点的类,因为链表是多个节点的连接。下方这个OneDirectionLinkListNote类就是单向链表的节点类。其中的data属性存储的是该节点所存储的数据,而变量next就是指向下一个节点的指针,链表中节点间的关系由next指针所关联。init和deinit就是该类的构造和析构函数了,就不做过多赘述了。

2.链表协议的创建

在创建链表之前,我们可以先创建一个链表协议ListProtocalType。在ListProtocalType协议中定义了链表所必须的方法,无论是单向链表还是双向链表都要遵循该协议。其实这就是“面向接口”的提现。单向链表与双向链表都遵循了这协议,那么说明这两种链表对外的接口是一致的,这样便于程序的维护,这也是面向接口编程的好处。下方就是我们事先定义好的ListProtocalType协议的部分内容。

下方协议中只给出了方法的定义,未给出具体实现。所有链表都要遵循该协议,ListProtocalType中定义了链表结构所必须的方法。可以说下方这个协议就是链表的大纲。

3.单向链表的构建

下方就是我们单向链表类的属性和构造函数。headNote就是我们链表的头结点,而tailNote是我们链表的尾结点。length就是我们链表的长度,也就是我们链表中元素的个数。一个空链表中tailNote = headNote。

下方我们将会介绍链表的正向创建和逆向创建。 下方这个截图中就是正向创建链表,其实就是将新创建的数据往链表的尾部插入,然后更新一下tail的位置即可。关键步骤就两步,第一步是tail->next = newNode,第二步是tail = newNode。插入步骤如下所示:

上面这个示意图是往链表的后方添加元素,接下来是王链表的头部插入元素。该过程也是由关键的两步组成,第一步就是newNode->next = head->next,第二步是head->next = newNode。经过这两步我们就可以把元素插入到头结点的后方了。

下方这段代码是正向创建链表的具体代码,就是一直往链表的尾部插入元素。逆向创建链表的代码就不往上粘贴了,与下方代码类似,github上会进行完整实例的分享。上面虽然是往头结点和尾部结点的插入,但是原理是适用于往两边中间插入元素的,在此就不做过多赘述了。

4.单向链表元素的移除

上面我们聊完元素的插入,解析来我们要聊一下元素的移除。要想移除单向链表中的一个元素,首先我们得找到被移除结点的前驱的位置,比如是pre。当前移除的元素是remove,让我我们让pre->next = remove->next, 然后再执行remove->next = nil。经过上面这些步骤,remove这个结点就与链表脱离关系了。示意图如下所示。

根据上述的示意图,我们就可以给出具体的代码实现了。单向链表的核心操作就介绍完了,其他更多细节请参考在github上分享的源代码,因为篇幅有限,关于单向链表的更多细节就不做过多赘述了。

四、双向链表

如果你对单向链表已经理解的话,那么理解双向链表来说并非难事。双向链表与单向链表相比多了一个指向前驱的节点。我们暂且称为将指向前驱的节点命名我pre指针。下方这个示意图就是双向链表的示意图,与单向链表相比,多了一个指向前驱的指针域。如下所示。接下来将会给出双向链表的插入和移除。

1.双向链表元素的插入

双向链表的插入要比单向链表的插入要复杂一些,不过也是蛮好理解的。下方示意图中就是往节点A后方插入一个节点D。主要分为四个步骤,第一步是将D节点的next指针指向A节点next指针指向的节点,也就是D->next = A->next。第二步是讲D节点的pre指针指向A节点,也就是D->pre = A。第三步是将A的next指针指向D,也就是A->next = D。最后将D节点的下一个节点的pre指针指向D,也就是D->next->pre = D。经过这几步,我们就可以将节点D插入到A与B的中间。当然这个顺序不是一定的,只要能保证链的正确关联即可。

下方是上述元素插入的核心代码,如下所示。主要将newItem节点,插入到cursor节点后方。

2.双向链表元素的删除

双向链表因为比单向链表多一个前驱指针域,所以元素的删除要麻烦一下,不过还是比较好理解的。下方这个截图就是删除B节点的示意图。首先将B节点前驱节点的next指针域指向B节点的后继,也就是B->pre->next = B->next。 然后将B节点的后继节点的前驱指针指向B的前驱节点,对应着B->next-pre = B->pre。最后将B的next和pre指针域置为nil。如下所示:

下方代码段就是双向链表移除节点的具体实现,如下所示。至于链表的遍历等其他操作在此就不做过多的赘述了,具体内容请看github上分享的源代码。

五、面向接口编程的优点

在上述我们实现两种链表时,我们先定义了一个链表协议ListProtocalType。无论是双向链表还是单向链表都遵循这个协议,也就是说,该协议就是链表对外统一的接口,该协议就是操作链表的一个规范。下方的testLinkedList()就是我们链表的测试方法,该函数的参数是遵循ListProtocalType协议的所有类的对象。也就是说只要遵循了ListProtocalType这个协议的类的对象,都可以作为该函数的参数。至于具体操作,那么不同的类会给出不同的操作的。

在调用该函数时,第一个传入的是单向链表的类的对象,第二个是双向链表的类的对象。虽然都是执行同一个方法,但是因为传入的类的对象不同,所以执行的结果显然是不同的。这也就是利用了面向对象的多态性,在之前设计模式系列的博客中介绍过,下方这种与策略模式类似。

本篇博客的内容也是挺多的了,当然博客中的内容是从Demo中挑出的关键点来讲的,具体细节请看下方github上所分享的链接:

https://github.com/lizelu/DataStruct-Swift/tree/master/ListDataStruct

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微信公众号:Java团长

对HashMap的思考及手写实现

HashMap是Java中常用的集合,而且HashMap的一些思想,对于我们平时解决业务上的一些问题,在思路上有帮助,基于此,本篇博客将分析HashMap底层设...

9310
来自专栏LanceToBigData

Java集合源码分析(三)Vevtor和Stack

前言   前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的。这一篇讲的可能大家在开发中很少去用到。但是有的时候也...

25960
来自专栏皮皮之路

【JDK1.8】JDK1.8集合源码阅读——Set汇总

322120
来自专栏小灰灰

Java容器篇小结之List自问自答

I. List篇 0. 什么是List 看到这个有点懵逼,一时还真不知道怎么解释,能让完全没有接触过的人都能听懂 列表,什么是列表呢? 好比你到了一个村里,看...

22480
来自专栏皮皮之路

【JDK1.8】JDK1.8集合源码阅读——Set汇总

11730
来自专栏Java进阶架构师

对HashMap的思考及手写实现

HashMap是Java中常用的集合,而且HashMap的一些思想,对于我们平时解决业务上的一些问题,在思路上有帮助,基于此,本篇博客将分析HashMap底层设...

10820
来自专栏Java帮帮-微信公众号-技术文章全总结

【Java提高十九】Iterator&fail-fast机制

【Java提高十九】Iterator&fail-fast机制 Iterator详解 迭代对于我们搞Java的来说绝对不陌生。我们常常使用JDK提供的迭代接口进行...

411110
来自专栏xingoo, 一个梦想做发明家的程序员

20120918-向量实现《数据结构与算法分析》

#include <iostream> #include <list> #include <string> #include <vector> #include...

22760
来自专栏Spark学习技巧

JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介

17930
来自专栏codingforever

经典算法巡礼(七) -- 排序之堆排序

很多时候,我们需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序。比如你可能启动了若干个定时器,那么下一次处理定时器事件只需要考虑距离现...

7620

扫码关注云+社区

领取腾讯云代金券