1
0
mirror of https://github.com/apachecn/lmpythw-zh.git synced 2025-05-28 12:02:19 +00:00
lmpythw-zh/ex14.md
wizardforcel b75c12436d ex14
2017-08-06 20:47:57 +08:00

104 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 练习 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`,而无需任何参考。