← 学习库 Think Python (2e) · 中英对照 目录

Appendix B  Analysis of Algorithms 附录 B 算法分析

本页译自 Think Python 2e(Allen B. Downey)· Appendix B Analysis of Algorithms。代码块保留英文原文不翻译;正文段段对照,中文块可用右下角按钮隐藏。

This appendix is an edited excerpt from Think Complexity, by Allen B. Downey, also published by O’Reilly Media (2011). When you are done with this book, you might want to move on to that one.

本附录改编自 Allen B. Downey 所著、O’Reilly Media(2011)出版的《Think Complexity》。读完本书后,你也许会想接着读那一本。

Analysis of algorithms is a branch of computer science that studies the performance of algorithms, especially their run time and space requirements. See http://en.wikipedia.org/wiki/Analysis_of_algorithms.

算法分析是计算机科学的一个分支,研究算法的性能,尤其是它们的运行时间和空间需求。参见 http://en.wikipedia.org/wiki/Analysis_of_algorithms

The practical goal of algorithm analysis is to predict the performance of different algorithms in order to guide design decisions.

算法分析的实际目标是预测不同算法的性能,以指导设计决策。

During the 2008 United States Presidential Campaign, candidate Barack Obama was asked to perform an impromptu analysis when he visited Google. Chief executive Eric Schmidt jokingly asked him for “the most efficient way to sort a million 32-bit integers.” Obama had apparently been tipped off, because he quickly replied, “I think the bubble sort would be the wrong way to go.” See http://www.youtube.com/watch?v=k4RRi_ntQc8.

在 2008 年美国总统竞选期间,候选人 Barack Obama 访问 Google 时被要求做一次即兴分析。执行官 Eric Schmidt 开玩笑地问他「对一个百万个 32 位整数排序最有效率的办法是什么」。Obama 显然事先得到了风声,因为他立刻答道:「我觉得冒泡排序是走错了方向。」参见 http://www.youtube.com/watch?v=k4RRi_ntQc8

This is true: bubble sort is conceptually simple but slow for large datasets. The answer Schmidt was probably looking for is “radix sort” (http://en.wikipedia.org/wiki/Radix_sort)1.

的确如此:冒泡排序概念上简单,但对大规模数据集很慢。Schmidt 大概想听到的答案是「基数排序」(http://en.wikipedia.org/wiki/Radix_sort)1

The goal of algorithm analysis is to make meaningful comparisons between algorithms, but there are some problems:

算法分析的目标是让算法之间有意义的比较成为可能,但这里有一些难题:

The good thing about this kind of comparison that it lends itself to simple classification of algorithms. For example, if I know that the run time of Algorithm A tends to be proportional to the size of the input, n, and Algorithm B tends to be proportional to n2, then I expect A to be faster than B for large values of n.

这种比较的好处在于,它很适合对算法做简单分类。例如,如果我知道算法 A 的运行时间倾向于与输入规模 n 成正比,而算法 B 倾向于与 n2 成正比,那么我预期对于较大的 n,A 会比 B 快。

This kind of analysis comes with some caveats, but we’ll get to that later.

这类分析附带一些注意事项,但我们稍后再谈。

B.1 Order of growth B.1 增长量级

Suppose you have analyzed two algorithms and expressed their run times in terms of the size of the input: Algorithm A takes 100n+1 steps to solve a problem with size n; Algorithm B takes n2 + n + 1 steps.

假设你分析了两种算法,并把它们的运行时间表示成输入规模的函数:算法 A 解决规模为 n 的问题需要 100n+1 步;算法 B 需要 n2 + n + 1 步。

The following table shows the run time of these algorithms for different problem sizes:

下表展示了这些算法在不同问题规模下的运行时间:
Input sizeRun time of Algorithm ARun time of Algorithm B
101 001111
10010 00110 101
1 000100 0011 001 001
10 0001 000 001> 1010
输入规模算法 A 的运行时间算法 B 的运行时间
101 001111
10010 00110 101
1 000100 0011 001 001
10 0001 000 001> 1010

At n=10, Algorithm A looks pretty bad; it takes almost 10 times longer than Algorithm B. But for n=100 they are about the same, and for larger values A is much better.

n=10 时,算法 A 看起来相当糟,它花的时间几乎是算法 B 的 10 倍。但当 n=100 时两者差不多,而更大的 n 下 A 要好得多。

The fundamental reason is that for large values of n, any function that contains an n2 term will grow faster than a function whose leading term is n. The leading term is the term with the highest exponent.

根本原因在于,对于较大的 n,任何含有 n2 项的函数,增长都会快于主项n 的函数。主项是幂次最高的项。

For Algorithm A, the leading term has a large coefficient, 100, which is why B does better than A for small n. But regardless of the coefficients, there will always be some value of n where a n2 > b n.

对算法 A 来说,主项的系数很大,是 100,这就是为什么在小的 n 下 B 比 A 好。但不管系数是什么,总存在某个 n 使得 a n2 > b n

The same argument applies to the non-leading terms. Even if the run time of Algorithm A were n+1000000, it would still be better than Algorithm B for sufficiently large n.

同样的道理也适用于非主项。即便算法 A 的运行时间是 n+1000000,对于足够大的 n,它仍然比算法 B 好。

In general, we expect an algorithm with a smaller leading term to be a better algorithm for large problems, but for smaller problems, there may be a crossover point where another algorithm is better. The location of the crossover point depends on the details of the algorithms, the inputs, and the hardware, so it is usually ignored for purposes of algorithmic analysis. But that doesn’t mean you can forget about it.

一般而言,我们预期主项较小的算法对大规模问题更好,但对较小的问题,可能会存在一个交叉点,在那里另一个算法更优。交叉点的位置取决于算法、输入和硬件的细节,因此在算法分析中通常会被忽略。但这并不意味着你可以把它抛到脑后。

If two algorithms have the same leading order term, it is hard to say which is better; again, the answer depends on the details. So for algorithmic analysis, functions with the same leading term are considered equivalent, even if they have different coefficients.

如果两个算法有相同的主项阶,就很难说哪个更好;答案同样取决于细节。所以在算法分析中,主项相同的函数被视为等价,即便它们的系数不同。

An order of growth is a set of functions whose asymptotic growth behavior is considered equivalent. For example, 2n, 100n and n+1 belong to the same order of growth, which is written O(n) in Big-Oh notation and often called linear because every function in the set grows linearly with n.

增长量级是一组渐近增长行为被视为等价的函数。例如,2n、100nn+1 属于同一个增长量级,用大 O 记号写作 O(n),常被称为线性,因为集合中的每个函数都随 n 线性增长。

All functions with the leading term n2 belong to O(n2); they are quadratic, which is a fancy word for functions with the leading term n2.

所有主项为 n2 的函数都属于 O(n2);它们是二次的,这不过是对主项为 n2 的函数一个花哨的说法。

The following table shows some of the orders of growth that appear most commonly in algorithmic analysis, in increasing order of badness.

下表按「糟糕程度」递增的顺序,列出了算法分析中最常出现的一些增长量级。
Order of growthName
O(1)constant
O(logb n)logarithmic (for any b)
O(n)linear
O(n logb n)“en log en”
O(n2)quadratic
O(n3)cubic
O(cn)exponential (for any c)
增长量级名称
O(1)常数
O(logb n)对数(对任意 b
O(n)线性
O(n logb n)「en log en」
O(n2)二次
O(n3)三次
O(cn)指数(对任意 c

For the logarithmic terms, the base of the logarithm doesn’t matter; changing bases is the equivalent of multiplying by a constant, which doesn’t change the order of growth. Similarly, all exponential functions belong to the same order of growth regardless of the base of the exponent. Exponential functions grow very quickly, so exponential algorithms are only useful for small problems.

对于对数项,对数的底无关紧要;换底相当于乘以一个常数,这不会改变增长量级。类似地,所有指数函数不管指数的底是什么,都属于同一个增长量级。指数函数增长极快,所以指数算法只对小规模问题有用。

Exercise 1

习题 1

Read the Wikipedia page on Big-Oh notation at http://en.wikipedia.org/wiki/Big_O_notation and answer the following questions:

阅读维基百科上关于大 O 记号的页面 http://en.wikipedia.org/wiki/Big_O_notation,并回答以下问题:
  1. What is the order of growth of n3 + n2? What about 1000000 n3 + n2? What about n3 + 1000000 n2?
  2. What is the order of growth of (n2 + n) · (n + 1)? Before you start multiplying, remember that you only need the leading term.
  3. If f is in O(g), for some unspecified function g, what can we say about af+b?
  4. If f1 and f2 are in O(g), what can we say about f1 + f2?
  5. If f1 is in O(g) and f2 is in O(h), what can we say about f1 + f2?
  6. If f1 is in O(g) and f2 is O(h), what can we say about f1 · f2?
  1. n3 + n2 的增长量级是多少?1000000 n3 + n2 呢?n3 + 1000000 n2 呢?
  2. (n2 + n) · (n + 1) 的增长量级是多少?在你动手相乘之前,记住你只需要主项。
  3. 如果 f 属于 O(g)(g 为某个未指定的函数),那么关于 af+b 我们能说什么?
  4. 如果 f1f2 都属于 O(g),那么关于 f1 + f2 我们能说什么?
  5. 如果 f1 属于 O(g) 且 f2 属于 O(h),那么关于 f1 + f2 我们能说什么?
  6. 如果 f1 属于 O(g) 且 f2 属于 O(h),那么关于 f1 · f2 我们能说什么?

Programmers who care about performance often find this kind of analysis hard to swallow. They have a point: sometimes the coefficients and the non-leading terms make a real difference. Sometimes the details of the hardware, the programming language, and the characteristics of the input make a big difference. And for small problems asymptotic behavior is irrelevant.

在乎性能的程序员常常觉得这类分析难以接受。他们有道理:有时系数和非主项确实会产生实质差异。有时硬件、编程语言和输入的特性的细节会造成很大差别。而对于小规模问题,渐近行为并不相干。

But if you keep those caveats in mind, algorithmic analysis is a useful tool. At least for large problems, the “better” algorithms is usually better, and sometimes it is much better. The difference between two algorithms with the same order of growth is usually a constant factor, but the difference between a good algorithm and a bad algorithm is unbounded!

但如果你把这些注意事项记在心里,算法分析就是一个有用的工具。至少对大规模问题来说,「更好」的算法通常确实更好,有时还好得多。两个增长量级相同的算法之间的差异通常只是一个常数因子,但好算法与坏算法之间的差异却是无界的!

B.2 Analysis of basic Python operations B.2 Python 基本操作的分析

Most arithmetic operations are constant time; multiplication usually takes longer than addition and subtraction, and division takes even longer, but these run times don’t depend on the magnitude of the operands. Very large integers are an exception; in that case the run time increases with the number of digits.

大多数算术运算是常数时间;乘法通常比加减法花的时间长,除法花的时间更长,但这些运行时间不取决于操作数的大小。极大的整数是个例外;那时运行时间随位数增加。

Indexing operations—reading or writing elements in a sequence or dictionary—are also constant time, regardless of the size of the data structure.

索引操作——在序列或字典中读取或写入元素——也是常数时间,与数据结构的大小无关。

A for loop that traverses a sequence or dictionary is usually linear, as long as all of the operations in the body of the loop are constant time. For example, adding up the elements of a list is linear:

只要循环体内的所有操作都是常数时间,遍历序列或字典的 for 循环通常是线性的。例如,把列表里的元素加起来就是线性的:
for000014

The built-in function for is also linear because it does the same thing, but it tends to be faster because it is a more efficient implementation; in the language of algorithmic analysis, it has a smaller leading coefficient.

内建函数 for 也是线性的,因为它做的是同一件事,但它往往更快,因为它是一个更高效的实现;用算法分析的术语来说,它的主项系数更小。

If you use the same loop to “add” a list of strings, the run time is quadratic because string concatenation is linear.

如果你用同样的循环去「加」一串字符串组成的列表,运行时间就是二次的,因为字符串拼接是线性的。

The string method for0 is usually faster because it is linear in the total length of the strings.

字符串方法 for0 通常更快,因为它关于字符串的总长度是线性的。

As a rule of thumb, if the body of a loop is in O(na) then the whole loop is in O(na+1). The exception is if you can show that the loop exits after a constant number of iterations. If a loop runs k times regardless of n, then the loop is in O(na), even for large k.

作为一个经验法则,如果循环体属于 O(na),那么整个循环就属于 O(na+1)。例外是:如果你能证明循环在常数次迭代后就退出。如果不管 n 是多少,循环都只跑 k 次,那么即便 k 很大,循环也属于 O(na)。

Multiplying by k doesn’t change the order of growth, but neither does dividing. So if the body of a loop is in O(na) and it runs n/k times, the loop is in O(na+1), even for large k.

乘以 k 不会改变增长量级,除以 k 也不会。所以如果循环体属于 O(na),而它跑 n/k 次,那么即便 k 很大,循环也属于 O(na+1)。

Most string and tuple operations are linear, except indexing and for, which are constant time. The built-in functions for and for are linear. The run-time of a slice operation is proportional to the length of the output, but independent of the size of the input.

大多数字符串和元组操作都是线性的,除了索引和 for,它们是常数时间。内建函数 forfor 是线性的。切片操作的运行时间与输出的长度成正比,但与输入的大小无关。

All string methods are linear, but if the lengths of the strings are bounded by a constant—for example, operations on single characters—they are considered constant time.

所有字符串方法都是线性的,但如果字符串的长度被某个常数限定——例如对单个字符的操作——它们就被视为常数时间。

Most list methods are linear, but there are some exceptions:

大多数列表方法是线性的,但也有一些例外:

Most dictionary operations and methods are constant time, but there are some exceptions:

大多数字典操作和方法都是常数时间,但也有一些例外:

The performance of dictionaries is one of the minor miracles of computer science. We will see how they work in Section B.4.

字典的性能是计算机科学中的小奇迹之一。我们将在 B.4 节看到它们是如何工作的。

Exercise 2

习题 2

Read the Wikipedia page on sorting algorithms at for000041 and answer the following questions:

阅读维基百科上关于排序算法的页面 for000042,并回答以下问题:
  1. What is a “comparison sort?” What is the best worst-case order of growth for a comparison sort? What is the best worst-case order of growth for any sort algorithm?
  2. What is the order of growth of bubble sort, and why does Barack Obama think it is “the wrong way to go?”
  3. What is the order of growth of radix sort? What preconditions do we need to use it?
  4. What is a stable sort and why might it matter in practice?
  5. What is the worst sorting algorithm (that has a name)?
  6. What sort algorithm does the C library use? What sort algorithm does Python use? Are these algorithms stable? You might have to Google around to find these answers.
  7. Many of the non-comparison sorts are linear, so why does does Python use an O(n logn) comparison sort?
  1. 什么是「比较排序」?比较排序在最坏情形下最好的增长量级是什么?任意排序算法在最坏情形下最好的增长量级是什么?
  2. 冒泡排序的增长量级是什么,为什么 Barack Obama 认为它是「走错了方向」?
  3. 基数排序的增长量级是什么?使用它需要哪些前提条件?
  4. 什么是稳定排序,为什么在实践中它可能很重要?
  5. 最差的(有名有姓的)排序算法是什么?
  6. C 标准库使用什么排序算法?Python 使用什么排序算法?这些算法稳定吗?你可能需要上网搜一搜才能找到这些答案。
  7. 许多非比较排序都是线性的,那么为什么 Python 用的是 O(n logn) 的比较排序?

B.3 Analysis of search algorithms B.3 搜索算法分析

A search is an algorithm that takes a collection and a target item and determines whether the target is in the collection, often returning the index of the target.

搜索是这样一种算法:它接受一个集合和一个目标项,判断目标是否在集合中,通常还会返回目标所在的索引。

The simplest search algorithm is a “linear search,” which traverses the items of the collection in order, stopping if it finds the target. In the worst case it has to traverse the entire collection, so the run time is linear.

最简单的搜索算法是「线性搜索」,它按顺序遍历集合中的项,找到目标就停下。在最坏情形下它必须遍历整个集合,所以运行时间是线性的。

The in operator for sequences uses a linear search; so do string methods like for0 and for00.

序列上的 in 运算符使用线性搜索;字符串方法 for0 和 for00 也是如此。

If the elements of the sequence are in order, you can use a bisection search, which is O(logn). Bisection search is similar to the algorithm you probably use to look a word up in a dictionary (a real dictionary, not the data structure). Instead of starting at the beginning and checking each item in order, you start with the item in the middle and check whether the word you are looking for comes before or after. If it comes before, then you search the first half of the sequence. Otherwise you search the second half. Either way, you cut the number of remaining items in half.

如果序列中的元素是有序的,你可以使用二分查找,它是 O(logn)。二分查找与你查纸质字典(真正的字典,不是数据结构)时大概会用的算法相似。它不是从开头起按顺序检查每一项,而是从中间的项入手,判断你要找的词在它之前还是之后。如果在之前,你就搜索序列的前半部分;否则搜索后半部分。无论哪种情况,你都把剩余项的数量砍掉一半。

If the sequence has 1,000,000 items, it will take about 20 steps to find the word or conclude that it’s not there. So that’s about 50,000 times faster than a linear search.

如果序列有一百万项,大约 20 步就能找到该词,或者断定它不在其中。所以这大约比线性搜索快 50,000 倍。

Exercise 3

习题 3

Write a function called for000049 that takes a sorted list and a target value and returns the index of the value in the list, if it’s there, or for0 if it’s not.

写一个名为 for000051 的函数,它接受一个已排序的列表和一个目标值,若值在列表中则返回其索引,否则返回 for0。

Or you could read the documentation of the for000 module and use that!

或者你也可以读一读 for000 模块的文档,直接用现成的!

Bisection search can be much faster than linear search, but it requires the sequence to be in order, which might require extra work.

二分查找可以比线性搜索快得多,但它要求序列是有序的,而这可能需要额外的工作。

There is another data structure, called a hashtable that is even faster—it can do a search in constant time—and it doesn’t require the items to be sorted. Python dictionaries are implemented using hashtables, which is why most dictionary operations, including the in operator, are constant time.

还有另一种数据结构,叫做散列表,它还要更快——它能以常数时间完成搜索——而且它不要求项是有序的。Python 字典就是用散列表实现的,这就是为什么包括 in 运算符在内的大多数字典操作都是常数时间。

B.4 Hashtables B.4 散列表

To explain how hashtables work and why their performance is so good, I start with a simple implementation of a map and gradually improve it until it’s a hashtable.

为了解释散列表如何工作、以及为什么它的性能这么好,我从一个简单的映射实现入手,逐步改进,直到变成散列表。

I use Python to demonstrate these implementations, but in real life you wouldn’t write code like this in Python; you would just use a dictionary! So for the rest of this chapter, you have to imagine that dictionaries don’t exist and you want to implement a data structure that maps from keys to values. The operations you have to implement are:

我用 Python 来演示这些实现,但在现实中你不会在 Python 里写出这样的代码;你直接用字典就好!所以在这一节的其余部分,你得假定字典并不存在,而你想实现一个把键映射到值的数据结构。你必须实现的操作是:
for000057:
Add a new item that maps from key k to value k. With a Python dictionary, k, this operation is written for00006.
for000062:
Look up and return the value that corresponds to key for000. With a Python dictionary, k, this operation is written for000065 or for000066.
for000067:
新增一个从键 k 映射到值 k 的项。在 Python 字典 k 中,这个操作写作 for00007。
for000072:
查找并返回与键 for000 对应的值。在 Python 字典 k 中,这个操作写作 for000075 或 for000076。

For now, I assume that each key only appears once. The simplest implementation of this interface uses a list of tuples, where each tuple is a key-value pair.

眼下我假设每个键只出现一次。这个接口最简单的实现使用元组列表,其中每个元组是一个键值对。
for000077

for appends a key-value tuple to the list of items, which takes constant time.

for 把一个键值元组追加到 items 列表末尾,这需要常数时间。

for uses a for loop to search the list: if it finds the target key it returns the corresponding value; otherwise it raises a for00008. So for is linear.

for 用一个 for 循环来搜索列表:如果找到目标键,就返回对应的值;否则抛出 for00008。所以 for 是线性的。

An alternative is to keep the list sorted by key. Then for could use a bisection search, which is O(logn). But inserting a new item in the middle of a list is linear, so this might not be the best option. There are other data structures (for000089) that can implement for and for in log time, but that’s still not as good as constant time, so let’s move on.

另一种办法是让列表按 key 保持有序。那样 for 就能用二分查找,即 O(logn)。但在列表中间插入新项是线性的,所以这可能不是最佳选择。还有别的数据结构(for000093)能在对数时间内实现 forfor,但仍不如常数时间好,所以我们就此打住。

One way to improve for000096 is to break the list of key-value pairs into smaller lists. Here’s an implementation called for000097, which is a list of 100 LinearMaps. As we’ll see in a second, the order of growth for for is still linear, but for000099 is a step on the path toward hashtables:

改进 for000100 的一种办法,是把键值对列表拆成更小的列表。下面是一个名为 for000101 的实现,它是一个由 100 个 LinearMap 组成的列表。我们马上会看到,for 的增长量级仍然是线性的,但 for000103 是通往散列表路上的一步:
for000104

for00010 makes a list of k for000107s.

for00010 创建一个由 kfor000110 组成的列表。

for00011 is used by for and for to figure out which map to put the new item in, or which map to search.

for00011 被 forfor 用来判断该把新项放进哪个 map,或者该去哪个 map 里搜索。

for00011 uses the built-in function for0, which takes almost any Python object and returns an integer. A limitation of this implementation is that it only works with hashable keys. Mutable types like lists and dictionaries are unhashable.

for00011 使用内建函数 for0,它几乎接受任何 Python 对象并返回一个整数。这个实现的一个限制是它只适用于可散列的键。像列表和字典这样的可变类型是「不可散列」的。

Hashable objects that are considered equal return the same hash value, but the converse is not necessarily true: two different objects can return the same hash value.

被视为相等的、可散列的对象会返回相同的散列值,但反过来不一定成立:两个不同的对象也可能返回相同的散列值。

for00012 uses the modulus operator to wrap the hash values into the range from 0 to for000122, so the result is a legal index into the list. Of course, this means that many different hash values will wrap onto the same index. But if the hash function spreads things out pretty evenly (which is what hash functions are designed to do), then we expect n/100 items per LinearMap.

for00012 用取模运算符把散列值卷叠到 0 到 for000124 的范围内,于是结果就成了列表的合法索引。当然,这意味着许多不同的散列值会被卷到同一个索引上。但如果散列函数把东西分布得相当均匀(这正是散列函数被设计来做的),那么我们预期每个 LinearMap 里大约有 n/100 个项。

Since the run time of for000125 is proportional to the number of items, we expect BetterMap to be about 100 times faster than LinearMap. The order of growth is still linear, but the leading coefficient is smaller. That’s nice, but still not as good as a hashtable.

由于 for000126 的运行时间与项的个数成正比,我们预期 BetterMap 大约比 LinearMap 快 100 倍。增长量级仍然是线性的,但主项系数更小了。这不错,但仍不如散列表。

Here (finally) is the crucial idea that makes hashtables fast: if you can keep the maximum length of the LinearMaps bounded, for000127 is constant time. All you have to do is keep track of the number of items and when the number of items per LinearMap exceeds a threshold, resize the hashtable by adding more LinearMaps.

下面(终于)是让散列表变快的关键想法:如果你能让 LinearMap 的最大长度有界,那么 for000128 就是常数时间。你只需要记录项的个数,并在每个 LinearMap 的项数超过某个阈值时,通过增加更多 LinearMap 来扩容散列表。

Here is an implementation of a hashtable:

下面是一个散列表的实现:
for000129

Each for0001 contains a for000131; for00013 starts with just 2 LinearMaps and initializes for, which keeps track of the number of items.

每个 for0001 包含一个 for000135;for00013 起始只有 2 个 LinearMap,并初始化 for,用来记录项的个数。

for just dispatches to for000139. The real work happens in for, which checks the number of items and the size of the for000141: if they are equal, the average number of items per LinearMap is 1, so it calls for000.

for 只是转交给 for000144。真正的工作发生在 for 里,它检查项的个数和 for000146 的大小:如果相等,每个 LinearMap 平均只有 1 个项,于是它调用 for000。

for000 make a new for000149, twice as big as the previous one, and then “rehashes” the items from the old map to the new.

for000 新建一个 for000151,大小是前一个的两倍,然后把旧 map 里的项「重新散列」到新 map 中。

Rehashing is necessary because changing the number of LinearMaps changes the denominator of the modulus operator in for00015. That means that some objects that used to wrap into the same LinearMap will get split up (which is what we wanted, right?).

重新散列是必要的,因为改变 LinearMap 的数量会改变 for00015 中取模运算符的分母。这意味着一些原本卷到同一个 LinearMap 的对象会被拆开(而这正是我们想要的,对吧?)。

Rehashing is linear, so for000 is linear, which might seem bad, since I promised that for would be constant time. But remember that we don’t have to resize every time, so for is usually constant time and only occasionally linear. The total amount of work to run for n times is proportional to n, so the average time of each for is constant time!

重新散列是线性的,所以 for000 是线性的,这看起来不太好,毕竟我保证过 for 是常数时间。但请记住,我们不必每次都扩容,所以 for 通常都是常数时间,只是偶尔变成线性的。for 运行 n 次的总工作量与 n 成正比,所以每次 for 的平均时间仍是常数时间!

To see how this works, think about starting with an empty HashTable and adding a sequence of items. We start with 2 LinearMaps, so the first 2 adds are fast (no resizing required). Let’s say that they take one unit of work each. The next add requires a resize, so we have to rehash the first two items (let’s call that 2 more units of work) and then add the third item (one more unit). Adding the next item costs 1 unit, so the total so far is 6 units of work for 4 items.

要看清楚这是怎么运作的,不妨想象从一个空的 HashTable 开始,往里依次添加一系列项。我们起始有 2 个 LinearMap,所以头两次 add 很快(不需要扩容)。假设它们各自花费 1 个单位工作量。下一次 add 需要扩容,所以我们得把头两项重新散列(姑且算是 2 个单位工作量),然后再加上第三项(又是 1 个单位)。再加下一项花费 1 个单位,所以到目前为止,4 个项总共是 6 个单位工作量。

The next for costs 5 units, but the next three are only one unit each, so the total is 14 units for the first 8 adds.

下一次 for 花费 5 个单位,但随后的三次每次只花 1 个单位,所以前 8 次 add 总共是 14 个单位。

The next for costs 9 units, but then we can add 7 more before the next resize, so the total is 30 units for the first 16 adds.

下一次 for 花费 9 个单位,但之后在下一次扩容之前还能再加 7 个,所以前 16 次 add 总共是 30 个单位。

After 32 adds, the total cost is 62 units, and I hope you are starting to see a pattern. After n adds, where n is a power of two, the total cost is 2n−2 units, so the average work per add is a little less than 2 units. When n is a power of two, that’s the best case; for other values of n the average work is a little higher, but that’s not important. The important thing is that it is O(1).

经过 32 次 add 后,总成本是 62 个单位,我希望你开始看出规律了。在 n 次 add 之后,当 n 是 2 的幂时,总成本是 2n−2 个单位,所以每次 add 的平均工作量略小于 2 个单位。当 n 是 2 的幂时,这是最好的情形;对于 n 的其他取值,平均工作量会略高一些,但这并不重要。重要的是它是 O(1)。

Figure B.1 shows how this works graphically. Each block represents a unit of work. The columns show the total work for each add in order from left to right: the first two for0 cost 1 units, the third costs 3 units, etc.

图 B.1 用图形展示了这是如何运作的。每个方块代表一个单位的工作量。各列从左到右依次显示每次 add 的总工作量:头两次 for0 各花 1 个单位,第三次花 3 个单位,依此类推。

Figure B.1: The cost of a hashtable add.

图 B.1:散列表 add 操作的成本。(原书插图未收录)

The extra work of rehashing appears as a sequence of increasingly tall towers with increasing space between them. Now if you knock over the towers, amortizing the cost of resizing over all adds, you can see graphically that the total cost after n adds is 2n − 2.

重新散列的额外工作量呈现为一连串越来越高的「塔」,彼此间隔也越来越大。现在如果你把这些塔推倒,把扩容的成本摊还到所有 add 上,你就能从图形上看出 n 次 add 后的总成本是 2n − 2。

An important feature of this algorithm is that when we resize the HashTable it grows geometrically; that is, we multiply the size by a constant. If you increase the size arithmetically—adding a fixed number each time—the average time per for is linear.

这个算法的一个重要特性是:当我们扩容 HashTable 时,它是按几何级数增长的;也就是说,我们把大小乘以一个常数。如果你按算术级数增大——每次加一个固定的数——那么每次 for 的平均时间就是线性的。

You can download my implementation of HashMap from for000172, but remember that there is no reason to use it; if you want a map, just use a Python dictionary.

你可以从 for000173 下载我实现的 HashMap,但请记住,没有理由去用它;如果你想要一个映射,直接用 Python 字典就好。
1
But if you get a question like this in an interview, I think a better answer is, “The fastest way to sort a million integers is to use whatever sort function is provided by the language I’m using. Its performance is good enough for the vast majority of applications, but if it turned out that my application was too slow, I would use a profiler to see where the time was being spent. If it looked like a faster sort algorithm would have a significant effect on performance, then I would look around for a good implementation of radix sort.”
1
不过,如果你在面试中遇到这样的问题,我觉得更好的回答是:「对一百万个整数排序最快的办法,是使用我所用语言提供的任何排序函数。它的性能对绝大多数应用来说都足够好了;但如果事实证明我的应用太慢,我会用一个性能分析器看看时间花在了哪里。如果看起来用一个更快的排序算法会对性能有显著影响,那我才会去找一个优秀的基数排序实现。」