# 练习 14:双链表
> 原文:[Exercise 14: Double Linked Lists](https://learncodethehardway.org/more-python-book/ex14.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
以前的练习可能需要花一段时间才能完成,因为你必须弄清楚如何使单个链表工作。希望视频为你提供完成练习的足够信息,并向你展示如何审计代码。在本练习中,你将实现更好的链表`DoubleLinkedList`。
在`SingleLinkedList`中,你应该已经意识到,涉及列表末尾的任何操作,都必须遍历每个节点,直到到达末尾。`SingleLinkedList`仅仅对于列表前面是高效的,那里你可以轻松地更改`next`指针。`shift`和`unshift`操作非常快,但`pop`和`push`的开销随链表增大而增大。你可以通过保留下一个元素到最后一个元素的引用来加速,但是如果要替换该元素,该怎么办?同样,你必须遍历所有的元素来找到这个元素。你可以通过细微变化来获得一些速度改进,但更好的解决方案是,修改结构,使其可以从任何位置工作。
`DoubleLinkedList`与`SingleLinkedList`几乎一样,但它还有`prev`(上一个)链接,指向它前面的`DoubleLinkedListNode`。每个节点有一个额外的指针,使许多操作突然变得容易得多。你还可以在`DoubleLinkedList`中,轻易添加一个指向`end`的指针,所以你可以直接访问头部和尾部。这使得`push`和`pop`效率更加高效,因为你可以直接访问尾部,并使用`node.prev`指针获取上一个节点。
考虑到这些变化,我们的节点类看起来像这样:
```py
class DoubleLinkedListNode(object):
def __init__(self, value, nxt, prev):
self.value = value
self.next = nxt
self.prev = prev
def __repr__(self):
nval = self.next and self.next.value or None
pval = self.prev and self.prev.value or None
return f"[{self.value}, {repr(nval)}, {repr(pval)}]"
```
所有添加的东西就是`self.prev = prev`,以及在`__repr__`中处理它。`DoubleLinkedList `类的实现使用`SingleLinkedList`的相同方式,除了你需要为链表末尾添加一个额外的变量。
```py
class DoubleLinkedList(object):
def __init__(self):
self.begin = None
self.end = None
```
## 引入不变条件
所有要实现的操作都一样,但是我们有一些额外的事情需要考虑:
```py
def push(self, obj):
"""将新的值附加到链表尾部。"""
def pop(self):
"""移除最后一个元素并返回它。"""
def shift(self, obj):
"""将新的值附加到链表头部。"""
def unshift(self):
"""移除第一个元素并返回它。"""
def detach_node(self, node):
"""你有时需要这个操作,但是多数都在 remove() 里面。它应该接受一个节点,将其从链表分离,无论节点是否在头部、尾部还是在中间。"""
def remove(self, obj):
"""寻找匹配的元素并从中移除。"""
def first(self):
"""返回第一个元素的*引用*,不要移除。"""
def last(self):
"""返回最后一个元素的*引用*,不要移除。"""
def count(self):
"""计算链表中的元素数量。"""
def get(self, index):
"""获取下标处的值。"""
def dump(self, mark):
"""转储链表内容的调试函数。"""
```
使用`self.end`指针,你现在必须在每个操作中处理更多的条件:
+ 是否有零个元素?那么`self.begin`和`self.end`都需要是`None`。
+ 如果有一个元素,那么`self.begin`和`self.end`必须相等(指向同一个节点)。
+ 第一个节点的`prev`必须始终为`None`。
+ 最后一个节点的`next`必须始终为`None`。
这些事实必须在`DoubleLinkedList`的生命周期中维持,这使得它们成为“不变条件”或者只是“不变量”。不变量的想法是,无论如何,这些基础检查显示了结构正常工作。查看不变量的一种方法是,任何重复调用的测试或者`assert`调用可以移动进一个函数,叫做`_invariant`,它执行这些检查。然后,你可以在测试中或每个函数的开始和结束处调用此函数。这样做会减少你的缺陷率,因为你假设“不管我做什么,这些都是真的”。
不变量检查的唯一问题是它们的运行花费时间。如果每个函数调用也调用另一个函数两次,那么你就为每个函数增加了潜在的重要负担。如果你的`_invariant`函数也会导致成本增加,就变得更糟。想象一下,如果你添加了不变量:“所有节点都有一个`next`和`prev`,除了第一个和最后一个。这意味着每个函数调用都遍历列表两次。当你必须确保类一直有效时,这是值得的。如果不是,那就是一个问题。
在这本书中,你可以使用`_invariant`函数,但请记住,你不需要始终使用它们。寻找方法,只在测试套件或调试中激活它们,或者在初始开发过程中使用它们,这是有效使用它们的关键。我建议你只在函数顶部调用`_invariant`,或者只在测试套件中调用它们。这是一个很好的权衡。
## 挑战练习
在本练习中,你将实现`DoubleLinkedList`的操作,但此时你还将使用`_invariant`函数来检查每个操作之前和之后是否正常。最好的方法是,在每个函数的顶部调用`_invariant`,然后在测试套件中的关键点调用它。`DoubleLinkedList`的测试套件几乎是`SingleLinkedList`测试的复制粘贴副本,除了在关键点添加`_invariant`调用。
与`SingleLinkedList`一样,你需要自己手动研究此数据结构。你应该在纸张上绘制节点结构,并手动执行某些操作。接下来,在`dllist.py`文件中手动实现`DoubleLinkedListNode`。之后,花费一两个 45 分钟的时间,来尝试黑掉一些操作来弄清楚。我推荐`push`和`pop`。之后,你可以观看视频以查看我的工作,以及如何组合使用我的代码的审计和`_invariant`函数,来检查我在做什么。
## 深入学习
与以前的练习一样,你要按照记忆再次实现此数据结构。把你所知道的东西放在一个房间里,你的笔记本电脑在另一个房间。你将要执行此操作,直到你可以按照记忆实现`DoubleLinkedList`,而无需任何参考。
- 笨办法学 Python · 续 中文版
- 引言
- 第一部分:预备知识
- 练习 0:起步
- 练习 1:流程
- 练习 2:创造力
- 练习 3:质量
- 第二部分:简单的黑魔法
- 练习 4:处理命令行参数
- 练习 5:cat
- 练习 6:find
- 练习 7:grep
- 练习 8:cut
- 练习 9:sed
- 练习 10:sort
- 练习 11:uniq
- 练习 12:复习
- 第三部分:数据结构
- 练习 13:单链表
- 练习 14:双链表
- 练习 15:栈和队列
- 练习 16:冒泡、快速和归并排序
- 练习 17:字典
- 练习 18:性能测量
- 练习 19:改善性能
- 练习 20:二叉搜索树
- 练习 21:二分搜索
- 练习 22:后缀数组
- 练习 23:三叉搜索树
- 练习 24:URL 快速路由
- 第四部分:进阶项目
- 练习 25:xargs
- 练习 26:hexdump
- 练习 27:tr
- 练习 28:sh
- 练习 29:diff和patch
- 第五部分:文本解析
- 练习 30:有限状态机
- 练习 31:正则表达式
- 练习 32:扫描器
- 练习 33:解析器
- 练习 34:分析器
- 练习 35:解释器
- 练习 36:简单的计算器
- 练习 37:小型 BASIC
- 第六部分:SQL 和对象关系映射
- 练习 38:SQL 简介
- 练习 39:SQL 创建
- 练习 40:SQL 读取
- 练习 41:SQL 更新
- 练习 42:SQL 删除
- 练习 43:SQL 管理
- 练习 44:使用 Python 的数据库 API
- 练习 45:创建 ORM
- 第七部分:大作业
- 练习 46:blog
- 练习 47:bc
- 练习 48:ed
- 练习 49:sed
- 练习 50:vi
- 练习 51:lessweb
- 练习 52:moreweb