python实现链表

目录

1. 链表结构

1.1 单链表和双链表

1.2 非连续性内存和节点

1.3 定义并使用单链表节点类

2. 单向链表的操作

2.1 遍历

2.2 搜索

2.3 替换

2.4 插入

2.4.1 在开始处插入

2.4.2 在末尾插入

2.4.3 在任意位置插入

2.5 删除

2.5.1 在开始处删除

2.5.2 在末尾删除

2.5.3 在任意位置删除

2.6 单链表操作的代码实现

2.7 剑指offer:从尾到头打印单链表

3. 链表的变体

3.1 循环链表

3.2 双向链表

1. 链表结构

声明:在以下内容中,术语链接、指针和引用会交叉使用,实际上他们指的是相同的。

数组之后,链表结构是最常用的数据结构。引用维基百科中的话:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表(数组)快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。

链表作为一种基础的数据结构可以用来生成其它类型的数据结构如堆栈或队列。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接("links")。如下图所示,以单向链表为例。

                                    

1.1 单链表和双链表

两种最简单的链表为单向链表和双向链表。

单向链表通过一个外部的头链接来访问第1项,单向链表的节点被分成两个部分,第一部分保存或显示关于节点的信息,第二部分储存下一节点地址。单向链表只能向一个方向遍历。单向链表中每个节点只有一个指针。单向链表的结构图如下所示:

                                             

双向链表中每个节点有两个指针,一个指向前一节点,另一个指向后一节点。最后一项没有指向下一项的指针,第一项没有指向前一项的指针。在双向链表中还有一个外部的tail指针,它允许直接访问结构中的最后一个节点。其结构图如下所示:

                           

链表无法进行随机访问,只能进行顺序访问。链表分配内存的方式和数组不同,在链表中插入或删除点时,可以直接进行插入或删除,不需要在内存中移动数据项;每一次插入和删除的过程,链表会调整大小,不需要额外的内存代价,也不需要复制数据项。

1.2 非连续性内存和节点

数组必须存储在连续的内存中,即数组中项的逻辑顺序和内存中的物理单元是紧密耦合的;而链表中项的逻辑顺序和物理单元是解耦的。链表中的内存为非连续性的。

链表中的基本单位是节点,单向链表的节点包含:数据项、指向下一节点的链接。双向链表的节点包含:数据项、指向下一节点的链接、指向上一节点的链接。

python中,任何变量都可以引用任何内容,包括None值,此处它表示空链接。通过定义包含两个字段(数据项和对下一节点的引用)的对象,从而定义了单向链表。

1.3 定义并使用单链表节点类

一个节点对象的实例变量通常不会有方法调用,并且在创建节点的时候,构造方法允许设置节点的链接。单链表节点类的实现代码如下

class Node(object):
	'''定义单链表节点类'''
	def __init__(self,data,next = None):
		'''data为数据项,next为下一节点的链接,初始化节点默认链接为None'''
		self.data = data
		self.next = next

接下来将实例化节点对象

# 定义3个节点对象分别为node1、node2和node3
node1 = None
node2 = Node(1,None)
node3 = Node('hello',node2)
print(node1,node2.data,node2,node3.next)

node1是一个没有指向的节点对象;node2的数据项为1,链接为空;node3的数据项为hello,链接指向node2。三个节点的结构示意图如下。

                                           

上面的代码中分别打印node1、node2.data、node2及node3.next,前两个的打印结果是:None和1;print(node2)打印的是node2的地址值,而node3.next同样是指向node2的链接,也是node2的地址值,所以二者是相同的。

node1.next = node3

output:
   AttributeError: 'NoneType' object has no attribute 'next'

 变量node1包含了None值,因此不能引用一个包含next字段的节点对象,如果想要将node1指向node3,可以通过以下两种方式实现:

# 实现方式1
node1 = Node('Kobe',node3)

# 实现方式2
node1 = Node('Kobe',None)
node1.next = node3

 像数组一样,链表也是用循环来处理的。可以使用循环创建一个链表结构,并访问其中的每一个节点。

链表生成结构示意图:

指针head生成了一个链表结构,它以这样一种方式操作:最近插入的项总是位于链表结构的开始处。由此经过5次for循环后,生成了如上图所示的链表结构图。

当显示数据的时候,head指针重新设置为下一节点,直到head指针变为None。 在输出数据之后,节点就从链表结构中删除了,并会在下一次垃圾回收的时候回收。

2. 单向链表的操作

几乎所有数组中的操作都是基于索引的,索引是数组中不可或缺的部分。在链表结构上,通过操作结构中的链接来模拟基于索引的操作。接下来介绍单向链表中的遍历、搜索、替换、插入及删除操作。

单链表相对于数组的主要优点不是时间性能,而是内存性能。因为链表不需要连续的内存空间。

2.1 遍历

上面说过,在输出数据后,节点会从链表结构中删除。但是很多时候我们只是想访问节点,而不想删除它们。此时,就会用到遍历。定义一个临时指针变量temp,此变量先初始化为链表的head指针,然后控制一个循环。

遍历链表的流程框图如下:

           

遍历结束后,temp指针为None,而head指针还是指向第一个节点。遍历在时间上是线性的,并且不需要额外的内存。

遍历单链表结构会访问每一个节点,并且当遇到一个空链接的时候终止。而None就是充当负责停止这个过程的哨兵。

2.2 搜索

数组的文章中说过,数组的访问方式是随机访问,可以直接获取第i项的位置,它的时间复杂度为O(1),而链表的访问方式是顺序访问,时间复杂度是线性的,所以链表的搜索和遍历是类似的。那么在搜索过程中充当哨兵角色的有两种:空链接或等于目标项的一个数据项。

2.3 替换

单链表的替换操作和搜索操作极为类似,只是替换操作比搜索多一步:将搜索到的项替换为新的值。同样的包括两种形式:用newItem替换某个值和用newItem替换某个索引处对应的数据项。替换的时间复杂度也是线性的。 

 

2.4 插入

链表中的插入操作在以下3个方面进行说明:在开始处插入、在末尾插入及在任意位置插入

2.4.1 在开始处插入

前面看到链表的遍历、搜索即替换操作的时间复杂度都是线性的,那么链表中是否存在由于线性操作的操作。答案是肯定的。在开始处插入数据的操作的时间和空间都是常数。在数组文章中说过,数组中中插入元素需要复制并移动数据。而在链表中是不需要做这样的操作的。实际上链表在开始处插入数据的操作很简单,我们在前面也有说到。

以上两种情况都可以在链表的开始处插入数据 

2.4.2 在末尾插入

在数组的末尾插入元素(Python列表的append()方法)需要的时间和空间复杂度都是常数,对于单链表来说在末尾插入元素有以下两种情况:

head指针为None,此时,将head指针设置为新的节点。

head指针不为None,搜索到最后一节点,并将next指向新的节点。

在3节点链表的末尾插入元素的实现框图如下: 

 

         

2.4.3 在任意位置插入

在数组的第i个位置插入元素需要将位置i到位置len(arr) - 1的元素都往下移动。实际上插入元素是在第i-1个位置与第i个位置之间。对于一个空数组来说,直接把要插入的元素放到开头处;对于一个索引大于n-1的数组来说,直接把元素插入到末尾即可。

那么链表中在第i个位置插入元素也要如此操作,首先要确定i的取值范围,如果i <= 0,那么就是在链表开头处插入元素,如果链表为空链表,那么也是在链表的开头插入元素;如果0<i<(len(linkedList) - 1),那么搜索第i-1位置处的节点,在此处插入新节点;如果i>=(len(linkedList)-1),那么在链表的末尾插入元素。具体的代码实现如下:

向节点数据项为10,8,6链表的索引2处添加元素100的框图如下:

         

2.5 删除

链表中的删除操作在以下3个方面进行说明:在开始处删除、在末尾删除及在任意位置删除

2.5.1 在开始处删除

数组中删除元素也需要进行复制数据和移动数据的操作,而在链表中是不需要这种操作。在开始处删除元素的时间和空间复杂度都是常数。其形式为:

while head ! = None:           # 要进行删除操作首先保证链表不为空
    removeItem = head.date     
    head = head.next           # 实际上.next操作就实现了删除的功能
    return removeItem          # 返回删除项

删除第一个节点实现框图如下:

           

2.5.2 在末尾删除

在数组的末尾删除元素(Python列表的pop()方法)需要的时间和空间复杂度都是常数,对于单链表来说在末尾删除元素有以下两种情况:

只有一个节点,head指针设置为None

在最后一个节点之后没有节点。搜索倒数第2个节点并将其next指针设置为None

在有三个节点的链表中删除末尾元素的过程框图

2.5.3 在任意位置删除

在任意位置删除元素和在任意位置添加元素类似,也是需要考虑任意位置index的大小,若index <= 0, 那么就是在链表开头删除元素;若0<index<(len(linkedList)- 1),那么需要搜索索引index - 1处的节点,并删除其后面的节点;若index>= len(linkedList - 1),那么就是删除链表的最后一个节点。

删除节点数据项为10,8,6,4链表的索引2处的元素框图如下:

2.6 单链表操作的代码实现

此部分代码包含了单链表节点类的定义,单链表的遍历,单链表的搜索,单链表的替换即单链表的插入和删除操作,具体代码实现如下:

2.7 剑指offer:从尾到头打印单链表

3. 链表的变体

此小节介绍链表的两种变体,它们带有额外的指针,能够提高性能并简化代码

3.1 循环链表

循环链表包含了从结构的最后一个节点返回到第一个节点的链接。在循环链表中至少总是有一个节点,这个节点称为哑头节点,它不包含数据,只是充当了链表的开头和结尾的一个标记。在空链表中,head指向了哑头节点,且哑头节点的next指针指回到了哑头节点自身。如下图所示。

                                       

第一个包含了数据的节点,位于哑头节点之后。此节点的next指针以循环的方式指回到哑头节点。如下图所示。

                                                

前面说过,如果在做任意位置插入或删除操作的话,需要考虑索引index的取值,分为三种情况;使用循环链表的优点就在于,做插入和删除操作的时候,只需要考虑0<index<(len(linkedlist) -1)的情况即可。因为当index为0时,它的前一个节点就是head节点,当index >= len(linkedist) -1的时候,其前一个节点就是最后一个节点,且其下一个节点就是head节点。代码实现如下:

3.2 双向链表

双向链表相比于单向链表多了以下功能:从给定节点,向左移动到前一个节点和直接移动到最后一个节点。包含3个节点的双向链表如下图所示:

                       

可以看到,每个节点有两个链接,分别称为:next和previous。而且还多了一个名为tail的指针,它可以直接访问双链表的最后一个节点。

通过在末尾添加想创建并反向遍历双向链表的代码实现如下:

在有3个节点的双向链表末尾插入新节点的框图如下 :

  • 63
    点赞
  • 323
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值