Python 链表实现

Python内部实现的列表是通过数组来实现的。数组实现有一个缺点,就是插入和删除元素的效率问题。由于数组是作为一个连续的内存块来维护的,因此插入的新元素如果需要位于数组的中间,那么就需要将原始内存里的值都向后移动,从而为新元素腾出空间。同样,执行删除操作也会以类似的方法产生一样的情况。因此,这个问题最基本的原因是:序列的顺序是通过在内存中使用有序的内存地址序列来维持的

但这不是维护一个序列数据唯一可行的方法。除了用它在内存中的位置来隐式地维护元素的序列信息,我们也可以显式地表示顺序。换句话说,我们可以将序列中的元素分散到内存中的任何位置,并且让每一个元素都去“记住”序列中的下一个元素所在的位置。这个方法会导致链式(linked)序列的产生。举一个具体的例子,假设我们有一个序列的数字,这个序列被称为myNums。
图4.5所示为序列的连续和链式实现。

在这里插入图片描述
可以看到,序列的链式版本并没有使用单个连续的内存部分。恰恰相反,我们创建了许多对象[通常称为节点(node)],每个对象都包含了对数据值的引用并指向列表中下一个元素的指针/引用。通过这种显式的引用,每一个节点都可以被存储在内存中的任何位置。

基于myNums的链式实现,我们可以执行那些基于数组的版本相同的所有操作。比如,要输出序列里的所有元素,我们可以使用下面这个算法:
在这里插入图片描述
要实现这个算法,需要知道节点的具体存储方式。这个存储方式应该包括获取节点中的两部分信息(数据和指向下一元素的链接)的方法,以及某种能够知道是否已经到达序列的末尾的方式。我们可以通过多种方式来做到这一点。

而可行的最直接的方式就是,创建一个简单的链表节点(ListNode)类来完成这项工作:

class ListNode(object):
    def __init__(self,item=None,link=None):
        self.item=item
        self.link=link

链表节点(ListNode)对象具有两个变量:

item——用来存储和节点相关联的数据的实例变量;
link——用来存储序列中的下一个元素的实例变量。

由于Python支持动态类型,所以item实例变量可以是对任何数据类型的引用。因此,就像你可以在内置Python列表中存储任何数据类型或混合数据类型一样,我们的链式实现也可以完成相应的功能。这个时候,我们还剩下了这样一个问题——如何处理link字段来表明已经到了序列的末尾。在Python里的特殊对象None,通常会被用来处理这个问题。

现在,让我们用一用链表节点(ListNode)类。下面的代码里,我们创建了包含3个元素的链式序列:

n3=ListNode(3)
n2=ListNode(2,n3)
n1=ListNode(1,n2)

如果我们追踪这段代码的执行结果,就能够得到图4.6所示的情况。

  • 在这个图里,每个双框图案都代表着一个链表节点(ListNode)对象,这个对象会有相应的数据元素和一个指向下一个链表节点(ListNode)对象的链接。

需要注意的一点是,为了简化这个图例,我们在链表节点(ListNode)的第一个框内直接显示了数字(不可变),而不是从链表节点(ListNode)的item部分(第一个框)绘制一个向数字对象的引用。

  • n2n1.link都是对包含数据值2的同一个链表节点(ListNode)对象的引用;
  • n3n2.link都是对包含数据值3的同一对象的引用。
  • 当然,我们还可以通过n1.link.link来访问包含数据值3的链表节点(ListNode)对象,以及通过n1.link.link.item来得到相应的数据值。

一般来说,我们不会用这样的方式去编写代码,但它展示了如何在链式结构里通过开头的元素找到它之后的每一个对象和数据值。

  • 通常,我们只会去存储对第一个链表节点(ListNode)对象的引用,然后通过第一个元素里的链接来访问列表中的其他元素。
    在这里插入图片描述
    假设我们要将值2.5插入到这个序列里,同时还要保持整个序列有序。下面的代码就能够完成这项操作:
n25=ListNode(2.5,n2.link)
n2.link=n25

图4.7显示了这段代码的执行过程:语句n25=ListNode(2.5,n2.link)

分配了一个新的链表节点(ListNode)并调用它的__init__方法。__init__的第一行——self.item=item在这个链表节点(ListNode)里设置了对2.5的引用。
在这里插入图片描述

  • 下一行的self.link=link则存储这个链式参数的引用,这个参数是链表节点(ListNode)n3对象。
    在这里插入图片描述

  • 在__init__方法执行完以后,语句n2.link=n25设置了链表节点(ListNode)n2对象的link实例变量,因此它之后会引用这个新创建的叫作n25的链表节点(ListNode)对象。

  • 在整个过程中,我们并没有对链表节点(ListNode)n1对象里的任何引用进行更改。可以看到,在链式结构中插入一个节点,只需要对需要插入节点之前的那个节点的链接进行更新就可以了

  • 由于插入新数据到链式结构里并不需要移动任何的现有数据,因此可以非常高效地完成这个操作。
    在这里插入图片描述

在这段代码里,需要注意的一点是,更新链接的顺序非常重要。如果我们把这一段代码改成下面这样来插入2.5,它将不能正常完成操作:

n25=ListNode(2.5)
n2.link=n25
n25.link=n2.link
  • 在这种情况下,语句n2.link=n25会导致对包含3的链表节点(ListNode)的引用被覆盖。
  • 这个链表节点(ListNode)的引用计数将会减少1,而这个时候如果没有其他的引用,这个链表节点(ListNode)将会被释放。
  • 这之后,语句n25.link=n2.link会把链表节点(ListNode)n25里的link实例变量设置为链表节点(ListNode)n25。这样的操作破坏了我们链式结构中的连接——因为它不再包含数据值为3的链表节点(ListNode)。不仅如此,它还会在我们的链式结构里产生一个循环。在这个时候,如果我们编写一个从链表节点(ListNode)n1开始,并依照link实例变量来一个一个往下走的循环,这个循环会在遇到值为None的链接的时候退出。这个循环将会是一个无限循环,因为链表节点(ListNode)n25的链接(link)指向的是链表节点(ListNode)n25它自己。我们的程序将会持续不断地执行下去,这也就是为什么使用链式结构进行编程可能会变得非常麻烦。而可以确保你的操作不出差错的最好方法是:一步一步地跟随你的代码并绘制出相应的图来描述它。

现在,让我们考虑一下从序列中删除一个元素会发生什么。要删除数字2的话,我们需要更新包含1的链表节点(ListNode)对象的链接(link)字段,从而让它“跳过”节点2。代码n1.link=n25就能够完成这项操作。而这就是全部所需要的代码了。所以,从序列中删除一个元素会比插入一个元素更容易。而且,如果没有其他对已删除节点的引用,通常会自动释放这个节点所占用的内存。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值