Skip to content

Commit e48ebf6

Browse files
committed
Deployed 8bdb901 with MkDocs version: 1.0.4
1 parent dcf2969 commit e48ebf6

File tree

22 files changed

+97
-162
lines changed

22 files changed

+97
-162
lines changed

01_抽象数据类型和面向对象编程/ADT_OOP/index.html

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,12 @@
227227

228228
<h1 id="python">Python 一切皆对象</h1>
229229
<p>举个例子,在 python 中我们经常使用的 list</p>
230-
<pre><code class="py">l = list() # 实例化一个 list 对象 l
230+
<pre><code class="language-py">l = list() # 实例化一个 list 对象 l
231231
l.append(1) # 调用 l 的 append 方法
232232
l.append(2)
233233
l.remove(1)
234234
print(len(l)) # 调用对象的 `__len__` 方法
235235
</code></pre>
236-
237236
<p>在后面实现新的数据类型时,我们将使用 python 的 class 实现,它包含属性和方法。
238237
属性一般是使用某种特定的数据类型,而方法一般是对属性的操作。
239238
这里你只需了解这么多就行了, 我们不会使用继承等特性。</p>
@@ -242,11 +241,10 @@ <h1 id="adt">什么是抽象数据类型 ADT</h1>
242241
<p>ADT: Abstract Data Type,抽象数据类型,我们在组合已有的数据结构来实现一种新的数据类型, ADT 定义了类型的数据和操作。</p>
243242
<p>我们以抽象一个背包(Bag) 数据类型来说明,背包是一种容器类型,我们可以给它添加东西,也可以移除东西,并且我们想知道背包里
244243
有多少东西。于是我们可以定义一个新的数据类型叫做 Bag.</p>
245-
<pre><code class="py">class Bag:
244+
<pre><code class="language-py">class Bag:
246245
&quot;&quot;&quot; 背包类型 &quot;&quot;&quot;
247246
pass
248247
</code></pre>
249-
250248
<h1 id="bag-adt">实现一个 Bag ADT</h1>
251249
<p>视频中我们将使用 python 的 class 来实现一个新的容器类型叫做 Bag。</p>
252250
<h1 id="adt_1">实现 ADT 我们应该注意什么?</h1>

03_链表/linked_list/index.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,19 +242,17 @@ <h1 id="_2">单链表</h1>
242242
这里可不要混淆了列表和链表(它们的中文发音类似,但是列表 list 底层其实还是线性结构,链表才是真的通过指针关联的链式结构)。
243243
看到指针你也不用怕,这里我们用的 python,你只需要一个简单赋值操作就能实现,不用担心 c 语言里复杂的指针。</p>
244244
<p>先来定义一个链接表的节点,刚才说到有一个指针保存下一个节点的位置,我们叫它 next, 当然还需要一个 value 属性保存值</p>
245-
<pre><code class="py">class Node(object):
245+
<pre><code class="language-py">class Node(object):
246246
def __init__(self, value, next=None):
247247
self.value = value
248248
self.next = next
249249
</code></pre>
250-
251250
<p>然后就是我们的单链表 LinkedList ADT:</p>
252-
<pre><code class="py">class LinkedList(object):
251+
<pre><code class="language-py">class LinkedList(object):
253252
&quot;&quot;&quot; 链接表 ADT
254253
[root] -&gt; [node0] -&gt; [node1] -&gt; [node2]
255254
&quot;&quot;&quot;
256255
</code></pre>
257-
258256
<p>实现我们会在视频中用画图来模拟并且手动代码实现,代码里我们会标识每个步骤的时间复杂度。这里请高度集中精力,
259257
虽然链表的思想很简单,但是想要正确写对链表的操作代码可不容易,稍不留神就可能丢失一些步骤。
260258
这里我们还是会用简单的单测来验证代码是否按照预期工作。</p>
@@ -292,15 +290,14 @@ <h1 id="_3">双链表</h1>
292290
并把它追加到访问表的最后一个位置,这个时候单链表就满足不了了,
293291
因为缓存在 dict 里查找的时间是 O(1),你更新访问顺序就 O(n)了,缓存就没了优势。</p>
294292
<p>这里就要使用到双链表了,相比单链表来说,每个节点既保存了指向下一个节点的指针,同时还保存了上一个节点的指针。</p>
295-
<pre><code class="py">class Node(object):
293+
<pre><code class="language-py">class Node(object):
296294
# 如果节点很多,我们可以用 __slots__ 来节省内存,把属性保存在一个 tuple 而不是 dict 里
297295
# 感兴趣可以自行搜索 python __slots__
298296
__slots__ = ('value', 'prev', 'next')
299297

300298
def __init__(self, value=None, prev=None, next=None):
301299
self.value, self.prev, self.next = value, prev, next
302300
</code></pre>
303-
304301
<p>对, 就多了 prev,有啥优势嘛?</p>
305302
<ul>
306303
<li>看似我们反过来遍历双链表了。反过来从哪里开始呢?我们只要让 root 的 prev 指向 tail 节点,不就串起来了吗?</li>

04_队列/queue/index.html

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,16 @@ <h1 id="queue">队列 Queue</h1>
242242
<p>我们先来看看 list 可以不?对照这个三个需求,看看能否满足:</p>
243243
<ul>
244244
<li>1.我们选择了 list</li>
245-
<li>2.看起来队列需要从头删除,向尾部增加元素,也就是 list.insert(0, element) 和 list.append(element)</li>
246-
<li>3.嗯,貌似 list.insert(0, element) 会导致所有list元素后移,O(n)复杂度。append 平均倒是O(1),但是如果内存不够还要重新分配内存。</li>
245+
<li>2.看起来队列需要从头删除,向尾部增加元素,也就是 list.pop(0) 和 list.append(element)</li>
246+
<li>3.嗯,貌似 list.pop(0) 会导致所有其后所有元素向前移动一个位置,O(n)复杂度。append 平均倒是O(1),但是如果内存不够还要重新分配内存。</li>
247+
</ul>
248+
<p>你看,使用了 list 的话频繁 pop(0) 是非常低效的。(当然list 实现还有另一种方式就是插入用 list.insert(0, item),删除用list.pop())</p>
249+
<p>脑子再转转, 我们第二章实现了 链表 LinkedList,看看能否满足要求:</p>
250+
<ul>
251+
<li>1.这里选择 LinkedList</li>
252+
<li>2.删除头元素 LinkedList.popleft(),追加 append(element)。都可以满足</li>
253+
<li>3.哇欧,这两个操作都是 O(1) 的,完美。</li>
247254
</ul>
248-
<p>你看,使用了 list 的话频繁 insert(0, element) 和 append 都是非常低效的。</p>
249-
<p>脑子再转转, 我们第二章实现了 链表 LinkedList,看看能否满足要求:
250-
- 1.这里选择 LinkedList
251-
- 2.删除头元素 LinkedList.popleft(),追加 append(element)。都可以满足
252-
- 3.哇欧,这两个操作都是 O(1) 的,完美。</p>
253255
<p>好, 就用 LinkedList 了,我们开始实现,具体看视频。这次实现我们还将演示自定义异常和测试异常。</p>
254256
<h1 id="_2">用数组实现队列</h1>
255257
<p>难道用数组就不能实现队列了吗?其实还是可以的。只不过数组是预先分配固定内存的,所以如果你知道了队列的最大长度,也是
@@ -261,11 +263,10 @@ <h1 id="_2">用数组实现队列</h1>
261263
<p>head = 0,1,2,3,4 ... 0,1,2,3,4 ...</p>
262264
<p>重头再来,循环往复,仿佛一个轮回。。。。
263265
怎么重头来呢?看上边数组的规律你如果还想不起来用取模,估计小学数学是体育老师教的。</p>
264-
<pre><code class="py">maxsize = 5
266+
<pre><code class="language-py">maxsize = 5
265267
for i in range(100):
266268
print(i % maxsize)
267269
</code></pre>
268-
269270
<p><img alt="" src="../array_queue.png" /></p>
270271
<p>我们来实现一个空间有限的循环队列。ArrayQueue,它的实现很简单,但是缺点是需要预先知道队列的长度来分配内存。</p>
271272
<h1 id="double-ended-queue">双端队列 Double ended Queue</h1>

05_栈/stack/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,10 @@ <h1 id="adt">栈 ADT</h1>
249249
<h1 id="stack-over-flow">Stack over flow 什么鬼?</h1>
250250
<p>嗯,stackoverflow 不是一个程序员问答网站吗?没错。
251251
函数的临时变量是存储在栈区的,如果你不幸写了一个没有出口的递归函数,就会这个错。不信你试试:</p>
252-
<pre><code class="py">def infinite_fib(n):
252+
<pre><code class="language-py">def infinite_fib(n):
253253
return infinite_fib(n-1) + infinite_fib(n-2)
254254
infinite_fib(10)
255255
</code></pre>
256-
257256
<p>一大段输出之后就会出现异常: RecursionError: maximum recursion depth exceeded。
258257
后边会讲到递归,递归是初学者比较难理解的概念,在树的遍历等地方还会看到它。</p>
259258
<h1 id="_2">数据结构头脑风暴法</h1>

06_算法分析/big_o/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ <h1 id="o">大 O 表示法</h1>
251251
</script>
252252
</p>
253253
<p>这里列举两种方式:</p>
254-
<pre><code class="py"># version1
254+
<pre><code class="language-py"># version1
255255
total_sum = 0
256256
for i in range(n):
257257
row_sum[i] = 0
@@ -267,7 +267,6 @@ <h1 id="o">大 O 表示法</h1>
267267
row_sum[i] = row_sum[i] + matrix[i, j]
268268
total_sum = total_sum + row_sum[i] # 注意这里和上边的不同
269269
</code></pre>
270-
271270
<p>v1 版本的关键操作在 j 循环里,两步加法操作,由于嵌套在第一个循环里,操作步骤是 <script type="math/tex"> (2n) * n = 2n^2 </script></p>
272271
<p>v2 版本的 total_sum 只有 n 次操作,它的操作次数是 <script type="math/tex"> n + n*n = n^2 + n </script></p>
273272
<p>这里你可能还感觉不到它们有多大差别,因为计算机执行的太快了,但是当 n 增长特别快的时候,总的操作次数差距就很明显了:</p>

07_哈希表/hashtable/index.html

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ <h1 id="_2">哈希表的工作过程</h1>
249249
<p>我们还是以书中的例子说明,假如我们有一个数组 T,包含 M=13 个元素,我们可以定义一个简单的哈希函数 h</p>
250250
<pre><code>h(key) = key % M
251251
</code></pre>
252-
253252
<p>这里取模运算使得 h(key) 的结果不会超过数组的长度下标。我们来分别插入以下元素:</p>
254253
<p>765, 431, 96, 142, 579, 226, 903, 388</p>
255254
<p>先来计算下它们应用哈希函数后的结果:</p>
@@ -263,7 +262,6 @@ <h1 id="_2">哈希表的工作过程</h1>
263262
h(903) = 903 % M = 6
264263
h(388) = 388 % M = 11
265264
</code></pre>
266-
267265
<p>下边我画个图演示整个插入过程(纯手工绘制,原谅我字写得不太优雅):</p>
268266
<p><img alt="" src="../insert_hash.png" /></p>
269267
<h1 id="collision">哈希冲突 (collision)</h1>
@@ -284,7 +282,7 @@ <h1 id="collision">哈希冲突 (collision)</h1>
284282
</ul>
285283
<p>我们选一个简单的二次探查函数 <script type="math/tex"> h(k, i) = (home + i^2) \% m </script>,它的意思是如果
286284
遇到了冲突,我们就在原始计算的位置不断加上 i 的平方。我写了段代码来模拟整个计算下标的过程:</p>
287-
<pre><code class="py">inserted_index_set = set()
285+
<pre><code class="language-py">inserted_index_set = set()
288286
M = 13
289287

290288
def h(key, M=13):
@@ -303,7 +301,6 @@ <h1 id="collision">哈希冲突 (collision)</h1>
303301
print('h({number}) = {number} % M = {index}'.format(number=number, index=index))
304302
inserted_index_set.add(index)
305303
</code></pre>
306-
307304
<p>这段代码输出的结果如下:</p>
308305
<pre><code>h(765) = 765 % M = 11
309306
h(431) = 431 % M = 2
@@ -321,7 +318,6 @@ <h1 id="collision">哈希冲突 (collision)</h1>
321318
h(388) = 388 % M = 7 collision
322319
h(388) = 388 % M = 1
323320
</code></pre>
324-
325321
<p>遇到冲突之后会重新计算,每个待插入元素最终的下标就是:</p>
326322
<p><img alt="" src="../quadratic_hash.png" /></p>
327323
<p><img alt="" src="../quadratic_result.png" /></p>
@@ -344,7 +340,6 @@ <h1 id="cpython">Cpython 如何解决哈希冲突</h1>
344340

345341
0 -&gt; 1 -&gt; 6 -&gt; 7 -&gt; 4 -&gt; 5 -&gt; 2 -&gt; 3 -&gt; 0 [and here it's repeating]
346342
</code></pre>
347-
348343
<h1 id="_3">哈希函数</h1>
349344
<p>到这里你应该明白哈希表插入的工作原理了,不过有个重要的问题之前没提到,就是 hash 函数怎么选?
350345
当然是散列得到的冲突越来越小就好啦,也就是说每个 key 都能尽量被等可能地散列到 m 个槽中的任何一个,并且与其他 key 被散列到哪个槽位无关。
@@ -366,7 +361,7 @@ <h1 id="hashtable-adt">HashTable ADT</h1>
366361
<li>get(key, default)</li>
367362
<li>remove(key)</li>
368363
</ul>
369-
<pre><code class="py">class Slot(object):
364+
<pre><code class="language-py">class Slot(object):
370365
&quot;&quot;&quot;定义一个 hash 表 数组的槽
371366
注意,一个槽有三种状态,看你能否想明白
372367
1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了
@@ -379,7 +374,6 @@ <h1 id="hashtable-adt">HashTable ADT</h1>
379374
class HashTable(object):
380375
pass
381376
</code></pre>
382-
383377
<p>具体的实现和代码编写在视频里讲解。这个代码可不太好实现,稍不留神就会有错,我们还是通过编写单元测试验证代码的正确性。</p>
384378
<h1 id="_4">思考题</h1>
385379
<ul>

08_字典/dict/index.html

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,26 +237,23 @@ <h1 id="dict-adt">实现 dict ADT</h1>
237237
<p>其实上边 HashTable 实现的三个基本方法就是我们使用字典最常用的三个基本方法, 这里我们继承一下这个类,
238238
然后实现更多 dict 支持的方法,items(), keys(), values()。不过需要注意的是,在 python2 和 python3 里这些方法
239239
的返回是不同的,python3 里一大改进就是不再返回浪费内存的 列表,而是返回迭代器,你要获得列表必须用 list() 转换成列表。 这里我们实现 python3 的方式返回迭代器。</p>
240-
<pre><code class="py">class DictADT(HashTable):
240+
<pre><code class="language-py">class DictADT(HashTable):
241241
pass
242242
</code></pre>
243-
244243
<p>视频里我们将演示如何实现这些方法,并且写单测验证正确性。</p>
245244
<h1 id="hashable">Hashable</h1>
246245
<p>作为 dict 的 key 必须是可哈希的,也就是说不能是 list 等可变对象。不信你在 ipython 里运行如下代码:</p>
247-
<pre><code class="py">d = dict()
246+
<pre><code class="language-py">d = dict()
248247
d[[1]] = 1
249248
# TypeError: unhashable type: 'list'
250249
</code></pre>
251-
252250
<p>我引用 python 文档里的说法,大家可以自己理解下:</p>
253251
<pre><code>An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() or __cmp__() method). Hashable objects which compare equal must have the same hash value.
254252

255253
Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally.
256254

257255
All of Python’s immutable built-in objects are hashable, while no mutable containers (such as lists or dictionaries) are. Objects which are instances of user-defined classes are hashable by default; they all compare unequal (except with themselves), and their hash value is derived from their id().
258256
</code></pre>
259-
260257
<h1 id="_1">思考题:</h1>
261258
<ul>
262259
<li>你能在哈希表的基础上实现 dict 的其他操作吗?</li>

09_集合/set/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,12 @@ <h1 id="python-frozenset">python frozenset</h1>
247247
它的常见就是用一个可迭代对象初始化它,然后只用来判重等操作。</p>
248248
<h1 id="set-adt">实现一个 set ADT</h1>
249249
<p>如何实现一个集合的 ADT 呢,其实还是个哈希表,哈希表不是有 key 和 value 嘛,咱把 value 置为 1 不就行了。</p>
250-
<pre><code class="py">class SetADT(HashTable):
250+
<pre><code class="language-py">class SetADT(HashTable):
251251

252252
def add(self, key):
253253
# 集合其实就是一个 dict,只不过我们把它的 value 设置成 1
254254
return super(SetADT, self).add(key, True)
255255
</code></pre>
256-
257256
<p>当然其它数学上的操作就麻烦点了,不过也很容易实现。</p>
258257
<h1 id="_2">思考题</h1>
259258
<ul>

10_递归/recursion/index.html

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,12 @@ <h1 id="_2">什么是递归?</h1>
246246
这里举一个和其他很多老套的教科书一样喜欢举的例子,阶乘函数,我觉得用来它演示再直观不过。它的定义是这样的:</p>
247247
<p><img alt="" src="../fact.png" /></p>
248248
<p>我们很容易根据它的定义写出这样一个递归函数,因为它本身就是递归定义的。</p>
249-
<pre><code class="py">def fact(n):
249+
<pre><code class="language-py">def fact(n):
250250
if n == 0:
251251
return 1
252252
else:
253253
return n * fact(n-1)
254254
</code></pre>
255-
256255
<p>看吧,几乎完全是按照定义来写的。我们来看下递归函数的几个特点:</p>
257256
<ul>
258257
<li>递归必须包含一个基本的出口(base case),否则就会无限递归,最终导致栈溢出。比如这里就是 n == 0 返回 1</li>
@@ -262,29 +261,26 @@ <h1 id="_2">什么是递归?</h1>
262261
<h1 id="_3">调用栈</h1>
263262
<p>看了上一个例子你可能觉得递归好简单,先别着急,我们再举个简单的例子,上边我们并没有讲递归如何工作的。
264263
假如让你输出从 1 到 10 这十个数字,如果你是个正常人的话,我想你的第一反应都是这么写:</p>
265-
<pre><code class="py">def print_num(n):
264+
<pre><code class="language-py">def print_num(n):
266265
for i in range(1, n + 1): # 注意很多编程语言使用的都是 从 0 开始的左闭右开区间, python 也不例外
267266
print(i)
268267

269268

270269
if __name__ == '__main__':
271270
print_num(10)
272271
</code></pre>
273-
274272
<p>我们尝试写一个递归版本,不就是自己调用自己嘛:</p>
275-
<pre><code class="py">def print_num_recursive(n):
273+
<pre><code class="language-py">def print_num_recursive(n):
276274
if n &gt; 0:
277275
print_num_recursive(n-1)
278276
print(n)
279277
</code></pre>
280-
281278
<p>你猜下它的输出?然后我们调换下 print 顺序,你再猜下它的输出</p>
282-
<pre><code class="py">def print_num_recursive_revserve(n):
279+
<pre><code class="language-py">def print_num_recursive_revserve(n):
283280
if n &gt; 0:
284281
print(n)
285282
print_num_recursive_revserve(n-1)
286283
</code></pre>
287-
288284
<p>你能明白是为什么吗?我建议你运行下这几个小例子,它们很简单但是却能说明问题。
289285
计算机内部使用调用栈来实现递归,这里的栈一方面指的是内存中的栈区,一方面栈又是之前讲到的后进先出这种数据结构。
290286
每当进入递归函数的时候,系统都会为当前函数开辟内存保存当前变量值等信息,每个调用栈之间的数据互不影响,新调用的函数
@@ -295,7 +291,7 @@ <h1 id="_3">调用栈</h1>
295291
<h1 id="_4">用栈模拟递归</h1>
296292
<p>刚才说到了调用栈,我们就用栈来模拟一把。之前栈这一章我们讲了如何自己实现栈,不过这里为了不拷贝太多代码,我们直接用 collections.deque 就可以
297293
快速实现一个简单的栈。</p>
298-
<pre><code class="py">from collections import deque
294+
<pre><code class="language-py">from collections import deque
299295

300296

301297
class Stack(object):
@@ -321,11 +317,10 @@ <h1 id="_4">用栈模拟递归</h1>
321317
while not s.is_empty(): # 参数弹出
322318
print(s.pop())
323319
</code></pre>
324-
325320
<p>这里结果也是输出 1 到 10,只不过我们是手动模拟了入栈和出栈的过程,帮助你理解计算机是如何实现递归的,是不是挺简单!现在你能明白为什么上边 print_num_recursive print_num_recursive_revserve 两个函数输出的区别了吗?</p>
326321
<h1 id="_5">尾递归</h1>
327322
<p>上边的代码示例(麻雀虽小五脏俱全)中实际上包含了两种形式的递归,一种是普通的递归,还有一种叫做尾递归:</p>
328-
<pre><code class="py">def print_num_recursive(n):
323+
<pre><code class="language-py">def print_num_recursive(n):
329324
if n &gt; 0:
330325
print_num_recursive(n-1)
331326
print(n)
@@ -336,7 +331,6 @@ <h1 id="_5">尾递归</h1>
336331
print(n)
337332
print_num_recursive_revserve(n-1) # 尾递归
338333
</code></pre>
339-
340334
<p>概念上它很简单,就是递归调用放在了函数的最后。有什么用呢?
341335
普通的递归, 每一级递归都产生了新的局部变量, 必须创建新的调用栈, 随着递归深度的增加, 创建的栈越来越多, 造成爆栈。虽然尾递归调用也会创建新的栈,
342336
但是我们可以优化使得尾递归的每一级调用共用一个栈!, 如此便可解决爆栈和递归深度限制的问题!
@@ -366,7 +360,7 @@ <h1 id="_6">汉诺塔问题</h1>
366360
<li>把 n-1 个盘子从 I 移动到 D,借助 S</li>
367361
</ul>
368362
<p>我们把它转换成代码:</p>
369-
<pre><code class="py">def hanoi_move(n, source, dest, intermediate):
363+
<pre><code class="language-py">def hanoi_move(n, source, dest, intermediate):
370364
if n &gt;= 1: # 递归出口,只剩一个盘子
371365
hanoi_move(n-1, source, intermediate, dest)
372366
print(&quot;Move %s -&gt; %s&quot; % (source, dest))
@@ -384,7 +378,6 @@ <h1 id="_6">汉诺塔问题</h1>
384378
Move A -&gt; C
385379
&quot;&quot;&quot;
386380
</code></pre>
387-
388381
<p><center>
389382
<img alt="三个盘子的汉诺塔解法" src="../hanoi.gif" />
390383
</center></p>

0 commit comments

Comments
 (0)