用Python实现一个 LinkedList(双向链表)

提到 LinkedList,我相信大部分 Java 开发者是知道的。但 Pythonner 也许并不知道。

在分享之前,我先说说为什么写这篇文章。

大部分读者知道我是一名 Android 开发者,而我最熟悉的语言也正是 Java 。集合其实在 Java 是一个很重要的概念,而 LinkedList 也只是集合中的一个实现类而已。当然 LinkedList 并不是 Java 中唯一的集合,但它却是 Java 中使用双向链表的一个代表类。

很多 Java 开发者也许知道双向链表的结构以及好处什么,毕竟 JDK 里面已经提供好了 LinkedList 这个类。

这次我也借着 JDK 中 LinkedList 的实现原理,用 Python 来实现一个这样的数据结构。这篇文章也将会使你更加深入的了解这个数据结构。

Ok,正文...

至于“集合”是什么就不用细说,可以理解为一组元素的合集。

其实提到 LinkedList ,会有一个相对的集合 ArrayList。这两个都是 Collection (集合) 的实现类。当然这两个也是有区别的,ArrayList 内部是由数组实现的,而 LinkedList 则是由链表(双向)实现的。

为什么会有这种数据结构的集合?

在讨论这个之前,我先科普下这两个数据结构。

数组大家应该都知道,可以理解为一组线性、连续存储的数据结构。而链表则是由指针(指向)关系起来的一个数据结构。

比如:

用Python实现一个 LinkedList(双向链表)

双向链表的意思则是既有一个 prev(前驱指针) ,又有一个 next(后驱指针)来构成的一种数据结构。这两个则是为了构建出节点之间的关系。

再回到之前的问题,为什么存在这两种数据结构的集合?

大家可以想一下,如果我们要做 get 、set、 remove 操作那么针对 ArrayList 其实只需要花费常数时间,因为数组的索引大大提高了效率。

然而如果进行插入新值或者删除操作时,效率则会很低。因为如果插入的是中间位置、或者最前端的位置,则需要对当前操作位置后面的数据都要向后或者向前移动。

这个不难理解。

就像一个队伍,如果你想插入到第一个位置,那么从第二个开始之后的每个人都需要向后移动一个位置。

因此这时候就需要链表这样的数据结构。每个节点都存在一个前驱和后驱,我们只要修改指针指向的节点就可以完成插入或者删除操作。

再拿一个队伍举例子:

在这个队伍中每一个人都知道了自己前面是谁、后面是谁。那么当你插入到第一个位置的时候,你只需要告诉队伍的第一个人你在他前面即可,而后面的人也不需要关注自己的位置在哪,他们只需要关注自己前面和后面的人是谁。

不过这里需要注意一点,像这样的双向链表会出现最前端没有前驱指针,后端没有后驱指针。因此双向链表会在前面追加一个 头节点、后端增加一个 尾节点。也可以称之为 “标记节点”。

用Python实现一个 LinkedList(双向链表)

当我们对链表进行 get 或者 set 操作的时候,其实也是需要花费常数时间。但链表的 get 其实效率比数组的低,因为链表的缺点就是不容易做索引,它不像数组可以有索引来查找。

不过链表的查找为了提高效率,也可以做相应的优化。比如双向链表的一个特点就是两端都可以作为遍历的起点。

这样做的好处就是,如果 查找 的位置是链表靠后的位置那么就可以直接从尾部开始向前查找。

当然这点并不是链表唯一的一个优点,另一个优点则是对 remove 和 insert 的操作只需要花费常数时间。因为链表的删除和插入只需要查找到对应位置,然后修改指针即可。

所以综上,我们如果对一个集合的查找频率比较高那么最好用数组作为数据结构,但如果对插入、删除频率比较高,那么选用链表则是一个高效率的决定。

扯了这么多,其实这两种集合在 JDK 中已经有提供。

那么 Python 能否也实现这样的集合呢?答案是肯定的。

不过这篇文章只会去实现 LinkedList (双向链表)的集合,另一种 ArrayList(数组)不做实现。

在实现 LinkedList 的过程中,我会用到 ”抽象类“ 的概念。这是为了让代码结构更清晰,我将 LinkedList 对外提供的方法抽象出来,然后在子类中具体实现即可。

Ok~

编码

1、在我们开始编写 LinkedList 之前先考虑下需要定义哪些抽象方法。

首先我这边先创建一个 Collection 的抽象类,而这个抽象类中提供了如下方法:

用Python实现一个 LinkedList(双向链表)

心细的朋友可能会注意到有个 iterator 的抽象方法。这个方法将会返回一个迭代器,用于我们对 LinkedList 的迭代,后面会讲到。

不过这里我可以提一下,其实 Python 源码中有一个抽象类 Iterator ,它的抽象方法是 next 。

2、接下来,我们就可以这样来定义 LinkedList 类:

class LinkedList(Collection):

让 LinedList 继承自 Collection ,来实现我们的具体方法。

3、到这里我们的集合类已经准备好,但还缺个东西就是数据类。也可以理解为元素类,不过在链表中的元素也可以叫做节点。

用Python实现一个 LinkedList(双向链表)

如上是一个节点类,节点类中包括了当前节点的数据、当前节点的前驱和当前节点的后驱。

4、在 Collection 中有个抽象方法 iterator 。这里我们还需要定义一个类,这个类则是自定义的迭代器。我们在 iterator 的方法中返回这个实例就行。

用Python实现一个 LinkedList(双向链表)

如上,继承自 Iterator 的 子类。我们在构造器中增加了 outter 这个入参,是因为我们在 LinkedListItertor 中需要使用到 LinkedList 中的参数,因此我们在构造器中增加 outter 传入 LinkedList 的实例

5、开始实现 LinkedList

上面几步已经提供了我们编写 LinkedList 需要的基础类,那么接下来我们就可以具体来实现逻辑了。

在实现逻辑之前需要再次提一下:前面的内容有提到两个”标记节点“,一个头节点、一个尾节点。

因此我们需要在构造器中就增加这两个标记节点。

A、构造器

用Python实现一个 LinkedList(双向链表)

之所以把这段逻辑单独抽离到 doClear 中,是因为这段逻辑也是一个清空数据的逻辑。

B、addIndex(self,index,data)

接下来就实现 ”在指定位置插入数据“。在链表中我们要插入一个数据,就必须先拿到链表中当前位置的节点。然后对其前驱进行指向即可。(回忆 前面的一个队伍的例子)

所以我们需要先提供一个

getNodeWithIndex(index)

方法来获取对应位置的节点(Node)

用Python实现一个 LinkedList(双向链表)

上面这段代码可以看到,我将逻辑实现放到了

getNodeWithIndexLowerUpper(index,lower,upper) 中。

这么做的原因也是上面提到的,既然这是一个双向链表,那么我们当然可以选择性的从两端开始遍历。

即 :如果当前 index 靠后 ,则从尾部开始向前遍历。靠前的话 则从头部向后遍历。

既然 getNodeWithIndex 我们已经写好了,那么接下来就可以完善 add 方法了。首先思考下,add 到指定 index 中的含义其实是需要先拿到当前 index 的 Node,然后将需要添加的 Node 插入到这个位置即可。

用Python实现一个 LinkedList(双向链表)

如上:

首先我们创建一个前驱指向当前位置节点的前驱,后驱指向当前节点的 Node.

然后通过变换当前节点前驱的指针和当前位置的前驱就实现了插入的操作。

C、addData(self,data)

接下来我们来实现在尾部插入一个 Node。 其实就是在链表的尾部添加数据即可,就相当于在 size() 的位置插入数据。调用 addIndex(size(),data) 就行。

用Python实现一个 LinkedList(双向链表)

D、size(self)

size 方法其实就很简单了,因为我们每次 add 的时候会对 thisSize 进行自增。因此 thisSize 必然就是这个链表的长度。

用Python实现一个 LinkedList(双向链表)

E、isEmpty(self)

直接判断 thisSize 即可。

F、remove(self,index)

理解了 add 的逻辑,那么 remove 则比较简单。我们拿到需要 remove 的 Node 然后修改前驱和后驱即可。

用Python实现一个 LinkedList(双向链表)

OK~ 以上就将所有基础的抽象方法已经实现。接下来,就可以实现一个迭代器,用来迭代我们编写的 LinkedList。

H、iterator(self)

在这里我们需要自定义一个迭代器。

其实自定义一个 Python 的迭代器也是很简单的。

我们只需要继承 Iterator,然后在抽象方法 next 中将数据遍历即可。

当然迭代器作用无疑是迭代操作,因此我们需要增加中断迭代的条件。

用Python实现一个 LinkedList(双向链表)

其中 current 表示当前节点的位置(指针),outter 是外部类(LinkedList)的引用。

在构造器中我们将当前指针初始到 beginMarker 的Next 节点,也就是第一个数据。

然后重写 Iterator 的 next 方法,一直遍历到指针指向 endMarker 。也就是指针到达尾节点的时候就中断迭代。

这时候,我们只需要实现 next 这个方法即可。

用Python实现一个 LinkedList(双向链表)

返回一个自定义迭代器的实例即可,入参是当前 LinkedList 实例。

最后我们来测试一下这个 LinkedList

用Python实现一个 LinkedList(双向链表)

打印结果如下:

用Python实现一个 LinkedList(双向链表)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容