-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
589 lines (297 loc) · 346 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>贾维斯的小屋-人工智能学习个人网站</title>
<link href="/atom.xml" rel="self"/>
<link href="http://yoursite.com/"/>
<updated>2020-06-04T07:20:02.000Z</updated>
<id>http://yoursite.com/</id>
<author>
<name>Xudong Li</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>数据结构与算法——优先队列</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97/</id>
<published>2020-06-04T07:19:12.000Z</published>
<updated>2020-06-04T07:20:02.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、优先队列"><a href="#一、优先队列" class="headerlink" title="一、优先队列"></a><strong>一、优先队列</strong></h1><p>优先队列按照队列的方式正常入队,但按照优先级出队。有两种实现方式:<strong>堆</strong>(二插堆、多项式堆等等)和<strong>二叉搜索树</strong>。这里重点讲解二叉堆,关于二叉搜索树的内容<a href="https://blog.csdn.net/u014157632/article/details/104842425" target="_blank" rel="noopener">见这篇文章</a>。</p><p>堆是一种特殊的完全二叉树。<strong>大根堆:</strong> 完全二叉树的任一节点都比其孩子节点大。<strong>小根堆:</strong> 完全二叉树的任一节点都比其孩子节点小。<strong>堆的向下调整:</strong> 假设根节点的左右子树都是堆,但根节点不满足堆的性质,可以通过一次向下调整将其变成一个堆。因为二叉堆是完全二叉树,一般可以用数组来表示,这样不会浪费空间。大根堆和小根堆的举例如下所示:<br><img src="https://img-blog.csdnimg.cn/20200421172621204.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>用数组表示的二叉堆,有如下性质:</p><ul><li>第$i$个节点的左孩子的下标为$2<em>i+1$,右孩子的下标为$2</em>i+2$。</li><li>第$i$个节点的父亲节点的下标为$(i-1)//2$</li></ul><p><strong>二叉堆的时间复杂度:</strong></p><ul><li>插入操作:$O(\log n)$</li><li>删除操作:$O(\log n)$</li><li>查询最小值:$O(1)$</li></ul><p><strong>python实现:heapq</strong></p><pre><code class="hljs python"><span class="hljs-keyword">import</span> heapq<span class="hljs-comment"># 将array列表转为堆的结构</span>heap.heapify(array)<span class="hljs-comment"># 弹出堆中的最小值</span>heapq.heappop(array)<span class="hljs-comment"># 往堆中插入新值a</span>heapq.heappush(array, a)<span class="hljs-comment"># 先进行heappop(array),再进行heappush(array, a)操作</span>heapq.heapreplace(array, a)<span class="hljs-comment"># 获得array中前k个最大的值</span>heapq.nlargest(k, array)<span class="hljs-comment"># 获得array中前k个最小的值</span>heapq.nsmallest(k, array)</code></pre><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h3 id="1、合并k个有序链表"><a href="#1、合并k个有序链表" class="headerlink" title="1、合并k个有序链表"></a><strong>1、合并k个有序链表</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/merge-k-sorted-lists/" target="_blank" rel="noopener">第23题</a><br>合并 k 个排序链表,返回合并后的排序链表。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> heapq<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mergeKLists</span><span class="hljs-params">(self, lists: List[ListNode])</span> -> ListNode:</span> head = p = ListNode(<span class="hljs-number">0</span>) <span class="hljs-comment"># 建立堆</span> a = [] <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(lists)): <span class="hljs-keyword">if</span> lists[i] : <span class="hljs-comment"># 元组在heapq里比较的机制是从元组首位0开始,即遇到相同,就比较元组下一位</span> <span class="hljs-comment"># 比如(1,2), (1,3),前者比后者小。</span> <span class="hljs-comment"># 这题刚好node值有重复的,同时ListNode无法被比较,所以会报错</span> heapq.heappush(a, (lists[i].val, i)) lists[i] = lists[i].next <span class="hljs-keyword">while</span> a: val, idx = heapq.heappop(a) p.next = ListNode(val) p = p.next <span class="hljs-keyword">if</span> lists[idx]: heapq.heappush(a, (lists[idx].val, idx)) lists[idx] = lists[idx].next <span class="hljs-keyword">return</span> head.next</code></pre><ul><li>时间复杂度:$O(n \log k)$</li><li>空间复杂度:$O(k+n)$,最小堆需要k个空间,新链表需要n个空间</li></ul><h3 id="2、数据流中的第k大元素"><a href="#2、数据流中的第k大元素" class="headerlink" title="2、数据流中的第k大元素"></a><strong>2、数据流中的第k大元素</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/" target="_blank" rel="noopener">第703题</a><br>设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add,返回当前数据流中第K大的元素。</p><pre><code class="hljs python"><span class="hljs-keyword">import</span> heapq<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">KthLargest</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, k: int, nums: List[int])</span>:</span> self.nums = nums self.k = k heapq.heapify(self.nums) <span class="hljs-comment"># 留下k个元素,即前k大的</span> <span class="hljs-keyword">while</span> len(self.nums) > k: heapq.heappop(self.nums) <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add</span><span class="hljs-params">(self, val: int)</span> -> int:</span> <span class="hljs-keyword">if</span> len(self.nums) < self.k: heapq.heappush(self.nums, val) <span class="hljs-keyword">elif</span> self.nums[<span class="hljs-number">0</span>] < val: <span class="hljs-comment"># 新的值更大则更新</span> heapq.heapreplace(self.nums, val) <span class="hljs-keyword">return</span> self.nums[<span class="hljs-number">0</span>]</code></pre><!--### **3、滑动窗口最大值**此题为leetcode[第239题](https://leetcode-cn.com/problems/sliding-window-maximum/)-->]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——并查集</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%B9%B6%E6%9F%A5%E9%9B%86/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%B9%B6%E6%9F%A5%E9%9B%86/</id>
<published>2020-06-04T07:18:27.000Z</published>
<updated>2020-06-04T07:18:56.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、并查集"><a href="#一、并查集" class="headerlink" title="一、并查集"></a><strong>一、并查集</strong></h1><p><strong>并查集(Union & find)</strong> 是一种树形的数据结构,用于处理一些不交集(Disjoint sets)的合并与查询的问题。初始化时把每个点所在集合初始化为其自身。<br><strong>Find:</strong> 确定元素属于哪一个子集,它可以被用来确定两个元素是否属于同一子集。<br><strong>Union:</strong> 将两个子集合并成同一个子集。</p><p>如下图所示,一开始有7个字母,每个都指向自己:<br><img src="https://img-blog.csdnimg.cn/2020041722325297.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>根据某种规则,将相关的字母合并起来,即某个字母会指向另一个字母。假设合并之后是下面的样子:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200417223339122.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="280" height="230"> </p><p align="center"> </p><p></p><p>上图是个树形的结构,左边的集合(a)的根节点为<code>a</code>,它的深度为2,这里树的深度我们称之为<strong>秩(rank)</strong>。对于这样的树结构,我们如果要合并两个树,可以将秩低的树合并到秩高的树,这样不会增加整个树的秩,如下图所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200417223935613.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="250" height="280"> </p><p align="center"> </p><p></p><p>对于并查集,还有一种优化的方式,即<strong>路径压缩</strong>。我们希望每个节点到根节点的路径尽可能地短,可以将每个节点的父节点设为根节点,比如(a)可以压缩为:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200417224312779.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="250" height="200"> </p><p align="center"> </p><p></p><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h3 id="(1)岛屿数量"><a href="#(1)岛屿数量" class="headerlink" title="(1)岛屿数量"></a><strong>(1)岛屿数量</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/number-of-islands/submissions/" target="_blank" rel="noopener">第200题</a><br>此题可以用DFS或BFS解,这两种解法<a href="https://blog.csdn.net/u014157632/article/details/104886479" target="_blank" rel="noopener">点这里</a>。下面我们用并查集的方法解题。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UnionFind</span><span class="hljs-params">(object)</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, grid)</span>:</span> m, n = len(grid), len(grid[<span class="hljs-number">0</span>]) self.count = <span class="hljs-number">0</span> self.parent = [<span class="hljs-number">-1</span>] * (m * n) <span class="hljs-comment"># 一维数组表示并查集</span> self.rank = [<span class="hljs-number">0</span>] * (m * n) <span class="hljs-comment"># 初始化,为1的格子指向自己</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n): <span class="hljs-keyword">if</span> grid[i][j] == <span class="hljs-number">1</span>: self.parent[i * n + j] = i * n + j self.count += <span class="hljs-number">1</span> <span class="hljs-comment"># 找根节点</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find</span><span class="hljs-params">(self, i)</span>:</span> <span class="hljs-keyword">if</span> self.parent[i] != i: self.parent[i] = self.find(self.parent[i]) <span class="hljs-keyword">return</span> self.parent[i] <span class="hljs-comment"># 合并</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">union</span><span class="hljs-params">(self, x, y)</span>:</span> rootx = self.find(x) rooty = self.find(y) <span class="hljs-keyword">if</span> rootx != rooty: <span class="hljs-keyword">if</span> self.rank[rootx] > self.rank[rooty]: <span class="hljs-comment"># 低秩合并到高秩</span> self.parent[rooty] = rootx <span class="hljs-keyword">elif</span> self.rank[rootx] < self.rank[rooty]: self.parent[rootx] = rooty <span class="hljs-keyword">else</span>: self.parent[rooty] = rootx self.rank[rootx] += <span class="hljs-number">1</span> self.count -= <span class="hljs-number">1</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">numIslands</span><span class="hljs-params">(self, grid: List[List[str]])</span> -> int:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> grid <span class="hljs-keyword">or</span> len(grid[<span class="hljs-number">0</span>]) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> grid = [[int(i) <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> a] <span class="hljs-keyword">for</span> a <span class="hljs-keyword">in</span> grid] <span class="hljs-comment"># str-->int</span> directions = [(<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">-1</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>)] uf = UnionFind(grid) <span class="hljs-comment"># 实例化并查集</span> m, n = len(grid), len(grid[<span class="hljs-number">0</span>]) <span class="hljs-comment"># 遍历每个元素</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n): <span class="hljs-keyword">if</span> grid[i][j] == <span class="hljs-number">0</span>: <span class="hljs-keyword">continue</span> <span class="hljs-comment"># 遍历4个方向</span> <span class="hljs-keyword">for</span> dx, dy <span class="hljs-keyword">in</span> directions: ii, jj = i + dx, j + dy <span class="hljs-comment"># 如果合法的话就合并</span> <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> <= ii < m <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> <= jj < n <span class="hljs-keyword">and</span> grid[ii][jj] == <span class="hljs-number">1</span>: uf.union(i * n + j, ii * n + jj) <span class="hljs-keyword">return</span> uf.count</code></pre><h3 id="(2)朋友圈"><a href="#(2)朋友圈" class="headerlink" title="(2)朋友圈"></a><strong>(2)朋友圈</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/friend-circles/" target="_blank" rel="noopener">第547题</a><br>班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findCircleNum</span><span class="hljs-params">(self, M: List[List[int]])</span> -> int:</span> <span class="hljs-keyword">if</span> len(M) < <span class="hljs-number">2</span>: <span class="hljs-keyword">return</span> len(M) <span class="hljs-keyword">if</span> len(M) == <span class="hljs-number">2</span>: <span class="hljs-keyword">if</span> M[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>] == <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">2</span> n = len(M) uf = UnionFind(M) <span class="hljs-comment"># 只需遍历右上三角即可(不包括对角线)</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(n<span class="hljs-number">-1</span>): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(i+<span class="hljs-number">1</span>, n): <span class="hljs-keyword">if</span> M[i][j] == <span class="hljs-number">1</span>: uf.union(i, j) <span class="hljs-keyword">return</span> uf.count <span class="hljs-comment"># 并查集</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UnionFind</span><span class="hljs-params">(object)</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, M)</span>:</span> n = len(M) self.count = <span class="hljs-number">0</span> self.parent = [<span class="hljs-number">-1</span>] * n <span class="hljs-comment"># 一维数组表示并查集</span> self.rank = [<span class="hljs-number">0</span>] * n <span class="hljs-comment"># 初始化,为1的格子指向自己</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(n): self.parent[i] = i self.count += <span class="hljs-number">1</span> <span class="hljs-comment"># 找根节点</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find</span><span class="hljs-params">(self, i)</span>:</span> <span class="hljs-keyword">if</span> self.parent[i] != i: self.parent[i] = self.find(self.parent[i]) <span class="hljs-keyword">return</span> self.parent[i] <span class="hljs-comment"># 合并</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">union</span><span class="hljs-params">(self, x, y)</span>:</span> rootx = self.find(x) rooty = self.find(y) <span class="hljs-keyword">if</span> rootx != rooty: <span class="hljs-keyword">if</span> self.rank[rootx] > self.rank[rooty]: <span class="hljs-comment"># 低秩合并到高秩</span> self.parent[rooty] = rootx <span class="hljs-keyword">elif</span> self.rank[rootx] < self.rank[rooty]: self.parent[rootx] = rooty <span class="hljs-keyword">else</span>: self.parent[rooty] = rootx self.rank[rootx] += <span class="hljs-number">1</span> self.count -= <span class="hljs-number">1</span></code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——LRU缓存</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94LRU%E7%BC%93%E5%AD%98/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94LRU%E7%BC%93%E5%AD%98/</id>
<published>2020-06-04T07:17:40.000Z</published>
<updated>2020-06-04T07:18:12.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、LRU缓存"><a href="#一、LRU缓存" class="headerlink" title="一、LRU缓存"></a><strong>一、LRU缓存</strong></h1><p>LRU(least recently used)最近最少使用缓存机制,在计算机的缓存满时,会最先淘汰近期最少使用的数据。示意图如下图所示:<br><img src="https://img-blog.csdnimg.cn/20200415174048180.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述">设缓存的大小为5,在缓存未满之前,ABCDEF依次进入缓存。当要缓存F时,A近期没有被使用,因此淘汰掉,F放到头的位置,剩下的往后挪。当再次进来C的时候,因为缓存里已经有C了,因此把C提到缓存的头来。再进来G的时候,G放到头,剩下的往后挪。</p><h1 id="二、实现LRU缓存"><a href="#二、实现LRU缓存" class="headerlink" title="二、实现LRU缓存"></a><strong>二、实现LRU缓存</strong></h1><p>此题为leetcode<a href="https://leetcode-cn.com/problems/lru-cache/" target="_blank" rel="noopener">第146题</a><br>运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。</p><ul><li>获取数据 get(key) :如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。</li><li>写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。</li></ul><pre><code class="hljs python"><span class="hljs-comment"># 有序字典</span><span class="hljs-keyword">import</span> collections<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LRUCache</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, capacity: int)</span>:</span> self.dic = collections.OrderedDict() self.remain = capacity <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get</span><span class="hljs-params">(self, key: int)</span> -> int:</span> <span class="hljs-keyword">if</span> key <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self.dic: <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span> v = self.dic.pop(key) <span class="hljs-comment"># 获取指定key的value,并在字典中删除</span> self.dic[key] = v <span class="hljs-comment"># 将key作为最新的一个</span> <span class="hljs-keyword">return</span> v <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">put</span><span class="hljs-params">(self, key: int, value: int)</span> -> <span class="hljs-keyword">None</span>:</span> <span class="hljs-keyword">if</span> key <span class="hljs-keyword">in</span> self.dic: self.dic.pop(key) <span class="hljs-keyword">else</span>: <span class="hljs-keyword">if</span> self.remain > <span class="hljs-number">0</span>: self.remain -= <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: self.dic.popitem(last=<span class="hljs-literal">False</span>) <span class="hljs-comment"># last为False时,删除最先放进来的键值对对并返回该键值对</span> self.dic[key] = value</code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——字典树</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%AD%97%E5%85%B8%E6%A0%91/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%AD%97%E5%85%B8%E6%A0%91/</id>
<published>2020-06-04T07:16:31.000Z</published>
<updated>2020-06-04T07:17:18.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、字典树"><a href="#一、字典树" class="headerlink" title="一、字典树"></a><strong>一、字典树</strong></h1><p><strong>字典树(Trie)</strong> 又称单词查找树或键树,是一种哈希树的变种。典型的应用是用于统计和排序大量的字符串(但不限于字符串),优点是可以可以最大限度地减少无畏的字符串比较,查询效率比哈希表高。Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询的时间。</p><p>比如有一个包含多个单词的列表:[‘word’, ‘work’, ‘code’, ‘coffe’],可以下面的字典树表示:</p><pre><code class="hljs mermaid">graph TDroot((root)) -- w --> w(( ))w -- o --> o(( ))o -- r --> r(( ))r -- d --> d(( ))r -- k --> k(( ))root -- c --> c(( ))c -- o --> o2(( ))o2 -- d --> d2(( ))d2 -- e --> e(( ))o2 -- f --> f(( ))f -- f --> f2(( ))f2 -- e --> e2(( ))style root fill:#f9fstyle d fill:#f9fstyle k fill:#f9fstyle e fill:#f9fstyle e2 fill:#f9f</code></pre><p>字典树的性质:</p><ul><li>根节点不包含字符,除根节点外每个节点只包含一个字符</li><li>从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串</li><li>每个节点的所有子节点包含的字符串都不同</li></ul><p><strong>从头实现字符串(leetcode<a href="https://leetcode-cn.com/problems/implement-trie-prefix-tree/" target="_blank" rel="noopener">第208题</a>):</strong><br>实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。示例:</p><pre><code class="hljs bash">Trie trie = new Trie(); trie.insert(<span class="hljs-string">"apple"</span>);trie.search(<span class="hljs-string">"apple"</span>); // 返回 <span class="hljs-literal">true</span>trie.search(<span class="hljs-string">"app"</span>); // 返回 <span class="hljs-literal">false</span>trie.startsWith(<span class="hljs-string">"app"</span>); // 返回 <span class="hljs-literal">true</span>trie.insert(<span class="hljs-string">"app"</span>); trie.search(<span class="hljs-string">"app"</span>); // 返回 <span class="hljs-literal">true</span></code></pre><p>说明:你可以假设所有的输入都是由小写字母 a-z 构成的;保证所有输入均为非空字符串。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Trie</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span> <span class="hljs-string">""" Initialize your data structure here. """</span> self.root = {} self.end_of_word = <span class="hljs-string">'#'</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insert</span><span class="hljs-params">(self, word: str)</span> -> <span class="hljs-keyword">None</span>:</span> <span class="hljs-string">""" Inserts a word into the trie. """</span> node = self.root <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> word: node = node.setdefault(c, {}) node[self.end_of_word] = <span class="hljs-number">1</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">search</span><span class="hljs-params">(self, word: str)</span> -> bool:</span> <span class="hljs-string">""" Returns if the word is in the trie. """</span> node = self.root <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> word: <span class="hljs-keyword">if</span> c <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> node: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> node = node[c] <span class="hljs-keyword">return</span> self.end_of_word <span class="hljs-keyword">in</span> node <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">startsWith</span><span class="hljs-params">(self, prefix: str)</span> -> bool:</span> <span class="hljs-string">""" Returns if there is any word in the trie that starts with the given prefix. """</span> node = self.root <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> prefix: <span class="hljs-keyword">if</span> c <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> node: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> node = node[c] <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre><p>上面是比较造轮子的一种方法,而在python里可以直接用字典来实现字典树</p><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h3 id="(1)单词搜索II"><a href="#(1)单词搜索II" class="headerlink" title="(1)单词搜索II"></a><strong>(1)单词搜索II</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/word-search-ii/" target="_blank" rel="noopener">第212题</a><br>给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。</p><p>思路:字典树+深度优先搜索</p><pre><code class="hljs python"><span class="hljs-comment"># 方向数组</span>dx = [<span class="hljs-number">-1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>]dy = [<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">1</span>]<span class="hljs-comment"># word结束标记</span>end_of_word = <span class="hljs-string">'#'</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findWords</span><span class="hljs-params">(self, board: List[List[str]], words: List[str])</span> -> List[str]:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> board <span class="hljs-keyword">or</span> len(board[<span class="hljs-number">0</span>]) == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> len(words) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> [] self.res = set() <span class="hljs-comment"># 结果保存在集合中</span> root = {} <span class="hljs-comment"># 初始化根节点为空字典</span> <span class="hljs-comment"># 对列表里的word构建字典树</span> <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> words: node = root <span class="hljs-comment"># 这里的root将会是一个字典层层嵌套的结构。</span> <span class="hljs-comment"># 如果c在node里,则取出键为c的值,如果不在,则在键c下新建空字典</span> <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> word: node = node.setdefault(c, {}) node[end_of_word] = end_of_word <span class="hljs-comment"># 标记word已结束</span> self.m, self.n = len(board), len(board[<span class="hljs-number">0</span>]) <span class="hljs-comment"># 遍历board,进行DFS</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(self.m): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(self.n): <span class="hljs-keyword">if</span> board[i][j] <span class="hljs-keyword">in</span> root: self._dfs(board, i, j, <span class="hljs-string">''</span>, root) <span class="hljs-keyword">return</span> list(self.res) <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_dfs</span><span class="hljs-params">(self, board, i, j, curr_word, curr_dict)</span>:</span> curr_word += board[i][j] curr_dict = curr_dict[board[i][j]] <span class="hljs-keyword">if</span> end_of_word <span class="hljs-keyword">in</span> curr_dict: self.res.add(curr_word) temp, board[i][j] = board[i][j], <span class="hljs-string">'*'</span> <span class="hljs-comment"># '*'表示暂时标记为已访问过</span> <span class="hljs-comment"># 在(i, j)的4个方向上遍历</span> <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">4</span>): x, y = i + dx[k], j + dy[k] <span class="hljs-comment"># 如果x、y没有越界,并且没有被访问过,并且当前字母在curr_dict中</span> <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> <= x < self.m <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> <= y < self.n <span class="hljs-keyword">and</span> board[x][y] != <span class="hljs-string">'*'</span> <span class="hljs-keyword">and</span> board[x][y] <span class="hljs-keyword">in</span> curr_dict: self._dfs(board, x, y, curr_word, curr_dict) board[i][j] = temp <span class="hljs-comment"># 恢复board[i][j]的值</span></code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——二分查找</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/</id>
<published>2020-06-04T07:15:39.000Z</published>
<updated>2020-06-04T07:16:14.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、二分查找"><a href="#一、二分查找" class="headerlink" title="一、二分查找"></a><strong>一、二分查找</strong></h1><p><strong>使用二分查找的条件:</strong></p><ul><li>单调递增或递减</li><li>存在上下界</li><li>能够通过索引访问(数组更适合二分查找,链表不适合)</li></ul><p><strong>程序模板:</strong></p><pre><code class="hljs python">left, right = <span class="hljs-number">0</span>, len(array)<span class="hljs-keyword">while</span> left <= right:mid = left + (right - left) // <span class="hljs-number">2</span><span class="hljs-keyword">if</span> array[mid] == target:<span class="hljs-keyword">return</span> <span class="hljs-literal">True</span><span class="hljs-keyword">elif</span> array[mid] < target:left = mid + <span class="hljs-number">1</span><span class="hljs-keyword">else</span>:right = mid <span class="hljs-number">-1</span></code></pre><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h3 id="(1)sqrt-x"><a href="#(1)sqrt-x" class="headerlink" title="(1)sqrt(x)"></a><strong>(1)sqrt(x)</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/sqrtx/submissions/" target="_blank" rel="noopener">第69题</a><br>实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mySqrt</span><span class="hljs-params">(self, x: int)</span> -> int:</span> <span class="hljs-keyword">if</span> x == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> x == <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> x left, right = <span class="hljs-number">2</span>, x // <span class="hljs-number">2</span> <span class="hljs-keyword">while</span> left <= right: mid = left + (right - left) // <span class="hljs-number">2</span> <span class="hljs-keyword">if</span> mid * mid == x: <span class="hljs-keyword">return</span> mid <span class="hljs-keyword">elif</span> mid * mid < x: left = mid + <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: right = mid - <span class="hljs-number">1</span> <span class="hljs-keyword">return</span> right</code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——十大排序算法</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%8D%81%E5%A4%A7%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%8D%81%E5%A4%A7%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/</id>
<published>2020-06-04T07:13:43.000Z</published>
<updated>2020-06-04T07:14:34.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、冒泡排序"><a href="#一、冒泡排序" class="headerlink" title="一、冒泡排序"></a><strong>一、冒泡排序</strong></h1><p><strong>排序过程:</strong></p><ul><li>列表每两个相邻的数,如果前者大于后者,则交换这两个数;遍历列表,完成一趟排序</li><li>继续从头遍历,重复上述过程,直到没有发生交换为止<br><img src="https://img-blog.csdnimg.cn/20200406182421626.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">BubbleSort</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(a) - <span class="hljs-number">1</span>): exchange = <span class="hljs-literal">False</span> <span class="hljs-comment"># 标志位,第i趟如果没有发生交换,则排序已经完成,不需要再进行后面的冒泡</span> <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(a) - i - <span class="hljs-number">1</span>): <span class="hljs-keyword">if</span> a[j] > a[j + <span class="hljs-number">1</span>]: a[j], a[j + <span class="hljs-number">1</span>] = a[j + <span class="hljs-number">1</span>], a[j] exchange = <span class="hljs-literal">True</span> <span class="hljs-keyword">if</span> exchange <span class="hljs-keyword">is</span> <span class="hljs-literal">False</span>: <span class="hljs-comment"># 没有发生交换,返回</span> <span class="hljs-keyword">return</span></code></pre></li></ul><p><strong>复杂度分析:</strong></p><ul><li>最好情况:$O(n)$</li><li>最坏情况:$O(n^2)$</li><li>平均情况:$O(n^2)$</li></ul><h1 id="二、选择排序"><a href="#二、选择排序" class="headerlink" title="二、选择排序"></a><strong>二、选择排序</strong></h1><p><strong>排序过程:</strong></p><ul><li>一趟遍历记录最小的数,放到一个位置</li><li>再遍历一趟,记录无需去最小的数,放到有序区的第二个位置</li><li>重复以上过程,直到列表结束</li></ul><p><img src="https://img-blog.csdnimg.cn/20200406184746809.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">SelectionSort</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(a) - <span class="hljs-number">1</span>): min_index = i <span class="hljs-comment"># 记录无序区最小数的位置</span> <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(i, len(a)): <span class="hljs-keyword">if</span> a[j] < a[min_index]: min_index = j <span class="hljs-keyword">if</span> min_index != i: a[i], a[min_index] = a[min_index], a[i]</code></pre></p><p><strong>复杂度分析:</strong></p><ul><li>最好情况:$O(n^2)$</li><li>最坏情况:$O(n^2)$</li><li>平均情况:$O(n^2)$</li></ul><h1 id="三、插入排序"><a href="#三、插入排序" class="headerlink" title="三、插入排序"></a><strong>三、插入排序</strong></h1><p><strong>排序过程:</strong></p><ul><li>步骤1:从第一个元素$a[i], i=0$开始,该元素为有序区</li><li>步骤2:取下一个元素$a[i+1]$,在有序区的元素序列中从后向前扫描</li><li>步骤3:如果有序区元素$a[j]$大于新元素$a[i+1]$,将该元素移到下一位置$a[j-1]$;</li><li>步骤4:重复步骤3,直到找到有序区的元素小于或者等于新元素的位置;</li><li>步骤5:将新元素$a[i+1]$插入到该位置后;</li><li>步骤6:重复步骤2~5</li></ul><p><img src="https://img-blog.csdnimg.cn/20200406222237652.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">InsertionSort</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(a)): temp = a[i] <span class="hljs-comment"># 取无序区的第一个数</span> j = i - <span class="hljs-number">1</span> <span class="hljs-comment"># 有序区的倒数第一个数索引为i-1</span> <span class="hljs-keyword">while</span> j >= <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> temp < a[j]: <span class="hljs-comment"># 遍历有序区</span> a[j + <span class="hljs-number">1</span>] = a[j] j -= <span class="hljs-number">1</span> a[j + <span class="hljs-number">1</span>] = temp <span class="hljs-comment"># 找到temp的位置</span></code></pre><p><strong>复杂度分析:</strong></p><ul><li>最好情况:$O(n)$</li><li>最坏情况:$O(n^2)$</li><li>平均情况:$O(n^2)$</li></ul><h1 id="四、快速排序"><a href="#四、快速排序" class="headerlink" title="四、快速排序"></a><strong>四、快速排序</strong></h1><p><strong>排序过程:</strong></p><ul><li>取一个元素(一般为第一个元素)P,使元素归位</li><li>归位操作后的列表被P分为两部分,P左边的元素都比P小,右边的都比P大</li><li>递归地把小于P的子数列和大于P的子数列排序</li></ul><p><img src="https://img-blog.csdnimg.cn/20200406231722396.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">partition</span><span class="hljs-params">(a, left, right)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> temp = a[left] <span class="hljs-keyword">while</span> left < right: <span class="hljs-comment"># 从右边找比temp小的数</span> <span class="hljs-keyword">while</span> left < right <span class="hljs-keyword">and</span> a[right] >= temp: right -= <span class="hljs-number">1</span> a[left] = a[right] <span class="hljs-comment"># 从左边找比temp大的数</span> <span class="hljs-keyword">while</span> left < right <span class="hljs-keyword">and</span> a[left] <= temp: left += <span class="hljs-number">1</span> a[right] = a[left] <span class="hljs-comment"># 找到了temp的位置</span> a[left] = temp <span class="hljs-comment"># 返回temp1的位置, return right也可以,因为left和right重合</span> <span class="hljs-keyword">return</span> left<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">QuickSort</span><span class="hljs-params">(a, left, right)</span>:</span> <span class="hljs-keyword">if</span> left < right: <span class="hljs-comment"># 选取a的第一个元素P,归位</span> mid = partition(a, left, right) <span class="hljs-comment"># 递归P的左边</span> QuickSort(a, left, mid - <span class="hljs-number">1</span>) <span class="hljs-comment"># 递归P的右边</span> QuickSort(a, mid + <span class="hljs-number">1</span>, right)</code></pre><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n \log n)$</li><li>最差情况:$O(n^2)$</li><li>平均情况:$O(n \log n)$<h1 id="五、堆排序"><a href="#五、堆排序" class="headerlink" title="五、堆排序"></a><strong>五、堆排序</strong></h1><strong>堆</strong>是一种特殊的完全二叉树。<strong>大根堆:</strong> 完全二叉树的任一节点都比其孩子节点大。<strong>小根堆:</strong> 完全二叉树的任一节点都比其孩子节点小。</li></ul><p><img src="https://img-blog.csdnimg.cn/20200407160345511.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><p><strong>堆的向下调整:</strong> 假设根节点的左右子树都是堆,但根节点不满足堆的性质,可以通过一次向下调整将其变成一个堆。</p><p><strong>排序过程:</strong></p><ul><li>步骤1:建立大根堆</li><li>步骤2:得到对顶元素,为最大元素</li><li>步骤3:堆顶元素和堆的最后一个元素交换,交换后可以通过一次调整使堆有序(不包括刚才及之前被换到堆后面的元素)</li><li>步骤4:堆顶元素为第二大元素,重复步骤3,直到堆变空</li></ul><p><img src="https://img-blog.csdnimg.cn/20200407160459489.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">adjustHeap</span><span class="hljs-params">(a, low, high)</span>:</span> i = low <span class="hljs-comment"># 指向根节点</span> j = <span class="hljs-number">2</span> * i + <span class="hljs-number">1</span> <span class="hljs-comment"># 指向根节点的左孩子</span> temp = a[i] <span class="hljs-keyword">while</span> j <= high: <span class="hljs-comment"># 如果有右孩子,并且右孩子比左孩子大</span> <span class="hljs-keyword">if</span> j + <span class="hljs-number">1</span> <= high <span class="hljs-keyword">and</span> a[j+<span class="hljs-number">1</span>] > a[j]: j += <span class="hljs-number">1</span> <span class="hljs-comment"># 指向右孩子</span> <span class="hljs-comment"># 如果子节点比temp大,交换a[i]和a[j]</span> <span class="hljs-keyword">if</span> a[j] > temp: a[i] = a[j] i = j j = <span class="hljs-number">2</span> * i + <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: <span class="hljs-keyword">break</span> a[i] = temp<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">HeapSort</span><span class="hljs-params">(a)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> n = len(a) <span class="hljs-comment"># 从底向上地建立大根堆,从最后一个非叶子节点开始,即(n-2)//2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range((n - <span class="hljs-number">2</span>) // <span class="hljs-number">2</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>): adjustHeap(a, i, n<span class="hljs-number">-1</span>) <span class="hljs-comment"># 从最后一个元素开始,与堆顶调换</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(n<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>): a[<span class="hljs-number">0</span>], a[i] = a[i], a[<span class="hljs-number">0</span>] <span class="hljs-comment"># 堆顶与a[i]调换</span> adjustHeap(a, <span class="hljs-number">0</span>, i<span class="hljs-number">-1</span>)</code></pre></p><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n \log n)$</li><li>最差情况:$O(n \log n)$</li><li>平均情况:$O(n \log n)$</li></ul><p><strong>python里堆的内置模块:heapq</strong></p><pre><code class="hljs python"><span class="hljs-comment"># 堆的内置模块</span><span class="hljs-keyword">import</span> heapqa = list(range(<span class="hljs-number">100</span>))random.shuffle(a)heapq.heapify(a) <span class="hljs-comment"># 建堆</span><span class="hljs-comment"># 依次出数</span><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(a)): print(heapq.heappop(a), end=<span class="hljs-string">','</span>)</code></pre><p><strong>top-k问题:</strong> 有n个数,取前k大的数(k<n)</p><ul><li>取列表前k个元素建立一个小根堆</li><li>从第k+1个开始依次向后遍历原列表,如果小于堆顶,则跳过改元素;如果大于堆顶,则将该元素放到堆顶,进行一次调整</li><li>遍历结束后,倒序弹出堆顶</li></ul><pre><code class="hljs python"><span class="hljs-comment"># 调整小根堆</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">adjustHeap</span><span class="hljs-params">(a, low, high)</span>:</span> i = low <span class="hljs-comment"># 指向根节点</span> j = <span class="hljs-number">2</span> * i + <span class="hljs-number">1</span> <span class="hljs-comment"># 指向根节点的左孩子</span> temp = a[i] <span class="hljs-keyword">while</span> j <= high: <span class="hljs-comment"># 如果有右孩子,并且右孩子比左孩子小</span> <span class="hljs-keyword">if</span> j + <span class="hljs-number">1</span> <= high <span class="hljs-keyword">and</span> a[j+<span class="hljs-number">1</span>] < a[j]: j += <span class="hljs-number">1</span> <span class="hljs-comment"># 指向右孩子</span> <span class="hljs-comment"># 如果子节点比temp小,交换a[i]和a[j]</span> <span class="hljs-keyword">if</span> a[j] < temp: a[i] = a[j] i = j j = <span class="hljs-number">2</span> * i + <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: <span class="hljs-keyword">break</span> a[i] = temp<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">top_k</span><span class="hljs-params">(a, k)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> heap = a[:k] <span class="hljs-comment"># 建堆</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range((k - <span class="hljs-number">2</span>) // <span class="hljs-number">2</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>): adjustHeap(heap, i, k<span class="hljs-number">-1</span>) <span class="hljs-comment"># 遍历</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(k, len(a) - <span class="hljs-number">1</span>): <span class="hljs-keyword">if</span> a[i] > heap[<span class="hljs-number">0</span>]: heap[<span class="hljs-number">0</span>] = a[i] adjustHeap(heap, <span class="hljs-number">0</span>, k<span class="hljs-number">-1</span>) <span class="hljs-comment"># 出数</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(k - <span class="hljs-number">1</span>, <span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>): heap[<span class="hljs-number">0</span>], heap[i] = heap[i], heap[<span class="hljs-number">0</span>] adjustHeap(heap, <span class="hljs-number">0</span>, i - <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> heap</code></pre><h1 id="六、归并排序"><a href="#六、归并排序" class="headerlink" title="六、归并排序"></a><strong>六、归并排序</strong></h1><p><strong>归并:</strong> 将两段有序列表合并成一个有序列表</p><p><strong>排序过程:</strong></p><ul><li>把长度为n的输入列表分为长度为n//2的子序列</li><li>对这两个子序列分别采用归并排序</li><li>将两个排序好的子序列合并成一个最终的排序序列<br><img src="https://img-blog.csdnimg.cn/20200407184153698.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br><img src="https://img-blog.csdnimg.cn/20200407210343142.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">merge</span><span class="hljs-params">(a, low, mid, high)</span>:</span> <span class="hljs-comment"># a的左右半边子序列已为有序</span> i, j = low, mid + <span class="hljs-number">1</span> temp = [] <span class="hljs-comment"># 依次比较</span> <span class="hljs-keyword">while</span> i <= mid <span class="hljs-keyword">and</span> j <= high: <span class="hljs-comment"># 只要两个子序列还有数</span> <span class="hljs-keyword">if</span> a[i] < a[j]: temp.append(a[i]) i += <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: temp.append(a[j]) j += <span class="hljs-number">1</span> <span class="hljs-comment"># 上面循环结束后至少有一个子序列已被完全遍历</span> <span class="hljs-comment"># 下面将可能剩下的部分添加到temp</span> temp.extend(a[i:mid+<span class="hljs-number">1</span>]) temp.extend(a[j:high+<span class="hljs-number">1</span>]) <span class="hljs-comment">#print(len(a[i:mid+1]), len(a[j:high]), len(a[low:high+1]), len(temp))</span> <span class="hljs-comment"># 替换原数组</span> a[low:high+<span class="hljs-number">1</span>] = temp<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">MergeSort</span><span class="hljs-params">(a, low, high)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">if</span> low < high: <span class="hljs-comment"># low为a的最左边,high为a的最右边,mid为左半边子序列的最右边</span> mid = (low + high) // <span class="hljs-number">2</span> <span class="hljs-comment"># 归并左半边子序列</span> MergeSort(a, low, mid) <span class="hljs-comment"># 归并右半边子序列</span> MergeSort(a, mid+<span class="hljs-number">1</span>, high) <span class="hljs-comment"># 合并两个有序子序列</span> merge(a, low, mid, high)</code></pre></li></ul><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n)$</li><li>最差情况:$O(n \log n)$</li><li>平均情况:$O(n \log n)$</li></ul><h1 id="七、希尔排序"><a href="#七、希尔排序" class="headerlink" title="七、希尔排序"></a><strong>七、希尔排序</strong></h1><p>希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破$O(n^2)$的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序。</p><p><strong>排序过程:</strong></p><ul><li>首先取一个整数$d_1= n // 2$,将列表分为$d_1$个组,每组相邻元素之间距离为$d_1$,在每组里进行插入排序</li><li>去第二个整数$d_2=d_1 // 2$,重复上述分组排序过程,直到$d_i=1$,即所有元素都在同一组内插入排序</li><li>希尔排序每趟使列表整体越来越接近有序<br><img src="https://img-blog.csdnimg.cn/20200407214935993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li></ul><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">InsertSortWithGap</span><span class="hljs-params">(a, gap)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(a)): temp = a[i] <span class="hljs-comment"># 取无序区的第一个数</span> j = i - gap <span class="hljs-comment"># 有序区的倒数第一个数索引为i-gap</span> <span class="hljs-keyword">while</span> j >= <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> temp < a[j]: <span class="hljs-comment"># 遍历有序区</span> a[j + gap] = a[j] j -= gap a[j + gap] = temp <span class="hljs-comment"># 找到temp的位置</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ShellSort</span><span class="hljs-params">(a)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> d = len(a) // <span class="hljs-number">2</span> <span class="hljs-keyword">while</span> d >= <span class="hljs-number">1</span>: InsertSortWithGap(a, d) d //= <span class="hljs-number">2</span></code></pre><p><strong>复杂度分析:</strong> 希尔排序的复杂度分析是个难题,根据选取的分组整数序列$d_1, d_2, \cdots, d_i$的不同,其时间复杂度也不同,有的复杂度由于数学上的难题仍然没有定论。对于我们介绍的一直除2的这种方式:</p><ul><li>最佳情况:$O(n \log ^2 n)$</li><li>最坏情况:$O(n \log ^2 n)$</li><li>平均情况:$O(n \log n)$</li></ul><h1 id="八、计数排序"><a href="#八、计数排序" class="headerlink" title="八、计数排序"></a><strong>八、计数排序</strong></h1><p>基数排序要求已知列表中<strong>数的范围</strong>在0到$k$之间</p><p><strong>排序过程:</strong></p><ul><li>步骤1:找出待排序的列表中最大和最小的元素;</li><li>步骤2:统计数组中每个值为$i$的元素出现的次数,存入数组$C$的第$i$项;</li><li>步骤3:遍历列表,对所有元素的计数累加;</li><li>步骤4:填充目标列表:将每个元素$i$放在新列表的第$C(i)$项,每放一个元素就将$C(i)$减去1。</li></ul><p><img src="https://img-blog.csdnimg.cn/20200407222358617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CountingSort</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># 找最大最小值</span> min_, max_ = a[<span class="hljs-number">0</span>], a[<span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> a: <span class="hljs-keyword">if</span> num > max_: max_ = num <span class="hljs-keyword">if</span> num < min_: min_ = num <span class="hljs-comment"># 初始化计数数组</span> C = [<span class="hljs-number">0</span>] * (max_ - min_ + <span class="hljs-number">1</span>) <span class="hljs-comment"># 遍历数组</span> <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> a: C[num] += <span class="hljs-number">1</span> <span class="hljs-comment"># 填充原数组</span> a.clear() <span class="hljs-keyword">for</span> i, c <span class="hljs-keyword">in</span> enumerate(C): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(c): a.append(i)</code></pre><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n+k)$</li><li>最差情况:$O(n+k)$</li><li>平均情况:$O(n+k)$</li></ul><h1 id="九、桶排序"><a href="#九、桶排序" class="headerlink" title="九、桶排序"></a><strong>九、桶排序</strong></h1><p>假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序</p><p><strong>排序过程:</strong></p><ul><li>步骤1:人为设置一个bins,即有多少个桶。比如$[0,1, \cdots, 9, 10]$这个列表,bins=5,那么第一个桶放0、1,第二个桶放2、3,第三个桶放4、5,依次类推;</li><li>步骤2:遍历列表,并且把元素一个一个放到对应的桶里去;</li><li>步骤3:对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;</li><li>步骤4:从不是空的桶里把排好序的数据拼接起来。<br><img src="https://img-blog.csdnimg.cn/20200408120614954.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">BucketSort</span><span class="hljs-params">(a, bins)</span>:</span><span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># 初始化桶</span> buckets = [[] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(bins)] <span class="hljs-comment"># 找到最大值最小值</span> min_, max_ = a[<span class="hljs-number">0</span>], a[<span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> a: <span class="hljs-keyword">if</span> num > max_: max_ = num <span class="hljs-keyword">if</span> num < min_: min_ = num <span class="hljs-comment"># 遍历</span> <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> a: i = min(num // (max_ // bins), bins<span class="hljs-number">-1</span>) <span class="hljs-comment"># 第几号桶</span> buckets[i].append(num) <span class="hljs-comment"># 保持桶内顺序</span> <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(buckets[i]) - <span class="hljs-number">1</span>, <span class="hljs-number">0</span>,<span class="hljs-number">-1</span>): <span class="hljs-keyword">if</span> buckets[i][j] < buckets[i][j - <span class="hljs-number">1</span>]: <span class="hljs-comment"># 桶内下小上大</span> buckets[i][j], buckets[i][j - <span class="hljs-number">1</span>] = buckets[i][j - <span class="hljs-number">1</span>], buckets[i][j] <span class="hljs-keyword">else</span>: <span class="hljs-keyword">break</span> <span class="hljs-comment"># 从桶内拿出</span> sorted_a = [] <span class="hljs-keyword">for</span> bucket <span class="hljs-keyword">in</span> buckets: sorted_a.extend(bucket) <span class="hljs-keyword">return</span> sorted_a</code></pre></li></ul><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n+k)$</li><li>最差情况:$O(n^2)$</li><li>平均情况:$O(n+k)$</li></ul><h1 id="十、基数排序"><a href="#十、基数排序" class="headerlink" title="十、基数排序"></a><strong>十、基数排序</strong></h1><p>基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。</p><p><strong>排序过程:</strong></p><ul><li>步骤1:取得数组中的最大数,并取得位数;</li><li>步骤2:取列表中元素的最低位,按最低位对元素进行分桶(10个桶,0-9);</li><li>步骤3:按桶的顺序还原列表,然后取元素的导数第二位,对元素再次分桶;</li><li>步骤4:重复上述分桶、还原的过程,直到最大元素的最高位</li></ul><p><img src="https://img-blog.csdnimg.cn/20200408154306258.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">RadixSort</span><span class="hljs-params">(a)</span>:</span> <span class="hljs-keyword">if</span> len(a) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> max_ = max(a) it = <span class="hljs-number">0</span> <span class="hljs-keyword">while</span> <span class="hljs-number">10</span> ** it <= max_: <span class="hljs-comment"># 按倒数第it位分桶</span> buckets = [[] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(<span class="hljs-number">10</span>)] <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> a: digit = (num // <span class="hljs-number">10</span> ** it) % <span class="hljs-number">10</span> <span class="hljs-comment"># 取倒数第it位数</span> buckets[digit].append(num) it += <span class="hljs-number">1</span> <span class="hljs-comment"># 出桶</span> a.clear() <span class="hljs-keyword">for</span> bucket <span class="hljs-keyword">in</span> buckets: a.extend(bucket)</code></pre><p><strong>复杂度分析:</strong></p><ul><li>最佳情况:$O(n \times k)$</li><li>最差情况:$O(n \times k)$</li><li>平均情况:$O(n \times k)$</li></ul><h1 id="十一、总结"><a href="#十一、总结" class="headerlink" title="十一、总结"></a><strong>十一、总结</strong></h1><div class="table-container"><table><thead><tr><th>排序算法</th><th>平均时间复杂度</th><th>最好情况</th><th>最坏情况</th><th>空间复杂度</th><th>排序方式</th><th>稳定</th></tr></thead><tbody><tr><td>冒泡排序</td><td>$O(n^2)$</td><td>O(n)</td><td>$O(n^2)$</td><td>$O(1)$</td><td>in-place</td><td>稳定</td></tr><tr><td>选择排序</td><td>$O(n^2)$</td><td>O(n^2)</td><td>$O(n^2)$</td><td>$O(1)$</td><td>in-place</td><td>不稳定</td></tr><tr><td>插入排序</td><td>$O(n^2)$</td><td>O(n)</td><td>$O(n^2)$</td><td>$O(1)$</td><td>in-place</td><td>稳定</td></tr><tr><td>快速排序</td><td>$O(n \log n)$</td><td>$O(n \log n)$</td><td>$O(n^2)$</td><td>$O(\log n)$</td><td>in-place</td><td>不稳定</td></tr><tr><td>堆排序</td><td>$O(n \log n)$</td><td>$O(n \log n)$</td><td>$O(n \log n)$</td><td>$O(1)$</td><td>in-place</td><td>不稳定</td></tr><tr><td>归并排序</td><td>$O(n \log n)$</td><td>$O(n \log n)$</td><td>$O(n \log n)$</td><td>$O(n)$</td><td>out-place</td><td>稳定</td></tr><tr><td>希尔排序</td><td>$O(n \log n)$</td><td>$O(n \log^2 n)$</td><td>$O(n \log^2 n)$</td><td>$O(1)$</td><td>in-place</td><td>不稳定</td></tr><tr><td>计数排序</td><td>$O(n+k)$</td><td>O(n+k)</td><td>$O(n+k)$</td><td>$O(k)$</td><td>out-place</td><td>稳定</td></tr><tr><td>桶排序</td><td>$O(n+k)$</td><td>O(n+k)</td><td>$O(n^2)$</td><td>$O(n+k)$</td><td>out-place</td><td>稳定</td></tr><tr><td>基数排序</td><td>$O(n \times k)$</td><td>$O(n \times k)$</td><td>$O(n \times k)$</td><td>$O(n+k)$</td><td>out-place</td><td>稳定</td></tr></tbody></table></div><p>上表中,<strong>in-place</strong>表示占用常数内存,不占额外内存(递归内存除外),<strong>out-place</strong>表示占用额外内存;$k$为桶的个数;<strong>稳定</strong>表示如果a原本在b前面且a==b,排序后a仍然在b前面,<strong>不稳定</strong>则是a可能出现在b后面。</p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a><strong>参考文献</strong></h1><p>[1] <a href="https://blog.csdn.net/weixin_41190227/article/details/86600821" target="_blank" rel="noopener">https://blog.csdn.net/weixin_41190227/article/details/86600821</a><br>[2] <a href="https://edu.csdn.net/course/detail/24449" target="_blank" rel="noopener">https://edu.csdn.net/course/detail/24449</a></p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
</entry>
<entry>
<title>数据结构与算法——动态规划</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/</id>
<published>2020-06-04T07:12:33.000Z</published>
<updated>2020-06-04T07:13:04.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、动态规划"><a href="#一、动态规划" class="headerlink" title="一、动态规划"></a><strong>一、动态规划</strong></h1><h2 id="1、从斐波那契数列说起"><a href="#1、从斐波那契数列说起" class="headerlink" title="1、从斐波那契数列说起"></a><strong>1、从斐波那契数列说起</strong></h2><p>斐波那契数列我们比较熟悉了,它的递推公式为$f(n)=f(n-1)+f(n-2)$,它的解法在<a href="https://blog.csdn.net/u014157632/article/details/104950741" target="_blank" rel="noopener">《数据结构与算法——递归》</a>这一节里也说过了。直接使用递归会造成很多重复计算,它的时间复杂度为$O(2^N)$;而使用记忆化,即将中间过程缓存起来,可以降到$O(N)$的时间复杂度。实际上这样的递归是“从上而下”的,我们可以“从下而上”地做,由$f(0)$和$f(1)$得到$f(2)$,再由$f(1)$和$f(2)$得到$f(3)$……这样的写法如下所示:<br><pre><code class="hljs python">F[<span class="hljs-number">0</span>], F[<span class="hljs-number">1</span>] = <span class="hljs-number">0</span>, <span class="hljs-number">1</span><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">2</span>, n):F[i] = F[i<span class="hljs-number">-1</span>] + F[i<span class="hljs-number">-2</span>]</code></pre></p><p>上面的“由下而上地使用递推公式”代表了动态规划的基本思想。上面的斐波那契数列的解法,是最最简单的动态规划,实际上的递推公式会更复杂,比如会有各种限制条件。</p><p><strong>动态规划的基本思想</strong>:问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,再构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解。[1]<br><strong>状态</strong>:在动规解题中,我们将和子问题相关的各个变量的一组取值,称之为一个“状态”。[2]<br><strong>状态转移方程</strong>:即递推公式。上面的斐波那契数列的递推公式是给定的,而一般情况下需要我们自己推导出来递推公式。</p><h2 id="2、举例进一步说明"><a href="#2、举例进一步说明" class="headerlink" title="2、举例进一步说明"></a><strong>2、举例进一步说明</strong></h2><p>现在举个例子进一步说明动态规划:数路径问题。有下面一个$m$行$n$列的方格格,从最左上角的$(0, 0)$出发,达到最右下角的$(m-1, n-1)$,一共有几条路?图上颜色的格子是障碍物不能走,并且只能向右或向下走。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200323225820923.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="350" height="300"> </p><p align="center"> </p><p></p><p>对于这个问题,我们可以递归地求解,如下图所示。从一开始,可以向右走到B,也可以向下走到A,那么<code>从start到end的有多少条路径=从B到end的路径数+从A到end的路径数</code>。同样<code>从B到end的路径数=从E到end的路径数+从C到end的路径树</code>,对于A也是类似的,这样可以一直递归下去。但是这样会有个问题,和斐波那契数列的原始递归解法一样,有很多路径被重复计算了,比如<code>从C到end的路径</code>,它的时间复杂度也会是指数级的。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200323230042806.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="650" height="300"> </p><p align="center"> </p><p></p><p>根据上节对斐波那契数列的分析,我们可以采用“自底向上”的方式,如下图所示,我们从end开始往前推。要到达end,只有从红色箭头指的两个格子那里走(因为只能向右走或向下走)。也就是说,当处于这两个格子之一的时候,只有一条路可以走,我们在格子里记“1”。对于最下面一排的格子,我们都只能向右走,所以都标记为“1”。从蓝色箭头指的格子出发,我们可以向右走也可以向下走,有两条路所以标记为“2”。这个“2”其实是<code>右边格子到end的路径数+下面格子到end的路径数</code>,也就是“1+1”。同理,白色箭头指的格子应该标记为<code>1+1=2</code>,也有两条路可走;绿色箭头指的格子应该标记为<code>2+1=3</code>,则有3条路可走。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200323230936954.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="400" height="330"> </p><p align="center"> </p><p></p><p>就这样从end开始一直往上遍历每个格子,我们可以把格子都标记上,一直到start。最后start右边是17,下边是10,因此这个题的结果就是27。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200323231914295.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="400" height="400"> </p><p align="center"> </p><p></p><p><strong>动态规划模板:</strong></p><pre><code class="hljs python"><span class="hljs-comment"># 以二维为例,只是一个大概的框架</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">DP</span><span class="hljs-params">()</span>:</span><span class="hljs-comment"># 定义状态</span>dp = [[<span class="hljs-number">0</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m + <span class="hljs-number">1</span>)] <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n + <span class="hljs-number">1</span>)]<span class="hljs-comment"># 状态初始化</span>dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>], dp[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>], ... = x, y, ...<span class="hljs-comment"># DP状态的推导</span><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m):<span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n):dp[i][j] = min(dp[i<span class="hljs-number">-1</span>][j], dp[i][j<span class="hljs-number">-1</span>])<span class="hljs-keyword">return</span> dp[m][n]<span class="hljs-comment"># 最优解</span></code></pre><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h2 id="1、爬楼梯"><a href="#1、爬楼梯" class="headerlink" title="1、爬楼梯"></a><strong>1、爬楼梯</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/climbing-stairs/" target="_blank" rel="noopener">第70题</a>。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200324224153777.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="300" height="300"> </p><p align="center"> </p><p></p><p>假设我们现在在第n个阶梯,我们要求的是走到第n个阶梯有多少种走法,即$F(n)$。我们知道只能走1步或2步,那么要走到第n个阶梯,只能从第n-1个阶梯或第n-2个阶梯走,那么走到第n个阶梯的走法个数等于走到第n-1个阶梯的走法个数加上走到第n-2个阶梯的走法个数。上述过程我们可以总结出此题的状态及状态转移方程:</p><ul><li>状态:$F(n)$,到第n个阶梯的走法个数</li><li>状态转移方程:$F(n)=F(n-1)+F(n-2)$</li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">climbStairs</span><span class="hljs-params">(self, n: int)</span> -> int:</span> <span class="hljs-keyword">if</span> n <= <span class="hljs-number">2</span>: <span class="hljs-keyword">return</span> n a, b = <span class="hljs-number">1</span>, <span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">2</span>, n): c = a + b b, a = c, b <span class="hljs-keyword">return</span> c</code></pre><ul><li>时间复杂度:$O(n)$</li><li>空间复杂度:$O(1)$</li></ul><h2 id="2、三角形最小路径和"><a href="#2、三角形最小路径和" class="headerlink" title="2、三角形最小路径和"></a><strong>2、三角形最小路径和</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/triangle/submissions/" target="_blank" rel="noopener">第120题</a></p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200324230515825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="350" height="280"> <img src="https://img-blog.csdnimg.cn/20200324230940884.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="350" height="280"> </p><p align="center"> </p><p></p><p>三角形是个二维数组,我们要求得从最上面的$(0, 01)$出发,到最底下一层的最小路径和,即DF(0, 0)。比如上图的最小路径和的路径是$2 \to 3 \to 5 \to 1$。此题不可以用贪心算法,反例如上面右图所示。要用动态规划,我们要从底向上地去思考。加入我们处于第$i$行,那么从$(i, j)$出发到底部的最小路径和为:(它的左下方格的最小路径和,它的右下方格的最小路径和)的最小值加上它自己。注意我们需要得到是“最小路径和”,而不是方格本身的最小值。由此我们可以确定本题的状态和状态转移方程:</p><ul><li>状态:$DF(i, j)$,第$(i, j)$个方格到最底部的最小路径和</li><li>状态转移方程:$DF[i, j] = min(DF[i+1, j], DF[i+1,j+1]) + Triangle[i, j]$,初始值为$DF[m-1, j]=Triangle[-1, j]$</li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">minimumTotal</span><span class="hljs-params">(self, triangle: List[List[int]])</span> -> int:</span> m, n = len(triangle), len(triangle[<span class="hljs-number">-1</span>]) DP = triangle[<span class="hljs-number">-1</span>] <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m<span class="hljs-number">-1</span>)[::<span class="hljs-number">-1</span>]: <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(triangle[i])): DP[j] = min(DP[j], DP[j+<span class="hljs-number">1</span>]) + triangle[i][j] <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><ul><li>时间复杂度:$O(m \times n)$</li><li>空间复杂度:$O(n)$</li></ul><h2 id="3、乘积最大子序列"><a href="#3、乘积最大子序列" class="headerlink" title="3、乘积最大子序列"></a><strong>3、乘积最大子序列</strong></h2><p>此题是leetcode<a href="https://leetcode-cn.com/problems/maximum-product-subarray/" target="_blank" rel="noopener">第152题</a>。<br>我们设有一个数组<code>a=[2, 3, -2, 4]</code>,其乘积最大的子序列为<code>2, 3</code>,最大乘积为6。假设我们在第<code>i</code>位,因为这个数可能为正也可能为负,所以我们需要记录最大乘积子序列和最小乘积子序列。我们可以这样定义状态和状态转移方程:</p><ul><li>状态:$DP_{max}[i]$和$DP_{min}[i]$。代表的意思是,在第$i$个数时,从$0 \to i$的最大乘积子序列的乘积值,注意这里包括第$i$个数。</li><li>状态转移方程:因为$a[i]$可能为正也可能为负,为负时要乘前面的最小子序列的乘积才能变为最大值,因此要区分$a[i]$为正负的情况:<script type="math/tex; mode=display">DP_{max}[i]=\begin{cases}DP_{max}[i-1] \times a[i], a[i] \geq 0 \\DP_{min}[i-1] \times a[i], a[i] < 0\end{cases}</script><script type="math/tex; mode=display">DP_{min}[i]=\begin{cases}DP_{min}[i-1] \times a[i], a[i] \geq 0 \\DP_{max}[i-1] \times a[i], a[i] < 0\end{cases}</script></li></ul><p>最后的结果为$max\{DP_{max}\}$</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProduct</span><span class="hljs-params">(self, nums: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> nums <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># 这里我们不需要为nums里的每个元素都开辟一个存储最大最小值的空间</span> <span class="hljs-comment"># 只需要当前元素和前一个元素的就行</span> DP_max, DP_min, res = [nums[<span class="hljs-number">0</span>], nums[<span class="hljs-number">0</span>]], [nums[<span class="hljs-number">0</span>], nums[<span class="hljs-number">0</span>]], nums[<span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(nums)): num = nums[i] x, y = i % <span class="hljs-number">2</span>, (i - <span class="hljs-number">1</span>) % <span class="hljs-number">2</span> DP_max[x] = max(DP_max[y] * num, DP_min[y] * num, num) DP_min[x] = min(DP_max[y] * num, DP_min[y] * num, num) res = max(DP_max[x], res) <span class="hljs-keyword">return</span> res</code></pre><ul><li>时间复杂度:$O(N^2)$</li><li>空间复杂度:$O(1)$</li></ul><h2 id="4、最长上升子序列"><a href="#4、最长上升子序列" class="headerlink" title="4、最长上升子序列"></a><strong>4、最长上升子序列</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/longest-increasing-subsequence/" target="_blank" rel="noopener">第300题</a>。<br>假设有一序列<code>a=[10, 9, 2, 5, 3, 7, 101, 18, 20]</code>,其最长上升子序列为<code>2, 3, 7, 18, 20</code>,长度为5。假如我们处于第$i$个位置,对于它前面的每个元素$j$,都有个从0到$j$的最长上升子序列长度,如果$a[i]>a[j]$,那么$a[0]$到$a[j]$的最长上升子序列再加上$a[i]$通用可以构成上升序列。所以对应每个$j$,我们找到他们当中加上$a[j]$后最长上升子序列的长度即可。</p><ul><li>状态:$DP[i]$,从0到$i$的最长上升子序列的长度(包含第$i$个元素)</li><li>状态转移方程:对于$i \in[0, n-1]$,$DP[i]=max\{DP[i], DP[j]+1\}$,其中$j \in [0, i-1]$且$a[j] < a[i]$</li></ul><p>最后的结果为$max\{DP[0], DP[1], \cdots, DP[n-1]\}$</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lengthOfLIS</span><span class="hljs-params">(self, nums: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> nums <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">or</span> len(nums) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> DP = [<span class="hljs-number">1</span>] * len(nums) res = <span class="hljs-number">1</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(nums)): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, i): <span class="hljs-keyword">if</span> nums[j] < nums[i]: DP[i] = max(DP[j] + <span class="hljs-number">1</span>, DP[i]) res = max(DP[i], res) <span class="hljs-keyword">return</span> res</code></pre><h2 id="5、零钱兑换"><a href="#5、零钱兑换" class="headerlink" title="5、零钱兑换"></a><strong>5、零钱兑换</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/coin-change/" target="_blank" rel="noopener">第322题</a>。<br>设有不同面额的硬币<code>coins=[1, 2, 5]</code>,和一个总金额<code>amount=11</code>。这个题可以转为类似爬楼梯的问题(上面第1题),每次可以爬1、2、5步,一共有11级台阶,所需的最少步数是多少。</p><ul><li>状态:$DP[i]$,到达第$i$阶时最少的步数</li><li>状态转移方程:$DP[i]=min\{DP[i-coins[j]]\} +1,j \in [0, n-1] \space \space \text{and} \space \space coins[j] \leq i$</li></ul><p>最终结果为$DP[amount]$</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">coinChange</span><span class="hljs-params">(self, coins: List[int], amount: int)</span> -> int:</span> <span class="hljs-comment"># 初始化一个长度为amount + 1的数组</span> DP = [<span class="hljs-number">0</span>] + [amount+<span class="hljs-number">1</span>] * (amount) <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, amount+<span class="hljs-number">1</span>): <span class="hljs-keyword">for</span> coin <span class="hljs-keyword">in</span> coins: <span class="hljs-keyword">if</span> coin <= i: DP[i] = min(DP[i], DP[i-coin] + <span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> DP[amount] > amount:<span class="hljs-comment"># 说明DP[i]没有被更新,没有可以组成amount的组合</span> <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span> <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> DP[amount]</code></pre><ul><li>时间复杂度:$O(amount \times n)$</li><li>空间复杂度:$O(amount)$</li></ul><h2 id="6、编辑距离"><a href="#6、编辑距离" class="headerlink" title="6、编辑距离"></a><strong>6、编辑距离</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/edit-distance/" target="_blank" rel="noopener">第72题</a><br>有两个单词<code>word1</code>和<code>word2</code>,假设我们处于<code>word1</code>的第$i$位和<code>word2</code>的第$j$位,分为两种情况。<strong>第一种情况</strong>,$w[i]==w[j]$,此时不需要任何变化,此时的最少的操作数为<code>word1的第0到i-1个字符变为word2第0至j-1个字符所需的最少操作数</code>。<strong>第二种情况</strong>,$w[i]!=w[j]$,那么可能执行三种操作(插入,删除,替换)。如果执行插入操作,那么到此步需要懂得最少操作数为<code>word1的第0至i-1个字符变为word2第0至j个字符所需的最少操作数</code>;如果执行删除操作,那么到此步需要懂得最少操作数为<code>word1的第0至i个字符变为word2第0至j-1个字符所需的最少操作数</code>;如果执行替换操作,那么和第一种情况是类似的。那到底选哪种操作呢,答案是选择使得当前状态最小的操作,即上面的三个操作中取$min$。<br><img src="https://img-blog.csdnimg.cn/20200328183436156.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><ul><li>状态:$DP[i][j]$,<code>word1</code>的前$i$个字符变为<code>word2</code>的前$j$个字符所需的最少步数</li><li>状态转移方程:<script type="math/tex; mode=display">DP[i][j] = \begin{cases}DP[i-1][j-1], \space \space if \space \space word[i] == word[j] \\min\{\underbrace{DP[i-1,j]}_{insert}, \underbrace{DP[i, j-1]}_{delete}, \underbrace{DP[i-1, j-1]}_{replace}\} + 1, \space \space if \space \space word[i] != word[j]\end{cases}</script></li></ul><p>最终结果为$DP[m][n]$</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">minDistance</span><span class="hljs-params">(self, word1: str, word2: str)</span> -> int:</span> m, n = len(word1), len(word2) <span class="hljs-comment"># 初始化一个二维数组,为(m+1) x (n+1)大小</span> DP = [[<span class="hljs-number">0</span> <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(n + <span class="hljs-number">1</span>)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(m + <span class="hljs-number">1</span>)] <span class="hljs-comment"># 初值</span> <span class="hljs-comment"># word1的第0至i个字符变为空需要i步操作</span> <span class="hljs-comment"># word1由空变为word的第0至j个字符需要j步操作</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m + <span class="hljs-number">1</span>): DP[i][<span class="hljs-number">0</span>] = i <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n + <span class="hljs-number">1</span>): DP[<span class="hljs-number">0</span>][j] = j <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, m + <span class="hljs-number">1</span>): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, n + <span class="hljs-number">1</span>): <span class="hljs-keyword">if</span> word1[i - <span class="hljs-number">1</span>] == word2[j - <span class="hljs-number">1</span>]: DP[i][j] = DP[i - <span class="hljs-number">1</span>][j - <span class="hljs-number">1</span>] <span class="hljs-keyword">else</span>: DP[i][j] = min(DP[i - <span class="hljs-number">1</span>][j], DP[i][j - <span class="hljs-number">1</span>], DP[i - <span class="hljs-number">1</span>][j - <span class="hljs-number">1</span>]) + <span class="hljs-number">1</span> <span class="hljs-keyword">return</span> DP[m][n]</code></pre><ul><li>时间复杂度:$O(m \times n)$</li><li>空间复杂度:$O(m \times n)$</li></ul><h2 id="7、股票买卖问题"><a href="#7、股票买卖问题" class="headerlink" title="7、股票买卖问题"></a><strong>7、股票买卖问题</strong></h2><p>这里我们讲解关于股票买卖的6道系列题:<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/" target="_blank" rel="noopener">121</a>、<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/" target="_blank" rel="noopener">122</a>、<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/" target="_blank" rel="noopener">123</a>、<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/" target="_blank" rel="noopener">188</a>、<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/" target="_blank" rel="noopener">309</a>、<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/" target="_blank" rel="noopener">714</a>。以上题目均可以用一个状态转移方程解决,只需稍微修改既可以。我们以一个通用的情况为例,即“每天可以完成$K$笔交易”。注意:(1)一次交易是包括买和卖的过程;(2)当前有股票时只能卖,不能再次买入</p><p>我们设数组$a$的长度为$N$,我们可以设状态为到第$i$天时所获得的最大利润,是一个一维数组。但我们会发现,在写状态转移方程时无法判断当前有无股票(无法判断当前应该买还是卖)、无法判断当前是第几次交易(达到最大交易次数则无法再交易),因此上面两个信息也应该出现在状态里。那么我们需要定义一个三维的状态:</p><ul><li><p>状态的定义:$DP[i][k][j]$,到第$i$天时的最大利润。其中:(1)$k \in [0, K]$,表示$i$之前总共进行了多少次交易;(2)$j=0 \space or \space 1$,0表示当前无股票,只能不动或买,1表示当前有股票,只能不动或卖。</p></li><li><p>状态转移方程:当处于第$i$天第$k$次交易时,可能出现有股票也可能无股票的情况,即$j$可取值为0或1。当$j=0$即没有股票时,可能是前一天也没有股票,当下也不做交易;也可能是前一天有股票,当下把它卖掉了,注意前一天有股票依然是第$k$次交易,因为只有再次卖掉才算一次交易。同理当$j=1$即有股票时,可能是前一天有股票,当下不做交易;也可能是前一天无股票,当下买入了,注意前一天无股票那么就是已经完成了$k-1$次交易。</p></li></ul><script type="math/tex; mode=display">DP[i, k, 0]=max\begin{cases}DP[i-1, k, 0] \text{,前一天无股票,不动}\\DP[i-1, k, 1] + a[i] \text{,前一天有股票,卖掉}\end{cases}</script><script type="math/tex; mode=display">DP[i, k, 1]=max\begin{cases}DP[i-1, k, 1] \text{,前一天有股票,不动}\\DP[i-1, k-1, 0] - a[i] \text{,前一天无股票,买入}\end{cases}</script><p>最终结果为$max\{DP[n-1, k, 0], k \in [0, K]\}$。在这样的状态和状态转移方程下,时间复杂度为$O(N \times K)$,空间复杂度为$O(N \times K)$。</p><p>用这么一个状态转移方程就可以解决下面6道类似的题目:</p><p><strong>188题:</strong> 最多可以完成$K$笔交易</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, K: int, prices: List[int])</span> -> int:</span> <span class="hljs-comment"># 特殊情况,只有0天或1天无法完成一次完整的交易,利润为0</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment"># 这里需要对K进行一下讨论:若K大于prices长度的一半,那么实际上可以进行不限次数的交易,和122题的情况一样</span> <span class="hljs-comment"># 不区分这样的情况也可以,但在leetcode上会超时</span> <span class="hljs-keyword">if</span> K < len(prices) // <span class="hljs-number">2</span>: <span class="hljs-comment"># 定义状态,三维数组</span> DP = [[[<span class="hljs-number">0</span>, <span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(K + <span class="hljs-number">1</span>)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(len(prices))] <span class="hljs-comment"># 初始化状态,第0天的每次交易有股票的情况下,利润为-prices[0]</span> <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, K+<span class="hljs-number">1</span>): DP[<span class="hljs-number">0</span>][k][<span class="hljs-number">1</span>] = -prices[<span class="hljs-number">0</span>] <span class="hljs-comment"># 根据状态方程遍历每天和每天的交易次数</span> res = <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(prices)): <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, K+<span class="hljs-number">1</span>): DP[i][k][<span class="hljs-number">0</span>] = max(DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">0</span>], DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">1</span>] + prices[i]) DP[i][k][<span class="hljs-number">1</span>] = max(DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">1</span>], DP[i<span class="hljs-number">-1</span>][k<span class="hljs-number">-1</span>][<span class="hljs-number">0</span>] - prices[i]) <span class="hljs-keyword">if</span> res < DP[i][k][<span class="hljs-number">0</span>]: res = DP[i][k][<span class="hljs-number">0</span>] <span class="hljs-keyword">return</span> res <span class="hljs-comment"># 和122题一样的情况,解释见下面的122题</span> <span class="hljs-keyword">else</span>: DP = [<span class="hljs-number">0</span>, -prices[<span class="hljs-number">0</span>]] <span class="hljs-keyword">for</span> price <span class="hljs-keyword">in</span> prices[<span class="hljs-number">1</span>:]: DP[<span class="hljs-number">0</span>] = max(DP[<span class="hljs-number">0</span>], DP[<span class="hljs-number">1</span>] + price) DP[<span class="hljs-number">1</span>] = max(DP[<span class="hljs-number">1</span>], DP[<span class="hljs-number">0</span>] - price) <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><p><strong>121题</strong>:只能进行一次交易</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, prices: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment"># 只能进行一次交易的话可以把K这一维去掉,同时我们只关注相邻的状态,DP的空间复杂度可以由O(N)变为O(1)</span> DP = [<span class="hljs-number">0</span>, -prices[<span class="hljs-number">0</span>]] <span class="hljs-keyword">for</span> price <span class="hljs-keyword">in</span> prices[<span class="hljs-number">1</span>:]: DP[<span class="hljs-number">0</span>] = max(DP[<span class="hljs-number">0</span>], DP[<span class="hljs-number">1</span>] + price) DP[<span class="hljs-number">1</span>] = max(DP[<span class="hljs-number">1</span>], -price) <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><p><strong>122题:</strong> 可以交易无限次</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, prices: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> DP = [<span class="hljs-number">0</span>, -prices[<span class="hljs-number">0</span>]] <span class="hljs-comment"># 可以交易无限次的话K这个维度也没有意义了,可以去掉</span> <span class="hljs-keyword">for</span> price <span class="hljs-keyword">in</span> prices[<span class="hljs-number">1</span>:]: DP[<span class="hljs-number">0</span>] = max(DP[<span class="hljs-number">0</span>], DP[<span class="hljs-number">1</span>] + price) DP[<span class="hljs-number">1</span>] = max(DP[<span class="hljs-number">1</span>], DP[<span class="hljs-number">0</span>] - price) <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><p><strong>123题:</strong> 最多可以完成2笔交易</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, prices: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment"># 是188题的特殊情况,可以直接设K=2</span> K = <span class="hljs-number">2</span> DP = [[[<span class="hljs-number">0</span>, <span class="hljs-number">0</span>] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(K + <span class="hljs-number">1</span>)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(len(prices))] <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, K+<span class="hljs-number">1</span>): DP[<span class="hljs-number">0</span>][k][<span class="hljs-number">1</span>] = -prices[<span class="hljs-number">0</span>] res = <span class="hljs-number">0</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, len(prices)): <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, K+<span class="hljs-number">1</span>): DP[i][k][<span class="hljs-number">0</span>] = max(DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">0</span>], DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">1</span>] + prices[i]) DP[i][k][<span class="hljs-number">1</span>] = max(DP[i<span class="hljs-number">-1</span>][k][<span class="hljs-number">1</span>], DP[i<span class="hljs-number">-1</span>][k<span class="hljs-number">-1</span>][<span class="hljs-number">0</span>] - prices[i]) <span class="hljs-keyword">if</span> res < DP[i][k][<span class="hljs-number">0</span>]: res = DP[i][k][<span class="hljs-number">0</span>] <span class="hljs-keyword">return</span> res</code></pre><p><strong>309题:</strong> 不限交易次数,但有一天冷冻期,即卖出股票后,你无法在第二天买入股票 </p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, prices: List[int])</span> -> int:</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> DP = [<span class="hljs-number">0</span>, -prices[<span class="hljs-number">0</span>]] prev_prev = <span class="hljs-number">0</span><span class="hljs-comment"># 只有在前前一天卖掉后才能买</span> <span class="hljs-keyword">for</span> price <span class="hljs-keyword">in</span> prices[<span class="hljs-number">1</span>:]: temp = DP[<span class="hljs-number">0</span>]<span class="hljs-comment"># 用一个临时变量保存前一天的DP[0]</span> DP[<span class="hljs-number">0</span>] = max(DP[<span class="hljs-number">0</span>], DP[<span class="hljs-number">1</span>] + price) DP[<span class="hljs-number">1</span>] = max(DP[<span class="hljs-number">1</span>], prev_prev - price) prev_prev = temp<span class="hljs-comment"># 当前操作完成后,temp就成了前前一天</span> <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><p><strong>714题:</strong> 不限交易次数,但买的时候有交易费。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxProfit</span><span class="hljs-params">(self, prices: List[int], fee: int)</span> -> int:</span> <span class="hljs-keyword">if</span> len(prices) <= <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment"># 整体和122题一样</span> DP = [<span class="hljs-number">0</span>, -prices[<span class="hljs-number">0</span>]-fee]<span class="hljs-comment"># 在买的时候减去交易费</span> <span class="hljs-keyword">for</span> price <span class="hljs-keyword">in</span> prices[<span class="hljs-number">1</span>:]: DP[<span class="hljs-number">0</span>] = max(DP[<span class="hljs-number">0</span>], DP[<span class="hljs-number">1</span>] + price) DP[<span class="hljs-number">1</span>] = max(DP[<span class="hljs-number">1</span>], DP[<span class="hljs-number">0</span>] - price - fee)<span class="hljs-comment"># 买的时候减去交易费</span> <span class="hljs-keyword">return</span> DP[<span class="hljs-number">0</span>]</code></pre><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a><strong>总结</strong></h1><p>动态规划的题目最重要的是定义好<strong>状态</strong>和<strong>状态转移方程</strong>,同时要注意边界条件</p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><p>[1] <a href="https://www.cnblogs.com/hithongming/p/9229871.html" target="_blank" rel="noopener">https://www.cnblogs.com/hithongming/p/9229871.html</a><br>[2] <a href="https://blog.csdn.net/ailaojie/article/details/83014821" target="_blank" rel="noopener">https://blog.csdn.net/ailaojie/article/details/83014821</a></p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法———位运算</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E4%BD%8D%E8%BF%90%E7%AE%97/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E4%BD%8D%E8%BF%90%E7%AE%97/</id>
<published>2020-06-04T07:10:49.000Z</published>
<updated>2020-06-04T07:11:30.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、位运算"><a href="#一、位运算" class="headerlink" title="一、位运算"></a><strong>一、位运算</strong></h1><p><strong>位运算:</strong> 直接对整数在内存中的二进制位进行操作。比如6的二进制是<code>110</code>,11的二进制是<code>1011</code>,那么<code>6 and 11</code>的结果就是2,它是二进制对应位进行逻辑运算的结果。由于位运算直接在内存数据进行操作,不需要转为十进制,因此速度非常快。</p><p><strong>常用的逻辑运算符:</strong><br>| 符号 | 描述 | 运算规则 |<br>| —— | —— | —————————————————————————————— |<br>| & | 与 | 两个都为1时,结果才为1 |<br>| | | 或 | 两个都为 0时,结果才为1 |<br>| ^ | 异或 | 相同为0,不同为1 |<br>| ~ | 取反 | 0变1,1变0 |<br>| << | 左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 |<br>| >> | 右移 | 各二进制位全部右移若干位,对于无符号数,高位补0,;对于有符号数,有的补符号位(算数右移),有的补0(逻辑右移) |</p><p><strong>异或的特点:</strong></p><ul><li><code>x^0 = x</code></li><li><code>x^1s = ~x</code>。<code>1s</code>的意思是全为1的二进制数</li><li><code>x^(~x) = 1s</code><ul><li><code>x^x = 0</code></li><li><code>a^b = c</code> $\rightarrow$ <code>a^c = b, b^c = a</code>。用来交换a、b的值。比如,设<code>a = 1001, b = 1100</code>,那么<code>c = a^b = 0101</code>,则<code>a^c = 1100, b^c = 1001</code>,正好可以交换a、b的值。</li><li><code>a^b^c = a^(b^c) = (a^b)^c</code></li></ul></li></ul><p><strong>常用的位运算操作:</strong></p><ul><li>判断奇偶数:<code>x & 1 ==1</code>为奇数,<code>x & 1 == 0</code>为偶数</li><li>清零最低位的1:<code>x & (x-1)</code></li><li>得到最低位的1:<code>x & -x</code>。<code>-x</code>为取反再加1</li></ul><p><strong>更为复杂的位运算操作:</strong></p><ul><li>将x最右边的n位清零:<code>x & (~ 0<<n)</code></li><li>获取x的第n位值(0或1):<code>(x >> n) & 1</code></li><li>获取x的第n位幂值:<code>x & (1 << (n-1))</code></li><li>仅将第n位置为1:<code>x | (1<<n)</code></li><li>仅将第n位置为0:<code>x & (~(1<<n))</code></li><li>将x最高位至第n位(含)清零:<code>x & ((1 << n)-1)</code></li><li>将x第n位至第0位(含)清零:<code>x & (~((1 << (n+1))-1))</code></li></ul><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h3 id="(1)位1的个数:"><a href="#(1)位1的个数:" class="headerlink" title="(1)位1的个数:"></a><strong>(1)位1的个数:</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/number-of-1-bits/" target="_blank" rel="noopener">第191题</a>。用“常用的位运算操作”里的“清零最低位的1”<br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hammingWeight</span><span class="hljs-params">(self, n: int)</span> -> int:</span> res = <span class="hljs-number">0</span> <span class="hljs-keyword">while</span> n != <span class="hljs-number">0</span>: res += <span class="hljs-number">1</span> n = n & (n - <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> res</code></pre></p><h3 id="(2)2的幂:"><a href="#(2)2的幂:" class="headerlink" title="(2)2的幂:"></a><strong>(2)2的幂:</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/power-of-two/" target="_blank" rel="noopener">第231题</a>。2的幂化成2进制,它们都只有一个1,只要判断1的个数即可<br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isPowerOfTwo</span><span class="hljs-params">(self, n: int)</span> -> bool:</span> <span class="hljs-keyword">return</span> n != <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> n & (n - <span class="hljs-number">1</span>) == <span class="hljs-number">0</span></code></pre></p><h3 id="(3)比特位计数"><a href="#(3)比特位计数" class="headerlink" title="(3)比特位计数"></a><strong>(3)比特位计数</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/counting-bits/" target="_blank" rel="noopener">第338题</a><br>给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">countBits</span><span class="hljs-params">(self, num: int)</span> -> List[int]:</span> res = [<span class="hljs-number">0</span>] * (num + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, num + <span class="hljs-number">1</span>): res[n] += (res[n&(n<span class="hljs-number">-1</span>)] + <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> res</code></pre><h3 id="(4)N皇后II,位运算解法"><a href="#(4)N皇后II,位运算解法" class="headerlink" title="(4)N皇后II,位运算解法"></a><strong>(4)N皇后II,位运算解法</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/n-queens-ii/submissions/" target="_blank" rel="noopener">第52题</a>,常规用深度优先搜索解法,会用到集合去保存横竖、斜角方向上的状态,解法<a href="https://blog.csdn.net/u014157632/article/details/104886479" target="_blank" rel="noopener">点这里</a>。而使用位运算可以直接获得棋盘上可以放皇后的位置,可以提高效率。<br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-comment"># 使用位运算解决</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">totalNQueens</span><span class="hljs-params">(self, n)</span>:</span> <span class="hljs-keyword">if</span> n < <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> [] self.count = <span class="hljs-number">0</span> self.DFS(n, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> self.count <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">DFS</span><span class="hljs-params">(self, n, row, col, pie, na)</span>:</span> <span class="hljs-keyword">if</span> row >= n: self.count += <span class="hljs-number">1</span> <span class="hljs-keyword">return</span> <span class="hljs-comment"># 获得可以放皇后的位置</span> bits = (~(col | pie | na)) & ((<span class="hljs-number">1</span> << n) - <span class="hljs-number">1</span>) <span class="hljs-comment"># 递归</span> <span class="hljs-keyword">while</span> bits: p = bits & -bits <span class="hljs-comment"># 获得最低位的1,也就是遍历合法的位置</span> self.DFS(n, row + <span class="hljs-number">1</span>, (col | p), (pie | p) << <span class="hljs-number">1</span>, (na | p) >> <span class="hljs-number">1</span>) bits = bits & (bits - <span class="hljs-number">1</span>) <span class="hljs-comment"># 去掉最低位的1</span></code></pre></p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法———哈希</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E5%93%88%E5%B8%8C/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E5%93%88%E5%B8%8C/</id>
<published>2020-06-04T07:09:34.000Z</published>
<updated>2020-06-04T07:10:32.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、哈希表"><a href="#一、哈希表" class="headerlink" title="一、哈希表"></a><strong>一、哈希表</strong></h1><h2 id="1、哈希表的原理"><a href="#1、哈希表的原理" class="headerlink" title="1、哈希表的原理"></a><strong>1、哈希表的原理</strong></h2><p>哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。哈希表由<strong>直接寻址表</strong>和<strong>哈希函数</strong>构成。哈希函数$h(K)$将元素关键字$K$作为自变量,返回元素的存储下标。直接寻址表如下所示:<br><img src="https://img-blog.csdnimg.cn/20200320171026518.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>$U$为键值的全域,包含了所有可能的键值,$K$为实际使用的键值,$T$为直接寻址表,每个关键字对应表中的一个位置,比如2、3、5、8这几个关键字对应了$T(2),T(3),T(5),T(8)$。$T(K)$则指向了关键字为$K$的元素。但直接寻址有这样的缺点:(1)当$U$很大时,需要消耗大量的内存;(2)若$U$很大而$K$很小,则有很多空间被浪费;(3)无法处理关键字不是数字的情况。为改进这些缺点,可以使用哈希函数。直接寻址表是键值为$K$的元素放到$T(K)$位置上,而哈希函数构建一个大小为$m$的寻址表$T$,键值为$K$的元素放到$h(K)$的位置上。$h(K)$是一个函数,将域$U$映射到表$T[0, 1, …, m-1]$。<br>假设有一个长度为7的哈希表,哈希函数为$h(K)=K\%7$。元素集合$\{14,22,3,5\}$的存储方式如下图:<br><img src="https://img-blog.csdnimg.cn/20200320172941666.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>14余7等于0,存在了$T(0)$的位置,22余7等于1,存在了$T(1)$的位置。这样做也会有问题,比如再存一个键值为15的元素,15余7等于1,但此时$T(1)$已经被占了,这就是<strong>哈希冲突</strong>。如果位置$i$被占,可以解决哈希冲突的方法:</p><ul><li>线性探查:探查$i+1,i+2,…$这些位置</li><li>二次探查:$i+1^2, i-1^2, i+2^2, i-2^2,…$这些位置</li><li>二度哈希:有$n$个哈希函数,当第一个哈希函数冲突时,则使用第二个、第三个……</li><li><p>拉链法:哈希表每个位置都连一个链表,冲突的元素将被加到该位置链表的最后。如下图所示:<br><img src="https://img-blog.csdnimg.cn/20200320174059576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>常用的哈希函数:</p></li><li><p>除法哈希:$h(K)=K\%m$</p></li><li>乘法哈希:$h(K)=floor(m \times (A*K\%1))$</li><li>全域哈希:$h(K)=((a \times K + b) \% p) \% m \space \space \space \space a,b=1,2,…p-1$</li></ul><h2 id="2、python里哈希的用法"><a href="#2、python里哈希的用法" class="headerlink" title="2、python里哈希的用法"></a><strong>2、python里哈希的用法</strong></h2><p>哈希表由两种不同的类型:<strong>哈希集合</strong>和<strong>哈希映射</strong></p><ul><li><strong>哈希集合</strong>是集合数据结构的实现之一,用于存储非重复值。python里可以用集合<code>set</code>实现:</li></ul><pre><code class="hljs python"><span class="hljs-comment"># 1. 初始化哈希集合</span>hashset = set() <span class="hljs-comment"># 2. 添加新的键</span>hashset.add(<span class="hljs-number">3</span>)hashset.add(<span class="hljs-number">2</span>)hashset.add(<span class="hljs-number">1</span>)<span class="hljs-comment"># 3. 删除键</span>hashset.remove(<span class="hljs-number">2</span>)<span class="hljs-comment"># 4. 检查键是否在哈希集合里</span><span class="hljs-keyword">if</span> (<span class="hljs-number">2</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hashset): print(<span class="hljs-string">"Key 2 is not in the hash set."</span>)<span class="hljs-comment"># 5. 获得哈希集合的大小</span>print(<span class="hljs-string">"Size of hashset is:"</span>, len(hashset)) <span class="hljs-comment"># 6. 迭代哈希集合</span><span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> hashset: print(x, end=<span class="hljs-string">" "</span>)print(<span class="hljs-string">"are in the hash set."</span>)<span class="hljs-comment"># 7. 清空哈希集合</span>hashset.clear() print(<span class="hljs-string">"Size of hashset:"</span>, len(hashset))</code></pre><ul><li><strong>哈希映射</strong>是映射 数据结构的实现之一,用于存储(key, value)键值对。在python可以用字典<code>dict</code>实现:</li></ul><pre><code class="hljs python"><span class="hljs-comment"># 1. 初始化哈希映射</span>hashmap = {<span class="hljs-number">0</span> : <span class="hljs-number">0</span>, <span class="hljs-number">2</span> : <span class="hljs-number">3</span>}<span class="hljs-comment"># 2. 插入(key, value) 或者更新已存在的键值对</span>hashmap[<span class="hljs-number">1</span>] = <span class="hljs-number">1</span>hashmap[<span class="hljs-number">1</span>] = <span class="hljs-number">2</span><span class="hljs-comment"># 3. 获得键值</span>print(<span class="hljs-string">"The value of key 1 is: "</span> + str(hashmap[<span class="hljs-number">1</span>]))<span class="hljs-comment"># 4. 删除键</span><span class="hljs-keyword">del</span> hashmap[<span class="hljs-number">2</span>]<span class="hljs-comment"># 5. 检查键是否在哈希集合里</span><span class="hljs-keyword">if</span> <span class="hljs-number">2</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hashmap: print(<span class="hljs-string">"Key 2 is not in the hash map."</span>)<span class="hljs-comment"># 6. 键和键值可以有不同的数据类型</span>hashmap[<span class="hljs-string">"pi"</span>] = <span class="hljs-number">3.1415</span><span class="hljs-comment"># 7. 获得哈希映射的大小</span>print(<span class="hljs-string">"The size of hash map is: "</span> + str(len(hashmap)))<span class="hljs-comment"># 8. 迭代哈希映射</span><span class="hljs-keyword">for</span> key <span class="hljs-keyword">in</span> hashmap: print(<span class="hljs-string">"("</span> + str(key) + <span class="hljs-string">","</span> + str(hashmap[key]) + <span class="hljs-string">")"</span>, end=<span class="hljs-string">" "</span>)print(<span class="hljs-string">"are in the hash map."</span>)<span class="hljs-comment"># 9. 获得所有键</span>print(hashmap.keys())<span class="hljs-comment"># 10. 清空哈希映射</span>hashmap.clear();print(<span class="hljs-string">"The size of hash map is: "</span> + str(len(hashmap)))</code></pre><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h2 id="1、哈希集合的应用"><a href="#1、哈希集合的应用" class="headerlink" title="1、哈希集合的应用"></a><strong>1、哈希集合的应用</strong></h2><p><strong>存在重复元素:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/contains-duplicate/" target="_blank" rel="noopener">第217题</a>。<br><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">containsDuplicate</span><span class="hljs-params">(self, nums: List[int])</span> -> bool:</span> hash_ = set() <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> nums: <span class="hljs-keyword">if</span> num <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hash_: hash_.add(num) <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span></code></pre></p><ul><li>时间复杂度 : $O(n)$。</li><li>空间复杂度 : $O(n)$。哈希表占用的空间与元素数量是线性关系。</li></ul><p><strong>两个数组的集合:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/intersection-of-two-arrays/" target="_blank" rel="noopener">第349题</a><br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">intersection</span><span class="hljs-params">(self, nums1: List[int], nums2: List[int])</span> -> List[int]:</span> set1 = set(nums1) set2 = set(nums2) <span class="hljs-keyword">return</span> list(set2 & set1)</code></pre></p><ul><li>时间复杂度:$O(m+n)$,其中 $n$ 和 $m$ 是数组的长度。$O(n)$ 的时间用于转换 <code>nums1</code> 在集合中,$O(m)$ 的时间用于转换 <code>nums2</code> 到集合中,并且平均情况下,集合的操作为 $O(1)$。<br>空间复杂度:$O(m+n)$,最坏的情况是数组中的所有元素都不同。</li></ul><h2 id="2、哈希映射的应用"><a href="#2、哈希映射的应用" class="headerlink" title="2、哈希映射的应用"></a><strong>2、哈希映射的应用</strong></h2><h3 id="(1)场景一:用映射来提供很多的信息"><a href="#(1)场景一:用映射来提供很多的信息" class="headerlink" title="(1)场景一:用映射来提供很多的信息"></a><strong>(1)场景一:用映射来提供很多的信息</strong></h3><p>哈希集合没有映射,只是单一地存储了不同的键。而哈希映射可以将键映射到键值,提供更多的信息。<br><strong>两数之和:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/two-sum/" target="_blank" rel="noopener">第1题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">twoSum</span><span class="hljs-params">(self, nums: List[int], target: int)</span> -> List[int]:</span> <span class="hljs-comment"># 哈希</span> hash_ = {} <span class="hljs-keyword">for</span> i, a <span class="hljs-keyword">in</span> enumerate(nums): b = target - a <span class="hljs-keyword">if</span> b <span class="hljs-keyword">in</span> hash_: <span class="hljs-keyword">return</span> [i, hash_[b]] hash_[a] = i <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span></code></pre><ul><li><p>时间复杂度:$O(n)$,我们只遍历了包含有 $n$ 个元素的列表一次。在表中进行的每次查找只花费 $O(1)$ 的时间。</p></li><li><p>空间复杂度:$O(n)$,所需的额外空间取决于哈希表中存储的元素数量,该表最多需要存储 $n$ 个元素。</p></li></ul><p><strong>三数之和:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/3sum/" target="_blank" rel="noopener">第15题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">threeSum</span><span class="hljs-params">(self, nums: List[int])</span> -> List[List[int]]:</span> <span class="hljs-keyword">if</span> len(nums) < <span class="hljs-number">3</span>: <span class="hljs-keyword">return</span> [] <span class="hljs-comment"># 先对数组排序, 遍历数组遇到与前一个元素相同的情况可直接跳过</span> nums.sort() res = set() <span class="hljs-keyword">for</span> i, x <span class="hljs-keyword">in</span> enumerate(nums[:<span class="hljs-number">-2</span>]): <span class="hljs-keyword">if</span> i >= <span class="hljs-number">1</span> <span class="hljs-keyword">and</span> nums[i<span class="hljs-number">-1</span>] == x: <span class="hljs-keyword">continue</span> hash_ = {} <span class="hljs-keyword">for</span> j, y <span class="hljs-keyword">in</span> enumerate(nums[i+<span class="hljs-number">1</span>:]): <span class="hljs-keyword">if</span> y <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hash_: hash_[-y-x] = <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: res.add((x, -x - y, y)) <span class="hljs-keyword">return</span> list(res)</code></pre><ul><li>时间复杂度:$O(n^2)$</li><li>空间复杂度:$O(n)$</li></ul><p><strong>同构字符串:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/isomorphic-strings/" target="_blank" rel="noopener">第205题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isIsomorphic</span><span class="hljs-params">(self, s: str, t: str)</span> -> bool:</span> hash_ = {} <span class="hljs-keyword">for</span> i, a <span class="hljs-keyword">in</span> enumerate(s): <span class="hljs-keyword">if</span> a <span class="hljs-keyword">in</span> hash_: <span class="hljs-keyword">if</span> hash_[a] != t[i]: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">elif</span> t[i] <span class="hljs-keyword">in</span> hash_.values(): <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">else</span>: hash_[a] = t[i] <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre><p><strong>两个列表的最小索引和:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/minimum-index-sum-of-two-lists/" target="_blank" rel="noopener">第599题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findRestaurant</span><span class="hljs-params">(self, list1: List[str], list2: List[str])</span> -> List[str]:</span> hash_ = {} temp = len(list1) + len(list2) res = [] <span class="hljs-keyword">for</span> i, a <span class="hljs-keyword">in</span> enumerate(list1): hash_[a] = i <span class="hljs-keyword">for</span> j, b <span class="hljs-keyword">in</span> enumerate(list2): <span class="hljs-keyword">if</span> b <span class="hljs-keyword">in</span> hash_: <span class="hljs-keyword">if</span> hash_[b] + j == temp: res.append(b) <span class="hljs-keyword">elif</span> hash_[b] + j < temp: res = [] res.append(b) temp = hash_[b] + j <span class="hljs-keyword">return</span> res</code></pre><h3 id="(2)场景二:按键聚合"><a href="#(2)场景二:按键聚合" class="headerlink" title="(2)场景二:按键聚合"></a><strong>(2)场景二:按键聚合</strong></h3><p>这类问题是统计键出现的次数,所以对应的键值为该键出现的次数<br><strong>两个数组的交际II:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/" target="_blank" rel="noopener">第350题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">intersect</span><span class="hljs-params">(self, nums1: List[int], nums2: List[int])</span> -> List[int]:</span> hash_ = {} res = [] <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> nums1: <span class="hljs-keyword">if</span> num <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hash_: hash_[num] = <span class="hljs-number">1</span> <span class="hljs-keyword">else</span>: hash_[num] += <span class="hljs-number">1</span> <span class="hljs-keyword">for</span> num <span class="hljs-keyword">in</span> nums2: <span class="hljs-keyword">if</span> num <span class="hljs-keyword">in</span> hash_ <span class="hljs-keyword">and</span> hash_[num] != <span class="hljs-number">0</span>: res.append(num) hash_[num] -= <span class="hljs-number">1</span> <span class="hljs-keyword">return</span> res</code></pre><ul><li>时间复杂度:$O(n+m)$。其中 $n,m$ 分别代表了数组的大小。<br>空间复杂度:$O(n)$。如果对较小的数组进行哈希映射使用的空间则为$O(min(n,m))$。</li></ul><p><strong>存在重复元素II:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/contains-duplicate-ii/" target="_blank" rel="noopener">第219题</a>。<a href="https://leetcode-cn.com/problems/contains-duplicate-ii/solution/hua-jie-suan-fa-219-cun-zai-zhong-fu-yuan-su-ii-by/" target="_blank" rel="noopener">解法动画</a><br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">containsNearbyDuplicate</span><span class="hljs-params">(self, nums: List[int], k: int)</span> -> bool:</span> hash_ = set() <span class="hljs-keyword">for</span> i, num <span class="hljs-keyword">in</span> enumerate(nums): <span class="hljs-keyword">if</span> num <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> hash_: hash_.add(num) <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">if</span> len(hash_) > k: hash_.remove(nums[i-k]) <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span></code></pre></p><ul><li>时间复杂度:$O(n)$。我们会做 $n$ 次搜索、删除、插入 操作,每次操作都耗费常数时间。</li><li>空间复杂度:$O(min(n,k))$。开辟的额外空间取决于散列表中存储的元素的个数,也就是滑动窗口的大小 $O(min(n,k))$。</li></ul><h2 id="(3)设计键"><a href="#(3)设计键" class="headerlink" title="(3)设计键"></a><strong>(3)设计键</strong></h2><p>上面的题中,键都是显而易见的,但有时候需要设计合适的键。<br><strong>字母异位词分组:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/group-anagrams/" target="_blank" rel="noopener">第49题</a><br><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">groupAnagrams</span><span class="hljs-params">(self, strs: List[str])</span> -> List[List[str]]:</span> hash_ = collections.defaultdict(list) <span class="hljs-keyword">for</span> str <span class="hljs-keyword">in</span> strs: hash_[tuple(sorted(str))].append(str) <span class="hljs-keyword">return</span> list(hash_.values())</code></pre></p><ul><li>时间复杂度:$O(NKlogK)$,其中 $N$ 是 <code>strs</code>的长度,而 $K$ 是 <code>strs</code> 中字符串的最大长度。当我们遍历每个字符串时,外部循环具有的复杂度为 $O(N)$。然后,我们在 $O(KlogK)$ 的时间内对每个字符串排序。</li><li>空间复杂度:$O(NK)$,排序存储在 <code>ans</code> 中的全部信息内容。</li></ul><p><strong>寻找重复子树:</strong> 此题为leetcode<a href="https://leetcode-cn.com/problems/find-duplicate-subtrees/" target="_blank" rel="noopener">第652题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">findDuplicateSubtrees</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[TreeNode]:</span> count = collections.Counter() ans = [] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">collect</span><span class="hljs-params">(node)</span>:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> node: <span class="hljs-keyword">return</span> <span class="hljs-string">"#"</span> serial = <span class="hljs-string">"{},{},{}"</span>.format(node.val, collect(node.left), collect(node.right)) count[serial] += <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> count[serial] == <span class="hljs-number">2</span>: ans.append(node) <span class="hljs-keyword">return</span> serial collect(root) <span class="hljs-keyword">return</span> ans</code></pre><ul><li>时间复杂度:$O(N^2)$,其中 $N$ 是二叉树上节点的数量。遍历所有节点,在每个节点处序列化需要时间 $O(N)$。</li><li>空间复杂度:$O(N^2)$,<code>count</code> 的大小。</li></ul><p><strong>设计键总结(参考自<a href="https://leetcode-cn.com/explore/learn/card/hash-table/206/practical-application-design-the-key/824/" target="_blank" rel="noopener">leetcode</a>):</strong></p><ul><li>当字符串 / 数组中每个元素的顺序不重要时,可以使用排序后的字符串 / 数组作为键:<br><img src="https://img-blog.csdnimg.cn/20200320184605145.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li><li>如果只关心每个值的偏移量,通常是第一个值的偏移量,则可以使用偏移量作为键:<br><img src="https://img-blog.csdnimg.cn/20200320184627589.png" srcset="/img/loading.gif" alt="在这里插入图片描述"></li><li>在树中,可能会直接使用 <code>TreeNode</code> 作为键。 但在大多数情况下,采用子树的序列化表述可能是一个更好的主意。<br><img src="https://img-blog.csdnimg.cn/20200320184719272.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li><li>在矩阵中,可以使用行索引或列索引作为键。</li><li>在数独中,可以将行索引和列索引组合来标识此元素属于哪个块<br><img src="https://img-blog.csdnimg.cn/2020032018475630.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li><li>有时,在矩阵中,您可能希望将值聚合在同一对角线中<br><img src="https://img-blog.csdnimg.cn/20200320184818218.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li></ul>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法———递归</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E9%80%92%E5%BD%92/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E9%80%92%E5%BD%92/</id>
<published>2020-06-04T07:06:55.000Z</published>
<updated>2020-06-04T07:07:38.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、递归"><a href="#一、递归" class="headerlink" title="一、递归"></a><strong>一、递归</strong></h1><p><strong>递归</strong>:在定义一个过程或函数时,出现本过程或本函数的成分称为递归。<br><strong>递归条件</strong>:可以用递归解决的问题应该满足以下三个条件:</p><ul><li>这个问题可以转化为一个或多个子问题来求解,而且这些子问题的求解方法与原问题完全相同</li><li>递归调用的次数是有限的</li><li>必须有终止条件</li></ul><p><strong>递归的底层原理</strong>: 函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移,大多数CPU使用栈来支持函数调用。单个函数调用操作所使用的的函数调用栈被称为<strong>栈帧</strong>。每次函数调用都会相应地创建一帧,返回函数地址、函数实参和局部变量值等,并将该帧压入调用栈。若该函数在返回之前又发生了新的调用,则将新函数对于的帧压入栈,成为栈顶。函数一旦执行完,对应的帧边出栈,控制权交给该函数的上层调用函数,并按照该帧保存的返回地址,确定程序中继续执行的位置。</p><p><strong>递归问题一般解决步骤:</strong></p><ul><li>对问题$f(s_{n})$进行分析,<strong>假设出合理的小问题$f(s_{n-1})$</strong></li><li>假设小问题$f(s_{n-1})$是可解的,在此基础上确定大问题$f(s_{n})$的解,<strong>即给出$f(s_{n})$与$f(s_{n-1})$之间的关系</strong></li><li><strong>确定一个特殊情况</strong>(如$f(s_{1})$或$f(s_{0})$的解),由此作为递归出口</li></ul><p><strong>时间复杂度:</strong> 时间复杂度$O(T)$通常是递归调用的数量(记作 $R$) 和计算的时间复杂度的乘积(表示为 $O(s)$)的乘积:</p><script type="math/tex; mode=display">O(T)=R*O(s)</script><p><strong>空间复杂度:</strong> 在计算递归算法的空间复杂度时,应该考虑造成空间消耗的两个部分:递归相关空间(recursion related space)和非递归相关空间(non-recursion related space)</p><p><strong>代码模板:</strong></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">recursion</span><span class="hljs-params">(level, param1, param2, ...)</span>:</span><span class="hljs-comment"># 递归终止条件</span><span class="hljs-keyword">if</span> level > MAX_LEVEL:<span class="hljs-keyword">return</span> result<span class="hljs-comment"># 当前level的逻辑</span>process_data(level, data)<span class="hljs-comment"># 递归</span>self.recurison(level, param1, param2, ...)<span class="hljs-comment"># 如果必要的话,还原此层的状态</span>reverse_state(level)</code></pre><h1 id="二、例题"><a href="#二、例题" class="headerlink" title="二、例题"></a><strong>二、例题</strong></h1><h2 id="1、两两交换链表中的节点"><a href="#1、两两交换链表中的节点" class="headerlink" title="1、两两交换链表中的节点"></a><strong>1、两两交换链表中的节点</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/swap-nodes-in-pairs/" target="_blank" rel="noopener">第24题</a>。示例:<code>给定 1->2->3->4, 你应该返回 2->1->4->3</code>。我们假设有一个链表,已经走到了中间某个节点,当前节点为head,这个一般情况可以表示如下:<br><img src="https://img-blog.csdnimg.cn/20200318211845285.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>我们希望<code>head</code>和<code>next</code>能够互换,希望达到如下效果:<br><img src="https://img-blog.csdnimg.cn/20200318213225604.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><ul><li>我们希望<code>head</code>和<code>next</code>互换,互换后<code>head</code>指向后面,<code>next</code>变为当前子链表的头结点</li><li>交换后,<code>head</code>应该指向后面的子链表,而后面的子链表应该是交换完成了的,也就是说,后面的子链表两两交换是个子问题,解决方式和当前的方式一样,所以这个子链表的头结点(即<code>head.next.nex</code>应该传入递归函数中)</li><li>因为交换后<code>next</code>为当前子链表的头结点,我们用一个指针指向它,即<code>res=head.next</code>,交换完后我们返回<code>res</code>即可,因为当前子链表也是上一层递归调用的子问题,我们要返回当前链表的头结点</li><li>交换的过程:首先我们需要将<code>head.next</code>指向后面子问题返回的头结点,然后<code>next</code>指向<code>head</code>,即<code>res.next=head</code></li><li>交换完后,返回此时当前子链表的头结点<code>res</code>即可</li><li>终止条件:当<code>head=None</code>(无节点)或<code>head.next=None</code>(此时为最后一个节点)时,直接返回<code>head</code></li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">swapPairs</span><span class="hljs-params">(self, head: ListNode)</span> -> ListNode:</span> <span class="hljs-comment"># 终止条件</span> <span class="hljs-keyword">if</span> head == <span class="hljs-literal">None</span> <span class="hljs-keyword">or</span> head.next == <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> head <span class="hljs-comment"># 交换后的头结点</span> res = head.next <span class="hljs-comment"># 子问题递归,head指向子问题返回来的节点</span> head.next = self.swapPairs(head.next.next) <span class="hljs-comment"># next指向head</span> res.next = head <span class="hljs-keyword">return</span> res</code></pre><ul><li>时间复杂度:$O(N)$,其中 $N$ 指的是链表的节点数量</li><li><p>空间复杂度:$O(N)$,递归过程中使用的堆栈空间</p><h2 id="2、反转链表"><a href="#2、反转链表" class="headerlink" title="2、反转链表"></a><strong>2、反转链表</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/reverse-linked-list/" target="_blank" rel="noopener">第206题</a>。<br>示例:<code>输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL</code><br>假设我们有如下链表:<br><img src="https://img-blog.csdnimg.cn/20200318221055712.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>当前节点为<code>head</code>,假设子问题返回的链表已经两两反转,我们希望节点2能指向节点1,节点1指向<code>None</code>。递归过程如下图所示:<br><img src="https://img-blog.csdnimg.cn/20200318221626423.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p></li><li><p>当前节点为<code>head</code>,我们期望后续的子链表指向<code>head</code>,而<code>head</code>指向<code>None</code>。那么后续子链表的两两交换问题为子问题。</p></li><li>子问题里的子链表节点两两交换后,原来的尾节点(图中的节点4)变成了头结点,并且要返回这个头结点;原来的头结点(图中的节点2)变为了尾节点,并且要指向<code>None</code>,因为尾节点最后都要指向<code>None</code>。注意此时<code>head</code>节点依然是指向节点2的,因为子问题并没有改变<code>head</code>的指向</li><li>交换过程:需要将节点2指向节点1,即<code>head.next.next=head</code>,然后<code>head</code>指向<code>None</code></li><li>终止条件:<code>head=None</code>(无节点)或<code>head.next=None</code>(最后一个节点)时,直接返回<code>head</code></li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">reverseList</span><span class="hljs-params">(self, head: ListNode)</span> -> ListNode:</span> <span class="hljs-comment"># 终止条件</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> head <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> head.next: <span class="hljs-keyword">return</span> head <span class="hljs-comment"># 子问题递归,返回交换完后的子链表的头结点</span> res = self.reverseList(head.next) <span class="hljs-comment"># 交换</span> head.next.next = head head.next = <span class="hljs-literal">None</span> <span class="hljs-keyword">return</span> res</code></pre><ul><li>时间复杂度:$O(N)$,其中 $N$ 指的是链表的节点数量</li><li>空间复杂度:$O(N)$,递归过程中使用的堆栈空间<h2 id="3、斐波那契数列"><a href="#3、斐波那契数列" class="headerlink" title="3、斐波那契数列"></a><strong>3、斐波那契数列</strong></h2>记忆化<br>此题为leetcode<a href="https://leetcode-cn.com/problems/fibonacci-number/" target="_blank" rel="noopener">第509题</a>。斐波那契数列有如下递归关系:<script type="math/tex; mode=display">f(0)=0, f(1)=1 \\f(n)=f(n-1) + f(n-2),n>1</script>给定$n$计算$f(n)$。以计算$f(4)$为例,我们可以得到:$f(4)=f(3)+f(2)=f(2)+f(1)+f(1)+f(0)=f(1)+f(0)+f(1)+f(1)+f(0)$,把这个递归过程画出来如下图所示:<br><img src="https://img-blog.csdnimg.cn/20200318223456628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>按说这个很容易找到子问题,并且终止条件也明确,但是我们观察发现,这其中有很多重复的计算,比如$f(2)$重复了两次,这会导致内存占用较多。为此,我们可以将中间结果暂存起来,到时候直接用就可以。</li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fib</span><span class="hljs-params">(self, N: int)</span> -> int:</span> <span class="hljs-comment"># 缓存字典</span> catch = {} <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">rec</span><span class="hljs-params">(N)</span>:</span> <span class="hljs-comment"># 若N在缓存中,直接返回对应值</span> <span class="hljs-keyword">if</span> N <span class="hljs-keyword">in</span> catch: <span class="hljs-keyword">return</span> catch[N] <span class="hljs-comment"># N小于2时直接返回自己</span> <span class="hljs-keyword">if</span> N < <span class="hljs-number">2</span>: res = N <span class="hljs-keyword">else</span>: res = rec(N<span class="hljs-number">-1</span>) + rec(N<span class="hljs-number">-2</span>) <span class="hljs-comment"># 放入缓存中</span> catch[N] = res <span class="hljs-keyword">return</span> res <span class="hljs-keyword">return</span> rec(N)</code></pre><ul><li>时间复杂度:$O(N)$</li><li>空间复杂度:$O(N)$,缓存空间大小</li></ul><p>注意这里我们使用了一个额外的函数<code>rec(N)</code>,然后直接在<code>fib</code>函数中直接返回<code>rec(N)</code>,我们称这样的递归为<strong>尾递归:尾递归函数是递归函数的一种,其中递归调用是递归函数中的最后一条指令。并且在函数中应该只有一次递归调用。</strong> 尾递归的好处是,它可以避免递归调用期间栈空间开销的累积,因为系统可以为每个递归调用重用栈中的固定空间。</p><h2 id="4、合并两个有序链表"><a href="#4、合并两个有序链表" class="headerlink" title="4、合并两个有序链表"></a><strong>4、合并两个有序链表</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/merge-two-sorted-lists/" target="_blank" rel="noopener">第21题</a>。示例:<code>输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4</code>。我们假设有如下两个有序链表:<br><img src="https://img-blog.csdnimg.cn/20200318230106554.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>设<code>l1</code>指向了第一个链表中的某个位置,<code>l2</code>指向了第二个链表中的某个位置,我们比较这两个节点的大小。如果<code>l1<l2</code>,那么<code>l1</code>应该在<code>l2</code>之后,<code>l2</code>应该和<code>l1</code>之后的子链表继续比较,由此形成子问题。如果<code>l1>l2</code>,那么<code>l2</code>应该在<code>l1</code>之后,<code>l1</code>继续和<code>l2</code>之后的子链表比较。整个过程如下所示:<br><img src="https://img-blog.csdnimg.cn/20200318232602262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><ul><li>若<code>l1<l2</code>,则将<code>l1.next</code>和<code>l2</code>传入递归函数中,将<code>l1</code>指向递归函数返回的节点;反之将<code>l2.next</code>和<code>l1</code>传入递归函数中,<code>l2</code>指向递归函数返回的节点。</li><li>当<code>l1=None</code>时,说明<code>l1</code>已提前遍历完(或<code>l1</code>本来为空),<code>l2</code>剩下的也不用比较了,直接返回<code>l2</code>;同理<code>l2=None</code>时,直接返回<code>l1</code>。</li></ul><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mergeTwoLists</span><span class="hljs-params">(self, l1, l2)</span>:</span> <span class="hljs-keyword">if</span> l1 <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> l2 <span class="hljs-keyword">elif</span> l2 <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> l1 <span class="hljs-keyword">elif</span> l1.val < l2.val: l1.next = self.mergeTwoLists(l1.next, l2) <span class="hljs-keyword">return</span> l1 <span class="hljs-keyword">else</span>: l2.next = self.mergeTwoLists(l1, l2.next) <span class="hljs-keyword">return</span> l2</code></pre><ul><li>时间复杂度:$O(n + m)$。因为每次递归调用都会将指向 <code>l1</code> 或 <code>l2</code> 的指针递增一次(逐渐接近每个列表末尾的 null),所以每个列表中的每个元素都会对 <code>mergeTwoLists</code> 进行一次调用。 因此,时间复杂度与两个列表的大小之和是线性相关的。</li><li>空间复杂度:$O(n + m)$。一旦调用 <code>mergetwolist</code>,直到到达 <code>l1</code>或 <code>l2</code> 的末尾时才会返回,因此 $n + m$的栈将会消耗 $O(n + m)$ 的空间。</li></ul><h2 id="5、Pow-x-n"><a href="#5、Pow-x-n" class="headerlink" title="5、Pow(x, n)"></a><strong>5、Pow(x, n)</strong></h2><p>此题为leetcode<a href="https://leetcode-cn.com/problems/powx-n/submissions/" target="_blank" rel="noopener">第50题</a></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">myPow</span><span class="hljs-params">(self, x: float, n: int)</span> -> float:</span> <span class="hljs-comment"># 递归1</span> <span class="hljs-comment"># def func(x, n):</span> <span class="hljs-comment"># if n == 0:</span> <span class="hljs-comment"># return 1</span> <span class="hljs-comment"># if x == 0:</span> <span class="hljs-comment"># return 0</span> <span class="hljs-comment"># temp = func(x, n >> 1)</span> <span class="hljs-comment"># # 位运算判断奇偶</span> <span class="hljs-comment"># if n & 1: # n为奇数</span> <span class="hljs-comment"># return temp * temp * x</span> <span class="hljs-comment"># else: # n为偶数</span> <span class="hljs-comment"># return temp * temp</span> <span class="hljs-comment"># if n >= 0:</span> <span class="hljs-comment"># res = func(x, n)</span> <span class="hljs-comment"># else:</span> <span class="hljs-comment"># res = 1 / func(x, -n) </span> <span class="hljs-comment"># return res</span> <span class="hljs-comment"># 递归2</span> <span class="hljs-comment"># if n == 0:</span> <span class="hljs-comment"># return 1</span> <span class="hljs-comment"># if n < 0:</span> <span class="hljs-comment"># return 1. / self.myPow(x, -n)</span> <span class="hljs-comment"># if n % 2: # 如果n是奇数,在下一层算n/2</span> <span class="hljs-comment"># return x * self.myPow(x, n - 1)</span> <span class="hljs-comment"># else: # 如果n是偶数</span> <span class="hljs-comment"># return self.myPow(x * x, n/2)</span> <span class="hljs-comment"># 非递归</span> <span class="hljs-keyword">if</span> n < <span class="hljs-number">0</span>: x = <span class="hljs-number">1.</span> / x n = -n res = <span class="hljs-number">1</span> <span class="hljs-keyword">while</span> n: <span class="hljs-keyword">if</span> n & <span class="hljs-number">1</span>: <span class="hljs-comment"># n为奇数</span> res *= x x *= x n >>= <span class="hljs-number">1</span> <span class="hljs-comment"># 右移一位,相当于n // 2</span> <span class="hljs-keyword">return</span> res</code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法———广度与深度优先搜索</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E5%B9%BF%E5%BA%A6%E4%B8%8E%E6%B7%B1%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E5%B9%BF%E5%BA%A6%E4%B8%8E%E6%B7%B1%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2/</id>
<published>2020-06-04T07:05:46.000Z</published>
<updated>2020-06-04T07:06:36.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、广度优先搜索"><a href="#一、广度优先搜索" class="headerlink" title="一、广度优先搜索"></a><strong>一、广度优先搜索</strong></h1><p><strong>广度优先搜索(BFS,Breadth First Search)</strong> 的一个常见应用是找出从根结点到目标结点的最短路径,其实现用到了队列。下面用一个例子来说明BFS的原理,在下图中,我们BFS 来找出根结点 A 和目标结点 G 之间的最短路径。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200315192813260.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="250" height="150"> </p><p align="center"> <em>图3:BFS例子</em> </p><p></p><ul><li>首先初始化一个队列<code>Q</code>,将根节点入队:<code>A</code></li><li><code>A</code>出队,将与<code>A</code>相邻的节点入队,此时队列为<code>BCD</code></li><li><code>B</code>出队,将与<code>B</code>相邻的节点入队,此时队列为<code>CDE</code></li><li><code>C</code>出队,将与<code>C</code>相邻的节点入队,此时队列为<code>DEF</code>(节点<code>E</code>已经被遍历过,不需要再入队)</li><li><code>D</code>出队,将与<code>D</code>相邻的节点入队,此时队列为<code>EFG</code></li><li><code>E</code>出队,下面没有节点</li><li><code>F</code>出队,下面是<code>G</code>,已遍历过,为目标值,遍历结束</li></ul><p><strong>BFS代码模板:</strong></p><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">BFS</span><span class="hljs-params">(graph, start, end)</span>:</span>queue = []queue.append([start])visited.add(start)<span class="hljs-comment"># 对于图来说,要标记节点已经被访问过,防止重复访问</span><span class="hljs-keyword">while</span> queue:node = queue.pop()<span class="hljs-comment"># 出队</span>visited.add(node)<span class="hljs-comment"># 标记访问</span>process(node)<span class="hljs-comment"># 对节点进行一些处理</span>nodes = generate_related_nodes(node)<span class="hljs-comment"># 得到当前节点的后继节点,并且没有被访问过</span>queue.push(nodes)<span class="hljs-comment"># 其他处理部分</span>...</code></pre><h1 id="二、深度优先搜索"><a href="#二、深度优先搜索" class="headerlink" title="二、深度优先搜索"></a><strong>二、深度优先搜索</strong></h1><p>与 BFS 类似,<strong>深度优先搜索(DFS,Depth-First-Search)</strong> 也可用于查找从根结点到目标结点的路径。下面通过一个例子,用DFS 找出从根结点 A 到目标结点 G 的路径。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200315192813260.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="250" height="150"> </p><p align="center"> <em>图3:BFS例子</em> </p><p></p><p>在上面的例子中,我们从根结点 <code>A</code> 开始。首先,我们选择结点 <code>B</code>的路径,并进行回溯,直到我们到达结点 <code>E</code>,我们无法更进一步深入。然后我们回溯到<code>A</code>并选择第二条路径到结点 <code>C</code>。从 <code>C</code> 开始,我们尝试第一条路径到 <code>E</code> 但是 <code>E</code>已被访问过。所以我们回到 <code>C</code> 并尝试从另一条路径到 <code>F</code>。最后,我们找到了 <code>G</code>。虽然我们成功找出了路径 <code>A-> C-> F-> G</code> ,但这不是从 A 到 G 的最短路径。</p><p><strong>DFS代码模板:</strong></p><pre><code class="hljs python"><span class="hljs-comment"># 递归写法</span>visited = set()<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">DFS</span><span class="hljs-params">(node, visited)</span>:</span>visited.add(node)<span class="hljs-comment"># process current node here</span>...<span class="hljs-keyword">for</span> next_node <span class="hljs-keyword">in</span> node.children():<span class="hljs-keyword">if</span> next_node <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> visited:DFS(next_node, visited)</code></pre><h1 id="三、例题"><a href="#三、例题" class="headerlink" title="三、例题"></a><strong>三、例题</strong></h1><h3 id="(1)岛屿数量问题"><a href="#(1)岛屿数量问题" class="headerlink" title="(1)岛屿数量问题"></a><strong>(1)岛屿数量问题</strong></h3><p>此题为leetcode的<a href="https://leetcode-cn.com/problems/number-of-islands/" target="_blank" rel="noopener">第200题</a>。有两个思路:</p><ul><li><strong>Flood fill 算法:</strong> 深度优先搜索或广度优先搜索</li><li><strong>并查集</strong></li></ul><blockquote><p>Flood fill 算法的含义:是从一个区域中提取若干个连通的点与其他相邻区域区分开(或分别染成不同颜色)的经典 算法。因为其思路类似洪水从一个区域扩散到所有能到达的区域而得名。在 GNU Go 和 扫雷 中,Flood Fill算法被用来计算需要被清除的区域。</p></blockquote><p><strong>对于这类问题,其实就是从一个点开始,遍历与这个起点连着的格子,遍历过的格子就标上“被访问过”的记号,找到与之相连的所有格子后,就是发现了一个岛屿。而这个遍历的过程可以用广度优先或深度优先实现</strong>,具体讲解可以参考<a href="https://leetcode-cn.com/problems/number-of-islands/solution/dfs-bfs-bing-cha-ji-python-dai-ma-java-dai-ma-by-l/" target="_blank" rel="noopener">这里</a>,动画很明白,这里只贴上代码:</p><pre><code class="hljs python"><span class="hljs-comment"># 广度优先搜索</span><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List<span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-comment"># x-1,y</span> <span class="hljs-comment"># x,y-1 x, y x,y+1</span> <span class="hljs-comment"># x+1,y</span> <span class="hljs-comment"># 方向数组,它表示了相对于当前位置的 4 个方向的横、纵坐标的偏移量,这是一个常见的技巧</span> directions = [(<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">-1</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>)] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">numIslands</span><span class="hljs-params">(self, grid: List[List[str]])</span> -> int:</span> m = len(grid) <span class="hljs-keyword">if</span> m == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> n = len(grid[<span class="hljs-number">0</span>]) marked = [[<span class="hljs-literal">False</span> <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(n)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(m)] count = <span class="hljs-number">0</span> <span class="hljs-comment"># 从第 1 行、第 1 格开始,对每一格尝试进行一次 DFS 操作</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n): <span class="hljs-comment"># 只要是陆地,且没有被访问过的,就可以使用 BFS 发现与之相连的陆地,并进行标记</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> marked[i][j] <span class="hljs-keyword">and</span> grid[i][j] == <span class="hljs-string">'1'</span>: <span class="hljs-comment"># count 可以理解为连通分量,你可以在广度优先遍历完成以后,再计数,</span> <span class="hljs-comment"># 即这行代码放在【位置 1】也是可以的</span> count += <span class="hljs-number">1</span> queue = deque() queue.append((i, j)) <span class="hljs-comment"># 注意:这里要标记上已经访问过</span> marked[i][j] = <span class="hljs-literal">True</span> <span class="hljs-keyword">while</span> queue: cur_x, cur_y = queue.popleft() <span class="hljs-comment"># 得到 4 个方向的坐标</span> <span class="hljs-keyword">for</span> direction <span class="hljs-keyword">in</span> self.directions: new_i = cur_x + direction[<span class="hljs-number">0</span>] new_j = cur_y + direction[<span class="hljs-number">1</span>] <span class="hljs-comment"># 如果不越界、没有被访问过、并且还要是陆地,就入队</span> <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> <= new_i < m <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> <= new_j < n <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> marked[new_i][new_j] <span class="hljs-keyword">and</span> grid[new_i][new_j] == <span class="hljs-string">'1'</span>: queue.append((new_i, new_j)) <span class="hljs-comment"># 入队后马上要标记为“被访问过”</span> marked[new_i][new_j] = <span class="hljs-literal">True</span> <span class="hljs-comment">#【位置 1】</span> <span class="hljs-keyword">return</span> count</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 深度优先搜索</span><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-comment"># x-1,y</span> <span class="hljs-comment"># x,y-1 x,y x,y+1</span> <span class="hljs-comment"># x+1,y</span> <span class="hljs-comment"># 方向数组,它表示了相对于当前位置的 4 个方向的横、纵坐标的偏移量,这是一个常见的技巧</span> directions = [(<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">-1</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>)] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">numIslands</span><span class="hljs-params">(self, grid: List[List[str]])</span> -> int:</span> m = len(grid) <span class="hljs-keyword">if</span> m == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> n = len(grid[<span class="hljs-number">0</span>]) marked = [[<span class="hljs-literal">False</span> <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(n)] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(m)] count = <span class="hljs-number">0</span> <span class="hljs-comment"># 从第 1 行、第 1 格开始,对每一格尝试进行一次 DFS 操作</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(m): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(n): <span class="hljs-comment"># 只要是陆地,且没有被访问过的,就可以使用 DFS 发现与之相连的陆地,并进行标记</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> marked[i][j] <span class="hljs-keyword">and</span> grid[i][j] == <span class="hljs-string">'1'</span>: <span class="hljs-comment"># 连通分量计数</span> count += <span class="hljs-number">1</span> <span class="hljs-comment"># DFS搜索</span> self.__dfs(grid, i, j, m, n, marked) <span class="hljs-keyword">return</span> count<span class="hljs-comment"># 递归进行深度优先搜索</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__dfs</span><span class="hljs-params">(self, grid, i, j, m, n, marked)</span>:</span> marked[i][j] = <span class="hljs-literal">True</span> <span class="hljs-keyword">for</span> direction <span class="hljs-keyword">in</span> self.directions: new_i = i + direction[<span class="hljs-number">0</span>] new_j = j + direction[<span class="hljs-number">1</span>] <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> <= new_i < m <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> <= new_j < n <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> marked[new_i][new_j] <span class="hljs-keyword">and</span> grid[new_i][new_j] == <span class="hljs-string">'1'</span>: self.__dfs(grid, new_i, new_j, m, n, marked)</code></pre><h3 id="(2)二叉树的最大深度"><a href="#(2)二叉树的最大深度" class="headerlink" title="(2)二叉树的最大深度"></a><strong>(2)二叉树的最大深度</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/" target="_blank" rel="noopener">第104题</a><br>给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。<br><strong>深度优先解法:</strong><br><pre><code class="hljs python"><span class="hljs-comment"># 深度优先搜索</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxDepth</span><span class="hljs-params">(self, root: TreeNode)</span> -> int:</span> <span class="hljs-comment"># 递归,深度优先搜索</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-keyword">else</span>: left_height = self.maxDepth(root.left) <span class="hljs-comment"># 左子树高度</span> right_height = self.maxDepth(root.right) <span class="hljs-comment"># 右子树高度</span> <span class="hljs-keyword">return</span> max(left_height, right_height) + <span class="hljs-number">1</span></code></pre></p><p><strong>广度优先解法:</strong><br><pre><code class="hljs python"><span class="hljs-comment"># 迭代,广度优先搜索</span><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">maxDepth</span><span class="hljs-params">(self, root: TreeNode)</span> -> int:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> depth = <span class="hljs-number">0</span> q = deque() q.append((<span class="hljs-number">1</span>, root)) <span class="hljs-keyword">while</span> q: curr_depth, node = q.pop() depth = max(depth, curr_depth) <span class="hljs-keyword">if</span> node.left: q.append((curr_depth + <span class="hljs-number">1</span>, node.left)) <span class="hljs-keyword">if</span> node.right: q.append((curr_depth + <span class="hljs-number">1</span>, node.right)) <span class="hljs-keyword">return</span> depth</code></pre></p><h3 id="(3)二叉树的最小深度"><a href="#(3)二叉树的最小深度" class="headerlink" title="(3)二叉树的最小深度"></a><strong>(3)二叉树的最小深度</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/" target="_blank" rel="noopener">第111题</a><br>给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。<br><strong>深度优先解法:</strong><br><pre><code class="hljs python"><span class="hljs-comment"># 递归,深度优先搜索</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">minDepth</span><span class="hljs-params">(self, root: TreeNode)</span> -> int:</span> <span class="hljs-comment"># 递归,深度优先搜索</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-keyword">if</span> root.left <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-comment"># 若左子树为空,root的深度为1+右子树深度</span> <span class="hljs-keyword">return</span> <span class="hljs-number">1</span> + self.minDepth(root.right) <span class="hljs-keyword">if</span> root.right <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-comment"># 若右子树为空,root的深入为1+左子树深度</span> <span class="hljs-keyword">return</span> <span class="hljs-number">1</span> + self.minDepth(root.left) left_height = self.minDepth(root.left) <span class="hljs-comment"># 左子树高度</span> right_height = self.minDepth(root.right) <span class="hljs-comment"># 右子树高度</span> <span class="hljs-keyword">return</span> min(left_height, right_height) + <span class="hljs-number">1</span></code></pre></p><p><strong>广度优先解法:</strong><br><pre><code class="hljs python"><span class="hljs-comment"># 迭代,广度优先搜索</span><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">minDepth</span><span class="hljs-params">(self, root: TreeNode)</span> -> int:</span> <span class="hljs-comment"># 迭代,广度优先搜索</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> q = deque() q.append((<span class="hljs-number">1</span>, root)) depth = <span class="hljs-number">1</span> <span class="hljs-keyword">while</span> q: curr_depth, node = q.popleft() <span class="hljs-comment"># 如果node为叶子节点,则此时深度为最小深度</span> <span class="hljs-keyword">if</span> node.left <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">and</span> node.right <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> curr_depth <span class="hljs-keyword">if</span> node.left: q.append((curr_depth + <span class="hljs-number">1</span>, node.left)) <span class="hljs-keyword">if</span> node.right: q.append((curr_depth + <span class="hljs-number">1</span>, node.right))</code></pre></p><h3 id="(4)括号生成"><a href="#(4)括号生成" class="headerlink" title="(4)括号生成"></a><strong>(4)括号生成</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/generate-parentheses/" target="_blank" rel="noopener">第22题</a><br>数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。</p><pre><code class="hljs python"><span class="hljs-comment"># 深度优先搜索加剪枝</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generateParenthesis</span><span class="hljs-params">(self, n: int)</span> -> List[str]:</span> self.res = [] self._gen(<span class="hljs-string">''</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, n) <span class="hljs-keyword">return</span> self.res <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_gen</span><span class="hljs-params">(self, result, left, right, n)</span>:</span> <span class="hljs-comment"># left为左括号用了几个,right为右括号用了几个</span> <span class="hljs-keyword">if</span> left == n <span class="hljs-keyword">and</span> right == n: self.res.append(result) <span class="hljs-keyword">return</span> <span class="hljs-comment"># 如果用的左括号小于n个,可以加左括号</span> <span class="hljs-keyword">if</span> left < n: self._gen(result+<span class="hljs-string">'('</span>, left+<span class="hljs-number">1</span>, right, n) <span class="hljs-comment"># 只有在用的右括号小于用的左括号时,才可以加右括号</span> <span class="hljs-keyword">if</span> right < left: self._gen(result+<span class="hljs-string">')'</span>, left, right+<span class="hljs-number">1</span>, n)</code></pre><h3 id="(5)N皇后问题"><a href="#(5)N皇后问题" class="headerlink" title="(5)N皇后问题"></a><strong>(5)N皇后问题</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/n-queens/submissions/" target="_blank" rel="noopener">第51题</a><br>n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。</p><p><img src="https://img-blog.csdnimg.cn/20200413220734683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">solveNQueens</span><span class="hljs-params">(self, n: int)</span> -> List[List[str]]:</span> <span class="hljs-keyword">if</span> n < <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> [] self.res = [] <span class="hljs-comment"># 记录最终结果</span> self.cols, self.pie, self.na = set(), set(), set() <span class="hljs-comment"># 标记横竖、斜角方向</span> self.DFS(n, <span class="hljs-number">0</span>, []) <span class="hljs-keyword">return</span> [[<span class="hljs-string">'.'</span> * j + <span class="hljs-string">'Q'</span> + <span class="hljs-string">'.'</span> * (n - j - <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> col] <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> self.res] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">DFS</span><span class="hljs-params">(self, n, row, curr_state)</span>:</span> <span class="hljs-comment"># curr_state代表每行的哪些列可以放置皇后,是一种可能的解决方案</span> <span class="hljs-comment"># 如果递归可以走到最后一层,说明curr_state是一种合法的解决方案,将其放入self.res里</span> <span class="hljs-keyword">if</span> row >= n: self.res.append(curr_state) <span class="hljs-keyword">return</span> <span class="hljs-comment"># 遍历列</span> <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(n): <span class="hljs-comment"># 如果当前位置上,可以被横竖、斜角方向上的皇后攻击到,则continue</span> <span class="hljs-keyword">if</span> col <span class="hljs-keyword">in</span> self.cols <span class="hljs-keyword">or</span> row + col <span class="hljs-keyword">in</span> self.pie <span class="hljs-keyword">or</span> row - col <span class="hljs-keyword">in</span> self.na: <span class="hljs-keyword">continue</span> <span class="hljs-comment"># 如果当前位置不会被攻击到,则标记横竖、斜角方向</span> self.cols.add(col) self.pie.add(row + col) self.na.add(row - col) <span class="hljs-comment"># 递归,row+1进入下一行,curr_state+[col]保存一种可能的状态</span> self.DFS(n, row + <span class="hljs-number">1</span>, curr_state + [col]) <span class="hljs-comment"># 上面的递归出来后,清除横竖、斜角方向的标记</span> <span class="hljs-comment"># 因为我们是在(n, col)位置上尝试放置皇后看是否可行</span> self.cols.remove(col) self.pie.remove(row + col) self.na.remove(row - col)</code></pre><h3 id="(6)解数独"><a href="#(6)解数独" class="headerlink" title="(6)解数独"></a><strong>(6)解数独</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/sudoku-solver/submissions/" target="_blank" rel="noopener">第37题</a><br>编写一个程序,通过已填充的空格来解决数独问题。下面给出最朴素的DFS:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">solveSudoku</span><span class="hljs-params">(self, board: List[List[str]])</span> -> <span class="hljs-keyword">None</span>:</span> <span class="hljs-string">""" Do not return anything, modify board in-place instead. """</span> <span class="hljs-keyword">if</span> board <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">or</span> len(board) == <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> self.solve(board) <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">solve</span><span class="hljs-params">(self, board)</span>:</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(board)): <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(len(board)): <span class="hljs-comment"># 循环遍历,找到空位置</span> <span class="hljs-keyword">if</span> board[i][j] == <span class="hljs-string">'.'</span>: <span class="hljs-comment"># 从1-9遍历</span> <span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, <span class="hljs-number">10</span>): c = str(k) <span class="hljs-keyword">if</span> self.isValid(board, i, j, c): <span class="hljs-comment"># 判断c是否可以放在(i, j)上</span> <span class="hljs-comment"># 如果在(i, j)上放数字c可行的话,先将c放上去</span> board[i][j] = c <span class="hljs-comment"># 继续递归,看放上c后,后面的是否可行</span> <span class="hljs-keyword">if</span> self.solve(board): <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-comment"># 放上c后可以解决整个数独则返回True</span> <span class="hljs-keyword">else</span>: board[i][j] = <span class="hljs-string">'.'</span> <span class="hljs-comment"># 不可行的话将c清空</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 1-9遍历完后还不行就返回False</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-comment"># 可以遍历完整个board就返回True</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isValid</span><span class="hljs-params">(self, board, row, col, c)</span>:</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">9</span>): <span class="hljs-keyword">if</span> board[i][col] != <span class="hljs-string">'.'</span> <span class="hljs-keyword">and</span> board[i][col] == c: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">if</span> board[row][i] != <span class="hljs-string">'.'</span> <span class="hljs-keyword">and</span> board[row][i] == c: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">if</span> board[<span class="hljs-number">3</span> * (row // <span class="hljs-number">3</span>) + i // <span class="hljs-number">3</span>][<span class="hljs-number">3</span> * (col // <span class="hljs-number">3</span>) + i % <span class="hljs-number">3</span>] != <span class="hljs-string">'.'</span> <span class="hljs-keyword">and</span> board[<span class="hljs-number">3</span> * (row // <span class="hljs-number">3</span>) + i // <span class="hljs-number">3</span>][<span class="hljs-number">3</span> * (col // <span class="hljs-number">3</span>) + i % <span class="hljs-number">3</span>] == c: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法———队列与栈</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E9%98%9F%E5%88%97%E4%B8%8E%E6%A0%88/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E2%80%94%E9%98%9F%E5%88%97%E4%B8%8E%E6%A0%88/</id>
<published>2020-06-04T07:02:44.000Z</published>
<updated>2020-06-04T07:04:44.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、队列"><a href="#一、队列" class="headerlink" title="一、队列"></a><strong>一、队列</strong></h1><p>队列(Queue)是一个数据集合,仅允许在列表的一端插入,在另一端删除。进行插入的一端称为“队尾”(rear),插入的动作称为入队;进行删除的一端称为“队头”(front),删除的动作称为出队。队列的性质是<strong>先进先出</strong>(FIFO,First-in-First-out)。下图为一个例子:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200315185619877.png" srcset="/img/loading.gif" alt="Sample" width="400" height="150"> </p><p align="center"> <em>图1:队列</em> </p><p></p><p><strong>队列的实现方式:环形队列</strong><br><img src="https://img-blog.csdnimg.cn/20200315190322623.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>设队列的最大长度为<code>maxsize</code>,队头指针为<code>front</code>,队尾指针为<code>rear</code>。上图中,队列最大长度为5,初始空队列<code>front</code>和<code>rear</code>都指向0。</p><ul><li>当<code>front == rear</code>时,队为空,如图(a)</li><li>当<code>(rear + 1) % maxsize == 0</code>,队满。对于图(d1)的情况<code>front == rear</code>,无法判断到底是队空还是队满,因此需要牺牲一个存储单元,如图(d2)。</li><li>出队时队头指针前进1,<code>front = (front + 1) % maxsize</code>。</li><li>入队时队尾指针前进1,<code>tear = (tear + 1) % maxsize</code>。</li></ul><p>上面的式子中,都有对<code>maxsize</code>的取余,因为比如当指针达到最大值5时,再加1就会超过5,此时取余可以重新进入队列循环。</p><h1 id="二、栈"><a href="#二、栈" class="headerlink" title="二、栈"></a><strong>二、栈</strong></h1><p>栈(stack)是一个数据集合,只能在一端进行插入和删除,其特点是后进先出(LIFO,lats-in-first-out)。栈的节本操作有进栈(push)、出栈(pop)、取栈顶(gettop)。其示意图如下所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200315212132498.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="240" height="200"> </p><p align="center"> <em>图1:队列</em> </p><p></p><p>对于python来说,使用列表即可实现栈。设列表为<code>stack</code>,其三个基本操作为:</p><ul><li>入栈:<code>stack.append()</code></li><li>出栈:<code>stack.pop()</code></li><li>取栈顶:<code>stack[-1]</code></li></ul><h1 id="三、例题"><a href="#三、例题" class="headerlink" title="三、例题"></a><strong>三、例题</strong></h1><h3 id="1、有效的括号"><a href="#1、有效的括号" class="headerlink" title="1、有效的括号"></a><strong>1、有效的括号</strong></h3><p><a href="https://leetcode-cn.com/problems/valid-parentheses/" target="_blank" rel="noopener">此题为leetcode第20题</a><br>思路:使用栈,从给定的括号字符串第一个开始,依次往后,左半边括号入栈,遇到与之对应的右边吧括号出栈,直到最后若栈为空,则为有效表达,否则为无效表达。代码如下:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isValid</span><span class="hljs-params">(self, s: str)</span> -> bool:</span> map = {<span class="hljs-string">')'</span>:<span class="hljs-string">'('</span>, <span class="hljs-string">']'</span>:<span class="hljs-string">'['</span>, <span class="hljs-string">'}'</span>:<span class="hljs-string">'{'</span>} stack = [] <span class="hljs-keyword">for</span> char <span class="hljs-keyword">in</span> s: <span class="hljs-comment"># 如果是右括号</span> <span class="hljs-keyword">if</span> char <span class="hljs-keyword">in</span> map: top = stack.pop() <span class="hljs-keyword">if</span> stack <span class="hljs-keyword">else</span> <span class="hljs-string">'#'</span> <span class="hljs-keyword">if</span> map[char] != top: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 如果是左括号</span> <span class="hljs-keyword">else</span>: stack.append(char) <span class="hljs-keyword">return</span> <span class="hljs-keyword">not</span> stack</code></pre><h3 id="2、用队列实现栈"><a href="#2、用队列实现栈" class="headerlink" title="2、用队列实现栈"></a><strong>2、用队列实现栈</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/implement-stack-using-queues/submissions/" target="_blank" rel="noopener">第225题</a><br>思路:使用两个队列可以实现栈,对于栈的push操作,可以由两个队列实现:<br><img src="https://img-blog.csdnimg.cn/20200411160645576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>对于pop操作,直接从<code>q1</code>弹出即可</p><pre><code class="hljs python"><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyStack</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span> <span class="hljs-string">""" Initialize your data structure here. """</span> self.q1 = deque() self.q2 = deque() <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">push</span><span class="hljs-params">(self, x: int)</span> -> <span class="hljs-keyword">None</span>:</span> <span class="hljs-string">""" Push element x onto stack. """</span> self.q2.append(x) <span class="hljs-keyword">while</span> self.q1: self.q2.append(self.q1.popleft()) self.q1, self.q2 = self.q2, self.q1 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pop</span><span class="hljs-params">(self)</span> -> int:</span> <span class="hljs-string">""" Removes the element on top of the stack and returns that element. """</span> <span class="hljs-keyword">return</span> self.q1.popleft() <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">top</span><span class="hljs-params">(self)</span> -> int:</span> <span class="hljs-string">""" Get the top element. """</span> <span class="hljs-keyword">return</span> self.q1[<span class="hljs-number">0</span>] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">empty</span><span class="hljs-params">(self)</span> -> bool:</span> <span class="hljs-string">""" Returns whether the stack is empty. """</span> <span class="hljs-keyword">return</span> len(self.q1) == <span class="hljs-number">0</span></code></pre><h3 id="3、用栈实现队列"><a href="#3、用栈实现队列" class="headerlink" title="3、用栈实现队列"></a><strong>3、用栈实现队列</strong></h3><p>此题为leetcode<a href="https://leetcode-cn.com/problems/implement-queue-using-stacks/" target="_blank" rel="noopener">第232题</a><br>思路:同样可以用两个栈实现队列,对于push操作:<br><img src="https://img-blog.csdnimg.cn/20200411162343217.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyQueue</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span> <span class="hljs-string">""" Initialize your data structure here. """</span> self.s1, self.s2 = [], [] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">push</span><span class="hljs-params">(self, x: int)</span> -> <span class="hljs-keyword">None</span>:</span> <span class="hljs-string">""" Push element x to the back of queue. """</span> <span class="hljs-keyword">while</span> self.s1: self.s2.append(self.s1.pop()) self.s1.append(x) <span class="hljs-keyword">while</span> self.s2: self.s1.append(self.s2.pop()) <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pop</span><span class="hljs-params">(self)</span> -> int:</span> <span class="hljs-string">""" Removes the element from in front of queue and returns that element. """</span> <span class="hljs-keyword">return</span> self.s1.pop() <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">peek</span><span class="hljs-params">(self)</span> -> int:</span> <span class="hljs-string">""" Get the front element. """</span> <span class="hljs-keyword">return</span> self.s1[<span class="hljs-number">-1</span>] <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">empty</span><span class="hljs-params">(self)</span> -> bool:</span> <span class="hljs-string">""" Returns whether the queue is empty. """</span> <span class="hljs-keyword">return</span> len(self.s1) == <span class="hljs-number">0</span></code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——平衡二叉树</title>
<link href="http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
<id>http://yoursite.com/2020/06/04/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/</id>
<published>2020-06-04T07:00:08.000Z</published>
<updated>2020-06-04T07:01:58.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、平衡二叉树"><a href="#一、平衡二叉树" class="headerlink" title="一、平衡二叉树"></a><strong>一、平衡二叉树</strong></h1><p><strong>什么是平衡二叉树:</strong> 每个节点的两个子树的高度不会相差超过1。普通树和平衡二叉树的对比:<br><img src="https://img-blog.csdnimg.cn/20200314185923811.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br><strong>为什么要用到平衡二叉树:</strong> 在<a href="https://blog.csdn.net/u014157632/article/details/104842425" target="_blank" rel="noopener">二叉搜索树</a>里面,搜索操作的时间复杂度从$O(logN)$到$O(N)$不等,这是一个巨大的性能差异。而平衡的二叉搜索树的搜索复杂度为$O(logN)$。<br><strong>常见的平衡二叉树的实现:</strong> 红黑树、AVL树、伸展树、树堆</p><h1 id="二、判断是否为平衡二叉树"><a href="#二、判断是否为平衡二叉树" class="headerlink" title="二、判断是否为平衡二叉树"></a><strong>二、判断是否为平衡二叉树</strong></h1><p><strong>自底至顶递归:</strong> 做先序遍历,从底部开始,判断左右子树高度差,若是平衡二叉树则返回子树的最大高度,若不是则直接输出false。<br>此题为leetcode第110题,代码如下:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">recur</span><span class="hljs-params">(self, root)</span>:</span> <span class="hljs-comment"># 越过叶子节点,返回高度0</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> left = self.recur(root.left) <span class="hljs-keyword">if</span> left == <span class="hljs-number">-1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span> right = self.recur(root.right) <span class="hljs-keyword">if</span> right == <span class="hljs-number">-1</span>: <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span> <span class="hljs-comment"># 如果高度差大于1则不是平衡二叉树,否则返回左右子树最大高度加1</span> <span class="hljs-keyword">return</span> max(left, right) + <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> abs(left - right) < <span class="hljs-number">2</span> <span class="hljs-keyword">else</span> <span class="hljs-number">-1</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isBalanced</span><span class="hljs-params">(self, root: TreeNode)</span> -> bool:</span> <span class="hljs-keyword">return</span> self.recur(root) != <span class="hljs-number">-1</span></code></pre><p>复杂度分析:</p><ul><li>时间复杂度:$O(N)$,N 为树的节点数;最差情况下,需要递归遍历树的所有节点。</li><li>空间复杂度:$O(N)$,最差情况下(树退化为链表时),系统递归需要使用 $O(N)$ 的栈空间。</li></ul>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据机构与算法——二叉搜索树</title>
<link href="http://yoursite.com/2020/03/13/%E6%95%B0%E6%8D%AE%E6%9C%BA%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/"/>
<id>http://yoursite.com/2020/03/13/%E6%95%B0%E6%8D%AE%E6%9C%BA%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/</id>
<published>2020-03-13T14:34:55.000Z</published>
<updated>2020-06-04T06:52:42.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、二叉搜索树"><a href="#一、二叉搜索树" class="headerlink" title="一、二叉搜索树"></a><strong>一、二叉搜索树</strong></h1><p>二叉搜索树(BST)满足的性质:</p><ul><li><strong>每个节点中的值必须大于(或等于)存储在其左侧子树中的任何值。</strong></li><li><strong>每个节点中的值必须小于(或等于)存储在其右子树中的任何值。</strong></li></ul><p>一个二叉搜索树的例子如下所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313154148778.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="300" height="250"> </p><p align="center"> <em>图1:二叉搜索树举例</em> </p><p></p><p>因为二叉树的中序遍历会得到一个递增的序列,因此在二叉搜索树里中序遍历比较常用。</p><p>给定一个二叉树,我们要判断它是否是二叉搜索树。二叉搜索树的特征比较明显,我们需要判断:(1)当前节点下,其左子树<strong>只包含小于</strong>当前节点的数;(2)其右子树<strong>只包含大于</strong>当前节点的数;(3)所有左子树右子树必须也是二叉搜索树。此题是leetcode的第98题,我们用递归、迭代、中序遍历三种方法实现。</p><p>【<strong>注意</strong>】直观来看,遍历每个节点,只要当前节点比左节点大比右节点小就行。但这样是不对的,因为我们还要右子树的所有节点都要比当前节点大,左子树的所有节点都要比当前节点小,刚才的做法无法保证这一点。比如下图,虽然根节点5小于右节点6,但右子树有个节点值为4,小于根节点,因此不是二叉搜索树。</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313172949831.png" srcset="/img/loading.gif" alt="Sample" width="200" height="200"> </p><p align="center"> <em>图2:非二叉搜索树举例</em> </p><p></p><p>这种情况下,在右子树里比较时,要有一个下界,这个子树里的值都不能小于这个下界,这个下界就是根节点的值。同理,根节点的值就是左子树的上界,所有值都不能大于这个上界。</p><h2 id="1、递归"><a href="#1、递归" class="headerlink" title="1、递归"></a><strong>1、递归</strong></h2><pre><code class="hljs python"><span class="hljs-comment"># 递归判断是否为二叉搜索树</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isValidBST</span><span class="hljs-params">(self, root: TreeNode)</span> -> bool:</span> <span class="hljs-comment"># 上界初始化为无穷,下界初始化为负无穷</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">helper</span><span class="hljs-params">(node, lower = float<span class="hljs-params">(<span class="hljs-string">'-inf'</span>)</span>, upper = float<span class="hljs-params">(<span class="hljs-string">'inf'</span>)</span>)</span>:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> node: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> val = node.val <span class="hljs-comment"># 如果当前节点小于下界或大于上界,则不是BST</span> <span class="hljs-keyword">if</span> val <= lower <span class="hljs-keyword">or</span> val >= upper: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span><span class="hljs-comment"># 考察右子树是否有低于下界的</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> helper(node.right, val, upper): <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 考察左子树是否有高于上界的</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> helper(node.left, lower, val): <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 以上都满足,则是BST</span> <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">return</span> helper(root)</code></pre><p>复杂度分析:</p><ul><li>时间复杂度:$O(N)$,每个节点访问了一次</li><li>空间复杂度:$O(N)$,跟进了整个树</li></ul><h2 id="2、迭代"><a href="#2、迭代" class="headerlink" title="2、迭代"></a><strong>2、迭代</strong></h2><pre><code class="hljs python"><span class="hljs-comment"># 迭代法,深度优先搜索</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isValidBST</span><span class="hljs-params">(self, root: TreeNode)</span> -> bool:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span> <span class="hljs-comment"># 初始化栈</span> stack = [(root, float(<span class="hljs-string">'-inf'</span>), float(<span class="hljs-string">'inf'</span>)), ] <span class="hljs-keyword">while</span> stack: <span class="hljs-comment"># 出栈</span> root, lower, upper = stack.pop() <span class="hljs-comment"># 如果not root,说明此节点的父节点没有左孩子或右孩子,continue即可</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">continue</span> val = root.val <span class="hljs-comment"># 判断当前节点</span> <span class="hljs-keyword">if</span> val <= lower <span class="hljs-keyword">or</span> val >= upper: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 压栈右孩子</span> stack.append((root.right, val, upper)) <span class="hljs-comment"># 压栈左孩子</span> stack.append((root.left, lower, val)) <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre><p>复杂度分析:</p><ul><li>时间复杂度:$O(N)$,每个节点访问了一次</li><li>空间复杂度:$O(N)$,跟进了整个树</li></ul><h2 id="3、中序遍历"><a href="#3、中序遍历" class="headerlink" title="3、中序遍历"></a><strong>3、中序遍历</strong></h2><pre><code class="hljs python"><span class="hljs-comment"># 中序遍历</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isValidBST</span><span class="hljs-params">(self, root: TreeNode)</span> -> bool:</span> stack, inorder = [], float(<span class="hljs-string">'-inf'</span>) <span class="hljs-keyword">while</span> stack <span class="hljs-keyword">or</span> root: <span class="hljs-comment"># 到最左边的叶子节点</span> <span class="hljs-keyword">while</span> root: stack.append(root) root = root.left <span class="hljs-comment"># 出栈</span> root = stack.pop() <span class="hljs-comment"># 如果它小于它前一个节点的值,则不是BST(因为是中序遍历,是按照递增排的)</span> <span class="hljs-keyword">if</span> root.val <= inorder: <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># 若它大于前一个值,则将当前值赋值给inorder值</span> inorder = root.val root = root.right <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span></code></pre><p>复杂度分析:</p><ul><li>时间复杂度:时间复杂度 : 最坏情况下(树为二叉搜索树或破坏条件的元素是最右叶结点)为$O(N)$</li><li>空间复杂度:$O(N)$用于存储<code>stack</code><h1 id="二、基本操作"><a href="#二、基本操作" class="headerlink" title="二、基本操作"></a><strong>二、基本操作</strong></h1><h2 id="1、在树中搜索某个值"><a href="#1、在树中搜索某个值" class="headerlink" title="1、在树中搜索某个值"></a><strong>1、在树中搜索某个值</strong></h2>在搜索二叉树中找到某个值并返回改节点,举例如下图所示。我们要找到值为4的节点,从根节点开始,它大于4,因此在左子树找。左子树的根节点小于4,因此再在其右子树找。此时当前节点值为4,返回该节点。此题对于leetcode的第700题。</li></ul><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313181301444.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="800" height="180"> </p><p align="center"> <em>图3:在二叉搜索树中实现搜索操作</em> </p><p></p><p><strong>递归实现</strong></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">searchBST</span><span class="hljs-params">(self, root: TreeNode, val: int)</span> -> TreeNode:</span> <span class="hljs-comment"># 如果给节点为空,或该节点值等于要找到的值,则返回改节点</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root <span class="hljs-keyword">or</span> root.val == val: <span class="hljs-keyword">return</span> root <span class="hljs-comment"># 若改节点值大于val,则在左子树找</span> <span class="hljs-keyword">if</span> root.val > val: <span class="hljs-keyword">return</span> self.searchBST(root.left,val) <span class="hljs-comment"># 否则在右子树找</span> <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> self.searchBST(root.right,val)</code></pre><p>复杂度分析:</p><ul><li>时间复杂度:$O(H)$,其中H是树高。平均时间复杂度:$O(logN)$;最坏时间复杂度:$O(N)$</li><li>空间复杂度:$O(H)$,递归栈的深度为H。平均情况下深度为$O(logN)$;最坏情况戏曲深度为$O(N)$</li></ul><p><strong>迭代实现</strong></p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">searchBST</span><span class="hljs-params">(self, root: TreeNode, val: int)</span> -> TreeNode:</span> <span class="hljs-keyword">while</span> root <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">and</span> root.val != val: root = root.left <span class="hljs-keyword">if</span> val < root.val <span class="hljs-keyword">else</span> root.right <span class="hljs-keyword">return</span> root</code></pre><p>复杂度分析:</p><ul><li>时间复杂度:$O(H)$,其中H是树高。平均时间复杂度:$O(logN)$;最坏时间复杂度:$O(N)$</li><li>空间复杂度:$O(1)$,恒定的额外空间</li></ul><h2 id="2、插入操作"><a href="#2、插入操作" class="headerlink" title="2、插入操作"></a><strong>2、插入操作</strong></h2><p>在这里实现的二叉搜索树的插入操作,主要思想是为目标节点找出合适的叶节点位置,<strong>然后将该节点作为叶节点插入</strong>。举例如下图所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313184618645.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="200" height="200"> <img src="https://img-blog.csdnimg.cn/20200313184711303.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="200" height="200"> </p><p align="center"> <em>图4:在二叉搜索树中实现插入操作</em> </p><p></p>在左边这个树里插入4,从根节点开始,4小于5,因此继续搜索左子树。4大于2,而且2没有右孩子,因此4作为2的右孩子插入到树中。此题对于leetcode的第701题。**递归实现:**<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insertIntoBST</span><span class="hljs-params">(self, root: TreeNode, val: int)</span> -> TreeNode:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">return</span> TreeNode(val) <span class="hljs-keyword">if</span> val > root.val: <span class="hljs-comment"># 在右子树插入</span> root.right = self.insertIntoBST(root.right, val) <span class="hljs-keyword">else</span>: <span class="hljs-comment"># 在左子树插入</span> root.left = self.insertIntoBST(root.left, val) <span class="hljs-keyword">return</span> root</code></pre>复杂度分析:- 时间复杂度:$O(H)$,其中H是树高。平均时间复杂度:$O(logN)$;最坏时间复杂度:$O(N)$- 空间复杂度:平均情况下 $O(H)$。最坏的情况下是 $O(N)$,是在递归过程中堆栈使用的空间。**迭代实现:**<pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insertIntoBST</span><span class="hljs-params">(self, root: TreeNode, val: int)</span> -> TreeNode:</span> node = root <span class="hljs-keyword">while</span> node: <span class="hljs-comment"># 如果val大于当前节点的值,则在右子树插入</span> <span class="hljs-keyword">if</span> val > node.val: <span class="hljs-comment"># 如果没有右孩子,则插入</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> node.right: node.right = TreeNode(val) <span class="hljs-keyword">return</span> root <span class="hljs-keyword">else</span>: node = node.right <span class="hljs-comment"># 如果val大于当前节点的值,则在左子树插入</span> <span class="hljs-keyword">else</span>: <span class="hljs-comment"># 如果没有左孩子,则插入</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> node.left: node.left = TreeNode(val) <span class="hljs-keyword">return</span> root <span class="hljs-keyword">else</span>: node = node.left <span class="hljs-keyword">return</span> TreeNode(val)</code></pre>复杂度分析:- 时间复杂度:$O(H)$,其中H是树高。平均时间复杂度:$O(logN)$;最坏时间复杂度:$O(N)$。- 空间复杂度:$O(1)$## **3、删除操作**这里的删除操作是用一个合适的子节点来替换要删除的目标节点,使整体操作变化最小。如果目标节点值大于当前节点,则指向右子树去寻找目标节点;如果目标节点值小于当前节点,则指向左子树去寻找目标节点。如果找到了目标节点,我们分为三种情况:- case1:目标节点没有子节点,可以直接删除目标节点。如下图所示:<p align="center"> <img src="https://img-blog.csdnimg.cn/20200313213353425.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="400" height="250"> </p><p align="center"> <em>图5:删除操作,情况1</em> </p><p></p><ul><li><p>case2:目标节点有右节点,则该节点可以由该节点的后继节点(successor,右子树最左边的节点)进行替代,该后继节点位于右子树中较低的位置。然后可以从后继节点的位置递归向下操作以删除后继节点。如下图所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313215056570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="600" height="300"> </p><p align="center"> <em>图6:删除操作,情况2</em> </p><p></p></li><li><p>case3:目标节点只有左节点,可以使用它的前驱节点(predecessor,左子树最右边的节点)进行替代,然后再递归的向下删除前驱节点。如下图所示:</p><p align="center"> <img src="https://img-blog.csdnimg.cn/20200313215446784.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="Sample" width="600" height="300"> </p><p align="center"> <em>图7:删除操作,情况3</em> </p><p></p></li></ul><p>此题对于leetcode的第450题。代码如下:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">successor</span><span class="hljs-params">(self, root)</span>:</span> <span class="hljs-comment"># 最左边的节点</span> root = root.right <span class="hljs-keyword">while</span> root.left: root = root.left <span class="hljs-keyword">return</span> root.val <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">predecessor</span><span class="hljs-params">(self, root)</span>:</span> <span class="hljs-comment"># 最右边的节点</span> root = root.left <span class="hljs-keyword">while</span> root.right: root = root.right <span class="hljs-keyword">return</span> root.val <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">deleteNode</span><span class="hljs-params">(self, root: TreeNode, key: int)</span> -> TreeNode:</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># 从右子树删除</span> <span class="hljs-keyword">if</span> key > root.val: root.right = self.deleteNode(root.right, key) <span class="hljs-comment"># 从左子树删除</span> <span class="hljs-keyword">elif</span> key < root.val: root.left = self.deleteNode(root.left, key) <span class="hljs-comment"># 找到了要删除的节点</span> <span class="hljs-keyword">else</span>: <span class="hljs-comment"># case 1</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> (root.left <span class="hljs-keyword">or</span> root.right): root = <span class="hljs-literal">None</span> <span class="hljs-comment"># case 2</span> <span class="hljs-keyword">elif</span> root.right: root.val = self.successor(root) root.right = self.deleteNode(root.right, root.val) <span class="hljs-comment"># case 3</span> <span class="hljs-keyword">else</span>: root.val = self.predecessor(root) root.left = self.deleteNode(root.left, root.val) <span class="hljs-keyword">return</span> root</code></pre><p>复杂度分析:</p><ul><li>时间复杂度:时间复杂度:${O}(\log N)$。在算法的执行过程中,我们一直在树上向左或向右移动。首先先用$O(H_{1})$的时间找到要删除的节点,$H_{1}$是从根节点到要删除节点的高度。然后删除节点需要 ${O}(H_2)$的时间,$H_{2}$指的是从要删除节点到替换节点的高度。由于 ${O}(H_1 + H_2) = {O}(H)$,$H$ 值得是树的高度,若树是一个平衡树则 $H = \log N$。</li><li>空间复杂度:空间复杂度:$O(H)$,递归时堆栈使用的空间,HH 是树的高度</li></ul><h1 id="三、例题"><a href="#三、例题" class="headerlink" title="三、例题"></a><strong>三、例题</strong></h1><p><strong>二叉搜索树的最近公共祖先</strong></p><p>此题为leetcode<a href="https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/submissions/" target="_blank" rel="noopener">第235题</a><br>给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。</p><p>递归写法:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lowestCommonAncestor</span><span class="hljs-params">(self, root: <span class="hljs-string">'TreeNode'</span>, p: <span class="hljs-string">'TreeNode'</span>, q: <span class="hljs-string">'TreeNode'</span>)</span> -> 'TreeNode':</span> <span class="hljs-comment"># 递归</span> <span class="hljs-comment"># 当p、q的值都小于root时,要在左子树找</span> <span class="hljs-keyword">if</span> p.val < root.val <span class="hljs-keyword">and</span> q.val < root.val: <span class="hljs-keyword">return</span> self.lowestCommonAncestor(root.left, p, q) <span class="hljs-comment"># 当p、q的值都大于root时,要在右子树找</span> <span class="hljs-keyword">if</span> p.val > root.val <span class="hljs-keyword">and</span> q.val > root.val: <span class="hljs-keyword">return</span> self.lowestCommonAncestor(root.right, p, q) <span class="hljs-comment"># 否则p或q分列左右子树,root即为最近公共祖先</span> <span class="hljs-keyword">return</span> root</code></pre><p>非递归写法:</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lowestCommonAncestor</span><span class="hljs-params">(self, root: <span class="hljs-string">'TreeNode'</span>, p: <span class="hljs-string">'TreeNode'</span>, q: <span class="hljs-string">'TreeNode'</span>)</span> -> 'TreeNode':</span> <span class="hljs-comment"># 非递归</span> <span class="hljs-keyword">while</span> root: <span class="hljs-keyword">if</span> p.val < root.val <span class="hljs-keyword">and</span> q.val < root.val: root = root.left <span class="hljs-keyword">elif</span> p.val > root.val <span class="hljs-keyword">and</span> q.val > root.val: root = root.right <span class="hljs-keyword">else</span>: <span class="hljs-keyword">return</span> root</code></pre>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>数据结构与算法——二叉树</title>
<link href="http://yoursite.com/2020/03/11/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
<id>http://yoursite.com/2020/03/11/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E2%80%94%E2%80%94%E4%BA%8C%E5%8F%89%E6%A0%91/</id>
<published>2020-03-11T14:49:14.000Z</published>
<updated>2020-06-04T06:38:58.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="一、二叉树"><a href="#一、二叉树" class="headerlink" title="一、二叉树"></a><strong>一、二叉树</strong></h1><p>二叉树是一种更为典型的树树状结构。如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。下面是个二叉树的例子:<br><img src="https://img-blog.csdnimg.cn/20200311183632554.png" srcset="/img/loading.gif" alt="图1"><br>用python定义二叉树的节点:</p><pre><code class="hljs python"><span class="hljs-comment"># 二叉树节点</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TreeNode</span>:</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, x)</span>:</span>self.val = xself.left = <span class="hljs-literal">None</span>self.right = <span class="hljs-literal">None</span></code></pre><h1 id="二、二叉树遍历"><a href="#二、二叉树遍历" class="headerlink" title="二、二叉树遍历"></a><strong>二、二叉树遍历</strong></h1><h2 id="1、前序遍历"><a href="#1、前序遍历" class="headerlink" title="1、前序遍历"></a><strong>1、前序遍历</strong></h2><p>前序遍历访问的顺序为:<strong>根节点</strong>、左子树、右子树。对于上图,遍历过程如下:</p><ul><li>一开始指向根节点,访问它,为<strong>E</strong></li><li>有左子树,指向他并访问,为<strong>A</strong></li><li>A没有左子树,指向其右子树并访问,为<strong>C</strong></li><li>C有左子树和右子树,按顺序访问,为<strong>BD</strong></li><li>E的左半边访问完毕,指向E的右子树并访问,为<strong>G</strong></li><li>G只有右子树,访问它,为<strong>F</strong></li><li>F往下已经没有叶子节点,遍历结束</li></ul><p>因此,前序遍历的结果为:<code>EACBDGF</code></p><h2 id="2、中序遍历"><a href="#2、中序遍历" class="headerlink" title="2、中序遍历"></a><strong>2、中序遍历</strong></h2><p>中序遍历访问的顺序为:左子树、<strong>根节点</strong>、右子树。对于上图,遍历过程如下:</p><ul><li>一开始指向根节点E,因为优先访问左子树,先看有没有左子树。发现根节点有左子树A,指向它</li><li>此时A有没有左子树,因此访问当前节点,为<strong>A</strong></li><li>A有右子树,指向其右子树C</li><li>此节点C有左子树,指向其左子树B</li><li>此时节点没有子树,访问其节点为<strong>B</strong></li><li>然后往回指向节点B的父节点,并访问,为<strong>C</strong></li><li>C有右子树,访问,为<strong>D</strong></li><li>此时指回根节点E,访问它,为<strong>E</strong></li><li>E有右子树,指向右子树G</li><li>因为G没有左子树,因此访问当前节点,为<strong>G</strong></li><li>G有右子树,访问,为<strong>F</strong></li><li>F无子树,遍历结束</li></ul><p>因此,中序遍历的结果为:<code>ABCDEGF</code>。通常来说,对于二叉搜索树,我们可以通过中序遍历得到一个递增的有序序列</p><h2 id="3、后序遍历"><a href="#3、后序遍历" class="headerlink" title="3、后序遍历"></a><strong>3、后序遍历</strong></h2><p>后序遍历访问的顺序为:左子树、右子树、<strong>根节点</strong>。对于上图,遍历过程如下:</p><ul><li>一开始指向根节点E,先指向左子树A</li><li>要优先访问左子树右子树,A无左子树、有右子树,因此指向A的右子树C</li><li>C有左子树,指向左子树B</li><li>B无子树,访问为<strong>B</strong></li><li>指回C,C有右子树,指向并访问,为<strong>D</strong></li><li>指回BD的根节点C,访问为<strong>C</strong></li><li>指回C的根节点,访问为<strong>A</strong></li><li>指回A的根节点E,访问其右子树G</li><li>G有右子树,访问为<strong>F</strong></li><li>指回F的根节点,访问为<strong>G</strong></li><li>指回G的根节点,访问为<strong>E</strong></li><li>E已为整个树的根节点,遍历结束</li></ul><p>因此,后序遍历的结果为:<code>BDCAFGE</code>。当你删除树中的节点时,用到后序遍历。 也就是说,当你删除一个节点时,你将首先删除它的左节点和它的右边的节点,然后再删除节点本身。</p><h2 id="4、层序遍历"><a href="#4、层序遍历" class="headerlink" title="4、层序遍历"></a><strong>4、层序遍历</strong></h2><p>层序遍历就是逐层遍历树结构,下图展示了它的层次结构:<br><img src="https://img-blog.csdnimg.cn/20200311203221842.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>二叉树的层序遍历即为<strong>广度优先搜索</strong>,该算法从一个根节点开始,首先访问节点本身。 然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。广度优先搜索需要用到<strong>队列</strong>。遍历过程如下:</p><ul><li>初始化队列q=[],并将根节点E入队,为q=[E]</li><li>q出队,为<strong>E</strong></li><li>E有左子树和右子树,两者入队,为q=[A, G]</li><li>q出队,为<strong>A</strong>,此时将A的右子树入队,为q=[G, C]</li><li>q出队,为<strong>G</strong>,此时G的右子树入队,为q=[C, F]</li><li>q出队,为<strong>C</strong>,此时C的左子树、右子树入队,为q=[F, B, D]</li><li>q出队,为<strong>F</strong>,此时q=[B, D]</li><li>F没有子树,q继续出队,为<strong>B</strong>,此时q=[D]</li><li>q出队,为<strong>D</strong></li><li>q为空,遍历结束</li></ul><p>因此,层序遍历的结果为:<code>EAGCFBD</code>。</p><h1 id="三、程序实现"><a href="#三、程序实现" class="headerlink" title="三、程序实现"></a><strong>三、程序实现</strong></h1><p>二叉树的前序、中序、后序和层序遍历,分别为leetcode的第144、94、145、102题,都可以用<strong>递归</strong>和<strong>迭代</strong>两种方法做。</p><h2 id="1、递归实现"><a href="#1、递归实现" class="headerlink" title="1、递归实现"></a><strong>1、递归实现</strong></h2><p>对于前序、中序和后序来说,递归的现实非常简单,他们的实现区别是根节点访问的顺序不一样。代码如下:</p><pre><code class="hljs python"><span class="hljs-comment"># 前序遍历递归</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">preorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root:<span class="hljs-keyword">return</span> []<span class="hljs-keyword">else</span>:<span class="hljs-keyword">return</span> [root.val] + self.preorderTraversal(root.left) + self.preorderTraversal(root.right)</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 中序遍历递归</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">inorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root:<span class="hljs-keyword">return</span> []<span class="hljs-keyword">else</span>:<span class="hljs-keyword">return</span> self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 后序遍历递归</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">postorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root:<span class="hljs-keyword">return</span> []<span class="hljs-keyword">else</span>:<span class="hljs-keyword">return</span> self.postorderTraversal(root.left) + self.postorderTraversal(root.right)+ [root.val]</code></pre><p>可以看出不同的地方就是<code>[root.val]</code>的位置不一样,前序遍历<code>[root.val]</code>就在最一开始,中序遍历<code>[root.val]</code>则在中间,后序遍历<code>[root.val]</code>就在最后。整个迭代结构下图所示:</p><p><img src="https://img-blog.csdnimg.cn/20200311212631168.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><p>对于层序遍历的递归实现,我们不仅要输出层序遍历的序列,还要有每个元素属于哪个层次的信息,可以用列表嵌套的方式,比如之前的例子里,层序遍历表示为<code>[[E], [AG], [CF], [BD]]</code>。</p><pre><code class="hljs python"><span class="hljs-comment"># 层序遍历递归</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">levelOrder</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[List[int]]:</span>levels = [] <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> root: <span class="hljs-keyword">return</span> levels <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">helper</span><span class="hljs-params">(node, level)</span>:</span> <span class="hljs-comment"># start the current level</span> <span class="hljs-keyword">if</span> len(levels) == level: levels.append([]) <span class="hljs-comment"># append the current node value</span> levels[level].append(node.val) <span class="hljs-comment"># process child nodes for the next level</span> <span class="hljs-keyword">if</span> node.left: helper(node.left, level + <span class="hljs-number">1</span>) <span class="hljs-keyword">if</span> node.right: helper(node.right, level + <span class="hljs-number">1</span>) helper(root, <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> levels</code></pre><p>我们设更节点的层级序号为0,依次往下为1、2、3……。<code>leaves</code>为保存结果的列表,每一层的结果又为一个列表,保存在<code>leaves</code>里,层级序号即为在<code>leaves</code>里的index,在第<code>i</code>层级,<code>leaves</code>里应该有<code>i+1</code>个列表,第<code>i+1</code>个列表为当前层级的节点。每个列表只append层级序号相同的节点。</p><h2 id="2、迭代实现"><a href="#2、迭代实现" class="headerlink" title="2、迭代实现"></a><strong>2、迭代实现</strong></h2><p>二叉树的迭代实现都需要用到<strong>栈</strong>。对于前序、中序、后序遍历,他们的迭代实现大同小异:</p><pre><code class="hljs python"><span class="hljs-comment"># 前序遍历迭代</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">preorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> [] stack, output = [root, ], [] <span class="hljs-keyword">while</span> stack: root = stack.pop() <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: output.append(root.val) <span class="hljs-keyword">if</span> root.right <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: stack.append(root.right) <span class="hljs-keyword">if</span> root.left <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: stack.append(root.left) <span class="hljs-keyword">return</span> output</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 中序遍历迭代</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">inorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> [] stack, output= [], [] curr = root <span class="hljs-keyword">while</span> curr <span class="hljs-keyword">or</span> len(stack) > <span class="hljs-number">0</span>: <span class="hljs-keyword">while</span> curr: stack.append(curr) curr = curr.left curr = stack.pop() output.append(curr.val) curr = curr.right <span class="hljs-keyword">return</span> output</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 后序遍历迭代</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">postorderTraversal</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[int]:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> [] stack, output = [root, ], [] <span class="hljs-keyword">while</span> stack: root = stack.pop() output.append(root.val) <span class="hljs-keyword">if</span> root.left <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: stack.append(root.left) <span class="hljs-keyword">if</span> root.right <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: stack.append(root.right) <span class="hljs-keyword">return</span> output[::<span class="hljs-number">-1</span>]</code></pre><ul><li>对于前序遍历,先初始化一个有根节点的栈,然后进入循环,出栈根节点,访问它。然后依次入栈右子树、左子树(注意栈是先进后出)。则下次循环,会先出栈左子树,访问它,然后入栈它的右子树、左子树。这样根节点E的右子树会在其左子树遍历完后才会出栈。循环停止条件是栈为空。</li><li>对于中序遍历,因为要优先遍历左子树,循环之前初始化一个空栈,进入循环后,用一个内循环依次入栈左子树,直到叶子节点(也为一个左子树),然后退出内循环,出栈并访问。</li><li>对于后序遍历,和前序遍历的整体结构是一样的,唯一不同的是在循环里先入栈左子树,后入栈右子树,循环结束后将结果逆序。</li></ul><p>对于层序遍历的迭代实现,循环之前初始化一个有根节点的队列,此时队列只有一个元素,进入循环后,出队并访问,然后将根节点的左右子树依次入队。下次循环,依次将上次队列里的元素出队,同队入队他们的左右子树。这样每次循环只出队当前层次的节点,并入队他们的左右子树,完成了层次之间的交换。这实际上是一种<strong>广度优先搜索</strong>。<strong>层序遍历迭代写法:</strong></p><pre><code class="hljs python"><span class="hljs-comment"># 层序遍历迭代,使用栈</span><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">levelOrder</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[List[int]]:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> [] q = [root] result = [] <span class="hljs-keyword">while</span> q: res = [] <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(q)): node = q.pop(<span class="hljs-number">0</span>) res.append(node.val) <span class="hljs-keyword">if</span> node.left: q.append(node.left) <span class="hljs-keyword">if</span> node.right: q.append(node.right) result.append(res) <span class="hljs-keyword">return</span> result</code></pre><pre><code class="hljs python"><span class="hljs-comment"># 层序遍历迭代,使用队列</span><span class="hljs-keyword">from</span> collections <span class="hljs-keyword">import</span> deque<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">levelOrder</span><span class="hljs-params">(self, root: TreeNode)</span> -> List[List[int]]:</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> [] res = [] q = deque() q.append(root) <span class="hljs-keyword">while</span> q: size = len(q) curr_level = [] <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(size): node = q.popleft() curr_level.append(node.val) <span class="hljs-keyword">if</span> node.left: q.append(node.left) <span class="hljs-keyword">if</span> node.right: q.append(node.right) res.append(curr_level) <span class="hljs-keyword">return</span> res</code></pre><h1 id="四、例题"><a href="#四、例题" class="headerlink" title="四、例题"></a><strong>四、例题</strong></h1><p><strong>二叉树的最近公共祖先</strong></p><p>此题为leetcode<a href="https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/" target="_blank" rel="noopener">第236题</a></p><p>给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。</p><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Solution</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lowestCommonAncestor</span><span class="hljs-params">(self, root: <span class="hljs-string">'TreeNode'</span>, p: <span class="hljs-string">'TreeNode'</span>, q: <span class="hljs-string">'TreeNode'</span>)</span> -> 'TreeNode':</span> <span class="hljs-comment"># 树为空返回None</span> <span class="hljs-keyword">if</span> root <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># 找到了p或q</span> <span class="hljs-keyword">if</span> root == p <span class="hljs-keyword">or</span> root == q: <span class="hljs-keyword">return</span> root <span class="hljs-comment"># 在root的左子树找p或q</span> left = self.lowestCommonAncestor(root.left, p, q) <span class="hljs-comment"># 在root的右子树找p或q</span> right = self.lowestCommonAncestor(root.right, p, q) <span class="hljs-keyword">if</span> left <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-comment"># 如果左子树没有p或q,返回右孩子</span> <span class="hljs-keyword">return</span> right <span class="hljs-keyword">elif</span> right <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: <span class="hljs-comment"># 如果右子树没有p或q,返回左孩子</span> <span class="hljs-keyword">return</span> left <span class="hljs-keyword">else</span>: <span class="hljs-comment"># 如果分别在左右子树找到了p或q,那么root就是最近公共祖先</span> <span class="hljs-keyword">return</span> root</code></pre><h1 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a><strong>五、总结</strong></h1><ul><li>前序、中序、后序遍历的递归实现几乎一模一样,区别是根节点的访问位置不一样</li><li>前序、中序、后序的迭代实现需要借助栈</li><li>层序遍历的递归和迭代实现需要借助队列</li></ul>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
<category term="数据结构" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>MobileNet系列</title>
<link href="http://yoursite.com/2019/12/13/MobileNet%E7%B3%BB%E5%88%97/"/>
<id>http://yoursite.com/2019/12/13/MobileNet%E7%B3%BB%E5%88%97/</id>
<published>2019-12-13T11:55:48.000Z</published>
<updated>2020-08-03T02:55:44.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><div class="note note-success"> <p>MobileNet是用在移动端的轻量级CNN,本文简单介绍MobileNet V1到V3的版本。</p> </div><h1 id="MobileNet-V1"><a href="#MobileNet-V1" class="headerlink" title="MobileNet V1"></a><strong>MobileNet V1</strong></h1><ul><li>主要特点:把卷积拆分为<strong>Depthwise和Pointwise</strong>两部分(深度可分离卷积Separable convolution),用步长为2的卷积代替池化。</li><li>Depthwise和Pointwise图解:</li></ul><p><img src="https://pic1.zhimg.com/v2-e6ef5e7b681a549831d98d094fb3d1c0_r.jpg" srcset="/img/loading.gif" alt></p><p>假设有$N \times H \times W \times C$的输入,普通卷积是做$k$个3x3的卷积,且same padding,$stride=1$,输出为$N \times H \times W \times k$。depthwise是将此输入分为$group=C$组,然后每组做一次卷积,相当于收集了每个channel的特征,输出依然是$N \times H \times W \times C$。pointwise是做$k$个普通的1x1卷积,相当于收集了每个点的特征。depthwise+pointwise的输出也为$N \times H \times W \times k$。</p><ul><li><p>普通卷积和MobileNet卷积对比如下图所示。计算一下两者的参数量:</p><ul><li>普通卷积为:$C \times k \times 3 \times 3$</li><li><p>depthwise+pointwise:$C \times 3 \times 3 + C \times k \times 1 \times 1$</p></li><li><p>压缩率为$\frac{depthwise+pointwise}{conv}=\frac{1}{k} + \frac{1}{3 \times 3}$</p></li></ul></li></ul><p><img src="https://pic2.zhimg.com/80/v2-93462fb68816168964c74c1354270b01_hd.jpg" srcset="/img/loading.gif" alt></p><ul><li>进一步压缩模型:引入了<strong>width multiplier</strong>,所有通道数乘以$\alpha \in (0,1]$(四舍五入),以降低模型的宽度。</li></ul><h1 id="MobileNet-V2"><a href="#MobileNet-V2" class="headerlink" title="MobileNet V2"></a><strong>MobileNet V2</strong></h1><ul><li>主要特点:引入残差结构;采用<strong>linear bottenecks + inverted residual</strong>结构,先升维后降维;使用<strong>relu6</strong>(最大输出为6)激活函数,使模型在低精度计算下有更强的鲁棒性。</li><li>linear bottenecks + inverted residual结构如下图所示。<ul><li>V2版本依然是使用depthwise和pointwise,不同的是在depthwise前加了一个1x1卷积来扩大通道数目扩张系数为$t$,即通道数目扩大$t$倍,以增加特征丰富性。在pointwise之后再加1x1卷积将通道数目压缩至原输入的数目。</li><li>V2版本去掉了第二个1x1卷积之后的激活函数,称为linear bottleneck。作者认为激活函数在高维空间能够有效地增加非线性,但在地位空间会破坏特征。</li></ul></li></ul><p><img src="https://img-blog.csdn.net/20181011141302981" srcset="/img/loading.gif" alt></p><ul><li>与残差模块的对比:</li></ul><p><img src="https://img-blog.csdnimg.cn/20191213220507813.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="image-20191213220501881"></p><ul><li>V2网络结构:</li></ul><p><img src="https://img2018.cnblogs.com/blog/1456303/201908/1456303-20190811233548813-66860156.png" srcset="/img/loading.gif" alt></p><h1 id="MobileNet-V3"><a href="#MobileNet-V3" class="headerlink" title="MobileNet V3"></a><strong>MobileNet V3</strong></h1><ul><li>主要特点:引入<strong>SE(squeeze and excitation)</strong>结构;使用<strong>hard swish</strong>激活函数;头部卷积通道数量由32变为16;V2在预测部分使用了一个bottleneck结构来提取特征,而V3用两个1x1代替了这个操作;结构用<strong>NAS</strong>技术生成</li><li>SE轻量级注意力结构,如下图所示。在depthwise后加入SE模块,首先globalpool,然后1x1卷积将其通道压缩为原来的1/4,然后再1x1卷积扩回去,再乘以SE的输入。SE即提高了精度,同时还没有增加时间消耗。</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/5164048-9d51444c171dbee6.png?imageMogr2/auto-orient/strip|imageView2/2/w/521/format/webp" srcset="/img/loading.gif" alt></p><ul><li>尾部修改:</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/5164048-057bfafd08511f23.png?imageMogr2/auto-orient/strip|imageView2/2/w/635/format/webp" srcset="/img/loading.gif" alt></p><ul><li>hard swish激活函数如下所示。swish激活函数可以提高精度,但计算量比较大,作者用relu近似模拟,称为hard swish</li></ul><script type="math/tex; mode=display">\Bbb{swich} x=x \cdot \sigma(x) \\\Bbb{h-swish}[x]=x\frac{ReLU6(x+3)}{6}</script><p><img src="https://upload-images.jianshu.io/upload_images/5164048-fcc9511e39d28f54.png?imageMogr2/auto-orient/strip|imageView2/2/w/664/format/webp" srcset="/img/loading.gif" alt="img"></p><ul><li>v2头部卷积为32x3x3,作者发现可以改为16,保证了精度且降低了延时时间。</li><li><p>网络结构搜索,借鉴了MansNet和NetAdapt,这部分以后再详细补充。</p></li><li><p>网络结构:</p></li></ul><p><img src="https://upload-images.jianshu.io/upload_images/5164048-3b918f050aa5fe4b.png?imageMogr2/auto-orient/strip|imageView2/2/w/556/format/webp" srcset="/img/loading.gif" alt="img"></p><p><img src="https://upload-images.jianshu.io/upload_images/5164048-3474f20abe84937a.png?imageMogr2/auto-orient/strip|imageView2/2/w/555/format/webp" srcset="/img/loading.gif" alt="img"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a><strong>参考文献</strong></h1><p>[1] <a href="https://zhuanlan.zhihu.com/p/35405071" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/35405071</a></p><p>[2] <a href="https://blog.csdn.net/mzpmzk/article/details/82976871" target="_blank" rel="noopener">https://blog.csdn.net/mzpmzk/article/details/82976871</a></p><p>[3] <a href="https://www.cnblogs.com/darkknightzh/p/9410540.html" target="_blank" rel="noopener">https://www.cnblogs.com/darkknightzh/p/9410540.html</a></p><p>[4] <a href="https://blog.csdn.net/DL_wly/article/details/90168883" target="_blank" rel="noopener">https://blog.csdn.net/DL_wly/article/details/90168883</a></p><p>[5] <a href="https://blog.csdn.net/Chunfengyanyulove/article/details/91358187" target="_blank" rel="noopener">https://blog.csdn.net/Chunfengyanyulove/article/details/91358187</a></p><p>[6] <a href="https://www.jianshu.com/p/9af2ae74ec04" target="_blank" rel="noopener">https://www.jianshu.com/p/9af2ae74ec04</a></p><p>[7] <a href="https://www.cnblogs.com/dengshunge/p/11334640.html" target="_blank" rel="noopener">https://www.cnblogs.com/dengshunge/p/11334640.html</a></p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="深度学习" scheme="http://yoursite.com/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
<category term="CNN" scheme="http://yoursite.com/tags/CNN/"/>
</entry>
<entry>
<title>读AutoDL论文——SCARLET-NAS</title>
<link href="http://yoursite.com/2019/12/13/%E8%AF%BBAutoDL%E8%AE%BA%E6%96%87%E2%80%94%E2%80%94SCARLET-NAS/"/>
<id>http://yoursite.com/2019/12/13/%E8%AF%BBAutoDL%E8%AE%BA%E6%96%87%E2%80%94%E2%80%94SCARLET-NAS/</id>
<published>2019-12-13T11:33:39.000Z</published>
<updated>2019-12-13T11:43:10.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h1><p>本文要解决的问题是在文献[1]和[2]中都提到过的跳跃连接聚集的问题。虽然跳跃连接可以使超网络的长度可以收缩,但它使超网络的训练便得不稳定,进而使评估模型变得困难。本文首先深入讨论了跳跃连接给训练带来的不稳定性,给出了造成这种现象的根本原因;然后提出了一种可学习的稳定子,使超网络在可变深度的情况下训练也变得稳定;最后利用这种稳定子在ImageNet上训练达到了76.9%的SOTA,且相比于Efficient-B0有更少的FLOPs。</p><p>本文使用了两个搜索空间:</p><ul><li>$S_{1}$:与ProxylessNAS类似,采用MobileNetV2作为它的backbone,包括19层,每层7种选择,一共$7^{19}$种可能。</li><li>$S_{2}$:在$S_{1}$的基础上,给每个inverted bottleneck一个squeeze-and-excitation的操作,类似于MnasNet,一共$13^{19}$种可能。</li></ul><h1 id="训练的不稳定性"><a href="#训练的不稳定性" class="headerlink" title="训练的不稳定性"></a>训练的不稳定性</h1><p><img src="https://img-blog.csdnimg.cn/20191210154843346.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt></p><p>作者用single-path的方式在$S_{1}$空间上搜索,发现跳跃连接带来了严重的训练不稳定,如图1蓝色标记所示,训练过程的方差非常大,且采样的子模型准确率也很低。作者计算了已训练好的超网络的第三层内不同choice blocks的余弦距离,绘制成热图如下图所示:</p><p><img src="https://img-blog.csdnimg.cn/20191210154910129.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt></p><p>可以看出前6个inverted bottlenecks有非常相似的特征(颜色较深),但跳跃连接的特征相似度就很低(颜色较浅),平均余弦距离低于0.6。在训练过程中,除跳跃连接外其他操作带来的都是相似的特征图,但跳跃连接带来了更多的特征增强,破坏了现有特征图的相似性,使超网络的训练恶化。</p><h1 id="方法"><a href="#方法" class="headerlink" title="方法"></a>方法</h1><p>作者解决这个问题的方法是,使用一个可学习的稳定子来代替无参的跳跃连接,以实现同其他choice blocks一样相似的特征,同时带稳定子的子模型必须和不带稳定子的子模型在表征能力上是等价的。满足上述要求的稳定子称为Equivariant Learnable Stabilizer(ELS),可用下式表示:</p><script type="math/tex; mode=display">f_{l+1}^{o}(x_{l}^{c_{l}})=f_{l+1}^{o}(f_{l}^{ELS}(x_{l}^{c_{l}})),\forall o \in \{0,1,2,\cdots,n-1\}</script><p>作者这里选择ELS函数为不带BN和激活函数的1x1卷积,关于其等价的证明可参考原文。</p><p>在搜索阶段作者多目标进化搜索,因为更加关注准确率和加乘数,并且移动端的开发更加防止欠拟合,作者对搜索目标进行了加权且设置了最大加乘数$madds_{max}$和最小准确率$acc_{min}$,最终的搜索过程描述为:</p><script type="math/tex; mode=display">\begin{array}{cl}{\max } & {\{a c c(m),-\operatorname{madds}(m), \operatorname{params}(m)\}} \\ {\text {s.t.}} & {m \in \text { search space } S} \\ {} & {w_{a c c}+w_{m a d d s}+w_{\text {params}}=1, \forall w>=0} \\ {} & {a c c(m)>\operatorname{acc}_{\text {min}}, \text {madds}(m)<\text { madds }_{\max }}\end{array}</script><h1 id="实验"><a href="#实验" class="headerlink" title="实验"></a>实验</h1><p>在ImageNet数据集上的结果如下图所示,其中SCARLET-A达到了76.9的SOTA。</p><p><img src="https://img-blog.csdnimg.cn/20191210155006534.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt></p><p>同时作者画出了训练后的cross-block的特征相似度和特征向量指示图:</p><p><img src="https://img-blog.csdnimg.cn/20191210155020950.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt></p><p>相比于图2,添加了稳定子之后skip同其他blocks有了更高的相似度,特征向量之间的夹角也更小了。并且在带约束的优化搜索相比于不带约束的搜索可以达到更好的效果:</p><p><img src="https://img-blog.csdnimg.cn/20191210155038808.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt></p><p>最后作者绘制了4个不同层的1x1卷积输入、输出通道之间的相似度,可以看出只有对角线上的权重是非常相似的,这符合我们之前对ELS的要求。</p><p><img src="https://img-blog.csdnimg.cn/20191210155050245.png" srcset="/img/loading.gif" alt></p><h1 id="总结、思考"><a href="#总结、思考" class="headerlink" title="总结、思考"></a>总结、思考</h1><p>本文揭示了可伸缩signal path one-shot在训练时不稳定的原因,并提出了等价可学习稳定子(ELS),在很大程度上缓解了超网络的训练不稳定。同时采用加权多目标进化搜索,在ImageNet上最终达到了SOTA的效果。本文使用了一个非常优雅、简单的技巧就缓解了训练过程的不稳定,也给出了理论证明与详实的实验分析,给此方面的研究提供了一个非常好的思路。同时,文中使用1x1卷积作为ELS,如何进一步提升实验效果、挖掘其他形式的ELS函数还需作进一步的研究。</p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><p>[1] Progressive Differentiable Architecture Search: Bridging the Depth Gap between Search and Evaluation </p><p>[2] Understanding and Robustifying Differentiable Architecture Search </p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="深度学习" scheme="http://yoursite.com/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
<category term="自动化" scheme="http://yoursite.com/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
<category term="NAS" scheme="http://yoursite.com/tags/NAS/"/>
<category term="AutoDL" scheme="http://yoursite.com/tags/AutoDL/"/>
</entry>
<entry>
<title>AutoDL论文解读(七):基于one-shot的NAS</title>
<link href="http://yoursite.com/2019/10/21/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E4%B8%83%EF%BC%89%EF%BC%9A%E5%9F%BA%E4%BA%8Eone-shot%E7%9A%84NAS/"/>
<id>http://yoursite.com/2019/10/21/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E4%B8%83%EF%BC%89%EF%BC%9A%E5%9F%BA%E4%BA%8Eone-shot%E7%9A%84NAS/</id>
<published>2019-10-21T09:09:41.000Z</published>
<updated>2020-08-03T04:50:26.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><div class="note note-success"> <p>自动化机器学习(AutoML)最近变得越来越火,是机器学习下个发展方向之一。其中的神经网络结构搜索(NAS)是其中重要的技术之一。人工设计网络需要丰富的经验和专业知识,神经网络有众多的超参数,导致其搜索空间巨大。NAS即是在此巨大的搜索空间里自动地找到最优的网络结构,实现深度学习的自动化。自2017年谷歌与MIT各自在ICLR上各自发表基于强化学习的NAS以来,已产出200多篇论文,仅2019年上半年就有100多篇论文。此系列文章将解读AutoDL领域的经典论文与方法,笔者也是刚接触这个领域,有理解错误的地方还请批评指正!</p><p><strong>此系列的文章列表:</strong></p><ul><li><a href="https://5663015.github.io/2019/09/30/AutoDL论文解读(一):基于强化学习的开创性工作/" target="_blank" rel="noopener">AutoDL论文解读(一):基于强化学习的开创性工作</a></li><li><a href="https://5663015.github.io/2019/10/08/AutoDL论文解读(二):基于遗传算法的典型工作/" target="_blank" rel="noopener">AutoDL论文解读(二):基于遗传算法的典型方法</a></li><li><a href="https://5663015.github.io/2019/10/10/AutoDL论文解读(三):基于层或块的搜索/" target="_blank" rel="noopener">AutoDL论文解读(三):基于块搜索的NAS</a></li><li><a href="https://5663015.github.io/2019/10/12/AutoDL论文解读(四):权值共享的搜索/" target="_blank" rel="noopener">AutoDL论文解读(四):权值共享的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(五):可微分方法的NAS/" target="_blank" rel="noopener">AutoDL论文解读(五):可微分方法的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(六):基于代理模型的NAS/" target="_blank" rel="noopener">AutoDL论文解读(六):基于代理模型的NAS</a></li><li><a href="https://5663015.github.io/2019/10/21/AutoDL论文解读(七):基于one-shot的NAS/" target="_blank" rel="noopener">AutoDL论文解读(七):基于one-shot的NAS</a></li></ul> </div><p>本篇介绍三篇论文:《SMASH: One-Shot Model Architecture Search through HyperNetworks》,《Understanding and Simplifying One-Shot Architecture Search》和《Single Path One-Shot Neural Architecture Search with Uniform Sampling》。</p><h1 id="一、SMASH-One-Shot-Model-Architecture-Search-through-HyperNetworks"><a href="#一、SMASH-One-Shot-Model-Architecture-Search-through-HyperNetworks" class="headerlink" title="一、SMASH: One-Shot Model Architecture Search through HyperNetworks"></a><strong>一、SMASH: One-Shot Model Architecture Search through HyperNetworks</strong></h1><h2 id="1、总览"><a href="#1、总览" class="headerlink" title="1、总览"></a><strong>1、总览</strong></h2><p>这篇论文作者通过训练一个辅助模型:超网络(HyperHet),去训练搜索过程中的候选模型,这个超网络动态地生成生成具有可变结构的主模型的权值。尽管这些生成的权重比固定的网络结构自由学习得到的权重更差,但是不同网络在早期训练中的相对性能(即与最优值的距离)为最优状态下性能提供了有意义的指导。作者同时开发了一个基于存储库(memory-back)读写的网络表示机制,来定义各种各样的网络结构。作者称此方法为SMASH(one-Shot Model Architecture Search through Hypernetworks)</p><h2 id="2、利用超网络的one-shot结构搜索"><a href="#2、利用超网络的one-shot结构搜索" class="headerlink" title="2、利用超网络的one-shot结构搜索"></a><strong>2、利用超网络的one-shot结构搜索</strong></h2><p>在SMASH中,我们的目标是根据一个网络的验证集性能对一组网络的性能进行排序,这个任务是通过超网络生成权重来完成的。在每个训练步,我们随机地采样一个网络结构,用超网络生成它的权重,然后训练这个网络。训练结束后,再随机采样一些网络,它们的权重是由超网络生成的,直接评估它们的性能。选择其中在验证集表现最好的网络,再正常地训练它的权重。SMASH由两个核心要点:(1)我们采样网络结构的方法;(2)给定抽样结构的权重的方法。对于第一点,作者提出了网络的存储库视图,允许将复杂的、分支的拓扑作为二值向量进行采样和编码。对于第二点,作者使用超网络,直接学习二值向量的结构编码到权重的映射。这里假设只要超网络生成了合理的权值,网络的验证集性能将与使用正常训练权值时的性能相关,而结构的差异是其性能变化的主要因素。</p><h2 id="3、定义Memory-Bank"><a href="#3、定义Memory-Bank" class="headerlink" title="3、定义Memory-Bank"></a><strong>3、定义Memory-Bank</strong></h2><p>为了能探索非常广阔搜索的空间,也为了能将网络结构简单地编码成向量输入到超网络中,作者提出了网络的存储库视图。这个方法将网络视为一组可以读写的存储库(初始的张量为0),每一层是一个操作,这个操作从存储库的子集中读取数据,修改数据,然后将它写入另一个存储库子集。对于一个单分支结构,网络有一个大的存储库,它在每个操作上对其进行读写(对于ResNet是相加)。DenseNet这样的分支架构从所有以前写入的存储库读取数据,并将数据写入空的存储库,而FractalNet有更复杂的读写模式,见下图:<br><img src="https://img-blog.csdnimg.cn/2019101712391345.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>这里一个基本的网络结构包括多个block,在给定特征图下每个block有多个存储库,特征图大小依次减半。下采样是通过1x1卷积和平均池化完成的,1x1卷积和输出层的权重是自由学习到的,而不是超网络生成的。示意图如下所示:<br><img src="https://img-blog.csdnimg.cn/20191017124604975.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>在采样结构时,每个block的存储库的的数目和通道数目是随机采样的。当定义一个block里的每层时,我们随机选择读写模式和作用在读取的数据上的操作。当从多个存储库读取读取时,我们沿通道这个轴连接数据,写入存储库时,将此数据与写入存储库中数据相加。在这篇论文中,作者只从一个block里的存储库中读写,虽然也可以从其他block里的存储库中读写。</p><p>每个操作包含一个1x1卷积(降低输入通道数目),然后接带非线性变换的可变数量的卷积,如上图(a)中所示,随机选择四个卷积中的哪个被激活,以及它们的过滤器大小、膨胀系数、组数和输出单元数。这里的1x1卷积是由超网络生成的(上面也有一个1x1卷积,那个是用来和池化一块来减小特征图高宽的,是自由学习到的,和这里的不一样)。为了确保可变深度,每个块都学习一个含4个卷积的集合,并在一个块内的所有操作中共享它。这里限制了最大的滤波器大小和输出单元的数目,当采样的操作使用的值小于其最大值时,我们简单地将张量切片至所需的大小。固定的变换卷积和输出层根据传入的非空存储库的数量使用相同的切片。</p><p>作者力求最小化学习到的静态参数的数量,将网络的大部分容量放在超网络中。这一目标的一个显著结果就是,我们只在下采样层和输出层之前使用BatchNorm,因为特定层的运行统计量很难动态生成。这里作者采用一个简化版的WeightNorm,每个生成的1 x1滤波器除以其欧几里得范数(而不是每个通道单单独规范化),用在固定结构的网络中仅导致小精度下降。操作中其他卷积是没有标准化的。</p><h2 id="4、学习结构到权重的映射"><a href="#4、学习结构到权重的映射" class="headerlink" title="4、学习结构到权重的映射"></a><strong>4、学习结构到权重的映射</strong></h2><p>超网络是用来给另一个网络参数化权重的,那个网络就是主网络(main network)。对于参数是$H$的静态超网络,主网络的权重$W$是一个已学习到的嵌入$z$的函数(例如感知机),因此,学习到的权值的数量通常小于主网络权值的完整数量。对于动态超网络,权值$W$的生成取决于网络输入$x$,或者对于循环神经网络,则取决于当前输入$x_{t}$和先前的隐藏状态$h_{t-1}$。</p><p>基于主网络结构$c$的编码,作者提出了一个动态超网络取生成权重$W$,目标是学习一个映射$W=H(c)$,也就是说给定$c$使其接近最优权值$W$,这样就可以根据使用超级网络生成的权重,得到验证集误差,并对每个$c$进行排序。</p><p>这里的超网络是全卷积的,这样输出张量$W$的维数就会随着输入$c$的维数而变化,这样我们就得到了标准格式BCHW的4D张量,批量大小为1,因此没有输出元素是完全独立的。这允许我们通过增加$c$的宽度或长度去改变主网络的深度和宽度。在这样的设计下,$W$的每个空间维度的切片都对应于$c$的一个特定子集。描述操作的信息被嵌入到相应的$c$的切片的通道维度中。</p><p>以下图为例,如果一个操作从存储库1,2,4中读取数据,然后写入到2,4中,然后相应的$c$的切片的第一、二、四个通道被填入1(表示读),切片的第六、八通道填入1(表示写)。其他的操作在余下的通道中用同样1-hot的方式编码。我们只根据操作的输出单元的数量对$c$的宽度进行编码,所以没有与$W$的任何元素对应的$c$元素都是空的。<br><img src="https://img-blog.csdnimg.cn/20191017152952440.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>上述方案的一个朴素实现是让$c$的大小等于$W$的大小,或者让超网络用上采用去产生更多的元素。但作者发现这样的效果很差,取而代之的是采用一种基于通道的权重压缩方案,该方案减小了$c$的大小,并使超级网络的表示能力与主要网络的表示能力成比例。作者使$c$的空间范围等于$W$大小的一部分$k$,在超网络的输出处放$k$个单元,然后将得到的$1\times k \times height \times width$重塑成要求的$W$的大小。$k$定为$DN^{2}$,$N$是最小的存储库大小,$D$是一个“深度压缩”超参数,表示$W$的多少个切片对应于$c$的一个切片。</p><p>标准的2D CNN的输入是$x \in \Bbb R^{B \times C \times H \times L}$,B,C,H,L分别是批大小、通道、高度、宽度。我们的嵌入张量是$c \in \Bbb R^{1 \times (2M+d_{max}) \times (N_{max}/N)^{2} \times n_{ch}/D}$,M是一个block里存储库的最大数目,$d_{max}$是最大的卷积膨胀率,$n_{ch}$是主网络所有1x1卷积输入通道数目的综合。条件嵌入(conditional embedding)$c$是一个每层用来读写的存储库的独热编码,它有$2M+d_{max}$个通道,前M个通道表示从哪些存储库中读取,下M个通道表示写入到哪些存储库中,最后$d_{max}$个通道是3x3卷积里膨胀率的独热编码。宽度这个维度表示每层的单元数目,长度这个维度表示网络深度,即输入通道的总数。这里将批大小保持为1,这样就不会有信号完全独立地通过超级网络传播。</p><p>超网络有$4DN^{2}$个输出通道,因此超网络的输出是$W=H(c) \in \Bbb R^{1 \times 4DN^{2} \times (N_{max}/N^{2})\times n_{ch}/D}$,然后重塑为$W \in \Bbb R^{N_{max} \times 4N_{max}n_{ch} \times 1 \times 1}$。我们一次生成整个主网络的权值,允许超网络根据邻近层的权值预测给定层的权值。超网络的接受域表示给定一个层,网络可以向上或向下多远来预测给定层的参数。当我们遍历主网络时,我们根据传入通道的数量沿其第二轴切片,并根据给定层的宽度沿第一轴切片。</p><h1 id="二、Understanding-and-Simplifying-One-Shot-Architecture-Search"><a href="#二、Understanding-and-Simplifying-One-Shot-Architecture-Search" class="headerlink" title="二、Understanding and Simplifying One-Shot Architecture Search"></a><strong>二、Understanding and Simplifying One-Shot Architecture Search</strong></h1><h2 id="1、总览-1"><a href="#1、总览-1" class="headerlink" title="1、总览"></a><strong>1、总览</strong></h2><p>这篇论文里作者也是认为模型之间的权重共享是个可行的方向,训练一个能够模拟搜索空间中任何架构的大型网络。一个简单的例子如下所示:<br><img src="https://img-blog.csdnimg.cn/20191017165558681.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>上图中,在网络的某个位置,有3x3卷积、5x5卷积或者最大池化三个操作可以选择,不同于分别训练三个模型,我们训练一个包括了这三个操作的模型(one-shot model),然后在验证阶段,我们选择性地剔除其中两个操作的输出,保留使预测准确率最高的操作。一个更复杂的例子,在一个网络中可能在很多位置上包含了很多不同的操作选择,搜索空间是随着选择数目指数地增长,而one-shot模型的大小只随选择数目线性增长。相同的权重可以用来评估很多不同的结构,极大地降低了计算量。</p><p>作者提出了这样的疑问,为什么不同的结构可以共享一个权重集合?one-shot模型仅在搜索过程中对结构的性能排序,搜索结束后表现最好的结构会被从头重新训练,但即使这样,固定的权重集合在非常广泛的结构集合里工作的很好,这也是违反直觉的。作者在这篇论文中目的是理解权重共享在NAS中所扮演的角色。作者发现,对于达到好的搜索结果,超网络和强化学习控制器都不是必须的。为此作者训练了一个大的one-shot模型,包含了搜索空间里的每个可能的操作。然后剔除一些操作,测量其对模型预测准确率的影响。作者发现网络自动将其能力集中在对产生良好预测最有用的操作上。剔除不太重要的操作对模型的预测的影响很小,而剔除很重要的操作对模型预测的影响很大。实际上,可以通过观察网络结构在训练集中无标签样例的行为,来预测网络在验证集上的准确率。</p><h2 id="2、搜索空间设计"><a href="#2、搜索空间设计" class="headerlink" title="2、搜索空间设计"></a><strong>2、搜索空间设计</strong></h2><p>为one-shot设计搜索空间需要满足以下几个要求:(1)搜索空间需要足够大来捕捉到多样的候选结构;(2)one-shot模型产生的验证集准确率必须能预测独立模型训练产生的精度;(3)在有限的计算资源下,one-shot模型需要足够小。下图给出了一个搜索空间的例子:<br><img src="https://img-blog.csdnimg.cn/20191017202414485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>它合并了关于网络结构和应用在网络中不同位置的操作的重要决策。在训练过程中,one-shot模型包括了三个不同的输入,这些输入被连接在一起。在评估阶段,它可以通过剔除Input 1和Input 3来模拟一个一个仅包含Input 2的网络。更一般地说,我们可以使能或禁止任何输入连接的组合。这样,搜索空间可以随着传入的跨连接数目指数级地增长,而one-shot模型大小只线性地增长。连接操作后面总是连着一个1x1卷积,使得无论有多少传入的跨连接,输出的滤波器数目都是常量。然后one-shot模型在1x1卷积的输出上应用不同的操作,将结果相加。在评估阶段,我们移除一些操作。上图中有4种操作:3x3卷积、5x5卷积、最大池化和Identity,但只有5x5卷积操作留了下来。这样的方法被用在更大的模型上,如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191017204211237.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>这里的网络也是由多个一样的cell堆叠而成的,每个cell被分成了几个固定数目的choice block。一个choice block来自于前驱cell的输出或者同一cell的前驱choice block。这里作者设每个cell里有$N_{choice}=4$个choice block,每个choice block最多有5个可能的输入:两个来自前驱cell,三个来自同一cell的前驱choice block。每个choice block最多可以选择两个操作,可选择的操作有:identity,一对3x3可分离卷积,一对5x5可分离卷积,一对7x7可分离卷积,1x7卷积跟7x1卷积,最大池化,平均池化。</p><h2 id="3、训练one-shot模型"><a href="#3、训练one-shot模型" class="headerlink" title="3、训练one-shot模型"></a><strong>3、训练one-shot模型</strong></h2><p>one-shot模型是个大型的网络,用带动量的SGD训练,为了保证特定架构的one-shot模型精度与独立模型精度之间的良好关系,作者考虑了以下几个方面:</p><ul><li><p>互相适应的鲁棒性。如果直接训练整个one-shot模型,模型里的各部分会相互耦合。即使移除不重要的操作,也会造成模型预测精度的急剧下降,one-shot模型和独立模型之间的准确率的关系也会退化。为了解决这个问题,作者在训练one-shot模型时也包含了path dropout(我理解的是类似于dropout,起到了正则化的作用),对于每个batch,也随机地剔除一些操作。通过实验发现,一开始的时候不用path dropout,然后随着时间逐渐地增加dropout的几率,可以达到很好的效果。dropout的几率是$r^{1/k}$,$0<r<1$是模型的超参数,$k$是给定一个操作下进来路径的数目。然而dropout一个节点的所有输入的几率是常数,这里作者设为5%。在单个cell里,不同的操作使彼此独立地被剔除。如果一个模型包含多个cell,那在每个cell里通用的操作会被剔除。</p></li><li><p>训练模型的稳定性。作者一开尝试实验的时候发现one-shot的训练很不稳定,但是仔细地应用BN可以增加稳定性,作者使用了BN-ReLU-Conv这样的卷积顺序。在评估阶段要剔除某些操作,这会使每层batch的统计量改变,因为对于候选结构无法提前得知其batch统计量。因此批BN在评估时的应用方式与在训练时完全相同——动态计算batch的统计信息。作者还发现,训练one-shot模型时,如果在一个batch里对每个样本都dropout同样的操作,训练也会不稳定。因此对于不同的样本子集,作者dropout不同的操作:作者将一个batch的样本分成多个小batch(文中称为ghost batch),一个batch有1024个样本,分成32个ghost batch,每个有32个样本,每个ghost batch剔除不同的操作。</p></li><li><p>防止过度正则化。在训练期间,L2正则化只应用于当前结构在one-shot模型里所用到的那部分。如果不这样,那些经常被删除的层就会更加规范化。</p></li></ul><h2 id="4、评估和选择"><a href="#4、评估和选择" class="headerlink" title="4、评估和选择"></a><strong>4、评估和选择</strong></h2><p>当one-shot模型训练好之后,我们由一个固定的概率分布独立地采样结构,然后在验证集上评估。作者注意到,随机采样也可以的用遗传算法或基于神经网络的强化学习代替。完成搜索之后,从头训练表现最好的结构,同时也可以扩展架构以增加其性能,也可以缩小架构以减少计算成本。作者在实验中,增加了过滤器的数量来扩展架构。</p><h2 id="5、理解one-shot模型"><a href="#5、理解one-shot模型" class="headerlink" title="5、理解one-shot模型"></a><strong>5、理解one-shot模型</strong></h2><p>具体的实验细节、超参数和结果可参考原论文,这里讨论一下为什么固定的模型权重集合可以在不同的结构里共享。作者通过这篇论文实验和上篇的SMASH的实验发现,one-shot模型的准确率在30%至90%之间,或者10%至60%之间,但搜索到的独立模型的准确率在92%至94.5%之间,或者70%至75%之间,为什么one-shot模型的准确率之间相差这么多?</p><p>注意作者之前做了这样的假设:one-shot模型可以学习网络中哪些操作最有用,并在可用时依赖于这些操作,移除不重要的操作对模型预测的准确率有较小的影响,移除很重要的操作对模型预测的准确率有较大的影响。为了去验证这样的假设,作者采样了一批结构(参照结构),大部分操作没有被移除($r=10^{-8}$)。作者将这些参考结构的预测与从实际搜索空间中采样的操作较少的候选结构的预测进行了比较,比较是在来自训练集的批样本上进行的。如果假设是正确的,那么由性能最佳的模型做出的预测与使能网络中的所有操作时做出的预测相似。</p><p>我们使用对称的KL散度来量化候选结构的预测与参考结构的预测的不同程度。one-shot模型用交叉熵损失函数,其输出是概率分布$(p_{1},p_{2}, \dots, p_{n})$,候选结构的输出概率分布为$(q_{1},q_{2}, \dots, q_{n})$,KL散度为$D_{KL}(p||q)=\sum_{i=1}^{n} p_{i} {\rm log}\frac{p_{i}}{q_{i}}$,对称KL散度为$D_{KL}(p||q)+D_{KL}(q||p)$。如果当前训练样本的分布几乎相同,则对称的KL散度将接近于0。相反,如果分布非常不同,对称的KL散度可以变化得得非常大。作者计算了64个随机样本的KL散度,并取了平均值。实验结果如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191018150908112.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>训练集测得的KL散度与验证集测得的预测准确率有很强的相关性。此外,KL散度的计算没有用到训练集的标签。这意味着,候选体系结构的预测与参考体系结构的预测越接近(其中,one-shot模型中的大多数操作都是使能的),在独立模型训练期间,它的性能通常就越高。权重共享迫使one-shot模型识别并集中于对生成良好预测最有用的操作。</p><p>作者接下来展示了KL散度随着时间变化的趋势。作者采样了六个不同的结构然后观察随着训练它的KL散度的变化情况,如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191018152641211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>一开始他们的K离散度是低的,因为模型的预测一开始置信度比较低,每类会分配一个大致相等的概率。随着训练,不同结构的预测开始变的不同,这导致了KL散度的激增,在训练后期,网络中最有用的操作对模型的预测有了强大的影响,KL散度开始下降。</p><h1 id="三、Single-path-one-shot-neural-architecture-search-with-uniform-sampling"><a href="#三、Single-path-one-shot-neural-architecture-search-with-uniform-sampling" class="headerlink" title="三、Single path one-shot neural architecture search with uniform sampling"></a><strong>三、Single path one-shot neural architecture search with uniform sampling</strong></h1><h2 id="1、总览-2"><a href="#1、总览-2" class="headerlink" title="1、总览"></a><strong>1、总览</strong></h2><p>大多数搜索空间松弛化的方法里,结构分布是被连续地参数化了,这些参数在超网络训练时一同被联合优化,因此结构搜索是在优化的过程中进行,优化结束后从结构分布中采样最好的结构。但这存在两个问题:(1)超网络里的参数是深度耦合的;(2)联合优化进一步引入了结构参数和超网络权重之间的耦合。梯度下降方法天生的贪婪特征会在结构分布和超网络权重中引入偏差,这可能会误导结构搜索。</p><p>one-shot方法为结构搜索提供了一个新的方向,并且没有结构松弛化和分布参数。结构搜索问题从超网络训练中解耦出来,变成一个单独的步骤,它结合了嵌套优化和联合优化方法的优点。刚才介绍的两篇论文就是这样的one-shot方法,解决了上面提到的第二个问题,但并没有很好地解决第一个问题,超网络的权重仍然是耦合的。在《Understanding and Simplifying One-Shot Architecture Search》里提到,one-shot成果的关键是一个使用权重继承的结构的准确率应该对已经优化好的结构有预测性。因此,作者提出超网络训练应该是随机的,所有的结构都可以同时优化它们的权重。为了减少超网络的权重耦合,作者提出了一个简单的搜索空间,单路径超网络(single path supernet)。对于超网络训练,作者采用均匀采样,平等地对待所有结构,没有超参数。</p><h2 id="2、NAS方法回顾"><a href="#2、NAS方法回顾" class="headerlink" title="2、NAS方法回顾"></a><strong>2、NAS方法回顾</strong></h2><p>我们用$\mathcal A$结构搜索空间,这是一个有向无环图(DAG),一个搜索到的结构是DAG的子图$a \in \mathcal A$,记为$\mathcal N(a,w)$,$w$为权重。NAS的目的是去解决两个有关联的问题,第一个是在标准深度学习里给定一个网络结构,优化其权重:</p><script type="math/tex; mode=display">\begin{equation}w_{a}=\underset{w}{\operatorname{argmin}} \mathcal{L}_{\text {train }}(\mathcal{N}(a, w))\end{equation} \tag{1}</script><p>$\mathcal L_{train}(\cdot)$是在训练集上的损失函数。第二个问题是结构优化,一般来说是通过验证集的准确率来寻找:</p><script type="math/tex; mode=display">\begin{equation}a^{\ast}=\underset{a \in \mathcal A}{\operatorname{argmin}} {\rm ACC}_{\text {val }}(\mathcal{N}(a, w_{a})) \tag{2}\end{equation}</script><p>${\rm ACC}_{\rm val}(\cdot)$是验证集准确率。真实情况下会对网络的内存消耗、FLOPs、latency、功耗等有要求,这些取决于结构$a$、软件和硬件等,但和权重$w_{a}$无关,因此作者称为“结构限制”。一个典型的限制网络的latency不大于预设的budget:</p><script type="math/tex; mode=display">\begin{equation}{\rm Latency(a^{\ast}) \leq {\rm Lat}_{\rm max}} \tag{3}\end{equation}</script><p>对于大多数方法来说,同时满足式(2)和式(3)是很有挑战性的。最近的NAS方法采用了权值共享的策略,结构搜索空间$\mathcal A$被编码进一个超网络,记作$\mathcal N(\mathcal A, W)$,$W$是超网络的权重。超网络被训练一次,所有的结构都从$W$里直接继承,因此他们在相同的图节点上式共享权值的。大多权值共享的方法将离散的搜索空间转为连续的,$\mathcal A$松弛化为$\mathcal A(\theta)$,$\theta$为表示结构分布的连续化参数。注意新的空间包含了原始的搜索空间:$\mathcal A \subseteq \mathcal A(\theta)$。这样松弛化的好处是可以用梯度方法联合优化权重和结构分布参数,表示如下:</p><script type="math/tex; mode=display">\begin{equation}\left(\theta^{*}, W_{\theta^{*}}\right)=\underset{\theta, W}{\operatorname{argmin}} \mathcal{L}_{t r a i n}(\mathcal{N}(\mathcal{A}(\theta), W)) \tag{4}\end{equation}</script><p>优化后,最好的结构$a^{\ast}$从$\mathcal A(\theta)$采样得到,然后从$W_{\theta^{\ast}}$继承权值、微调。理论上这样做很合理,但优化式(4)是具有挑战性的。首先,在超网络里的图节点的权重是互相依赖的、深度耦合的,但从$W$中继承的权重解耦了,尚不明确为什么这样做是有效的。第二,联合训练结构参数$\theta$和权值$W$进一步引入了耦合。满足结构限制也是困难的,一些工作使用精心设计的soft损失项来扩充式(4)中的损失函数$\mathcal L_{train}$,但也很难满足像式(3)中的限制。作者总结了一下这部分提到的方法,连同作者提出的方法一同作了比较:<br><img src="https://img-blog.csdnimg.cn/20191021151840136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><h2 id="3、one-shot-NAS-回顾"><a href="#3、one-shot-NAS-回顾" class="headerlink" title="3、one-shot NAS 回顾"></a><strong>3、one-shot NAS 回顾</strong></h2><p>通过上面的分析,耦合的结构搜索和权重优化是困难的,我们能否同时拥有问题解耦和权值共享的优点?这引出了叫做one-shot的方法(即上面解读的两篇论文),这两种方法依然训练一个超网络然后共享其中的参数,超网络训练和结构搜索是解耦成两个步骤,这既不同于嵌套式优化也不同于联合优化。首先,第一步优化超网络权重:</p><script type="math/tex; mode=display">W_{\mathcal{A}}=\underset{W}{\operatorname{argmin}} \mathcal{L}_{\text {train }}(\mathcal{N}(\mathcal{A}, W)) \tag{5}</script><p>相比于式(4),搜索空间的连续化参数消失了,只有权重被优化。第二步,结构搜索表示为:</p><script type="math/tex; mode=display">a^{*}=\underset{a \in \mathcal{A}}{\operatorname{argmax}} \mathrm{ACC}_{\mathrm{val}}\left(\mathcal{N}\left(a, W_{\mathcal{A}}(a)\right)\right) \tag{6}</script><p>在搜索过程中,每个采样的结构$a$从继承$W_{\mathcal A}$继承权重,记为$W_{\mathcal A}(a)$。同式(1)、式(2)相比,式(6)的主要不同是,结构的权重是合理初始化的,估计$ACC_{val}(\cdot)$只需推断,没有微调或重新训练。找到最优的结构$a^{\ast}$后,通过微调来获得$w_{a^{\ast}}$。这样的搜索也是灵活的,任何适当的搜索方法都可以,这里作者采用遗传算法。结构限制,式(3)的结构限制也被精确地满足。一旦训练好超网络,搜索可以用不同的结构限制(如100ms latency 和 200ms latency)在超网络上被重复很多次,之前的方法都没有这样的特性。但超网络的权重依然是耦合的。</p><h2 id="4、单路径超网络和均匀采样"><a href="#4、单路径超网络和均匀采样" class="headerlink" title="4、单路径超网络和均匀采样"></a><strong>4、单路径超网络和均匀采样</strong></h2><p>将式(1)作为理想的情况,one-shot要求权重$W_{\mathcal A}(a)$接近于最优权值$w_{a}$,近似的程度取决于训练损失$\mathcal L_{train}(\mathcal N(a, W_{\mathcal A}(a)))$被优化的程度。这引出一个原则,超网络的权重$W_{\mathcal A}$应该与搜索空间中的所有子结构的优化同时进行,如下式:</p><script type="math/tex; mode=display">W_{\mathcal{A}}=\underset{W}{\operatorname{argmin}} \mathbb{E}_{a \sim \Gamma(\mathcal{A})}\left[\mathcal{L}_{\text {train }}(\mathcal{N}(a, W(a)))\right] \tag{7}</script><p>$\Gamma(\mathcal A)$是$a \in \mathcal A$的先验分布。注意,式(7)是式(5)的实现,在每步优化中,结构$a$是随机采样的,只有权重$W(a)$是被更新的。从这个意义上说,超级网络本身不再是一个有效的网络,它表现为一个随机超网络(stochastic supernet)。</p><p>为了较少权重之间的互相适应,作者建议将搜索空间简化到极限,只包含但路径结构,这可以看做是path dropout策略的极端情况,每次只保留一条路径,其他全部drop,如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191021163132387.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>它包括一系列的choice block,每个包含几个choices,每次只有一个choice被调用。这里不包含任何dorpout参数或微调。先验分布$\Gamma (\mathcal A)$可能是重要的,作者发现均匀采样已经足够好了。</p><h2 id="5、超网络和choice-block"><a href="#5、超网络和choice-block" class="headerlink" title="5、超网络和choice block"></a><strong>5、超网络和choice block</strong></h2><p>和《Understanding and Simplifying One-Shot Architecture Search》中的一样,choice block用于构建随机结构,一个choice block包含多个结构选择。对这里的但路径超网络,每次只有一个choice被调用,通过采样所有的choice block来获得一条路径。这里作者设计了两种choice block:</p><ul><li>通道数目搜索。这个choice block搜索一个卷积层的通道数目,它会预先分配一个最大通道数目的权重张量(max_c_out, max_c_in, ksize),在超网络训练过程中,系统随机选择当前输出通道数目$c_out$,然后从其中切片出张量子集$[:c_out, : c_in, :]$,如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191021164338551.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></li><li>混合精度量化搜索。这个choice block用于搜索卷积层的权值和特征的量化精度。在超网络训练过程中,特征图的位宽和滤波器权值是随机选择的,如下图所示:<br><img src="https://img-blog.csdnimg.cn/2019102116462811.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><h2 id="6、进化结构搜索"><a href="#6、进化结构搜索" class="headerlink" title="6、进化结构搜索"></a><strong>6、进化结构搜索</strong></h2>对于式(6)的结构搜索,前面两篇论文使用随机搜索,而作者使用遗传算法,算法伪代码如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191021164915185.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>对于所有的实验,种群大小$P=50$,最大迭代次数$\mathcal T=20$,$k=10$,对于交叉,随机选择两个候选结构去交叉生成一个新的结构,对于变异,在每个choice block以0.1的概率从候选变异中随机选择一种变异,产生一个新的结构。在对结构进行推断之前,BN的统计量从训练集中随机选择子集样本重新计算,因为超网络的BN统计量一般来说并不适合于候选结构。</li></ul><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a><strong>参考文献</strong></h1><p>[1] Brock A , Lim T , Ritchie J M , et al. SMASH: One-Shot Model Architecture Search through HyperNetworks[J]. 2017.<br>[2] Bender, Gabriel, et al. “Understanding and simplifying one-shot architecture search.” International Conference on Machine Learning. 2018.<br>[3] Guo, Zichao, et al. “Single path one-shot neural architecture search with uniform sampling.” arXiv preprint arXiv:1904.00420 (2019).<br>[4] <a href="https://www.jiqizhixin.com/articles/2019-04-02-8" target="_blank" rel="noopener">https://www.jiqizhixin.com/articles/2019-04-02-8</a><br>[5] 《深入理解AutoML和AutoDL》</p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="深度学习" scheme="http://yoursite.com/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
<category term="自动化" scheme="http://yoursite.com/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
<category term="NAS" scheme="http://yoursite.com/tags/NAS/"/>
<category term="AutoDL" scheme="http://yoursite.com/tags/AutoDL/"/>
</entry>
<entry>
<title>AutoDL论文解读(六):基于代理模型的NAS</title>
<link href="http://yoursite.com/2019/10/15/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E5%85%AD%EF%BC%89%EF%BC%9A%E5%9F%BA%E4%BA%8E%E4%BB%A3%E7%90%86%E6%A8%A1%E5%9E%8B%E7%9A%84NAS/"/>
<id>http://yoursite.com/2019/10/15/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E5%85%AD%EF%BC%89%EF%BC%9A%E5%9F%BA%E4%BA%8E%E4%BB%A3%E7%90%86%E6%A8%A1%E5%9E%8B%E7%9A%84NAS/</id>
<published>2019-10-15T12:42:19.000Z</published>
<updated>2020-08-03T02:54:00.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><div class="note note-success"> <p>自动化机器学习(AutoML)最近变得越来越火,是机器学习下个发展方向之一。其中的神经网络结构搜索(NAS)是其中重要的技术之一。人工设计网络需要丰富的经验和专业知识,神经网络有众多的超参数,导致其搜索空间巨大。NAS即是在此巨大的搜索空间里自动地找到最优的网络结构,实现深度学习的自动化。自2017年谷歌与MIT各自在ICLR上各自发表基于强化学习的NAS以来,已产出200多篇论文,仅2019年上半年就有100多篇论文。此系列文章将解读AutoDL领域的经典论文与方法,笔者也是刚接触这个领域,有理解错误的地方还请批评指正!</p><p><strong>此系列的文章列表:</strong></p><ul><li><a href="https://5663015.github.io/2019/09/30/AutoDL论文解读(一):基于强化学习的开创性工作/" target="_blank" rel="noopener">AutoDL论文解读(一):基于强化学习的开创性工作</a></li><li><a href="https://5663015.github.io/2019/10/08/AutoDL论文解读(二):基于遗传算法的典型工作/" target="_blank" rel="noopener">AutoDL论文解读(二):基于遗传算法的典型方法</a></li><li><a href="https://5663015.github.io/2019/10/10/AutoDL论文解读(三):基于层或块的搜索/" target="_blank" rel="noopener">AutoDL论文解读(三):基于块搜索的NAS</a></li><li><a href="https://5663015.github.io/2019/10/12/AutoDL论文解读(四):权值共享的搜索/" target="_blank" rel="noopener">AutoDL论文解读(四):权值共享的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(五):可微分方法的NAS/" target="_blank" rel="noopener">AutoDL论文解读(五):可微分方法的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(六):基于代理模型的NAS/" target="_blank" rel="noopener">AutoDL论文解读(六):基于代理模型的NAS</a></li><li><a href="https://5663015.github.io/2019/10/21/AutoDL论文解读(七):基于one-shot的NAS/" target="_blank" rel="noopener">AutoDL论文解读(七):基于one-shot的NAS</a></li></ul> </div><p>本篇讲述基于代理模型的渐进式搜索《Progressive Neural Architecture Search》。代理模型就是训练并利用一个计算成本较低的模型去模拟原本计算成本较高的那个模型的预测结果,当需要计算的大模型的数量较多时,可以避开很多计算。</p><h1 id="一、Progressive-Neural-Architecture-Search"><a href="#一、Progressive-Neural-Architecture-Search" class="headerlink" title="一、Progressive Neural Architecture Search"></a><strong>一、Progressive Neural Architecture Search</strong></h1><h2 id="1、总览"><a href="#1、总览" class="headerlink" title="1、总览"></a><strong>1、总览</strong></h2><p>这篇论文也是搜寻卷积cell,每个cell包含B个block,每个block连接两个输入,然后cell堆叠起来组成整个网络,同NASNet(《Learning Transferable Architectures for Scalable Image Recognition》)里的基本一样。不过作者是从简单的浅层的cell开始,逐渐地搜索到复杂的cell,而不是每次预测固定大小的cell。在算法的第b次迭代,有K个候选的cell(每个cell有b个block),这些会被训练和评估。因为这个过程是非常费时的,作者在这里使用代理模型去预测候选cell的效果。将K个包含b个block的候选cell扩展成$K^{\prime}>>K$个cell,每个cell有b+1个block。使用代理模型去预测他们的表现,然后选取最高的K个,训练并评估。直到b=B,达到允许的最大block数目。</p><h2 id="2、搜索空间"><a href="#2、搜索空间" class="headerlink" title="2、搜索空间"></a><strong>2、搜索空间</strong></h2><p>这里作者不再区分normal cell和reduction cell,不过如果步长为2,同样可以实现reduction cell的功能。cell是由B个block组成的,每个block将两个输入组合成一个输出。一个在第$c$个cell里的第$b$个block 可以用一个元组表示$(I_{1},I_{2},O_{1},O_{2})$,这里$I_{1},I_{2} \in {\mathcal I}_{b}$表示输入,$O_{1},O_{2} \in {\mathcal O}_{b}$表示作用在输入$I_{i}$上的操作。对于两个输入的连接方式,NASNet里有相加和沿深度轴连接两种选择,不过作者发现沿深度轴连接在实验中从来没有被选择过,因此这里只将两个输入相加,生成block的输出$H_{b}^{c}$。可能的输入集合$\mathcal I_{b}$是这个cell里所有的前驱block的输出$\{H_{1}^{c}, \dots, H_{b-1}^{c}\}$,加上前一个cell的输出$H_{B}^{c-1}$,再加上前前一个cell的输出$H_{B}^{c-2}$。操作空间$\mathcal O$包括以下几种:<br><img src="https://img-blog.csdnimg.cn/20191015170158825.png" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>令第$b$个block可能的结构空间是$\mathcal B_{b}$,那么这个空间的大小为$|\mathcal B_{b}|=|\mathcal I_{b}|^{2} \times |\mathcal O|^{2}$,$|\mathcal I_{b}|=(2+b-1)$,$|\mathcal O|=8$。对于$b=1$,只有$\mathcal I_{1}=\{H_{B}^{c-1}, H_{B}^{c-2}\}$,所以总共有$|\mathcal B|_{1}=4 \times 4 \times 8^{2}=256$个可能的cell。这里作者令$B=5$,那么一种有$\left|\mathcal{B}_{1: 5}\right|=(2^{2} \times 8^{2}) \times (3^{2} \times 8^{2}) \times (4^{2} \times 8^{2}) \times (5^{2} \times 8^{2} )\times (6^{2} \times 8^{2})=5.6 \times 10^{14}$种可能。但这些可能的cell里有一些对称的结构,例如b=1时只有136个独一无二的cell,那么整个搜索空间大小大概是$10^{12}$数量级,比NASNet的$10^{28}$小了不少。</p><p>下图展示了可能的cell结构和cell的堆叠方式:<br><img src="https://img-blog.csdnimg.cn/20191015172256285.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"></p><h2 id="3、渐进式搜索"><a href="#3、渐进式搜索" class="headerlink" title="3、渐进式搜索"></a><strong>3、渐进式搜索</strong></h2><p>作者从最简单的模型开始,按渐进的方式搜索。首先从只含一个block的cell开始,构造出所有可能的cell空间$\mathcal B_{1}$,将它们放入队列中,并且训练、评估队列中所有的模型。然后扩展其中的每个cell至含有2个block,所有可能的cell空间为$\mathcal B_{2}$,此时所有的候选cell有$|\mathcal B_{1}| \times |\mathcal B_{2}|=256 \times ((2+1)^{2} \times 8^{2})=147,456$种可能。训练和评估这么多种模型是基本不可行的,因此作者使用预测函数(即代理模型)去预测这些模型的表现。之前已经训练评估过一些模型了,那么代理模型就从这些已经评估出来的模型和结果中训练。然后去预测上面147,456种可能的模型,选出K个效果最好的模型,将它们放入队列中,然后重复整个过程,直到cell包含B个block。具体算法如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191015173609738.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>下图展示了一个渐进式搜索的例子:<br><img src="https://img-blog.csdnimg.cn/2019101520232744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>这个例子设cell里最多有B=3个block,$\mathcal B_{b}$表示有b个block时候选cell的集合。当cell包含一个block时,$\mathcal S_{1}= \mathcal B_{1}$,训练这些并评估这些cell,然后更新代理模型;在第二轮,将$\mathcal S_{1}$里的每个cell扩展到2个block,此时$\mathcal S_{2}^{\prime}=\mathcal B_{1:2}$,预测它们的性能,选择其中最高的$K$个组成$\mathcal S_{2}$,训练并评估它们,然后更新代理模型;在第三轮,将$\mathcal S_{2}$里的每个cell扩展为3个block,得到有3个block的cell的集合的子集$S_{3}^{\prime} \subseteq \mathcal B_{1:3}$(在第二轮里已经选择了K个最好的cell,已经是子集了,再扩展地话依然是子集),预测它们的性能,选择最好的K个,训练评估它们,最后得到最好的模型。</p><h2 id="4、用代理模型预测"><a href="#4、用代理模型预测" class="headerlink" title="4、用代理模型预测"></a><strong>4、用代理模型预测</strong></h2><p>我们需要一个代理模型去预测模型的性能,这样的代理模型三友三个特点:(1)能处理可变长度的输入。即使代理模型是在仅包含b个block的cell上训练的,那它也得能预测包含b+1个block的cell的性能;(2)与真实性能呈相关关系。我们并不一定非要获得较低的均方误差,但我们确实希望代理模型预测的性能排序同真实的大致相同;(3)样本效率,我们希望训练和评估尽可能少的cell,这意味着用于代理模型的训练数据将是稀缺的。</p><p>作者尝试了两种代理模型:LSTM和MLP。对于LSTM,将长度为$4b$(表示每个block的$I_{1},I_{2},O_{1},O_{2}$)的序列作为输入,然后预测验证集准确率,使用L1损失训练模型。作者对$I_{1},I_{2} \in \mathcal I$使用$D$维的嵌入,对于$O_{1},O_{2} \in \mathcal O$使用另一套嵌入。对于MLP,作者将cell转为固定长度的向量:将每个block的操作嵌入成$D$维向量,连接block里的操作为$4D$向量,对于cell里所有block的$4D$维向量取平均。对于训练代理模型,因为样本量比较少,作者训练5个模型做集成。</p><h2 id="5、实现细节"><a href="#5、实现细节" class="headerlink" title="5、实现细节"></a><strong>5、实现细节</strong></h2><p>对MLP代理模型,嵌入向量长度为100,使用两个全连接层,每层有100个神经元。对于LSTM代理模型,隐藏状态长度和嵌入向量长度为100。嵌入向量从[-0.1, 0.1]之间均匀采样初始化。最后全连接层的偏置设为1.8(sigmoid之后为0.68),这是b=1的所有模型的平均准确率。在搜索过程中,每个阶段评估K=256个模型(在第一阶段评估136个),cell最多有B=5个block,第一个卷积cell里有F=24个滤波器,在最终的网络结构里重复N=2次,训练20轮。</p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a><strong>参考文献</strong></h1><p>[1] Liu C , Zoph B , Neumann M , et al. Progressive Neural Architecture Search[J]. 2017.<br>[2] 《深入理解AutoML和AutoDL》</p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="深度学习" scheme="http://yoursite.com/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
<category term="自动化" scheme="http://yoursite.com/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
<category term="NAS" scheme="http://yoursite.com/tags/NAS/"/>
<category term="AutoDL" scheme="http://yoursite.com/tags/AutoDL/"/>
</entry>
<entry>
<title>AutoDL论文解读(五):可微分方法的NAS</title>
<link href="http://yoursite.com/2019/10/15/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E5%8F%AF%E5%BE%AE%E5%88%86%E6%96%B9%E6%B3%95%E7%9A%84NAS/"/>
<id>http://yoursite.com/2019/10/15/AutoDL%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB%EF%BC%88%E4%BA%94%EF%BC%89%EF%BC%9A%E5%8F%AF%E5%BE%AE%E5%88%86%E6%96%B9%E6%B3%95%E7%9A%84NAS/</id>
<published>2019-10-15T12:40:44.000Z</published>
<updated>2020-08-03T02:54:44.000Z</updated>
<content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" class="aplayer-secondary-script-marker"></script><div class="note note-success"> <p>自动化机器学习(AutoML)最近变得越来越火,是机器学习下个发展方向之一。其中的神经网络结构搜索(NAS)是其中重要的技术之一。人工设计网络需要丰富的经验和专业知识,神经网络有众多的超参数,导致其搜索空间巨大。NAS即是在此巨大的搜索空间里自动地找到最优的网络结构,实现深度学习的自动化。自2017年谷歌与MIT各自在ICLR上各自发表基于强化学习的NAS以来,已产出200多篇论文,仅2019年上半年就有100多篇论文。此系列文章将解读AutoDL领域的经典论文与方法,笔者也是刚接触这个领域,有理解错误的地方还请批评指正!</p><p><strong>此系列的文章列表:</strong></p><ul><li><a href="https://5663015.github.io/2019/09/30/AutoDL论文解读(一):基于强化学习的开创性工作/" target="_blank" rel="noopener">AutoDL论文解读(一):基于强化学习的开创性工作</a></li><li><a href="https://5663015.github.io/2019/10/08/AutoDL论文解读(二):基于遗传算法的典型工作/" target="_blank" rel="noopener">AutoDL论文解读(二):基于遗传算法的典型方法</a></li><li><a href="https://5663015.github.io/2019/10/10/AutoDL论文解读(三):基于层或块的搜索/" target="_blank" rel="noopener">AutoDL论文解读(三):基于块搜索的NAS</a></li><li><a href="https://5663015.github.io/2019/10/12/AutoDL论文解读(四):权值共享的搜索/" target="_blank" rel="noopener">AutoDL论文解读(四):权值共享的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(五):可微分方法的NAS/" target="_blank" rel="noopener">AutoDL论文解读(五):可微分方法的NAS</a></li><li><a href="https://5663015.github.io/2019/10/15/AutoDL论文解读(六):基于代理模型的NAS/" target="_blank" rel="noopener">AutoDL论文解读(六):基于代理模型的NAS</a></li><li><a href="https://5663015.github.io/2019/10/21/AutoDL论文解读(七):基于one-shot的NAS/" target="_blank" rel="noopener">AutoDL论文解读(七):基于one-shot的NAS</a></li></ul> </div><p>此篇博文介绍CMU的《DARTS:Differentiable Architecture Search》。之前介绍的NAS方法搜索空间都是离散的,而可微分方法将搜索空间松弛化使其变成连续的,则可以使用梯度的方法来解决。</p><h1 id="一、DARTS-Differentiable-Architecture-Search"><a href="#一、DARTS-Differentiable-Architecture-Search" class="headerlink" title="一、DARTS:Differentiable Architecture Search"></a><strong>一、DARTS:Differentiable Architecture Search</strong></h1><h2 id="1、搜索空间"><a href="#1、搜索空间" class="headerlink" title="1、搜索空间"></a><strong>1、搜索空间</strong></h2><p>DARTS也是搜索卷积cell然后堆叠cell形成最终的网络。这里的cell是一个包含有向无环图,包含一个有$N$个节点的有序序列。每个节点$x^{(i)}$是一个隐含表示(比如特征图),每个有向的边$(i,j)$是变换$x^{(i)}$的操作$o^{(i,j)}$。作者假设cell有两个输入加点和一个输出节点,cell的输入输出设置和《Learning Transferable Architectures for Scalable Image Recognition》里的一致。每个中间节点是它所有的前驱节点计算得到:</p><script type="math/tex; mode=display">x^{(i)}=\sum_{j<i}o^{(i,j)}(x^{(j)})</script><p>一个特殊的操作:$zero$,包括在可能的操作集合里,表示两个节点之间没有连接。学习cell结构的任务就转换成了学习边上的操作。</p><h2 id="2、松散化和优化"><a href="#2、松散化和优化" class="headerlink" title="2、松散化和优化"></a><strong>2、松散化和优化</strong></h2><p>令$\mathcal O$为可选操作的集合(比如卷积、最大池化、$zero$),每个操作表示作用在$x^{(i)}$上的函数$o(\cdot)$。为了使搜索空间连续化,作者将特定操作的选择松弛化为在所有可能操作上的softmax:</p><script type="math/tex; mode=display">\bar{o}^{(i, j)}(x)=\sum_{o \in \mathcal{O}} \frac{\exp \left(\alpha_{o}^{(i, j)}\right)}{\sum_{o^{\prime} \in \mathcal{O}} \exp \left(\alpha_{o^{\prime}}^{(i, j)}\right)} o(x)</script><p>一对节点$(i,j)$之间的操作被一个$|\mathcal O|$维向量$\alpha^{(i,j)}$参数化。松弛化之后,搜索任务就变成了学习一组连续的变量${\alpha} = \{\alpha^{(i,j)} \}$,如下图所示:<br><img src="https://img-blog.csdnimg.cn/20191014212110262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>图(a)表示初始化的边,操作是未知的。图(b)通过在每条边放置混合的候选操作来松弛搜索空间,每个颜色的线表示不同的操作。图(c)是通过解决一个优化问题,联合训练候选操作的概率和网络的权重,不同的粗细表示了$\alpha^{(i,j)}$的大小。图(d)是最终学习到的结构。</p><p>学习到了所有操作的可能性$\bar{o}^{(i, j)}$后,选择其中最优可能的操作,也就是$o^{(i,j)}={\rm argmax}_{o \in {\mathcal O}}\alpha_{o}^{(i,j)}$。接下来,我们都用$\alpha$表示结构。</p><p>松弛化之后,我们的目标就是共同地学习结构$\alpha$和权重$w$,DARTS的目标是用梯度下降优化验证集损失。令$\mathcal L_{train}$和$\mathcal L_{val}$分别表示训练和验证损失,我们就是要找到一个最优的$\alpha^{\ast}$最小化验证集损失$\mathcal L_{val}(w^{\ast},\alpha^{\ast})$,其中$w^{\ast}$通过最小化训练集损失$\mathcal L_{train}(w,\alpha^{\ast})$。这是一个双层优化问题(bilevel opyimization problem),$\alpha$是上层变量,$w$是下层变量:</p><script type="math/tex; mode=display">\begin{align}\begin{array}{cl}{\min _{\alpha}} & {\mathcal{L}_{v a l}\left(w^{*}(\alpha), \alpha\right)} \tag1\\ {\text { s.t. }} & {w^{*}(\alpha)=\operatorname{argmin}_{w} \mathcal{L}_{\text {train}}(w, \alpha)}\end{array} \end{align}</script><h2 id="3、近似迭代求解优化问题"><a href="#3、近似迭代求解优化问题" class="headerlink" title="3、近似迭代求解优化问题"></a><strong>3、近似迭代求解优化问题</strong></h2><p>解决上面的双层优化问题是困难的,因为任何$\alpha$的改变都会要求重新计算$w^{\ast}(\alpha)$。因此作者提出了一种近似的迭代解法,用梯度下降在权重空间和结构空间中轮流地优化$w$和$\alpha$:<br><img src="https://img-blog.csdnimg.cn/20191014220549165.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQxNTc2MzI=,size_16,color_FFFFFF,t_70" srcset="/img/loading.gif" alt="在这里插入图片描述"><br>在第k步,给定当前结构$\alpha_{k-1}$,我们通过在最小化${\mathcal L}_{train}(w_{k-1},\alpha_{k-1})$的方向上移动$w_{k-1}$来获得$w_{k}$。然后固定$w_{k}$,用单步梯度下降最小化验证集损失,以此更新结构:</p><script type="math/tex; mode=display">\mathcal{L}_{v a l}\left(w_{k}-\xi \nabla_{w} \mathcal{L}_{t r a i n}\left(w_{k}, \alpha_{k-1}\right), \alpha_{k-1}\right) \tag2</script><p>其中$\xi$是学习率。通过求用式(2)关于$\alpha$的导数得到结构梯度(为了简介,省略了角标k):</p><script type="math/tex; mode=display">\nabla_{\alpha} \mathcal{L}_{v a l}\left(w^{\prime}, \alpha\right)-\xi \nabla_{\alpha, w}^{2} \mathcal{L}_{t r a i n}(w, \alpha) \nabla_{w^{\prime}} \mathcal{L}_{v a l}\left(w^{\prime}, \alpha\right) \tag3</script><p>其中$w^{\prime}=w-\xi \nabla_{w} \mathcal{L}_{t r a i n}(w, \alpha)$。上式第二项包含了矩阵向量乘积,难以计算。不过使用有限差分近似可以大大降低复杂度,令$\epsilon$是一个很小的数,$w^{+}=w+\epsilon \nabla_{w^{\prime}} \mathcal{L}_{v a l}\left(w^{\prime}, \alpha\right)$ ,$w^{-}=w-\epsilon \nabla_{w^{\prime}} \mathcal{L}_{v a l}\left(w^{\prime}, \alpha\right)$,那么:</p><script type="math/tex; mode=display">\nabla_{\alpha, w}^{2} \mathcal{L}_{t r a i n}(w, \alpha) \nabla_{w^{\prime}} \mathcal{L}_{v a l}\left(w^{\prime}, \alpha\right) \approx \frac{\nabla_{\alpha} \mathcal{L}_{t r a i n}\left(w^{+}, \alpha\right)-\nabla_{\alpha} \mathcal{L}_{t r a i n}\left(w^{-}, \alpha\right)}{2 \epsilon} \tag4</script><p>当$\epsilon=0$时,式(3)的二阶导数消失,结构梯度仅由$\nabla_{\alpha} \mathcal{L}_{v a l}(w, \alpha)$提供,通过假设$\alpha$和$w$相互独立来启发式地最优化验证集损失。这会加速计算,但通过实验发现效果并不好。因此要选择一个合适的$\epsilon$值。作者称$\epsilon=0$的情况为一阶近似,$\epsilon>0$为二阶近似</p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a><strong>参考文献</strong></h1><p>[1] Liu, Hanxiao, Karen Simonyan, and Yiming Yang. “Darts: Differentiable architecture search.” arXiv preprint arXiv:1806.09055 (2018).<br>[2] 《深度理解AutoML和AutoDL》</p>]]></content>
<summary type="html">
<link rel="stylesheet" class="aplayer-secondary-style-marker" href="\assets\css\APlayer.min.css"><script src="\assets\js\APlayer.min.js" cla
</summary>
<category term="原创教程" scheme="http://yoursite.com/categories/%E5%8E%9F%E5%88%9B%E6%95%99%E7%A8%8B/"/>
<category term="深度学习" scheme="http://yoursite.com/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"/>
<category term="自动化" scheme="http://yoursite.com/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
<category term="NAS" scheme="http://yoursite.com/tags/NAS/"/>
<category term="AutoDL" scheme="http://yoursite.com/tags/AutoDL/"/>
</entry>
</feed>