|
| 1 | +# 练习 13:单链表 |
| 2 | + |
| 3 | +> 原文:[Exercise 13: Single Linked Lists](https://learncodethehardway.org/more-python-book/ex13.html) |
| 4 | +
|
| 5 | +> 译者:[飞龙](https://github.com/wizardforcel) |
| 6 | +
|
| 7 | +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) |
| 8 | +
|
| 9 | +> 自豪地采用[谷歌翻译](https://translate.google.cn/) |
| 10 | +
|
| 11 | +你将实现的第一个数据结构是单链表。我将描述数据结构,列出你应该实现的所有操作,并给你实现需要通过的单个测试。你应该首先尝试使用此数据结构,然后再观看我的实现和审计视频,以便你了解该过程。 |
| 12 | + |
| 13 | +> 警告 |
| 14 | +
|
| 15 | +> 这些都不是数据结构的高效实现。它们故意做成朴素和缓慢的,以便我们可以在练习 18 和 19 中讲解度量和优化。如果你在行业工作中尝试使用这些数据结构,就会有性能问题。 |
| 16 | +
|
| 17 | +## 描述 |
| 18 | + |
| 19 | +在面向对象语言(如 Python)中处理许多数据结构时,你需要理解三个常见概念: |
| 20 | + |
| 21 | ++ “节点”,通常是数据结构的容器或存储单元。你的值保存在这里。 |
| 22 | ++ “边”,但我们会叫它“指针”或“链接”,它指向其他节点。这些都放在每个节点内,通常作为实例变量。 |
| 23 | ++ “控制器”,它是一些类,知道如何使用节点中的指针来正确构造数据。 |
| 24 | + |
| 25 | +在 Python 中,我们将映射这些概念,如下所示: |
| 26 | + |
| 27 | ++ 节点只是一个类定义的对象。 |
| 28 | ++ 指针(边)只是节点对象中的实例变量。 |
| 29 | ++ 控制器是另一个简单的类,它使用节点存储所有内容并构建数据。这是所有的操作(`push`,`pop`,`list`等)的地方,通常控制器的使用者从来没有真正处理节点或指针。 |
| 30 | + |
| 31 | +在一些关于算法的书中,你将看到这样的实现,将节点和控制器组合成一个类,但这是非常混乱的,也违反了设计中的问题分离。最好将节点与控制类分开,以便只做一件事并且把它做好,以及你知道错误在哪里。 |
| 32 | + |
| 33 | +想象一下,我们想要存储一系列汽车。我们有第一辆车,后面是第二辆,直到最后一辆。想象这个列表,我们可以开始设想一个节点/指针/控制器设计: |
| 34 | + |
| 35 | ++ 节点包含每个车的描述。也许这只是一个`Car`类的`node.value`变量。如果你很懒,我们可以调用这个`SingleLinkedListNode`或`SLLNode`。 |
| 36 | ++ 然后,每个`SLLNode`具有一个链接,指向链表中下一个节点。访问`node.next`可以让你访问下一辆车。 |
| 37 | ++ 控制器,简单地称为`SingleLinkedList`,具有诸如`push`,`pop`,`first`或`count`之类的操作,它们接受`Car`,并且使用节点在内部进行存储。当你将汽车`push`到`SingleLinkedList`控制器上时,它将处理在一个节点的内部链表,来将其存储在最后。 |
| 38 | + |
| 39 | +> 注 |
| 40 | +
|
| 41 | +> 当 Python 有个相当好用并且快速的`list`时,为什么我们要这么做呢?完全是为了学习数据结构。在真实世界中,你可以使用 Python 的`list`并继续。 |
| 42 | +
|
| 43 | +为了实现`SingleLinkedListNode`,我们需要一个简单的类,如下: |
| 44 | + |
| 45 | +```py |
| 46 | +class SingleLinkedListNode(object): |
| 47 | + |
| 48 | + def __init__(self, value, nxt, prev): |
| 49 | + self.value = value |
| 50 | + self.next = nxt |
| 51 | + |
| 52 | + def __repr__(self): |
| 53 | + nval = self.next and self.next.value or None |
| 54 | + return f"[{self.value}:{repr(nval)}]" |
| 55 | +``` |
| 56 | + |
| 57 | +我们必须使用单词`nxt`,因为`next`是 Python 中的保留字。除此之外,这是一个非常简单的课程。最复杂的是`__repr__`函数。当你使用`%r`格式或在节点上调用`repr()`时,这会打印调试输出。它应该返回一个字符串。 |
| 58 | + |
| 59 | +> 注 |
| 60 | +
|
| 61 | +> 现在花时间了解如何使用`SingleLinkedListNode`类手动构建列表,然后手动遍历它。这是一个很好的45分钟 hack spike,尝试练习它。 |
| 62 | +
|
| 63 | +## 控制器 |
| 64 | + |
| 65 | +一旦我们在`SingleLinkedListNode`类中定义了我们的节点,我们可以确切地知道控制器应该做什么。每个数据结构都有所需的常用操作列表,使其有用。不同的操作花费不同的内存(空间)和时间,一些是昂贵的,另一些是快速的。`SingleLinkedListNode`的结构使得一些操作非常快,但是许多其他操作非常慢。在实现过程中,你将会了解到它。 |
| 66 | + |
| 67 | +查看操作的最简单方法是,查看`SingleLinkedList`类的框架版本: |
| 68 | + |
| 69 | +```py |
| 70 | +class SingleLinkedList(object): |
| 71 | + |
| 72 | + def __init__(self): |
| 73 | + self.begin = None |
| 74 | + self.end = None |
| 75 | + |
| 76 | + def push(self, obj): |
| 77 | + """将新的值附加到链表尾部。""" |
| 78 | + |
| 79 | + def pop(self): |
| 80 | + """移除最后一个元素并返回它。""" |
| 81 | + |
| 82 | + def shift(self, obj): |
| 83 | + """将新的值附加到链表头部。""" |
| 84 | + |
| 85 | + def unshift(self): |
| 86 | + """移除第一个元素并返回它。""" |
| 87 | + |
| 88 | + def remove(self, obj): |
| 89 | + """寻找匹配的元素并从中移除。""" |
| 90 | + |
| 91 | + def first(self): |
| 92 | + """返回第一个元素的*引用*,不要移除。""" |
| 93 | + |
| 94 | + def last(self): |
| 95 | + """返回最后一个元素的*引用*,不要移除。""" |
| 96 | + |
| 97 | + def count(self): |
| 98 | + """计算链表中的元素数量。""" |
| 99 | + |
| 100 | + def get(self, index): |
| 101 | + """获取下标处的值。""" |
| 102 | + |
| 103 | + def dump(self, mark): |
| 104 | + """转储链表内容的调试函数。""" |
| 105 | +``` |
| 106 | + |
| 107 | +在其他练习中,我只会告诉你这些操作,并留给你来弄清楚,但是对于这个练习,我会指导你实现。查看`SingleLinkedList`中的函数列表,来查看每个操作以及如何使用的注释。 |
| 108 | + |
| 109 | +## 测试 |
| 110 | + |
| 111 | +我现在要向你提供测试,实现这个类时,你必须使其能够工作。你会看到我已经遍历了每一个操作,并试图覆盖大部分的边界情况,但是当我进行审计时,你会发现实际上我可能错过了一些。人们常常不会对一些案例进行测试,例如“零个元素”和“一个元素”。 |
| 112 | + |
| 113 | +```py |
| 114 | +from sllist import * |
| 115 | + |
| 116 | +def test_push(): |
| 117 | + colors = SingleLinkedList() |
| 118 | + colors.push("Pthalo Blue") |
| 119 | + assert colors.count() == 1 |
| 120 | + colors.push("Ultramarine Blue") |
| 121 | + assert colors.count() == 2 |
| 122 | + |
| 123 | +def test_pop(): |
| 124 | + colors = SingleLinkedList() |
| 125 | + colors.push("Magenta") |
| 126 | + colors.push("Alizarin") |
| 127 | + assert colors.pop() == "Alizarin" |
| 128 | + assert colors.pop() == "Magenta" |
| 129 | + assert colors.pop() == None |
| 130 | + |
| 131 | +def test_unshift(): |
| 132 | + colors = SingleLinkedList() |
| 133 | + colors.push("Viridian") |
| 134 | + colors.push("Sap Green") |
| 135 | + colors.push("Van Dyke") |
| 136 | + assert colors.unshift() == "Viridian" |
| 137 | + assert colors.unshift() == "Sap Green" |
| 138 | + assert colors.unshift() == "Van Dyke" |
| 139 | + assert colors.unshift() == None |
| 140 | + |
| 141 | +def test_shift(): |
| 142 | + colors = SingleLinkedList() |
| 143 | + colors.shift("Cadmium Orange") |
| 144 | + assert colors.count() == 1 |
| 145 | + |
| 146 | + colors.shift("Carbazole Violet") |
| 147 | + assert colors.count() == 2 |
| 148 | + |
| 149 | + assert colors.pop() == "Cadmium Orange" |
| 150 | + assert colors.count() == 1 |
| 151 | + assert colors.pop() == "Carbazole Violet" |
| 152 | + assert colors.count() == 0 |
| 153 | + |
| 154 | +def test_remove(): |
| 155 | + colors = SingleLinkedList() |
| 156 | + colors.push("Cobalt") |
| 157 | + colors.push("Zinc White") |
| 158 | + colors.push("Nickle Yellow") |
| 159 | + colors.push("Perinone") |
| 160 | + assert colors.remove("Cobalt") == 0 |
| 161 | + colors.dump("before perinone") |
| 162 | + assert colors.remove("Perinone") == 2 |
| 163 | + colors.dump("after perinone") |
| 164 | + assert colors.remove("Nickle Yellow") == 1 |
| 165 | + assert colors.remove("Zinc White") == 0 |
| 166 | + |
| 167 | +def test_first(): |
| 168 | + colors = SingleLinkedList() |
| 169 | + colors.push("Cadmium Red Light") |
| 170 | + assert colors.first() == "Cadmium Red Light" |
| 171 | + colors.push("Hansa Yellow") |
| 172 | + assert colors.first() == "Cadmium Red Light" |
| 173 | + colors.shift("Pthalo Green") |
| 174 | + assert colors.first() == "Pthalo Green" |
| 175 | + |
| 176 | +def test_last(): |
| 177 | + colors = SingleLinkedList() |
| 178 | + colors.push("Cadmium Red Light") |
| 179 | + assert colors.last() == "Cadmium Red Light" |
| 180 | + colors.push("Hansa Yellow") |
| 181 | + assert colors.last() == "Hansa Yellow" |
| 182 | + colors.shift("Pthalo Green") |
| 183 | + assert colors.last() == "Hansa Yellow" |
| 184 | + |
| 185 | +def test_get(): |
| 186 | + colors = SingleLinkedList() |
| 187 | + colors.push("Vermillion") |
| 188 | + assert colors.get(0) == "Vermillion" |
| 189 | + colors.push("Sap Green") |
| 190 | + assert colors.get(0) == "Vermillion" |
| 191 | + assert colors.get(1) == "Sap Green" |
| 192 | + colors.push("Cadmium Yellow Light") |
| 193 | + assert colors.get(0) == "Vermillion" |
| 194 | + assert colors.get(1) == "Sap Green" |
| 195 | + assert colors.get(2) == "Cadmium Yellow Light" |
| 196 | + assert colors.pop() == "Cadmium Yellow Light" |
| 197 | + assert colors.get(0) == "Vermillion" |
| 198 | + assert colors.get(1) == "Sap Green" |
| 199 | + assert colors.get(2) == None |
| 200 | + colors.pop() |
| 201 | + assert colors.get(0) == "Vermillion" |
| 202 | + colors.pop() |
| 203 | + assert colors.get(0) == None |
| 204 | +``` |
| 205 | + |
| 206 | +仔细研究此测试,以便你在尝试实现之前,先了解每个操作应如何工作。我不会一次将所有这些代码写入文件。相反,最好每次只做一个测试,并使其小部分能够工作。 |
| 207 | + |
| 208 | +> 注 |
| 209 | +
|
| 210 | +> 这里,如果你不熟悉自动化测试,你可能想要观看视频,来看我怎么做。 |
| 211 | +
|
| 212 | +## 审计入门 |
| 213 | + |
| 214 | +当你执行每个测试时,你将审计代码来找到缺陷。最终,你将跟踪你在审计中找到的缺陷数量,但现在你需要在写完代码之后执行审计。“审计”类似于政府认为你偷税漏税的时候,税务局所做的工作。他们遍历每笔交易,每笔收入金额,所有支出金额,以及你为什么这样来花费。代码审核与之类似,因为你遍历每个函数,并分析所有输入参数,以及所有输出值。 |
| 215 | + |
| 216 | +要进行基本的审计,你将执行此操作: |
| 217 | + |
| 218 | ++ 从你的测试用例开始。在这个例子中我们来审计`test_push`。 |
| 219 | ++ 查看第一行代码,并确定正在调用什么以及正在创建什么。在这种情况下,它的`colors = SingleLinkeList()`。这意味着我们正在创建`colors`变量,并调用`SingleLinkeList.__ init__`函数。 |
| 220 | ++ 跳到`__init__`函数的顶部,保持测试用例和目标函数(`__init__`)并排。确认你已经这样做了。然后,确认你使用数值和类型正确的函数参数来调用它。在这种情况下`__init__`只需要`self`,它应该是正确的类型。 |
| 221 | ++ 然后进入`__init__`并逐行审计,以相同的方式确认每个函数调用和变量。它的参数数量正确吗?类型正确吗? |
| 222 | ++ 在每个分支(`if`语句,`for`循环,`while`循环)中,确认逻辑是正确的,并且它处理逻辑中的任何可能的条件。`if`语句的`else`子句有错误吗?循环能结束吗?然后潜入每个分支,以相同方式跟踪函数,潜入,检查变量,回来,并检查返回值。 |
| 223 | ++ 当你到达一个函数结尾或任何`return`的时候,跳回到`test_push`调用者,来检查返回值是否匹配期望值,当你调用它的时候。记住,尽管如此,你也可以对`__init__`中的每个调用搞这么做。 |
| 224 | ++ 最后,当你到达`test_push`函数的末尾时,你就完成了,并且已经完成了它调用的每个函数的递归检查。 |
| 225 | + |
| 226 | +这个流程一开始似乎很乏味,是的,但是你会越来越快,在视频中你会看到,在运行每个测试之前我都这么做(或至少我真的努力尝试这么做)。我按照以下流程: |
| 227 | + |
| 228 | ++ 写一些测试代码。 |
| 229 | ++ 编写代码使测试工作。 |
| 230 | ++ 审计二者。 |
| 231 | ++ 运行测试,看看我是否正确。 |
| 232 | + |
| 233 | +## 挑战练习 |
| 234 | + |
| 235 | +我们现在到达了这个部分,你已经准备好尝试它了。首先,浏览测试并研究它的作用,并研究`sllist.py`中的代码,来弄清楚你需要做什么。我建议当你尝试在`SingleLinkeList`中实现一个函数时,首先写一些注释来描述它做了什么,然后填充 Python 代码来使这些注释工作。你会看到我在视频中这样做。 |
| 236 | + |
| 237 | +当你花了一两个 45 分钟的会话来 Hack 它并试图让它工作时,现在是观看视频的时候了。你首先需要尝试它,以便更好地了解我正在尝试的事情,这样可以使视频更容易理解。视频中我只是编程而不说话,但我会做一个旁白来讨论发生了什么。视频也更快来节省时间,我会剪切掉任何无聊的错误或时间的浪费。 |
| 238 | + |
| 239 | +一旦你看到我是怎么做的,你已经做了笔记(对吗?),然后去尝试更严格的东西,并尽可能仔细地执行代码审核过程。 |
| 240 | + |
| 241 | +## 审计 |
| 242 | + |
| 243 | +编写代码后,请确保执行第三部分中描述的审计流程。如果你不太确定如何完成,我也将在视频中为这个练习执行审计。 |
| 244 | + |
| 245 | +## 深入学习 |
| 246 | + |
| 247 | +为这次练习准备的深入学习是,完全根据我在第三部分的介绍中描述的方式,尝试再次实现该算法。你还应该尝试思考,这个数据结构中的哪些操作最有可能很慢。完成后,对你创建的内容执行审计。 |
0 commit comments