在上一节《python3实现单向链表图文详解》中我们实现了单向链表,但是单向链表受其只能从前往后进行遍历的限制,在有的时候效率并不是很高,例如需要从特定结点往前进行查询时。这一节我们就将其改进版本双向链表也来实现一下。当然单向链表的改进并不只有双向链表这一种,在以后的文章中我们再实现下单向循环链表甚至双向循环链表。
双向链表
和单向链表的结构比起来,双向链表的每个节点除了包含后继结点的地址,还包含前驱结点的地址。
这样在进行类似从某结点往前查找的时候就不用再从头遍历了,但是每个节点占用的内存就多了,属于是典型的空间换时间。
下面直接看看代码实现。
python3实现
同样和单向链表一样,先搞定结点再搞定链表类。
结点类
只是比单向链表多个一个前驱的地址,加上一个实例属性即可
class Node(object):
def __init__(self, value):
self.value = value
self.next = None
self.prev = None
这里的prev
就是前驱的地址,同样默认为None
链表类
构造函数没啥区别,同样是一个头地址即可,依然只供内部访问
属性 | 说明 | 类型 |
---|---|---|
__head | 指向第一个结点的地址 | 对象属性,仅内部使用 |
最后还是和单向链表一样实现下面这些方法
方法 | 说明 | 类型 |
---|---|---|
isEmpty | 链表长度为0返回True,否则返回False | 对象方法 |
length | 返回链表数据的长度 | 对象方法 |
travel | 遍历整个链表,依次打印每个结点的值 | 对象方法 |
append | 在链表尾端添加结点 | 对象方法 |
shift | 在链表头部添加结点 | 对象方法 |
insert | 在指定下标添加结点 | 对象方法 |
remove | 删除第一次出现的某个值 | 对象方法 |
exist | 检查某个值是否在链表中 | 对象方法 |
仔细想一想大家应该就能发现,这上面的isEmpty
,length
,travel
和exist
这四个方法的实现逻辑并没有用到前驱结点的地址,所以和单向链表的实现方法是一模一样,完全不用改。
既然是面向对象,那我们完全可以import前面实现单向链表的类,继承下来实现双向链表,这样这四个方法就不用重写了,只需要关心另外的四个即可。
下面来逐一看看剩下的四个方法。
shift
从头插入结点,如下图所示
还是秉承着先建立后断开的原则,第一步先将新结点的next
指向原先的第一个结点,然后将self.__head
指向新结点,最后将原先第一个结点的prev
也指向新结点即可。
当然还是要考虑原先是空链表的特殊情况,这时候只需要将self.__head
指向新结点即可。
def shift(self, value):
node = Node(value)
if self.isEmpty() == True:
self.__head = node
else:
# link before break
node.next = self.__head
self.__head.prev = node
self.__head = node
append
尾部插入就简单得多,同样也是游标移动到最后一个结点的时候,将原先最后节点的next
指向新结点,不过这里要多的一步就是将新结点的prev
也指向原先的最后结点。
同样也要考虑原先是空链表的特殊情况,也是直接将新结点赋值给self.__head
即可
def append(self, value):
node = Node(value)
if self.isEmpty():
self.__head = node
else:
cur = self.__head
while cur.next != None:
cur = cur.next
cur.next = node
node.prev = cur
只有最后一句代码是新加的。
insert
任意位置插入结点就稍微复杂一点,因为涉及到一共四根线的断开和连接
同样秉承先建立后断开的原则,先将新结点的prev
和next
指向前后两个结点,然后再修改原先前驱结点的next
以及后继结点的prev
,指向新结点。
值得一提的就是,在单向链表中因为要操作前驱结点,游标只能提留在前驱结点上。而在双向链表中,则没有这个限制,游标可以在后继结点上。体现在代码上就是循环的退出条件。
def insert(self, pos, value):
"""insert a node at specific position"""
if pos