1
0
mirror of https://github.com/apachecn/lmpythw-zh.git synced 2025-05-31 13:37:42 +00:00
lmpythw-zh/ex13.md
wizardforcel 9136ec9fa0 ex13
2017-08-06 16:59:50 +08:00

248 lines
12 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.

# 练习 13单链表
> 原文:[Exercise 13: Single Linked Lists](https://learncodethehardway.org/more-python-book/ex13.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
你将实现的第一个数据结构是单链表。我将描述数据结构,列出你应该实现的所有操作,并给你实现需要通过的单个测试。你应该首先尝试使用此数据结构,然后再观看我的实现和审计视频,以便你了解该过程。
> 警告
> 这些都不是数据结构的高效实现。它们故意做成朴素和缓慢的,以便我们可以在练习 18 和 19 中讲解度量和优化。如果你在行业工作中尝试使用这些数据结构,就会有性能问题。
## 描述
在面向对象语言(如 Python中处理许多数据结构时你需要理解三个常见概念
+ “节点”,通常是数据结构的容器或存储单元。你的值保存在这里。
+ “边”,但我们会叫它“指针”或“链接”,它指向其他节点。这些都放在每个节点内,通常作为实例变量。
+ “控制器”,它是一些类,知道如何使用节点中的指针来正确构造数据。
在 Python 中,我们将映射这些概念,如下所示:
+ 节点只是一个类定义的对象。
+ 指针(边)只是节点对象中的实例变量。
+ 控制器是另一个简单的类,它使用节点存储所有内容并构建数据。这是所有的操作(`push``pop``list`等)的地方,通常控制器的使用者从来没有真正处理节点或指针。
在一些关于算法的书中,你将看到这样的实现,将节点和控制器组合成一个类,但这是非常混乱的,也违反了设计中的问题分离。最好将节点与控制类分开,以便只做一件事并且把它做好,以及你知道错误在哪里。
想象一下,我们想要存储一系列汽车。我们有第一辆车,后面是第二辆,直到最后一辆。想象这个列表,我们可以开始设想一个节点/指针/控制器设计:
+ 节点包含每个车的描述。也许这只是一个`Car`类的`node.value`变量。如果你很懒,我们可以调用这个`SingleLinkedListNode``SLLNode`
+ 然后,每个`SLLNode`具有一个链接,指向链表中下一个节点。访问`node.next`可以让你访问下一辆车。
+ 控制器,简单地称为`SingleLinkedList`,具有诸如`push``pop``first``count`之类的操作,它们接受`Car`,并且使用节点在内部进行存储。当你将汽车`push``SingleLinkedList`控制器上时,它将处理在一个节点的内部链表,来将其存储在最后。
>
> 当 Python 有个相当好用并且快速的`list`时,为什么我们要这么做呢?完全是为了学习数据结构。在真实世界中,你可以使用 Python 的`list`并继续。
为了实现`SingleLinkedListNode`,我们需要一个简单的类,如下:
```py
class SingleLinkedListNode(object):
def __init__(self, value, nxt, prev):
self.value = value
self.next = nxt
def __repr__(self):
nval = self.next and self.next.value or None
return f"[{self.value}:{repr(nval)}]"
```
我们必须使用单词`nxt`,因为`next`是 Python 中的保留字。除此之外,这是一个非常简单的课程。最复杂的是`__repr__`函数。当你使用`%r`格式或在节点上调用`repr()`时,这会打印调试输出。它应该返回一个字符串。
>
> 现在花时间了解如何使用`SingleLinkedListNode`类手动构建列表然后手动遍历它。这是一个很好的45分钟 hack spike尝试练习它。
## 控制器
一旦我们在`SingleLinkedListNode`类中定义了我们的节点,我们可以确切地知道控制器应该做什么。每个数据结构都有所需的常用操作列表,使其有用。不同的操作花费不同的内存(空间)和时间,一些是昂贵的,另一些是快速的。`SingleLinkedListNode`的结构使得一些操作非常快,但是许多其他操作非常慢。在实现过程中,你将会了解到它。
查看操作的最简单方法是,查看`SingleLinkedList`类的框架版本:
```py
class SingleLinkedList(object):
def __init__(self):
self.begin = None
self.end = None
def push(self, obj):
"""将新的值附加到链表尾部。"""
def pop(self):
"""移除最后一个元素并返回它。"""
def shift(self, obj):
"""将新的值附加到链表头部。"""
def unshift(self):
"""移除第一个元素并返回它。"""
def remove(self, obj):
"""寻找匹配的元素并从中移除。"""
def first(self):
"""返回第一个元素的*引用*,不要移除。"""
def last(self):
"""返回最后一个元素的*引用*,不要移除。"""
def count(self):
"""计算链表中的元素数量。"""
def get(self, index):
"""获取下标处的值。"""
def dump(self, mark):
"""转储链表内容的调试函数。"""
```
在其他练习中,我只会告诉你这些操作,并留给你来弄清楚,但是对于这个练习,我会指导你实现。查看`SingleLinkedList`中的函数列表,来查看每个操作以及如何使用的注释。
## 测试
我现在要向你提供测试,实现这个类时,你必须使其能够工作。你会看到我已经遍历了每一个操作,并试图覆盖大部分的边界情况,但是当我进行审计时,你会发现实际上我可能错过了一些。人们常常不会对一些案例进行测试,例如“零个元素”和“一个元素”。
```py
from sllist import *
def test_push():
colors = SingleLinkedList()
colors.push("Pthalo Blue")
assert colors.count() == 1
colors.push("Ultramarine Blue")
assert colors.count() == 2
def test_pop():
colors = SingleLinkedList()
colors.push("Magenta")
colors.push("Alizarin")
assert colors.pop() == "Alizarin"
assert colors.pop() == "Magenta"
assert colors.pop() == None
def test_unshift():
colors = SingleLinkedList()
colors.push("Viridian")
colors.push("Sap Green")
colors.push("Van Dyke")
assert colors.unshift() == "Viridian"
assert colors.unshift() == "Sap Green"
assert colors.unshift() == "Van Dyke"
assert colors.unshift() == None
def test_shift():
colors = SingleLinkedList()
colors.shift("Cadmium Orange")
assert colors.count() == 1
colors.shift("Carbazole Violet")
assert colors.count() == 2
assert colors.pop() == "Cadmium Orange"
assert colors.count() == 1
assert colors.pop() == "Carbazole Violet"
assert colors.count() == 0
def test_remove():
colors = SingleLinkedList()
colors.push("Cobalt")
colors.push("Zinc White")
colors.push("Nickle Yellow")
colors.push("Perinone")
assert colors.remove("Cobalt") == 0
colors.dump("before perinone")
assert colors.remove("Perinone") == 2
colors.dump("after perinone")
assert colors.remove("Nickle Yellow") == 1
assert colors.remove("Zinc White") == 0
def test_first():
colors = SingleLinkedList()
colors.push("Cadmium Red Light")
assert colors.first() == "Cadmium Red Light"
colors.push("Hansa Yellow")
assert colors.first() == "Cadmium Red Light"
colors.shift("Pthalo Green")
assert colors.first() == "Pthalo Green"
def test_last():
colors = SingleLinkedList()
colors.push("Cadmium Red Light")
assert colors.last() == "Cadmium Red Light"
colors.push("Hansa Yellow")
assert colors.last() == "Hansa Yellow"
colors.shift("Pthalo Green")
assert colors.last() == "Hansa Yellow"
def test_get():
colors = SingleLinkedList()
colors.push("Vermillion")
assert colors.get(0) == "Vermillion"
colors.push("Sap Green")
assert colors.get(0) == "Vermillion"
assert colors.get(1) == "Sap Green"
colors.push("Cadmium Yellow Light")
assert colors.get(0) == "Vermillion"
assert colors.get(1) == "Sap Green"
assert colors.get(2) == "Cadmium Yellow Light"
assert colors.pop() == "Cadmium Yellow Light"
assert colors.get(0) == "Vermillion"
assert colors.get(1) == "Sap Green"
assert colors.get(2) == None
colors.pop()
assert colors.get(0) == "Vermillion"
colors.pop()
assert colors.get(0) == None
```
仔细研究此测试,以便你在尝试实现之前,先了解每个操作应如何工作。我不会一次将所有这些代码写入文件。相反,最好每次只做一个测试,并使其小部分能够工作。
>
> 这里,如果你不熟悉自动化测试,你可能想要观看视频,来看我怎么做。
## 审计入门
当你执行每个测试时,你将审计代码来找到缺陷。最终,你将跟踪你在审计中找到的缺陷数量,但现在你需要在写完代码之后执行审计。“审计”类似于政府认为你偷税漏税的时候,税务局所做的工作。他们遍历每笔交易,每笔收入金额,所有支出金额,以及你为什么这样来花费。代码审核与之类似,因为你遍历每个函数,并分析所有输入参数,以及所有输出值。
要进行基本的审计,你将执行此操作:
+ 从你的测试用例开始。在这个例子中我们来审计`test_push`
+ 查看第一行代码,并确定正在调用什么以及正在创建什么。在这种情况下,它的`colors = SingleLinkeList()`。这意味着我们正在创建`colors`变量,并调用`SingleLinkeList.__ init__`函数。
+ 跳到`__init__`函数的顶部,保持测试用例和目标函数(`__init__`)并排。确认你已经这样做了。然后,确认你使用数值和类型正确的函数参数来调用它。在这种情况下`__init__`只需要`self`,它应该是正确的类型。
+ 然后进入`__init__`并逐行审计,以相同的方式确认每个函数调用和变量。它的参数数量正确吗?类型正确吗?
+ 在每个分支(`if`语句,`for`循环,`while`循环)中,确认逻辑是正确的,并且它处理逻辑中的任何可能的条件。`if`语句的`else`子句有错误吗?循环能结束吗?然后潜入每个分支,以相同方式跟踪函数,潜入,检查变量,回来,并检查返回值。
+ 当你到达一个函数结尾或任何`return`的时候,跳回到`test_push`调用者,来检查返回值是否匹配期望值,当你调用它的时候。记住,尽管如此,你也可以对`__init__`中的每个调用搞这么做。
+ 最后,当你到达`test_push`函数的末尾时,你就完成了,并且已经完成了它调用的每个函数的递归检查。
这个流程一开始似乎很乏味,是的,但是你会越来越快,在视频中你会看到,在运行每个测试之前我都这么做(或至少我真的努力尝试这么做)。我按照以下流程:
+ 写一些测试代码。
+ 编写代码使测试工作。
+ 审计二者。
+ 运行测试,看看我是否正确。
## 挑战练习
我们现在到达了这个部分,你已经准备好尝试它了。首先,浏览测试并研究它的作用,并研究`sllist.py`中的代码,来弄清楚你需要做什么。我建议当你尝试在`SingleLinkeList`中实现一个函数时,首先写一些注释来描述它做了什么,然后填充 Python 代码来使这些注释工作。你会看到我在视频中这样做。
当你花了一两个 45 分钟的会话来 Hack 它并试图让它工作时,现在是观看视频的时候了。你首先需要尝试它,以便更好地了解我正在尝试的事情,这样可以使视频更容易理解。视频中我只是编程而不说话,但我会做一个旁白来讨论发生了什么。视频也更快来节省时间,我会剪切掉任何无聊的错误或时间的浪费。
一旦你看到我是怎么做的,你已经做了笔记(对吗?),然后去尝试更严格的东西,并尽可能仔细地执行代码审核过程。
## 审计
编写代码后,请确保执行第三部分中描述的审计流程。如果你不太确定如何完成,我也将在视频中为这个练习执行审计。
## 深入学习
为这次练习准备的深入学习是,完全根据我在第三部分的介绍中描述的方式,尝试再次实现该算法。你还应该尝试思考,这个数据结构中的哪些操作最有可能很慢。完成后,对你创建的内容执行审计。