在上一节《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 <= 0:
self.shift(value)
elif pos > (self.length() - 1):
self.append(value)
else:
node = Node(value)
cur = self.__head
count = 0
while count < pos:
cur = cur.next
count += 1
node.next = cur
node.prev = cur.prev
cur.prev.next = node
cur.prev = node
注意这里的循环退出从pos-1
改为了pos
使得游标停留在后继结点上。
同时最后这四句代码,如果换一种顺序也可以写作
node.next = cur
node.prev = cur.prev
cur.prev = node
node.prev.next = node
因为先修改了后继结点的prev
指向,所以只能通过新结点去找到前驱结点。方法不止一种,主要是理解思路。
remove
删除一个结点
比之前的单向链表要多删除一个后继结点的prev
引用即可。
补充一下python的内存清理机制,是某内存的值被别的变量引用的个数减为0的时候就被清理。所以当被删除的结点被前驱结点的next和后继结点的prev引用都删除之后,其引用个数减为0,地址就会被回收。而它的pre和next无论指向的是不是None都无所谓
同时因为涉及到删除后继结点的prev,当后继结点为None的时候也要考虑进去,也就是只有一个结点的时候
def remove(self, value):
cur = self.__head
# remove the first node
if cur.value == value:
if cur.next is None:
self.__head = None
else:
self.__head = cur.next
cur.next.prev = None
return value
elif self.__head is None:
return
else:
while cur.next != None:
if cur.value == value:
cur.next.prev = cur.prev
cur.prev.next = cur.next
return value
else:
cur = cur.next
if cur.value == value:
cur.prev.next = None
return value
return
好,还是和单向链表一样的代码去验证
if __name__ == "__main__":
dll = DoubleleLinkedList()
print(dll.exist(10)) # False
print(dll.length()) # 0
print(dll.isEmpty()) # True
dll.append(1)
print(dll.length()) # 1
print(dll.isEmpty()) # False
dll.append(2)
dll.append(3)
dll.append(4)
dll.append(5)
dll.append(6)
dll.shift(8) # 8123456
dll.insert(2, 9) # 81923456
dll.insert(-2, 10) # 10 81923456
dll.insert(100, 11) # 10 81923456 11
dll.travel() # 10 8 1 9 2 3 4 5 6 11
print(dll.length()) # 10
print(dll.isEmpty()) # False
print(dll.exist(100)) # False
print(dll.exist(6)) # True
dll.remove(6)
dll.remove(10)
dll.remove(11)
print(dll.exist(6)) # False
dll.travel() # 8 1 9 2 3 4 5
最后的结果也是没问题
False
0
True
1
False
10 8 1 9 2 3 4 5 6 11
10
False
False
True
False
8 1 9 2 3 4 5
和单向链表对比
目前似乎还看不太出来和单向链表对比时间复杂度上的提升,这是因为我们的链表类只保留了__head
这一个头部地址。如果再加一个__tail
保留尾部地址的话,当进行从后往前遍历的时候就容易多了,例如进行类似dll[-1]
的操作。当然这也同样是用空间换时间了。
完整代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time: 2020-Jul-09
# @Author: xiaofu
class Node(object):
def __init__(self, value):
self.value = value
self.next = None
self.prev = None
class DoubleLinkedList(object):
def __init__(self, node=None):
self.__head = node
def isEmpty(self):
"""check if the list is empty"""
return self.__head is None
def length(self):
"""length of the list"""
# cur is current position
cur = self.__head
count = 0
while cur != None:
cur = cur.next
count += 1
return count
def travel(self):
"""traverse through the list"""
# cur is current position
cur = self.__head
while cur != None:
print(cur.value, end=" ")
cur = cur.next
print('')
def shift(self, value):
"""add a node to the start"""
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
def append(self, value):
"""add a node to the end"""
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
def insert(self, pos, value):
"""insert a node at specific position"""
if pos <= 0:
self.shift(value)
elif pos > (self.length() - 1):
self.append(value)
else:
node = Node(value)
cur = self.__head
count = 0
while count < pos:
cur = cur.next
count += 1
node.next = cur
node.prev = cur.prev
cur.prev.next = node
cur.prev = node
def remove(self, value):
"""remove a node from list when a value first appears"""
cur = self.__head
# remove the first node
if cur.value == value:
if cur.next is None:
self.__head = None
else:
self.__head = cur.next
cur.next.prev = None
return value
# elif self.length() == 0:
elif self.__head is None:
return
else:
while cur.next != None:
if cur.value == value:
cur.next.prev = cur.prev
cur.prev.next = cur.next
return value
else:
cur = cur.next
if cur.value == value:
cur.prev.next = None
return value
return
def exist(self, value):
"""whether a node exists in list"""
cur = self.__head
while cur != None:
if cur.value == value:
return True
else:
cur = cur.next
return False
if __name__ == "__main__":
dll = DoubleLinkedList()
print(dll.exist(10))
print(dll.length())
print(dll.isEmpty())
dll.append(1)
print(dll.length())
print(dll.isEmpty())
dll.append(2)
dll.append(3)
dll.append(4)
dll.append(5)
dll.append(6)
dll.shift(8)
dll.insert(2, 9)
dll.insert(-2, 10)
dll.insert(100, 11)
dll.travel()
print(dll.length())
print(dll.isEmpty())
print(dll.exist(100))
print(dll.exist(6))
dll.remove(6)
dll.remove(10)
dll.remove(11)
print(dll.exist(6))
dll.travel()
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。