-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
2916 lines (1404 loc) · 837 KB
/
search.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
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>goroutine多线程交替打印数字</title>
<link href="/2023/09/17/wg/"/>
<url>/2023/09/17/wg/</url>
<content type="html"><![CDATA[<h1 id="goroutine多线程交替打印数字"><a href="#goroutine多线程交替打印数字" class="headerlink" title="goroutine多线程交替打印数字"></a>goroutine多线程交替打印数字</h1><pre><code>// 声明两个通道用于同步信号var ch1 = make(chan int, 0)var ch2 = make(chan int, 0)// 定义index记录当前打印到哪个数字了var index = 1func main() { // 创建计数器 wg := sync.WaitGroup{} go func() { //每创建一个goroutine,需要增加一个计数 wg.Add(1) for { <-ch1 fmt.Println("第一个goroutine打印:", index) index++ ch2 <- 1 } }() go func() { for { <-ch2 fmt.Println("第二个goroutine打印:", index) index++ if index >= 100 { // 说明打印完成,需要减少一个计数 wg.Done() return } ch1 <- 1 } }() // 开始,触发 ch1 <- 1 // 结束 wg.Wait()}</code></pre>]]></content>
<categories>
<category> 算法 </category>
</categories>
<tags>
<tag> goroutine </tag>
<tag> channel </tag>
</tags>
</entry>
<entry>
<title>前序和后序遍历二叉树(非递归)</title>
<link href="/2023/09/17/preorderertravers/"/>
<url>/2023/09/17/preorderertravers/</url>
<content type="html"><![CDATA[<h1 id="前序遍历二叉树(非递归)"><a href="#前序遍历二叉树(非递归)" class="headerlink" title="前序遍历二叉树(非递归)"></a>前序遍历二叉树(非递归)</h1><pre><code>// 前序遍历二叉树func PreOrderTraverse(root *TreeNode) { // 判空 if root == nil { return } // 设置一个p指针用于遍历 p := new(TreeNode) // 设置一个栈用于存放节点 stack := make([]*TreeNode, 0) // 开始遍历 // 只要p指针和栈有一个不为空 for p != nil || len(stack) > 0 { // 如果p非空,就先把节点压入栈,然后向左遍历 if p != nil { fmt.Print(p.val) stack = append(stack, p) p = p.left } else { // p为空,说明到左边到底了,此时从栈顶弹出一个元素,然后遍历右边 p = stack[len(stack)-1] stack = stack[:len(stack)-1] p = p.right } }}</code></pre><h1 id="中序遍历二叉树(非递归)"><a href="#中序遍历二叉树(非递归)" class="headerlink" title="中序遍历二叉树(非递归)"></a>中序遍历二叉树(非递归)</h1><pre><code>// 中序遍历二叉树func InOrderTraverse(root *TreeNode) { // 判空 if root == nil { return } // 设置一个p指针用于遍历 p := new(TreeNode) // 设置一个栈用于存放节点 stack := make([]*TreeNode, 0) // 开始遍历 // 只要p指针和栈有一个不为空 for p != nil || len(stack) > 0 { // 如果p非空,就先把节点压入栈,然后向左遍历 if p != nil { stack = append(stack, p) p = p.left } else { // p为空,说明到左边到底了,此时从栈顶弹出一个元素,然后遍历右边 p = stack[len(stack)-1] stack = stack[:len(stack)-1] fmt.Print(p.val) p = p.right } }}</code></pre>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> tree </tag>
<tag> 栈 </tag>
<tag> 二叉树 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-990】等式方程的可满足性</title>
<link href="/2021/07/18/leetcode-990/"/>
<url>/2021/07/18/leetcode-990/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/satisfiability-of-equality-equations/" target="_blank" rel="noopener">990. 等式方程的可满足性</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:”a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。</p><p>只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。</p><p>示例 1:</p><pre><code>输入:["a==b","b!=a"]输出:false解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。</code></pre><p>示例 2:</p><pre><code>输入:["b==a","a==b"]输出:true解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。</code></pre><p>示例 3:</p><pre><code>输入:["a==b","b==c","a==c"]输出:true</code></pre><p>示例 4:</p><pre><code>输入:["a==b","b!=c","c==a"]输出:false</code></pre><p>示例 5:</p><pre><code>输入:["c==c","b==d","x!=z"]输出:true</code></pre><p>提示:</p><ul><li>1 <= equations.length <= 500</li><li>equations[i].length == 4</li><li>equations[i][0] 和 equations[i][3] 是小写字母</li><li>equations[i][1] 要么是 ‘=’,要么是 ‘!’</li><li>equations[i][2] 是 ‘=’</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>动态连通性其实就是一种等价关系,具有“自反性”,“传递性”和对称性,其实==也是一种等价关系,具有这些性质,所以这个问题可以用Union-Find算法,也就是并查集解决。</p><p>核心思想是,将equations中的算式根据==和!=分成两部分,先处理==算式,使得他们通过相等关系互相连通,然后处理!=关系,检查不等关系是否破坏了相等关系的连通性。</p><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func equationsPossible(equations []string) bool { uf := new(unionFind) // 最多有26哥英文字母,也就是26个互不连通的节点 uf.UnionFind(26 + 'a') // 先让相等的字母形成连通分量 for _, eq := range equations { if eq[1] == '=' { uf.union(int(eq[0]), int(eq[3])) } } // 检查不等的关系是否打破相等关系的连通性 for _, eq := range equations { if eq[1] == '!' { // 如果连通,说明逻辑冲突 if uf.connected(int(eq[0]), int(eq[3])) { return false } } } return true}type unionFind struct { // 连通分量的个数 count int // 存储每个节点的父节点 parent []int // 记录每棵树的重量 size []int}// 构造函数,n为图的节点总数func (uf *unionFind) UnionFind(n int) { uf.count = n // 初始化parent数组 uf.parent = make([]int, n) // 初始化size数组 uf.size = make([]int, n) // 初始状态下,每个节点互不连通,父节点指针指向自身,重量为i for i := 0; i < n; i++ { uf.parent[i] = i uf.size[i] = i }}// 将p和q连接func (uf *unionFind) union (p, q int) { // 先找到各自的根节点 rootP := uf.find(p) rootQ := uf.find(q) if rootP == rootQ { return } // 小树接到大树下面,较平衡,parent数组与size数组同步更新 if uf.size[rootP] > uf.size[rootQ] { uf.parent[rootQ] = rootP uf.size[rootP] += uf.size[rootQ] } else { uf.parent[rootP] = rootQ uf.size[rootQ] += uf.size[rootP] } // 连通分量的数量-1 uf.count--}// 找当前节点的根节点func (uf *unionFind) find(n int) int { for uf.parent[n] != n { // 进行路径压缩 uf.parent[n] = uf.parent[uf.parent[n]] // 往父节点寻找 n = uf.parent[n] } return n}// 判断p和q是否连通,即根节点是否是同一个func (uf *unionFind) connected(p, q int) bool { rootP := uf.find(p) rootQ := uf.find(q) return rootP == rootQ}</code></pre><h3 id="时间复杂度:O-n-ClogC"><a href="#时间复杂度:O-n-ClogC" class="headerlink" title="时间复杂度:O((n+ClogC))"></a>时间复杂度:O((n+ClogC))</h3><p>其中 n 是 equations 中的方程数量,C 是变量的总数,在本题中变量都是小写字母,C≤26。上面的并查集代码中使用了路径压缩优化,对于每个方程的合并和查找的均摊时间复杂度都是 O((logC))。由于需要遍历每个方程,因此总时间复杂度是 O((n+Clog C))。</p><h3 id="空间复杂度:O-C-。"><a href="#空间复杂度:O-C-。" class="headerlink" title="空间复杂度:O((C))。"></a>空间复杂度:O((C))。</h3><p>创建一个数组 parent 存储每个变量的连通分量信息,由于变量都是小写字母,因此 parent 是长度为 C。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>Union Find 算法,也就是并查集算法其实是DFS算法的延伸,使用并查集算法的关键在于如何把原问题转化为图的动态连通性问题,比如本题的算式合法性问题就可以直接利用等价关系。</p><h3 id="本题的注意点"><a href="#本题的注意点" class="headerlink" title="本题的注意点"></a>本题的注意点</h3><ol><li>并查集的数据结构</li><li>初始化并查集</li><li>连接两个节点</li><li>判断两个节点是否相连</li><li>相等和不等关系与连通性的转化</li></ol><blockquote><p>值得注意的是,在本题中size数组似乎没啥用,去掉也可以</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 并查集 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-20】有效的括号</title>
<link href="/2021/07/17/leetcode-20/"/>
<url>/2021/07/17/leetcode-20/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/valid-parentheses/" target="_blank" rel="noopener">20. 有效的括号</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。</p><p>有效字符串需满足:</p><p>左括号必须用相同类型的右括号闭合。<br>左括号必须以正确的顺序闭合。</p><p>示例 1:</p><pre><code>输入:s = "()"输出:true</code></pre><p>示例 2:</p><pre><code>输入:s = "()[]{}"输出:true</code></pre><p>示例 3:</p><pre><code>输入:s = "(]"输出:false</code></pre><p>示例 4:</p><pre><code>输入:s = "([)]"输出:false</code></pre><p>示例 5:</p><pre><code>输入:s = "{[]}"输出:true</code></pre><p>提示:</p><ul><li>1 <= s.length <= 104</li><li>s 仅由括号 ‘()[]{}’ 组成</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>我们先来设想一下一种最简单的情况,就是只有一种类型的括号,比如“(”,我们该如何判定合法性,很显然,对于每一个右括号的左边必须有一个左括号和它匹配。</p><p>那么根据这个思路,我们就可以写出算法:</p><pre><code>func isValid(s string) bool { // 待匹配的括号数量 left := 0 for range _, c := s { // 遇到左括号 if c == '(' { left++ // 遇到右括号 } else { left-- } if left < 0 { return false } } return left == 0}</code></pre><p>如果只有一种括号。就可以采用这种思路,但是如果是三种括号了就不行了。因为如果简单地用三个变量,多几个if else分支的话,会遇到这种情况 [(]) 显然是不能简单地用左右括号的个数进行判定,那么该怎么办呢?<br>很简单,遇到括号合法性的问题应该首先想到能不能用栈的思路进行解决。</p><p>我们这道题就可以用left的栈代替之前的那个left变量,遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配。</p><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func isValid(s string) bool { // 用一个栈存储所有的左括号 left := make([]byte, 0) // 遍历字符串 for _, c := range s { // 如果是左括号就直接入栈 if c == '(' || c == '[' || c == '{' { left = append(left, byte(c)) // 如果有右括号就和栈顶元素做匹配 } else { // 如果刚好配对,就弹出栈顶元素 if len(left) > 0 && left[len(left)-1] == leftOf(byte(c)) { left = left[:len(left)-1] } else { // 如果不配对,则直接返回false return false } } } // 最后检查一下是否所有的左括号都匹配了 return len(left) == 0}// 返回对应的左括号func leftOf(c byte) byte { switch c { case ')': return '(' case ']': return '[' default: return '{' }}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>n是字符串的长度</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>栈的深度最多为字符串的字符个数,也是O((n))</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>判定括号的合法性是一个非常实用的问题,而栈在处理这类问题时尤其有用。</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leecode </tag>
<tag> 栈 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-452】用最少数量的箭引爆气球</title>
<link href="/2021/07/15/leetcode-452/"/>
<url>/2021/07/15/leetcode-452/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/" target="_blank" rel="noopener">452. 用最少数量的箭引爆气球</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。</p><p>一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。</p><p>给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。</p><p>示例 1:</p><pre><code>输入:points = [[10,16],[2,8],[1,6],[7,12]]输出:2解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球</code></pre><p>示例 2:</p><pre><code>输入:points = [[1,2],[3,4],[5,6],[7,8]]输出:4</code></pre><p>示例 3:</p><pre><code>输入:points = [[1,2],[2,3],[3,4],[4,5]]输出:2</code></pre><p>示例 4:</p><pre><code>输入:points = [[1,2]]输出:1</code></pre><p>示例 5:</p><pre><code>输入:points = [[2,3],[2,3]]输出:1</code></pre><p>提示:</p><ul><li>1 <= points.length <= 104<br>points[i].length == 2</li><li>-231 <= xstart < xend <= 231 - 1</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题可以做一个转化,因为一个箭头可以将边界互相重叠的所有气球射穿,那么要保证射穿所有的气球就是求最多有多少个不重叠的区间。</p><p>然后就可以直接用<a href="https://leetcode-cn.com/problems/non-overlapping-intervals/" target="_blank" rel="noopener">435. 无重叠区间</a>中的方法,只是需要做一点微小的改动。因为在<a href="https://leetcode-cn.com/problems/non-overlapping-intervals/" target="_blank" rel="noopener">435. 无重叠区间</a>中如果两个边界触碰,不算重叠,而本题中箭头碰到气球的边界也会爆炸,相当于边界触碰也算重叠。</p><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func findMinArrowShots(points [][]int) int { if len(points) == 0 { return 0 } // 按照end(结束时间)升序排序 sort.Slice(points, func(i, j int) bool { return points[i][1] < points[j][1] }) // 至少有一个区间不相交 count := 1 // 排序后,第一个区间都是end最小的区间,记录下end minEnd := points[0][1] for _, interval := range points { start := interval[0] // 注意这里是大于,因为箭头碰到气球的边界也会爆炸,相当于边界触碰也算重叠 if start > minEnd { count++ // 新的区间的end将作为新的minEnd,继续寻找这个区间的不相交区间 minEnd = interval[1] } } return count}</code></pre><h3 id="时间复杂度:O-NlogN"><a href="#时间复杂度:O-NlogN" class="headerlink" title="时间复杂度:O((NlogN))"></a>时间复杂度:O((NlogN))</h3><p>其中 n 是数组points的数量。我们需要 O((nlogn)) 的时间对所有的气球按照右端点进行升序排序(其实就是快速排序),并且需要 O((n)) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O((nlogn))。</p><h3 id="空间复杂度:O-logN"><a href="#空间复杂度:O-logN" class="headerlink" title="空间复杂度:O((logN))"></a>空间复杂度:O((logN))</h3><p>O((logN)) 即为排序需要使用的栈空间</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>只有具备贪心选择性质的问题才可以用贪心算法,贪心选择性质就是每一步都做出一个局部最优的选择,最终的结果就是全局最优。这是一种特殊性质,实际上只有一部分问题具有这个性质。</p><p>这道题基本就是<a href="https://leetcode-cn.com/problems/non-overlapping-intervals/" target="_blank" rel="noopener">435. 无重叠区间</a>的变体,只是做了一个转化,求的是互不重叠区间集合个数了。</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 贪心算法 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-435】无重叠区间</title>
<link href="/2021/07/14/leetcode-435/"/>
<url>/2021/07/14/leetcode-435/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/non-overlapping-intervals/" target="_blank" rel="noopener">435. 无重叠区间</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。</p><p>注意:</p><p>可以认为区间的终点总是大于它的起点。<br>区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。</p><p>示例 1:</p><pre><code>输入: [ [1,2], [2,3], [3,4], [1,3] ]输出: 1解释: 移除 [1,3] 后,剩下的区间没有重叠。</code></pre><p>示例 2:</p><pre><code>输入: [ [1,2], [1,2], [1,2] ]输出: 2解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。</code></pre><p>示例 3:</p><pre><code>输入: [ [1,2], [2,3] ]输出: 0解释: 你不需要移除任何区间,因为它们已经是无重叠的了。</code></pre><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题可以先做一个转化,即求这些区间中最多有几个不相交的区间。思路如下:</p><ol><li>从区间集合intvs中选择一个区间x,这个区间是在当前所有区间中结束最早的(end最小)。</li><li>把所有区间与x区间相交的区间集合intvs中删除</li><li>重复步骤1和步骤2,直到invts为空。在这个过程中选出的的x就是最大的互不相交的区间子集。</li><li>最后总的区间数减去这个不相交的区间子集个数就是我们要的答案了。</li></ol><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func eraseOverlapIntervals(intervals [][]int) int { // 总的区间个数减去不相交的区间个数就是需要去除的区间 return len(intervals) - intervalSchedule(intervals)}// 计算最多有几个不重复的区间func intervalSchedule(intvs [][]int) int { if len(intvs) == 0 { return 0 } // 按照end(结束时间)升序排序 sort.Slice(intvs, func(i, j int) bool { return intvs[i][1] < intvs[j][1] }) // 至少有一个区间不相交 count := 1 // 排序后,第一个区间都是end最小的区间,记录下end minEnd := intvs[0][1] for _, interval := range intvs { start := interval[0] // start大于等于end,说明找到第一个不相交的区间了,因为边界相同也算是不相交 if start >= minEnd { count++ // 新的区间的end将作为新的minEnd,继续寻找这个区间的不相交区间 minEnd = interval[1] } } return count}</code></pre><h3 id="时间复杂度:O-NlogN"><a href="#时间复杂度:O-NlogN" class="headerlink" title="时间复杂度:O((NlogN))"></a>时间复杂度:O((NlogN))</h3><p>其中 n 是区间的数量。我们需要 O((nlogn)) 的时间对所有的区间按照右端点进行升序排序(其实就是快速排序),并且需要 O((n)) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O((nlogn))。</p><h3 id="空间复杂度:O-logN"><a href="#空间复杂度:O-logN" class="headerlink" title="空间复杂度:O((logN))"></a>空间复杂度:O((logN))</h3><p>O((logN)) 即为排序需要使用的栈空间</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>只有具备贪心选择性质的问题才可以用贪心算法,贪心选择性质就是每一步都做出一个局部最优的选择,最终的结果就是全局最优。这是一种特殊性质,实际上只有一部分问题具有这个性质。</p><h4 id="注意点"><a href="#注意点" class="headerlink" title="注意点"></a>注意点</h4><p>计算不重叠区间个数有固定的套路:</p><ol><li>按照右边界大小排序</li><li>设置最小右边界minEnd</li><li>遍历比较左右边界,计数,更新minEnd</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 贪心算法 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-45】跳跃游戏 II</title>
<link href="/2021/07/10/leetcode-45/"/>
<url>/2021/07/10/leetcode-45/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/jump-game-ii/" target="_blank" rel="noopener">45. 跳跃游戏 II</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个非负整数数组,你最初位于数组的第一个位置。</p><p>数组中的每个元素代表你在该位置可以跳跃的最大长度。</p><p>你的目标是使用最少的跳跃次数到达数组的最后一个位置。</p><p>假设你总是可以到达数组的最后一个位置。</p><p>示例 1:</p><pre><code>输入: [2,3,1,1,4]输出: 2解释: 跳到最后一个位置的最小跳跃数是 2。 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。</code></pre><p>示例 2:</p><pre><code>输入: [2,3,0,1,4]输出: 2</code></pre><p>提示:</p><ul><li>1 <= nums.length <= 1000</li><li>0 <= nums[i] <= 105</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题是<a href="https://leetcode-cn.com/problems/jump-game/" target="_blank" rel="noopener">55. 跳跃游戏</a>的进阶版。场景是一样,只不过问题换了一下,这次是保证你可以到达最后一个位置,求跳过最后一个位置所需要的最少跳跃数。碰到这种求最少次数的第一反应肯定是动态规划,但是如果真的用动态规划会发现时间复杂度是O((n^2)),因为动态规划需要递归。</p><p>为了减少时间复杂度,不用递归,我们还是选择贪心算法,之所以可以用贪心算法解决,是因为本题具有贪心选择性质,那什么是贪心选择性质呢?</p><h5 id="贪心选择性质就是不需要递归的计算出所有选择的具体结果然后比较求最值,而只需做出那个最有潜力、看起来最优的选择即可。"><a href="#贪心选择性质就是不需要递归的计算出所有选择的具体结果然后比较求最值,而只需做出那个最有潜力、看起来最优的选择即可。" class="headerlink" title="贪心选择性质就是不需要递归的计算出所有选择的具体结果然后比较求最值,而只需做出那个最有潜力、看起来最优的选择即可。"></a>贪心选择性质就是不需要递归的计算出所有选择的具体结果然后比较求最值,而只需做出那个最有潜力、看起来最优的选择即可。</h5><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func jump(nums []int) int { n := len(nums) // 站在索引i,最多能跳到索引end end := 0 // 从索引[i...end]起跳,最远能跳到的距离 farthest := 0 // 记录跳跃次数 jumps := 0 // 对于每一个位置都计算一下能跳到的最远距离 for i := 0; i < n - 1; i++ { farthest = max(nums[i] + i, farthest) // 说明从索引[i...end]起跳,所有能跳到的距离已经计算完毕,其中最远的就是farthest if end == i { // 往前跳一步,i直接跳到end+1,然后选择这个farthest作为新的可选最远边界end // 相当于i更新为end+1,end更新为farthest jumps++ end = farthest } } return jumps}func max(a, b int) int { if a > b { return a } return b}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>其中 n 为数组的大小。只需要访问 nums 数组一遍,共 n 个位置。</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><p>不需要额外的开销。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><h2 id="注意点"><a href="#注意点" class="headerlink" title="注意点"></a>注意点</h2><p>本题代码很简洁,但是思路上其实并不好理解,可能最好还是要手动模拟一下~</p><p>本题仍然使用贪心算法,和<a href="https://leetcode-cn.com/problems/jump-game/" target="_blank" rel="noopener">55. 跳跃游戏</a>最大的不同可能就是在于本题并不是求最远能跳到的距离,而是跳到最后一个位置所用的最小步数,这就意味着并不是每次跳的越远越好,而是每次跳完之后,下一跳可选的范围越大越好,这样就能覆盖更多的距离,反向推导就是同样到达最后一个点所用的步数更少.</p><blockquote><p>值得注意的是这里循环最多只遍历到n-2是因为n-1已经是最后一个位置了,不需要再跳了,而n-2才需要考虑能不能跳到n-1</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 贪心算法 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-55】跳跃游戏 I</title>
<link href="/2021/07/10/leetcode-55/"/>
<url>/2021/07/10/leetcode-55/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/jump-game/" target="_blank" rel="noopener">55. 跳跃游戏</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个非负整数数组 nums ,你最初位于数组的第一个下标 。</p><p>数组中的每个元素代表你在该位置可以跳跃的最大长度。</p><p>判断你是否能够到达最后一个下标。</p><p>示例 1:</p><pre><code>输入:nums = [2,3,1,1,4]输出:true解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。</code></pre><p>示例 2:</p><pre><code>输入:nums = [3,2,1,0,4]输出:false解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。</code></pre><p>提示:</p><ul><li>1 <= nums.length <= 3 * 104</li><li>0 <= nums[i] <= 105</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>在审题的过程中很容易发现,每两次跳跃之间是有联系的,也就是说下一次跳跃的结果是依赖于上一次跳跃的状态的,并且我们在同一个位置可以有不同的选择,这些选择决定了跳跃后不同的状态。由此我们可以从以上分析中提炼出两个关键点:</p><ol><li>不同的选择决定了不同状态</li><li>相邻状态之间是有联系的</li></ol><p>至此我们就马上可以联想到动态规划。然后,有关动态规划的问题,大部分都是让你求最值的,但是本题却好像不是。这没关系,我们可以对题意做一个转换,就成求最值问题了。</p><p>能否跳到最后一个位置其实就是在跳的最远的情况下,能否越过最后一个位置。</p><p>然后我们就可以用动态规划的思路求解,即对于nums[0…n-1],计算当前能跳到的最远距离,最后得出能跳的最远的距离,这个巨苦可以越过n-1,就说明能够跳到最后一个位置。</p><p>具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func canJump(nums []int) bool { n := len(nums) // 能跳到的最远距离 farthest := 0 // 对于每一个位置都计算一下能跳到的最远距离 for i := 0; i < n - 1 ; i++ { // 计算当前能跳到的最远距离 farthest = max(farthest, i + nums[i]) // 当前最远也就能调到这里了,无法再往下跳了(可能碰到了0,卡住就跳不动了) if farthest <= i { return false } } // 最远的距离是否超过最后一个位置 return farthest >= n - 1}func max(a, b int) int { if a > b { return a } return b}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>其中 n 为数组的大小。只需要访问 nums 数组一遍,共 n 个位置。</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><p>不需要额外的开销。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>实际上我们可以看到本题用的是贪心算法,贪心算法的复杂度比动态规划更低,这是因为少了递归过程,只需要着眼于当下最优子结构的解决,局部最优解能决定全局最优解。</p><p>贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。</p><blockquote><p>如果到了最远距离刚好是最后一个位置也算达到,所以循环到n-1而不是n,这样就可以包含这种情况了,这个需要特别注意一下</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 贪心算法 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-5】最长回文子串</title>
<link href="/2021/07/04/leetcode-5/"/>
<url>/2021/07/04/leetcode-5/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/longest-palindromic-substring/" target="_blank" rel="noopener">5. 最长回文子串</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个字符串 s,找到 s 中最长的回文子串。</p><p>示例 1:</p><pre><code>输入:s = "babad"输出:"bab"解释:"aba" 同样是符合题意的答案。</code></pre><p>示例 2:</p><pre><code>输入:s = "cbbd"输出:"bb"</code></pre><p>示例 3:</p><pre><code>输入:s = "a"输出:"a"</code></pre><p>示例 4:</p><pre><code>输入:s = "ac"输出:"a"</code></pre><p>提示:</p><ul><li>1 <= s.length <= 1000</li><li>s 仅由数字和英文字母(大写和/或小写)组成</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>核心思想是:从左到右依次以s[i]为中心,向两边扩散来寻找回文串,并留下最长的那个。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func longestPalindrome(s string) string { res := "" // 从左到右依次以s[i]为中心,寻找回文串 for i := 0; i < len(s); i++ { // 寻找长度为奇数的回文子串 s1 := palindrome(s, i, i) // 寻找长度为偶数的回文串 s2 := palindrome(s, i, i+1) // 留下最长的那个 res = longestString(res, s1, s2) } return res}// 从s[l]和s[r]开始向两端扩散// 返回以s[l]和s[r]为中心的最长回文串func palindrome(s string, l, r int) string { // 防止索引越界 for l >= 0 && r < len(s) && s[l] == s[r] { l-- r++ } return s[l+1:r]}func longestString(s1, s2, s3 string) string { if len(s1) > len(s2) { if len(s1) > len(s3) { return s1 } else { return s3 } } else { if len(s2) < len(s3) { return s3 } else { return s2 } }}</code></pre><h3 id="时间复杂度:O-n-2"><a href="#时间复杂度:O-n-2" class="headerlink" title="时间复杂度:O((n^2))"></a>时间复杂度:O((n^2))</h3><p>其中 n 是字符串的长度。长度为 1 和 2 的回文中心分别有 n 和 n-1 个,每个回文中心最多会向外扩展 O((n)) 次。</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><p>维护两个指针即可,因此只需要使用常数的额外空间。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题的核心思想是:从中间开始向两边扩散来判断回文串。<br>值得注意的是在寻找回文串的时候可以采用双指针技巧,可以简化下标的推导过程。</p><h3 id="值得注意的是,无论在判断回文还是取长度都需要注意下标可能要-1或-1,否则容易出错"><a href="#值得注意的是,无论在判断回文还是取长度都需要注意下标可能要-1或-1,否则容易出错" class="headerlink" title="值得注意的是,无论在判断回文还是取长度都需要注意下标可能要-1或+1,否则容易出错"></a>值得注意的是,无论在判断回文还是取长度都需要注意下标可能要-1或+1,否则容易出错</h3>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> 双指针 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-26】删除有序数组中的重复项</title>
<link href="/2021/07/04/leetcode-26/"/>
<url>/2021/07/04/leetcode-26/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/" target="_blank" rel="noopener">26. 删除有序数组中的重复项</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。</p><p>不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O((1)) 额外空间的条件下完成。</p><p>说明:</p><p>为什么返回数值是整数,但输出的答案是数组呢?</p><p>请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。</p><p>你可以想象内部操作如下:</p><pre><code>// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝int len = removeDuplicates(nums);// 在函数里修改输入数组对于调用者是可见的。// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。for (int i = 0; i < len; i++) { print(nums[i]);}</code></pre><p>示例 1:</p><pre><code>输入:nums = [1,1,2]输出:2, nums = [1,2]解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。</code></pre><p>示例 2:</p><pre><code>输入:nums = [0,0,1,1,1,2,2,3,3,4]输出:5, nums = [0,1,2,3,4]解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。</code></pre><p>提示:</p><ul><li>0 <= nums.length <= 3 * 104</li><li>-104 <= nums[i] <= 104</li><li>nums 已按升序排列</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>对于数组相关的算法问题,有一个通用的技巧:尽量避免在中间删除元素,而是想办法把这个元素换到最后。再一个个弹出,这样每次操作的时间复杂度也就降到O((1))了。按照这个思路,又可以衍生出双指针技巧:快慢指针。</p><p>本题就是采用了快慢指针的思想,让慢指针slow走在后面,快指针fast走在前面探路,找到一个不重复的元素,就填充到slow的位置,然后slow向前移动一步,这样slow就永远指向第一个重复的元素(换言之,slow之前的都是不重复的元素)。如果fast遇到的是重复的元素,则直接跳过,继续往前走,直到再找到一个不重复的元素。这样,当fast指针遍历完整个数组后,nums[0..slow]保存的就是不重复元素,之后的所有元素都是重复元素。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func removeDuplicates(nums []int) int { n := len(nums) if n == 0 { return 0 } // 快指针走前,慢指针走后 slow, fast := 0, 1 // 快指针没有走到头 for fast < n { // 发现了一个新元素(不和之前重复的元素) if nums[fast] != nums[slow] { // slow永远指向第一个重复的元素 slow++ // 用新元素覆盖重复的元素 nums[slow] = nums[fast] } // 走到这里,说明元素重复了,直接跳过 fast++ } // 索引是从0开始的,所以长度=索引+1 return slow + 1}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><p>维护两个指针即可,因此只需要使用常数的额外空间</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>思路就是快慢指针,慢指针指向第一个重复元素,快指针指向第一个不重复元素,然后快指针替换慢指针</p><h4 id="值得注意的是慢指针不会像快指针一样不断移动,只有在每次替换的时候菜需要移动。"><a href="#值得注意的是慢指针不会像快指针一样不断移动,只有在每次替换的时候菜需要移动。" class="headerlink" title="值得注意的是慢指针不会像快指针一样不断移动,只有在每次替换的时候菜需要移动。"></a>值得注意的是慢指针不会像快指针一样不断移动,只有在每次替换的时候菜需要移动。</h4><blockquote><p>快慢指针的应用相当广泛,建议做完本题,还可以顺手去把 <a href="https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/" target="_blank" rel="noopener">83. 删除排序链表中的重复元素</a>做了,加深印象。</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> 双指针 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-42】接雨水</title>
<link href="/2021/07/04/leetcode-42/"/>
<url>/2021/07/04/leetcode-42/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/trapping-rain-water/" target="_blank" rel="noopener">42. 接雨水</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。</p><p><img src="https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/22/rainwatertrap.png" alt></p><p>示例 1:</p><pre><code>输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]输出:6解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 </code></pre><p>示例 2:</p><pre><code>输入:height = [4,2,0,3,2,5]输出:9</code></pre><p>提示:</p><ul><li>n == height.length</li><li>0 <= n <= 3 * 104</li><li>0 <= height[i] <= 105</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题难度较大,第一次看到这个问题时,读者可能毫无思路。但是我们先不要考虑总共能装多少水,只考虑对于某个位置i,能装下多少水。然后我们就可以很快得到位置i能装的水量之和它左右最高高度柱子的高度有关。两者中较低的那个减去当前位置的高度就是能装的最大水量(短板效应)。</p><p>然后我们就可以用伪代码表示位置i最多能装的水为:</p><pre><code>water[i] = min(# 左边最高的柱子max(height[0..i]),# 右边最高的柱子min(height[i..n-1])) - height[i]</code></pre><p>至此,我们就可以马上写出一个暴力算法:</p><pre><code>func trap(height []int) int { n := len(height) sum := 0 for(i := 1; i < n-1; i++) { l_max, r_max := 0, 0 // 找右边最高的柱子 for(j := i; j < n-1; j++) { r_max = max(r_max, height[j]) } // 找左边最高的柱子 for(j := 0; j < i; j++) { l_max = max(l_max, height[j]) } sum += min(l_max, r_max) - height[i] } return sum}</code></pre><p>整体思路就是对于每个位置i,求出它的左边柱子最高高度和右边柱子的最高高度,取两者较小值,然后再减去当前柱子高度值就是水量,最后把所有水量相加即可。</p><p>思路很好理解,但是这个算法有一个致命的缺陷,就是双层for循环,这就导致它的时间复杂度为O((n^2)),这显然不是我们可以接受的,因为对于每个位置都要遍历一遍求最大高度值,这显然是有很多重复的工作。</p><p>这里我们采用的优化策略就是双指针,不再每个都遍历了,而是边走边算当前位置左右两边的高度最值。这样只需要维护一个最值变量就好了。具体解法如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><h2 id="题解一"><a href="#题解一" class="headerlink" title="题解一"></a>题解一</h2><pre><code>// 常规版func trap(height []int) int { // 计算所有柱子能接水的总和,先要计算每个柱子能接多少水 // 每个柱子能接的水量 = min(左边最高柱子的高度, 右边最高柱子的高度)-当前柱子高度 // 依次遍历每个柱子,计算左右最高柱子高度 sum := 0 for i := 0; i < len(height); i++ { // 计算左边最高柱子高度 leftMax := 0 for j := 0; j < i; j++ { if leftMax < height[j] { leftMax = height[j] } } // 计算右边最高柱子高度 rightMax := 0 for k := i+1; k < len(height); k++ { if rightMax < height[k] { rightMax = height[k] } } // 计算当前柱子能接的水量 = min(左边最高柱子的高度, 右边最高柱子的高度)-当前柱子高度 // 注意水量不能为负值,所以这里要加一层判断 water := min(leftMax, rightMax) - height[i] if water > 0 { sum += water } } return sum}</code></pre><h2 id="题解二"><a href="#题解二" class="headerlink" title="题解二"></a>题解二</h2><pre><code>// 改进版(双指针)func trap(height []int) int { // 特例判断 n := len(height) if n == 0 { return 0 } // 初始化最后的结果 sum := 0 // 初始化左右指针 left, right := 0, n-1 // 初始化左右最大值 // l_max 代表 height[0..left] 中(左指针以左)最高柱子的高度 // r_max 代表 height[right..n-1] 中(右指针以右)最高柱子的高度 leftMax, rightMax := 0, 0 // 两边逐渐向中间移动 for left < right { // 边走边更新左右最大值 leftMax = max(leftMax, height[left]) rightMax = max(rightMax, height[right]) // 对于左边柱子来说,只需要知道他的左侧高度最大值leftMax,然后右边存在比他高的(并不需要知道右边所有的柱子高度) // 这样就能保证左右高度最大值之间的较小值一定落在左侧,也就是左侧高度最大值 // 而左侧高度最大值刚好是我们已知的leftMax, 这样就可以根据公式直接计算了 if leftMax < rightMax { sum += leftMax - height[left] // 当前左边柱子计算完了,计算下一个左边柱子 left++ } else { // 右边与左边类似,反之亦然 sum += rightMax - height[right] right-- } } return sum}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>其中 n 是数组 height 的长度。两个指针的移动总次数不超过 n</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><p>维护两个指针即可,因此只需要使用常数的额外空间</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题的第一种暴力解法应当是首先想到的方法,其本质思想就是:</p><ol><li>首先计算所有柱子能接水的总和,先要计算每个柱子能接多少水</li><li>然后有公式:<pre><code>每个柱子能接的水量 = min(左边最高柱子的高度, 右边最高柱子的高度)-当前柱子高度</code></pre></li><li>最后依次遍历每个柱子,计算左右最高柱子高度,根据公式计算水量,求和</li></ol><p>但是这种思路有一个致命缺陷,就是双循环,那么如何去掉这个双重循环呢,实际上双重循环出现的原因是因为我们每次都要计算左右所有高度,并求最大高度,实际上没必要。我们重新审视一下公式:</p><pre><code>每个柱子能接的水量 = min(左边最高柱子的高度, 右边最高柱子的高度)-当前柱子高度</code></pre><h5 id="根据公式,我们并不需要知道左右所有柱子高度,只需要知道其中一边的最高高度,然后保证这个高度比另一边最高高度小就可以了。"><a href="#根据公式,我们并不需要知道左右所有柱子高度,只需要知道其中一边的最高高度,然后保证这个高度比另一边最高高度小就可以了。" class="headerlink" title="根据公式,我们并不需要知道左右所有柱子高度,只需要知道其中一边的最高高度,然后保证这个高度比另一边最高高度小就可以了。"></a>根据公式,我们并不需要知道左右所有柱子高度,只需要知道其中一边的最高高度,然后保证这个高度比另一边最高高度小就可以了。</h5><p>而第二种思路就是这样做的,这里我们利用了双指针。以左指针left举例:</p><ol><li>对于左边柱子来说,只需要知道他的左侧高度最大值leftMax</li><li>然后右边存在比他高的(并不需要知道右边所有的柱子高度),这样就能保证左右高度最大值之间的较小值一定落在左侧,也就是左侧高度最大值</li><li>而左侧高度最大值刚好是我们已知的leftMax, 这样就可以根据公式直接计算了,右边反之亦然。</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> 双指针 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-1011】在 D 天内送达包裹的能力</title>
<link href="/2021/07/03/leetcode-1011/"/>
<url>/2021/07/03/leetcode-1011/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/" target="_blank" rel="noopener">1011. 在 D 天内送达包裹的能力</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。</p><p>传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。</p><p>返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。</p><p>示例 1:</p><pre><code>输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5输出:15解释:船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:第 1 天:1, 2, 3, 4, 5第 2 天:6, 7第 3 天:8第 4 天:9第 5 天:10请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 </code></pre><p>示例 2:</p><pre><code>输入:weights = [3,2,2,4,1,4], D = 3输出:6解释:船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:第 1 天:3, 2第 2 天:2, 4第 3 天:1, 4</code></pre><p>示例 3:</p><pre><code>输入:weights = [1,2,3,1,1], D = 4输出:3解释:第 1 天:1第 2 天:2第 3 天:3第 4 天:1, 1</code></pre><p>提示:</p><ul><li>1 <= D <= weights.length <= 5 * 104</li><li>1 <= weights[i] <= 500</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题和 <a href="https://leetcode-cn.com/problems/koko-eating-bananas/" target="_blank" rel="noopener">875. 爱吃香蕉的珂珂</a> 是一样的,都是二分查找在实际问题中的应用。</p><p>首先需要确定最小载重的范围,最小值max((weights),最大值sum((weights))。然后就是在这个范围内进行搜索,这里我们采用的是二分搜索。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func shipWithinDays(weights []int, days int) int { // 套用搜索左侧边界的算法框架,注意左右边界, //载重最小为最重货物的重量(保证一次至少能运送一件货物),最大为所有货物的重量 left, right := getMax(weights), getSum(weights) + 1 for left < right { mid := left + (right - left) / 2 if canFinish(weights, mid, days) { right = mid } else { left = mid +1 } } return left}// 当载重为cap时,能否在days天内运送玩所有货物func canFinish(weights []int, cap, days int) bool { i := 0 // 按照天数遍历,每天都是新的开始,运力为最大,即cap for day := 0; day < days; day++ { maxCap := cap // 只要还有富余空间就运 for maxCap - weights[i] >= 0 { maxCap -= weights[i] i++ // 每次运送一件货物就检查一下是否到最后了 if i == len(weights) { return true } } } return false}// 在货物中找到最大的那个func getMax(weights []int) int { max := 0 for _, weight := range weights { if weight > max { max = weight } } return max}func getSum(weights []int) int { sum := 0 for _, v := range weights { sum += v } return sum}</code></pre><h3 id="时间复杂度:O-nlog-Σw"><a href="#时间复杂度:O-nlog-Σw" class="headerlink" title="时间复杂度:O((nlog((Σw))))"></a>时间复杂度:O((nlog((Σw))))</h3><p>其中 n 是数组 weights 的长度,Σw 是数组 weights 中元素的和。二分查找需要执行的 weights 进行依次遍历,时间为 O((n)),相乘即可得到总时间复杂度。</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题思路基本与<a href="https://leetcode-cn.com/problems/koko-eating-bananas/" target="_blank" rel="noopener">875. 爱吃香蕉的珂珂</a>一致,也是确定左右边界后用二分查找,值得注意的是判断是否能装载的finish方法的逻辑需要仔细考虑,这是一个值得推敲的细节问题。</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> 动态规划 </tag>
<tag> 二分 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-875】爱吃香蕉的珂珂</title>
<link href="/2021/07/03/leetcode-875/"/>
<url>/2021/07/03/leetcode-875/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/koko-eating-bananas/" target="_blank" rel="noopener">875. 爱吃香蕉的珂珂</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。</p><p>珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉</p><p>珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。</p><p>返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。</p><p>示例 1:</p><pre><code>输入: piles = [3,6,7,11], H = 8输出: 4</code></pre><p>示例 2:</p><pre><code>输入: piles = [30,11,23,4,20], H = 5输出: 30</code></pre><p>示例 3:</p><pre><code>输入: piles = [30,11,23,4,20], H = 6输出: 23</code></pre><p>提示:</p><ul><li>1 <= piles.length <= 10^4</li><li>piles.length <= H <= 10^9</li><li>1 <= piles[i] <= 10^9</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题如果直接求解最小速度会比较困难,因为涉及的场景较复杂。不妨先按照题目的意思假设在 H 小时内吃掉所有香蕉的最小速度为speed,然后我们可以思考一下这个speed的取值范围是多少。</p><p>显然,最小速度为1,也就是一个小时至少吃一个香蕉,最大速度为最大堆的香蕉个数 max((piles)),因为一个小时最多吃一堆香蕉,这个是在题目中规定好的。</p><p>然后我们就可以暴力搜索当speed从1到max((piles)) ,一旦发现某个值可以吃掉所有的香蕉,立刻返回答案。</p><p>因为是从1到max((piles)),连续的线性空间搜索,所以可以考虑二分查找,同时求的是最小速度,所以是搜索左侧边界的线性查找。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func minEatingSpeed(piles []int, h int) int { // 套用搜索左侧边界的算法框架,注意左右边界,每小时最少吃一个香蕉,最多吃一堆香蕉(最多的那堆) left, right := 1, getMax(piles) + 1 for left < right { // 防止溢出 mid := left + (right - left) / 2 if canFinish(piles, mid, h) { right = mid } else { left = mid + 1 } } // 因为求的是最小速度,所以是搜索左侧边界 return left}// 在所有堆中找到最大的那堆func getMax(piles []int) int { max := 0 for _, pile := range piles { if pile > max { max = pile } } return max}// 以speed的速度能不能在h小时内吃完所有的香蕉func canFinish(piles []int, speed, h int) bool { time := 0 // 总时间等于吃完每堆香蕉的时间累加 for _, piles := range piles { time += timeOf(piles, speed) } return time <= h}// 吃完一堆香蕉所用的时间func timeOf(pile int, speed int) int { // 如果不能刚好吃完,剩下的需要再多花一个小时吃 if pile % speed > 0 { return pile / speed + 1 } else { return pile / speed }}</code></pre><h3 id="时间复杂度:O-NlogW"><a href="#时间复杂度:O-NlogW" class="headerlink" title="时间复杂度:O((NlogW))"></a>时间复杂度:O((NlogW))</h3><p>外层二分查找的范围时1~W(最大的香蕉堆的大小),所以复杂度是O((logW))<br>内层for循环需要遍历所有的堆,所以时间复杂度是 O((N))</p><h3 id="空间复杂度:O-1"><a href="#空间复杂度:O-1" class="headerlink" title="空间复杂度:O((1))"></a>空间复杂度:O((1))</h3><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题是二分查找的变体,本身实现上并不难,只是比较难想到是二分查找。主要注意点如下:</p><ol><li>如何确定二分查找的范围,也就是最小速度的取值范围,这里认为1 <= K <= 最大堆香蕉的数量,因为一小时最多吃一堆,所以速度大于最大堆数量也是没有意义的(注意左边界是1不是0,一小时至少吃一根)</li><li>二分查找求的是最小值,所以最后返回的是左边界,至于l = mid + 1这个只能靠debug了</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> 二分 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-372】超级次方</title>
<link href="/2021/06/27/leetcode-372/"/>
<url>/2021/06/27/leetcode-372/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/super-pow/" target="_blank" rel="noopener">372. 超级次方</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>你的任务是计算 ab 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。</p><p>示例 1:</p><pre><code>输入:a = 2, b = [3]输出:8</code></pre><p>示例 2:</p><pre><code>输入:a = 2, b = [1,0]输出:1024</code></pre><p>示例 3:</p><pre><code>输入:a = 1, b = [4,3,3,8,5,2]输出:1</code></pre><p>示例 4:</p><pre><code>输入:a = 2147483647, b = [2,0,0]输出:1198</code></pre><p>提示:</p><ul><li>1 <= a <= 231 - 1</li><li>1 <= b.length <= 2000</li><li>0 <= b[i] <= 9</li><li>b 不含前导 0</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题主要有三个难点:</p><ul><li>一是如何处理用数组表示的指数</li></ul><p>现在 b 是一个数组,也就是说 b 可以非常大,没办法直接转成整型,否则可能溢出。你怎么把这个数组作为指数,进行运算呢?</p><ul><li>二是如何得到求模之后的结果?</li></ul><p>按道理,起码应该先把幂运算结果算出来,然后做 % 1337 这个运算。但问题是,指数运算你懂得,真实结果肯定会大得吓人,也就是说,算出来真实结果也没办法表示,早都溢出报错了。</p><ul><li>三是如何高效进行幂运算</li></ul><p>进行幂运算也是有算法技巧的,如果你不了解这个算法,后文会讲解。</p><p>那么对于这几个问题,我们分开思考,逐个击破。</p><h4 id="如何处理数组指数"><a href="#如何处理数组指数" class="headerlink" title="如何处理数组指数"></a>如何处理数组指数</h4><pre><code> a^[1,5,6,4]= a^4 * a^[1,5,6,0]= a^4 * (a^[1,5,6])^10</code></pre><h4 id="如何处理-mod-运算"><a href="#如何处理-mod-运算" class="headerlink" title="如何处理 mod 运算"></a>如何处理 mod 运算</h4><p>对乘法的结果求模,等价于先对每个因子都求模,然后对因子相乘的结果再求模。</p><pre><code>(a * b) % k = (a % k)(b % k) % k</code></pre><h4 id="如何高效求幂"><a href="#如何高效求幂" class="headerlink" title="如何高效求幂"></a>如何高效求幂</h4><pre><code> a * a ^ (b - 1)a ^ b = (a ^ (b / 2)) ^ 2</code></pre><h1 id="解法一"><a href="#解法一" class="headerlink" title="解法一"></a>解法一</h1><pre><code>var base = 1337// 处理数组表示的指数func superPow(a int, b []int) int { // 递归的 base case,任何数的0次方都是1 if len(b) == 0 { return 1 } // 取出最后一个数 last := b[len(b)-1] b = b[:len(b)-1] // 将原问题化简,也就是递归迭代 part1 := myPow(a, last) part2 := myPow(superPow(a, b), 10) return (part1 * part2) % base}// 计算a的k次方然后与base求模的结果func myPow(a, k int) int { // 方法一:拆分求模 //// 对因子求模 //a %= base //res := 1 // //// 把a^k进行拆解,需要进行k次求模运算 //for i := 0; i < k; i++ { // res *= a // res %= base //} //return res // 方法二 递归求模 // 递归的 base case,任何数的0次方都是1 if k == 0 { return 1 } // 如果是奇数次方,a ^ b = a * a ^ (b - 1) if k % 2 == 1 { return (a * myPow(a, k - 1)) % base } else { // 如果是偶数次方,a ^ b = (a ^ (b / 2)) ^ 2 sub := myPow(a, k/2) return (sub * sub) % base }}</code></pre><h3 id="时间复杂度:"><a href="#时间复杂度:" class="headerlink" title="时间复杂度:"></a>时间复杂度:</h3><h3 id="空间复杂度:"><a href="#空间复杂度:" class="headerlink" title="空间复杂度:"></a>空间复杂度:</h3><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题很容易无从下手,切入点在b上,因为b是一个非常大的数,因此肯定不能直接转成整型。</p><p>注意点</p><h2 id="1"><a href="#1" class="headerlink" title="1."></a>1.</h2><p>我们知道(a <em> b) % base = ((a % base) </em> (b % base)) % base,所以得想办法拆分。</p><p>a^b为一个数乘以另一个数的模式。举个例子,比如</p><pre><code>a ^ 23 = (a ^ 2) ^ 10 * a ^ 3</code></pre><p>很明显此处可以用递归,这样就实现了降幂。</p><h2 id="2"><a href="#2" class="headerlink" title="2."></a>2.</h2><p>此外,在求 a的b(b为个位数)次方的时候,可以使用一个小技巧,就是分b为奇数和偶数两种情况。比如:</p><pre><code> a ^ (b-1) * a (b为奇数)a ^ b = (a ^ (b/2))^2 (b为偶数)</code></pre><h2 id="3"><a href="#3" class="headerlink" title="3."></a>3.</h2><h3 id="还需要注意的一点是只要是涉及到递归,都要在一开始考虑出口。也就是递归结束的条件!"><a href="#还需要注意的一点是只要是涉及到递归,都要在一开始考虑出口。也就是递归结束的条件!" class="headerlink" title="还需要注意的一点是只要是涉及到递归,都要在一开始考虑出口。也就是递归结束的条件!"></a>还需要注意的一点是只要是涉及到递归,都要在一开始考虑出口。也就是递归结束的条件!</h3><p>本题的处理技巧包括递归、模运算、幂运算技巧,还是很有价值的</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 递归 </tag>
<tag> 幂运算 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-560】和为K的子数组</title>
<link href="/2021/06/27/leetcode-560/"/>
<url>/2021/06/27/leetcode-560/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/subarray-sum-equals-k/" target="_blank" rel="noopener">560. 和为K的子数组</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。</p><p>示例 1 :</p><pre><code>输入:nums = [1,1,1], k = 2输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。</code></pre><p>说明 :</p><ul><li>数组的长度为 [1, 20,000]。</li><li>数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>为了快速得到某个连续子数组的和(不是通过遍历子数组),这里用到了一个技巧,就是前缀和。</p><p>所谓的前缀和就是对一个给定的数组,额外开辟一个前缀和数组进行预处理</p><pre><code>n := len(nums)// 构造前缀和sum := make([]int, n + 1)sum[0] = 0for i := 0; i < n; i++ { sum[i + 1] = sum[i] + nums[i]}</code></pre><p>这样以来,如果我们想要知道子数组nums[i…j]的和,只需要进一步操作preSum[j+1]-preSum[i]即可,不需要重新遍历子数组了</p><p>然后我们就可以借助这个技巧快速写出一个解法。</p><h1 id="解法一"><a href="#解法一" class="headerlink" title="解法一"></a>解法一</h1><pre><code>func subarraySum(nums []int, k int) int { n := len(nums) // 构造前缀和 sum := make([]int, n + 1) sum[0] = 0 for i := 0; i < n; i++ { sum[i + 1] = sum[i] + nums[i] } // 记录答案个数 cnt := 0 // 穷举所有子数组 for i := 0; i <= n; i++ { for j := 0; j < i; j++ { // 只要有两个子数组之差为k,意味着nums[i-j]中间这些数字之和为k if sum[i] - sum[j] == k { cnt++ } } } return cnt}</code></pre><h3 id="时间复杂度:O-n-2"><a href="#时间复杂度:O-n-2" class="headerlink" title="时间复杂度:O((n^2))"></a>时间复杂度:O((n^2))</h3><p>双层for循环</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>需要一个长度为n+1的前缀和数组</p><h1 id="解法二"><a href="#解法二" class="headerlink" title="解法二"></a>解法二</h1><p>然而以上解法虽然很好理解,但是通过对时间复杂度进行分析,含有两层for循环,性能受到严重影响,因此需要进行优化。</p><p>切入点当然就是针对双重for循环进行改造了,通过将if语句里的条件判断移项</p><pre><code>sum[j] = sum[i] - k</code></pre><p>也就是在计算前缀和的时候,直接记录一下有几个sum[j]和sum[i]-k相等,这样就避免了一层循环。而负责记录前缀和出现的次数则可以用哈希表。</p><p>优化后的代码如下</p><pre><code>// 优化版func subarraySum(nums []int, k int) int { n := len(nums) // 记录前缀和以及该前缀和出现的次数 preSum := make(map[int]int, 0) // base case preSum[0] = 1 // 记录答案个数 cnt := 0 // 初始化前缀和 sum0_i := 0 // 穷举所有子数组 for i := 0; i < n; i++ { sum0_i += nums[i] // 这是我们想要找的前缀和 sum0_j := sum0_i - k // 如果前面有这个前缀和,则直接更新答案 if times, ok := preSum[sum0_j] ; ok { cnt += times } // 注意同步更新一下当前的前缀和 preSum[sum0_i] += 1 } return cnt}</code></pre><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>其中 n 为数组的长度。我们遍历数组的时间复杂度为 O((n)),中间利用哈希表查询删除的复杂度均为 O((1)),因此总时间复杂度为 O((n))。</p><h3 id="空间复杂度:O-n-1"><a href="#空间复杂度:O-n-1" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>其中 n 为数组的长度。哈希表在最坏情况下可能有 nn 个不同的键值,因此需要 O((n))的空间复杂度。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>前缀和的技巧在求解数组和问题的时候非常有用,此外,适当的运用哈希表能够在某些情况下优化时间复杂度,减少一层循环。</p><p>本题需要注意的点是:</p><ol><li>前缀和数组长度比原数组+1,这样才能使得preSum[j] - preSum[i] 表示nums[i..j]的和</li><li>可以通过哈希表边记录前缀和,边判断前缀和是否满足条件,但是表达式要准确,即 n, ok := visited[preSum[i]-k],这样可以减少一层循环。</li></ol><h4 id="本题用到的前缀和和哈希表的技巧不难,关键是如何想到要用这个技巧,可以先简单认为在求【连续子数组的和】的时候前缀和技巧比较常用。"><a href="#本题用到的前缀和和哈希表的技巧不难,关键是如何想到要用这个技巧,可以先简单认为在求【连续子数组的和】的时候前缀和技巧比较常用。" class="headerlink" title="本题用到的前缀和和哈希表的技巧不难,关键是如何想到要用这个技巧,可以先简单认为在求【连续子数组的和】的时候前缀和技巧比较常用。"></a>本题用到的前缀和和哈希表的技巧不难,关键是如何想到要用这个技巧,可以先简单认为在求【连续子数组的和】的时候前缀和技巧比较常用。</h4>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 前缀和 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-969】煎饼排序</title>
<link href="/2021/06/26/leetcode-969/"/>
<url>/2021/06/26/leetcode-969/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/pancake-sorting/" target="_blank" rel="noopener">969. 煎饼排序</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个整数数组 arr ,请使用 煎饼翻转 完成对数组的排序。</p><p>一次煎饼翻转的执行过程如下:</p><p>选择一个整数 k ,1 <= k <= arr.length<br>反转子数组 arr[0…k-1](下标从 0 开始)<br>例如,arr = [3,2,1,4] ,选择 k = 3 进行一次煎饼翻转,反转子数组 [3,2,1] ,得到 arr = [1,2,3,4] 。</p><p>以数组形式返回能使 arr 有序的煎饼翻转操作所对应的 k 值序列。任何将数组排序且翻转次数在 10 * arr.length 范围内的有效答案都将被判断为正确。</p><p>示例 1:</p><pre><code>输入:[3,2,4,1]输出:[4,2,4,3]解释:我们执行 4 次煎饼翻转,k 值分别为 4,2,4,和 3。初始状态 arr = [3, 2, 4, 1]第一次翻转后(k = 4):arr = [1, 4, 2, 3]第二次翻转后(k = 2):arr = [4, 1, 2, 3]第三次翻转后(k = 4):arr = [3, 2, 1, 4]第四次翻转后(k = 3):arr = [1, 2, 3, 4],此时已完成排序。</code></pre><p>示例 2:</p><pre><code>输入:[1,2,3]输出:[]解释:输入已经排序,因此不需要翻转任何内容。请注意,其他可能的答案,如 [3,3] ,也将被判断为正确。</code></pre><p>提示:</p><ul><li>1 <= arr.length <= 100</li><li>1 <= arr[i] <= arr.length</li><li>arr 中的所有整数互不相同(即,arr 是从 1 到 arr.length 整数的一个排列)</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这个问题可以分成两个部分:</p><ol><li><p>首先,我们需要先找到最大的那个煎饼,把它放到最下面,然后在剩下的n-1个煎饼中找打最大的那个,再放到最底下,这很明显可以用递归解决,因为解决问题是类似的,只是问题的规模在不断变小。</p></li><li><p>其次,就是具体怎么做才能将某块烧饼放到最下面呢?</p></li></ol><p>具体分为两步:</p><p>比如第3块烧饼是最大的,</p><ul><li>将前3块烧饼进行翻转,这样最大的烧饼就到了最上面</li><li>将前n块烧饼进行翻转,这样原来最上面的那个最大烧饼此时就到最下面</li></ul><p>至此,就实现了将某个烧饼翻转到最下面的操作。</p><p>具体代码如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func pancakeSort(arr []int) []int { // 先重置一下数组 res = []int{} sortCakes(arr, len(arr)) return res}// 记录翻转操作序列var res []intfunc sortCakes(cakes []int, n int) { // base case: 一块烧饼就不用翻了 if n == 1 { return } // 寻找最大烧饼的索引 maxCake, maxCakeIndex := 0, 0 for i := 0; i < n; i++ { if cakes[i] > maxCake { maxCake = cakes[i] maxCakeIndex = i } } // 第一次翻转,将最大烧饼翻转到最上面 reverseCake(cakes, 0, maxCakeIndex) // 记录这一次翻转,翻转前maxCakeIndex+1个 res = append(res, maxCakeIndex + 1) // 第二次翻转,将最大烧饼翻转到最下面 reverseCake(cakes, 0, n - 1) // 记录这一次翻转,翻转前n个 res = append(res, n) // 递归调用,翻转剩下的烧饼 sortCakes(cakes, n - 1)}// 翻转arr[i]到arr[j]的元素func reverseCake(arr []int, i, j int) { for i < j { temp := arr[i] arr[i] = arr[j] arr[j] = temp // 翻转下一对 i++ j-- }}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n-2"><a href="#时间复杂度:O-n-2" class="headerlink" title="时间复杂度:O((n^2))"></a>时间复杂度:O((n^2))</h3><p>因为递归的调用次数是n,最多处理n次将某个烧饼翻转到最下面的操作,而每次递归操作都需要寻找最大烧饼,调用一次翻转方法,所以复杂度也是n,所以总的复杂度就是O((n^2))</p><h3 id="空间复杂度:O-n-2"><a href="#空间复杂度:O-n-2" class="headerlink" title="空间复杂度:O((n^2))"></a>空间复杂度:O((n^2))</h3><p>由于用到了递归,空间复杂度就是 递归的次数 <em> 每次递归需要的栈空间,也就是 n </em> O((n)) = O((n^2))</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><h1 id="反思-1"><a href="#反思-1" class="headerlink" title="反思"></a>反思</h1><p>本题的整体思路就是先找到最大的那个烧饼下标index,然后翻转[0, index]煎饼,然后再全部翻转一次,这样每次就能保证最大的那个煎饼再最下面,然后再递归处理剩下的煎饼。值得注意的点:</p><ol><li>翻转煎饼本质是两两交换,可以考虑采用双指针避免纠结于交换元素的下标。</li><li>两次翻转煎饼操作类似,只是操作的数组范围不同,可以考虑复用交换的方法。<h4 id="3-涉及到全局变量res-记得开始要重置一下数组。但是当主函数作为递归函数的时候,是不能在主函数中重置结果数组的,这样会导致最后的结果都被重置了,所以这种情况下只能想办法单独写一个递归函数出来,避免主函数成为递归函数,然后在主函数中重置结果数组。"><a href="#3-涉及到全局变量res-记得开始要重置一下数组。但是当主函数作为递归函数的时候,是不能在主函数中重置结果数组的,这样会导致最后的结果都被重置了,所以这种情况下只能想办法单独写一个递归函数出来,避免主函数成为递归函数,然后在主函数中重置结果数组。" class="headerlink" title="3. 涉及到全局变量res, 记得开始要重置一下数组。但是当主函数作为递归函数的时候,是不能在主函数中重置结果数组的,这样会导致最后的结果都被重置了,所以这种情况下只能想办法单独写一个递归函数出来,避免主函数成为递归函数,然后在主函数中重置结果数组。"></a>3. 涉及到全局变量res, 记得开始要重置一下数组。但是当主函数作为递归函数的时候,是不能在主函数中重置结果数组的,这样会导致最后的结果都被重置了,所以这种情况下只能想办法单独写一个递归函数出来,避免主函数成为递归函数,然后在主函数中重置结果数组。</h4></li></ol><ul><li>进一步思考</li></ul><p>这个操作结果并不是最优的,如果需要计算最短操作序列该怎么办呢?</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 递归 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-772】基本计算器 III</title>
<link href="/2021/06/20/leetcode-772/"/>
<url>/2021/06/20/leetcode-772/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/basic-calculator-iii/" target="_blank" rel="noopener">772. 基本计算器 III</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>实现一个基本的计算器来计算简单的表达式字符串。</p><p>表达式字符串只包含非负整数,算符 +、-、*、/ ,左括号 ( 和右括号 ) 。整数除法需要 向下截断 。</p><p>你可以假定给定的表达式总是有效的。所有的中间结果的范围为 [-231, 231 - 1] 。</p><p>示例 1:</p><pre><code>输入:s = "1+1"输出:2</code></pre><p>示例 2:</p><pre><code>输入:s = "6-4/2"输出:4</code></pre><p>示例 3:</p><pre><code>输入:s = "2*(5+5*2)/3+(6/2+8)"输出:21</code></pre><p>示例 4:</p><pre><code>输入:s = "(2+6*3+5-(3*14/7+2)*5)+3"输出:-12</code></pre><p>示例 5:</p><pre><code>输入:s = "0"输出:0</code></pre><p>提示:</p><ul><li>1 <= s <= 104</li><li>s 由整数、’+’、’-‘、’*’、’/‘、’(‘ 和 ‘)’ 组成</li><li>s 是一个 有效的 表达式</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>本题属于hard级别,关键在于层层拆解,化整为零,逐个击破。<br>通过拆解后需要解决以下几个问题:</p><ol><li>字符串如何转整数</li><li>如何处理加减法(栈)</li><li>如何处理乘除法(和栈顶元素结合)</li><li>如何处理括号(递归)</li></ol><p>这里值得注意的是,在处理括号的时候,需要将处理后的string和计算结果一并传回来,因为在Go语言中,string和普通变量一样是值传递,因此为了保证退出递归后,括号内的结果计算完毕后,原来字符串的整个括号部分也应该同步消掉,所以改变后的字符串也要传出来,否则会在外层会进行重复计算括号内的结果。这算是一个坑</p><p>具体代码如下:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func calculate(s string) int { res, _ := recursive(s) return res}func recursive(s string) (int, string) { // 用一个栈存储处理后的数字 stack := make([]int, 0) // 初始状态默认sign是'+',num是0 sign := byte('+') num := 0 for len(s) > 0 { c := s[0] s = s[1:] // 如果是数字就直接转成int类型的数字 if isDigit(c) { num = num * 10 + int(c - '0') } // 如果是左括号就递归计算num if c == '(' { // 这里尤其需要注意的是string和普通变量一样是值传递,因此 // 为了保证退出递归后,括号内的结果计算完毕后,原来字符串的整个括号部分也应该同步消掉 // 所以改变后的字符串也要传出来,否则会在外层会进行重复计算括号内的结果 num, s = recursive(s) } // 如果不是数字也不是空格(说明是个运算符)或者已经走到了尽头,都将数字入栈 if !isDigit(c) && c != ' ' || len(s) == 0 { switch sign { case '+': stack = append(stack, num) case '-': stack = append(stack, -num) case '*': pre := stack[len(stack)-1] stack = stack[:len(stack)-1] stack = append(stack, pre * num) case '/': pre := stack[len(stack)-1] stack = stack[:len(stack)-1] stack = append(stack, pre / num) } // 更新符号为当前符号,数字清零 num = 0 sign = c } // 遇到右括号返回递归结果 if c == ')' { break } } // 把栈中所有数字相加 return sum(stack), string(s)}func isDigit(c byte) bool { return c >= '0' && c <= '9'}func sum(stack []int) int { res := 0 for _, n := range stack { res += n } return res}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>因为整个过程下来也就相当于把字符串完整遍历一遍,所以时间复杂度就是字符串的长度</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>由于用到了递归,空间复杂度就是 递归的次数 <em> 每次递归需要的栈空间,也就是 K </em> O((n)) ,其中K是括号的个数,而每次递归开辟的栈最多也不会超过字符串的长度</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><h4 id="本题还是比较难的,需要好好理清思路"><a href="#本题还是比较难的,需要好好理清思路" class="headerlink" title="本题还是比较难的,需要好好理清思路"></a>本题还是比较难的,需要好好理清思路</h4><ol><li>首先,遇到这种表达式求值的首先应该想到用栈。</li><li>其次,考虑到可能有括号,所以需要用到递归,递归计算括号中的数字。</li><li>本质还是从左向右遍历字符串,然后维护一个数字num和一个符号sign,考虑遇到数字、加减号、左右括号分别应该如果处理,分情况讨论。</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
</tags>
</entry>
<entry>
<title>【leetcode-15】三数之和</title>
<link href="/2021/06/19/leetcode-15/"/>
<url>/2021/06/19/leetcode-15/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/3sum/" target="_blank" rel="noopener">15. 三数之和</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。</p><p>注意:答案中不可以包含重复的三元组。</p><p>示例 1:</p><pre><code>输入:nums = [-1,0,1,2,-1,-4]输出:[[-1,-1,2],[-1,0,1]]</code></pre><p>示例 2:</p><pre><code>输入:nums = []输出:[]</code></pre><p>示例 3:</p><pre><code>输入:nums = [0]输出:[]</code></pre><p>提示:</p><ul><li>0 <= nums.length <= 3000</li><li>-105 <= nums[i] <= 105</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>3sum问题其实是从2sum问题演变而来的,所以我们可以先考虑求解一个2sum问题,而对于2sum问题可以先对数组进行排序,然后利用双指针从两端逐渐向中间逼近就行了,并不难。而3Sum则可以先穷举一个数nums[i],然后对nums[i]利用2Sum计算能得到target-nums[i]的两数组合,最后再加上nums[i],就是我们要的三个数的组合了。</p><p>这里值得注意的是,计算2Sum的时候要把已经用掉的nums[i]排除在外,因为一个数字只能用一次。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 先求两数之和func twoSum(nums []int, start, target int) [][]int { // 用于存储结果组合 res := make([][]int, 0) //// 先排序,确保数字有序 //sort.Ints(nums) // 设置左右两个指针,注意这里左指针从start开始 l, r := start, len(nums) - 1 // 从左右两端向中间逼近,根据大小移动左右指针 for l < r { sum := nums[l] + nums[r] // 记录索引l和r最初对应的值,这步很关键,下面会用到,为了避免重复 left, right := nums[l], nums[r] if sum == target { // 满足要求,加入集合 res = append(res, []int{nums[l], nums[r]}) // 左右指针都避开重复数字 for l < r && nums[l] == left { l++ } for l < r && nums[r] == right { r-- } } else if sum < target { // 左指针避开重复数字 for l < r && nums[l] == left { l++ } } else { // 右指针避开重复数字 for l < r && nums[r] == right { r-- } } } return res}func threeSum(nums []int) [][]int { // 用于存储结果组合 res_3 := make([][]int, 0) // 现将数组排个序 sort.Ints(nums) // 穷举threeSum的第一个数 for i := 0; i < len(nums); i++ { // 利用twoSum计算两个数字之和为target-nums[i]的组合(这里的target为0) // 注意,左指针从start开始,这是为了排除已经用掉的数字 res_2 := twoSum(nums, i + 1, 0 - nums[i]) // 如果存在这样的若干个二元组的话,那么把它加上nums[i]就有了若干个三元组 for _, res := range res_2 { res = append(res, nums[i]) res_3 = append(res_3, res) } // 这里和twoSum一样,需要跳过重复的数字 for i < len(nums) - 1 && nums[i] == nums[i+1] { i++ } } return res_3}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n-2"><a href="#时间复杂度:O-n-2" class="headerlink" title="时间复杂度:O((n^2))"></a>时间复杂度:O((n^2))</h3><p>因为2Sum使用的双指针是O((n)),然后再乘以3Sum外层的遍历O((n))就是O((n^2))</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n!))"></a>空间复杂度:O((n!))</h3><p>最后的结果是一个二维数组,每个一维数组的大小都为3,最坏的情况下会有n!个这样的组合。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>做完了这道题还可以尝试一下四数之和(4Sum),以及拓展到n个数字之和(nSum)</p><h3 id="注意点"><a href="#注意点" class="headerlink" title="注意点"></a>注意点</h3><ol><li><p>这道题用到的技巧主要是双指针,建议先从两数之和开始做,尤其需要注意的是不管是两数还是三数都需要注意避开重复数字,而避开重复数字的技巧是一个难点</p></li><li><p>对于双指针,只要移动指针,就要时刻注意左指针小于右指针,否则会重复,还有就是降维的时候(就是由threeSum转化为twoSum的时候要注意从当前的下标+1开始遍历,这样能避免重复)</p></li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 双指针 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-773】滑动谜题</title>
<link href="/2021/06/16/leetcode-773/"/>
<url>/2021/06/16/leetcode-773/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/sliding-puzzle/" target="_blank" rel="noopener">773. 滑动谜题</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示.</p><p>一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.</p><p>最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。</p><p>给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。</p><p>示例:</p><pre><code>输入:board = [[1,2,3],[4,0,5]]输出:1解释:交换 0 和 5 ,1 步完成</code></pre><pre><code>输入:board = [[1,2,3],[5,4,0]]输出:-1解释:没有办法完成谜板</code></pre><pre><code>输入:board = [[4,1,2],[5,0,3]]输出:5解释:最少完成谜板的最少移动次数是 5 ,一种移动路径:尚未移动: [[4,1,2],[5,0,3]]移动 1 次: [[4,1,2],[0,5,3]]移动 2 次: [[0,1,2],[4,5,3]]移动 3 次: [[1,0,2],[4,5,3]]移动 4 次: [[1,2,0],[4,5,3]]移动 5 次: [[1,2,3],[4,5,0]]</code></pre><pre><code>输入:board = [[3,2,4],[1,5,0]]输出:14</code></pre><p>提示:</p><ul><li>board 是一个如上所述的 2 x 3 的数组.</li><li>board[i][j] 是一个 [0, 1, 2, 3, 4, 5] 的排列.</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>对于求最小步数的问题,我们要敏锐得想到BFS算法,也就是层序遍历,也就是从一个起点出发,一层层地向外扩张,第一次到达终点的时候,扩张的层数就是最小步数。</p><p>只不过这道题稍微有些特殊,就是每次不是向外走一步,而是和相邻的数字交换一下。这就需要我们将和相邻数字交换这个操作转化成向相邻路径走一步。</p><p>我们这里的 board 仅仅是 2x3 的二维数组,所以可以压缩成一个一维字符串。其中比较有技巧性的点在于,二维数组有「上下左右」的概念,压缩成一维后,如何得到某一个索引上下左右的索引?</p><p>很简单,我们只要手动写出来这个映射就行了:</p><pre><code>vector<vector<int>> neighbor = { { 1, 3 }, { 0, 4, 2 }, { 1, 5 }, { 0, 4 }, { 3, 1, 5 }, { 4, 2 }};</code></pre><p>这个含义就是,在一维字符串中,索引 i 在二维数组中的的相邻索引为 neighbor[i],至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 BFS 算法框架的代码框架,直接就可以套出解法代码了。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><h1 id="题解-1"><a href="#题解-1" class="headerlink" title="题解"></a>题解</h1><pre><code>func slidingPuzzle(board [][]int) int { // 定义相邻元素下标数组 neighbours := [][]int{ {1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}, } // 定义起点 start := "" for i := 0; i < 2; i++ { for j := 0; j < 3; j++ { start += strconv.Itoa(board[i][j]) } } // 定义终点 target := "123450" // 定义visited数组,防止重复访问 visited := make(map[string]bool, 0) // 定义队列 q := make([]string, 0) // 将起点入队 q = append(q, start) // 定义步数 step := 0 // 开始遍历 for len(q) > 0 { // 注意求步数需要双层遍历 size := len(q) for i := 0; i < size; i++ { // 从队头拿出一个元素 cur := q[0] q = q[1:] // 检查是否到了终点 if cur == target { return step } // 尝试移动滑板 // 先找到0所在的位置 index := 0 for ; index < len(cur); index++ { if cur[index] == '0' { break } } // 利用neighbours数组找到可以交换的元素,依次进行交换 neighbour := neighbours[index] for j := 0; j < len(neighbour); j++ { // 用一个新的board存储交换后的结果 newBoard := make([]byte, len(cur)) copy(newBoard, cur) // 将0和相邻元素两辆交换 tmp := newBoard[neighbour[j]] newBoard[neighbour[j]] = newBoard[index] newBoard[index] = tmp // 如果之前没有访问过,则将新的board加入队列 if !visited[string(newBoard)] { q = append(q, string(newBoard)) visited[string(newBoard)] = true } } } // 向下遍历了一层,记录步数+1 step++ } return -1}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-R∗C∗-R∗C"><a href="#时间复杂度:O-R∗C∗-R∗C" class="headerlink" title="时间复杂度:O(R∗C∗(R∗C)!)"></a>时间复杂度:O(R∗C∗(R∗C)!)</h3><p>BFS的时间复杂度即可能存在的状态数。</p><p>R, C 为棋盘的行数和列数。最多有 O((R∗C)!) 种可能的棋盘状态。</p><p>对于每种状态都需要进行一次append操作,而一次操作的开销就是状态的长度,也就是O(R * C)</p><h3 id="空间复杂度:O-R∗C∗-R∗C"><a href="#空间复杂度:O-R∗C∗-R∗C" class="headerlink" title="空间复杂度:O(R∗C∗(R∗C)!)"></a>空间复杂度:O(R∗C∗(R∗C)!)</h3><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题的困难之处在于一开始会找不到头绪,可以先思考本题考察的是什么算法:</p><ol><li>求最小步数,一层层向外扩张即BFS算法。</li><li>然后想办法套用BFS的模版,就是使用队列的数据结构。</li><li>然后再考虑起点和终点。</li><li>考虑如何向外扩张一层,也就是遍历相邻的元素(找到0的位置,然后通过构造neighbour数组)</li><li>注意交换相邻元素的时候需要构造一个新的board,将原来的拷贝过去。</li><li>注意在队列循环的时候如果发现结果次数明显大于预期的次数,可以考虑加一层循环:</li></ol><pre><code>length := len(q)for i := 0; i < length; i++ {}</code></pre><p>也就是提前取好长度,因为如果在循环中len(q),则q一直在增长,不固定。</p><ol start="7"><li>最后记得设置visited数组,防止重复遍历,导致无限循环。</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> BFS </tag>
<tag> leecode </tag>
</tags>
</entry>
<entry>
<title>【leetcode-22】括号生成</title>
<link href="/2021/06/06/leetcode-22/"/>
<url>/2021/06/06/leetcode-22/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/generate-parentheses/" target="_blank" rel="noopener">22. 括号生成</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。</p><p>示例 1:</p><pre><code>输入:n = 3输出:["((()))","(()())","(())()","()(())","()()()"]</code></pre><p>示例 2:</p><pre><code>输入:n = 1输出:["()"]</code></pre><p>提示:</p><ul><li>1 <= n <= 8</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>关于括号问题,需要记住以下性质:</p><ol><li>一个“合法”括号组合的左括号数量一定等于右括号数量</li><li>对于一个“合法”的括号字符串P,从左往右算的话,肯定是左括号的数量大于等于右括号的数量,到最后左括号和右括号的数量相等。</li></ol><p>明白了以上性质,就可以把这道题转化以下问题:</p><ul><li>现在有2n个位置,每个位置可以放置字符(或者),组成的所有括号组合中,有多少个是合法的?</li></ul><p>然后我们就可以对回溯算法模版进行改造,得到以下解法:</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>var res []stringfunc generateParenthesis(n int) []string { // 特例直接返回 if n == 0 { return []string{""} } // 重置一下结果 res = []string{} // 保存一条路径 var track string // 回溯剪枝 backTrack(n, n, track) return res}func backTrack(left, right int, track string) { // 数量小于0肯定不合法 if left < 0 || right < 0 { return } // 如果剩下的左括号剩下的多肯定不合法,因为意味着已经用的右括号比左括号多 if left > right { return } // 如果左括号和右括号都恰好用完,说明得到一个合法的括号组合 if left == 0 && right == 0 { res = append(res, track) return } // 尝试添加一个左括号 track += "(" // 继续往下走 backTrack(left - 1, right, track) // 撤销选择 track = track[:len(track)-1] // 尝试添加一个右括号 track += ")" // 继续往下走 backTrack(left, right - 1, track) // 撤销选择 track = track[:len(track)-1]}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h1 id="时间复杂度:"><a href="#时间复杂度:" class="headerlink" title="时间复杂度:"></a>时间复杂度:</h1><p>我们的复杂度分析依赖于理解 generateParenthesis((n)) 中有多少个元素。这个分析超出了本文的范畴。</p><h1 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h1><p>除了答案数组之外,我们所需要的空间取决于递归栈的深度,每一层递归函数需要 O((1)) 的空间,最多递归 2n 层,因此空间复杂度为 O((n))。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题是涉及到括号的合法性问题:</p><ol><li>首先第一反应就是要知道括号合法性的两个性质,上文有写。</li><li>其次就是这里其实也是做选择,每次可以选择左括号和右括号,所以可以考虑回溯算法,值得注意的是,这里用剩余左右括号的数量来标记算法的进度而不是以往的某个数组下标</li><li>关于括号的合法性问题还应该想到的数据结构是【栈】,但是本题没有用到这个性质。</li></ol><h3 id="本题的关键在于将题意转化为我们熟悉的回溯算法的模版"><a href="#本题的关键在于将题意转化为我们熟悉的回溯算法的模版" class="headerlink" title="本题的关键在于将题意转化为我们熟悉的回溯算法的模版"></a>本题的关键在于将题意转化为我们熟悉的回溯算法的模版</h3><pre><code>现在有2n个位置,每个位置可以放置字符“(”或者“)”,组成的所有括号组合中,有多少个是合法的?</code></pre><h3 id="然后回溯算法一上来是先判断合法性,然后才是检查有没有到终点,这个顺序不要搞错了!"><a href="#然后回溯算法一上来是先判断合法性,然后才是检查有没有到终点,这个顺序不要搞错了!" class="headerlink" title="然后回溯算法一上来是先判断合法性,然后才是检查有没有到终点,这个顺序不要搞错了!"></a>然后回溯算法一上来是先判断合法性,然后才是检查有没有到终点,这个顺序不要搞错了!</h3>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leecode </tag>
<tag> 回溯剪枝 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-37】解数独</title>
<link href="/2021/06/06/leetcode-37/"/>
<url>/2021/06/06/leetcode-37/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/sudoku-solver/" target="_blank" rel="noopener">37. 解数独</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>编写一个程序,通过填充空格来解决数独问题。</p><p>数独的解法需遵循如下规则:</p><p>数字1-9在每一行只能出现一次。<br>数字1-9在每一列只能出现一次。<br>数字1-9在每一个以粗实线分隔的3x3宫内只能出现一次。(请参考示例图)<br>数独部分空格内已填入了数字,空白格用’.’表示。</p><p>示例:</p><pre><code>输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]解释:输入的数独如上图所示,唯一有效的解决方案如下所示:</code></pre><p>提示:</p><ul><li>board.length == 9</li><li>board[i].length == 9</li><li>board[i][j] 是一位数字或者 ‘.’</li><li>题目数据 保证 输入数独仅有一个解</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>数独也是典型的回溯剪枝算法,与N皇后问题非常类似,主要不同可能就在于一个是判断棋子合法性的方法不同,还有就是N皇后需要得到所有的解,而本题只要找到符合要求的一个解即可,因此最终返回的判定条件也有所不同。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func solveSudoku(board [][]byte) { backtrack(board, 0,0)}// 规定好边界 9*9的棋盘var m,n = 9,9func backtrack(board [][]byte, i, j int) bool { // 如果穷举到最后一列,就换一行重新开始 if j == n { return backtrack(board, i+1, 0) } // 如果找到一个可行解,直接返回 if i == m { return true } // 如果当前位置已经有数字了,直接跳过 if board[i][j] != '.' { return backtrack(board, i, j+1) } // 将1-9依次进行穷举 for ch := byte('1'); ch <= byte('9'); ch++ { // 尝试填入一个数字,检查填入后是否符合要求 if !isValid(board, i, j, ch) { // 不合法,就跳过,换个数字试试 continue } // 走到这里说明合法了,就直接填入 board[i][j] = ch // 继续向下遍历,如果找到一个可行解,立即结束 if backtrack(board, i, j+1) { return true } // 走到这里说明填满了格子仍旧没找到可行解,回退一格 board[i][j] = '.' } // 穷举完1-9,仍旧没有找到可行解 // 需要前面的格子换个数字穷举 return false}// 判断board[i][j]是否可以填入数字chfunc isValid(board [][]byte, row, col int, ch byte) bool { for i := 0; i < 9; i++ { // 判断同行是否重复 if board[row][i] == ch { return false } // 判断同列是否重复 if board[i][col] == ch { return false } // 判断同单元(3*3)是否重复 // 这里需要一点技巧,需要找到当前格子所在单元的起始格子,然后依次遍历 // [0,0]、[0,1]、[0,2]、[1,0]、[1,1]、[1,2]、[2,0]、[2,1]、[2,2]、 if board[(row/3)*3+i/3][(col/3)*3+i%3] == ch { return false } } return true}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-9-M"><a href="#时间复杂度:O-9-M" class="headerlink" title="时间复杂度:O((9^M))"></a>时间复杂度:O((9^M))</h3><p>在最坏的情况下,需要对每个空着的格子枚举9个数,所以时间复杂度就是O((9^M)),其中M是棋盘中空着的格子数量。</p><p>当然在实际情况下复杂度并没有这么高,因为很多数字不需要穷举所有可能,因为当我们找到一个可行的解的时候就结束了,后续的递归都没有展开。所以这个O((9^M))的时间复杂度实际上是完全穷举,或者说是找到所有可行解的时间复杂度。</p><h3 id="空间复杂度"><a href="#空间复杂度" class="headerlink" title="空间复杂度"></a>空间复杂度</h3><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题需要注意的点如下:</p><h1 id="反思-1"><a href="#反思-1" class="headerlink" title="反思"></a>反思</h1><h3 id="注意本题与N皇后的异同"><a href="#注意本题与N皇后的异同" class="headerlink" title="注意本题与N皇后的异同"></a>注意本题与N皇后的异同</h3><ol><li>本题只需要找到一个可行解即可,因此回溯方法backtrack需要设置返回值为bool,找到一个可行解就立即返回ture(这个可以和N皇后进行对比)。</li><li>而且本题的行和列都放在回溯方法参数里面了,在回溯方法中是对数字进行枚举,而N皇后则只把行放在了参数里面,在方法体中对列进行枚举。</li></ol><p>在判断合法性的时候需要注意可以将多个循环尽量写在一起。这样可以降低时间复杂度。</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leecode </tag>
<tag> 回溯剪枝 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-77】组合</title>
<link href="/2021/06/06/leetcode-77/"/>
<url>/2021/06/06/leetcode-77/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/combinations/" target="_blank" rel="noopener">77. 组合</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。</p><p>示例:</p><pre><code>输入: n = 4, k = 2输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4],]</code></pre><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题其实与 <a href="https://leetcode-cn.com/problems/subsets/" target="_blank" rel="noopener">78.子集</a> 非常相似,也是属于回溯算法,主要不同在于遍历决策树的时候子集是所有节点都算是子集,都需要更新res,而组合则是只有在到达叶子节点时才算是一种组合,才需要更新res。实际上77、78和46三道题可以一起看,感受其中的相同与不同,因为这三道都是典型的回溯算法。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 记录所有组合var res [][]intfunc combine(n int, k int) [][]int { // 先重置一下结果 res = [][]int{} // 特殊情况直接返回 if k <= 0 || n <= 0 { return res } // 记录一条路径 track := make([]int, 0) // 开始回溯 backtrack(n, k, 1, track) return res}func backtrack(n, k, start int, track []int) { // 如果已经到达叶子节点,说明已经有了一种组合,记录下来 if len(track) == k { temp := make([]int, len(track)) copy(temp, track) res = append(res, temp) return } // 从start开始继续遍历 for i := start; i <= n; i++ { // 选择把节点i加入 track = append(track, i) // 继续向下遍历 backtrack(n, k, i+1,track) // 回退一层 track = track[:len(track)-1] }}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n-2-n"><a href="#时间复杂度:O-n-2-n" class="headerlink" title="时间复杂度:O((n * 2^n))"></a>时间复杂度:O((n * 2^n))</h3><p>时间复杂度就是决策树节点总数,因为每个节点至少遍历一次,也就是2^n,但是由于每次遍历到叶子节点都需要把当前路径track做一次append操作,也是O((n))的时间复杂度,所以总的时间复杂度就是O((n * 2^n))</p><h3 id="空间复杂度:O-n-2-n"><a href="#空间复杂度:O-n-2-n" class="headerlink" title="空间复杂度:O((n * 2^n))"></a>空间复杂度:O((n * 2^n))</h3><p>空间复杂度就是最后返回的结果res所占的空间也就是所有的状态树,对应决策树的所有状态所占的空间:</p><p>O((2^n)) <em> n = O((n </em> 2^n))</p><p>最多有2^((n-1))个状态,每个状态最多占用大小为n的空间(相当于全集)</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>实际上<strong>78.子集</strong>、<strong>77.组合</strong>、和<strong>46.排列</strong>三道题可以一起看,感受其中的相同与不同,因为这三道都是典型的回溯算法。</p><h4 id="值得注意的是向下遍历的时候新的起点是i-1而不是start-1,这样才能避免遍历到重复的数字"><a href="#值得注意的是向下遍历的时候新的起点是i-1而不是start-1,这样才能避免遍历到重复的数字" class="headerlink" title="值得注意的是向下遍历的时候新的起点是i+1而不是start+1,这样才能避免遍历到重复的数字"></a>值得注意的是向下遍历的时候新的起点是i+1而不是start+1,这样才能避免遍历到重复的数字</h4>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leecode </tag>
<tag> 回溯剪枝 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-78】子集</title>
<link href="/2021/06/05/leetcode-78/"/>
<url>/2021/06/05/leetcode-78/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/subsets/" target="_blank" rel="noopener">78. 子集</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。</p><p>解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。</p><p>示例 1:</p><pre><code>输入:nums = [1,2,3]输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]</code></pre><p>示例 2:</p><pre><code>输入:nums = [0]输出:[[],[0]]</code></pre><p>提示:</p><ul><li>1 <= nums.length <= 10</li><li>-10 <= nums[i] <= 10</li><li>nums 中的所有元素 互不相同</li></ul><h1 id="解体思路"><a href="#解体思路" class="headerlink" title="解体思路"></a>解体思路</h1><p>对于子集问题可以采用穷举的方式解决,而回溯算法本质就是进行穷举,可以认为是暴力穷举的一种优化。所以本题我们可以采取回溯算法。</p><p>回溯算法无非就是构造一棵决策树,而对于本题的决策树如下:</p><p><img src="https://vegard-bear.github.io/images/leetcode-78.png" alt></p><p>然后我们根据这个改造回溯算法的模版就好了</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 保存所有子集合var res [][]intfunc subsets(nums []int) [][]int { // 上来先重置一下集合 res = [][]int{} // 记录一条路径 track := make([]int, 0) // 回溯剪枝 backTrack(nums, 0, track) return res}func backTrack(nums []int, start int, track []int) { // 上来就放一条路径,因为这个决策树比较特殊,不只是到叶子节点才算一条完整路径, // 而是从根节点出发到每一个节点的路径都算一个子集 //res := append(res, track) // 注意:这里不能直接append,因为切片底层共用数据, // 这意味着下面切片path一旦改变了,res也会随之改变,而这不是我们希望看到的 // 所以只能重新开辟一个新的切片将内容拷贝过去 tmp := make([]int, len(track)) copy(tmp, track) res = append(res, tmp) // 依次开始做选择,注意从上一层遍历走到位置出发 for i := start; i < len(nums); i++ { // 把当前这个节点加入路径 track = append(track, nums[i]) // 继续向下层遍历,注意i+1 backTrack(nums, i + 1, track) // 回退一层,把刚刚做的选择拿掉,相当于剪枝 track = track[:len(track)-1] }}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n-2-n"><a href="#时间复杂度:O-n-2-n" class="headerlink" title="时间复杂度:O((n * 2^n))"></a>时间复杂度:O((n * 2^n))</h3><p>时间复杂度就是决策树节点总数因为每个节点至少遍历一次,也就是2^n,但是由于每次遍历一个节点都需要把当前路径track做一次append操作,也是O((n))的时间复杂度,所以总的时间复杂度就是O((n * 2^n))</p><h3 id="空间复杂度:O-n-2-n"><a href="#空间复杂度:O-n-2-n" class="headerlink" title="空间复杂度:O((n * 2^n))"></a>空间复杂度:O((n * 2^n))</h3><p>空间复杂度就是最后返回的结果res所占的空间也就是所有的状态树,对应决策树的所有状态所占的空间:</p><p>O((2^n)) <em> n = O((n </em> 2^n))</p><p>最多有2^n个状态,每个状态最多占用大小为n的空间(相当于全集)</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>所有的穷举问题都可以采取回溯算法解决,比如子集问题,然后我们根据题意改造回溯算法的模版就好了。</p><h4 id="这道题和【77-组合】的主要区别在于append结果的条件不同:"><a href="#这道题和【77-组合】的主要区别在于append结果的条件不同:" class="headerlink" title="这道题和【77.组合】的主要区别在于append结果的条件不同:"></a>这道题和【77.组合】的主要区别在于append结果的条件不同:</h4><ol><li>组合必须保证有k个数才算一个合法的结果,而这道题则不需要到叶子节点就可以算一个结果</li><li>另外就是这道题上来就把当前的路径算一个结果,然后在判断递归终止的条件,这个先后顺序也要注意</li><li>另外一个与组合类似,就是遍历下一层决策树的时候新的起点是i+1而不是start+1, 避免了集合中数字重复</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leecode </tag>
<tag> 回溯剪枝 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-234】回文链表</title>
<link href="/2021/05/30/leetcode-234/"/>
<url>/2021/05/30/leetcode-234/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/palindrome-linked-list/" target="_blank" rel="noopener">234. 回文链表</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>请判断一个链表是否为回文链表。</p><p>示例 1:</p><pre><code>输入: 1->2输出: false</code></pre><p>示例 2:</p><pre><code>输入: 1->2->2->1输出: true</code></pre><ul><li>进阶:<br>你能否用O((n)) 时间复杂度和 O((1)) 空间复杂度解决此题?</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题我们用两种解法,第一种是采用借助递归的堆栈把链表节点放入一个栈,然后再拿出来,这时候元素的顺序是反的,空间复杂度是O((n)),类似于二叉树的后序遍历。</p><p>第二种则进一步优化了空间复杂度为O((1)),具体采用同三步走:</p><ol><li>先通过“双指针技巧”中的快、慢指针来找到链表的中点</li><li>从链表的中点开始反转后面的链表</li><li>设置两个指针分别指向头节点和反转后子链表的头节点,两两比较回文串</li></ol><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><h2 id="解法一-利用递归函数堆栈"><a href="#解法一-利用递归函数堆栈" class="headerlink" title="解法一: 利用递归函数堆栈"></a>解法一: 利用递归函数堆栈</h2><pre><code>// 左侧指针var left *ListNodefunc isPalindrome(head *ListNode) bool { // 左侧指针初始指向头节点 left = head return traverse(head)}// 利用递归,倒序遍历单链表func traverse(right *ListNode) bool { // 右指针已经到了链表结尾,也就是最右边,可以直接返回了 if right == nil { return true } // 有指针没到结尾,继续向右寻找 res := traverse(right.Next) // 开始头尾两两成对比较,注意当前比较要带上上一次比较的结果 res = res && (left.Val == right.Val) // 继续比较下一组 // 注意left要向右边移动,right则是利用递归返回,相当于向左移动 left = left.Next return res}</code></pre><h2 id="解法二-快慢指针-反转链表"><a href="#解法二-快慢指针-反转链表" class="headerlink" title="解法二: 快慢指针 + 反转链表"></a>解法二: 快慢指针 + 反转链表</h2><pre><code>func isPalindrome(head *ListNode) bool { // 第一步先利用快慢指针找到链表中点 slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next } // slow 指针此时指向链表中点 // 对于奇数个节点的链表,中间那个节点不需要做匹配,直接跳过 // 而如果fast指针没有指向nil,说明链表长度为奇数 if fast != nil { slow = slow.Next } // 第二步是从slow开始反转后面的链表,然后就可以两两配对了 left := head right := reverse(slow) // 此时right指向反转后子链表的头节点 // 这里是以反转后的链表作为判空条件,需要特别注意 for right != nil { // 依次两两比较 if left.Val != right.Val { return false } left = left.Next right = right.Next } return true}// 反转以head为头的链表,返回反转后的头节点func reverse(head *ListNode) *ListNode { // 设置前驱节点和当前节点,原来的前驱节点会作为新的后继节点,所以初始设置为nil var pre, cur *ListNode pre, cur = nil, head // 开始依次反转 for cur != nil { // 先暂存后继节点 next := cur.Next // 原来的前驱节点会作为新的后继节点 cur.Next = pre // 当前节点作为前驱节点,后继节点作为新的当前节点,相当于整体向后移动一位 pre = cur cur = next } return pre}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-N"><a href="#时间复杂度:O-N" class="headerlink" title="时间复杂度:O((N))"></a>时间复杂度:O((N))</h3><p>解法一和解法二都需要遍历整个链表,所以时间复杂度都是O((n))</p><h3 id="空间复杂度:O-1-O-n"><a href="#空间复杂度:O-1-O-n" class="headerlink" title="空间复杂度:O((1))/O((n))"></a>空间复杂度:O((1))/O((n))</h3><ul><li><p>解法一利用的是递归函数的堆栈,堆栈的深度即链表的长度,所以是O((n))。</p></li><li><p>解法二不需要开辟额外的空间,采用的是就地反转链表,空间复杂度是O((1))</p></li></ul><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,因此可以考虑对链表进行反转。比如用栈结构倒序处理链表。</p><p>对于回文链表,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到O(1)</p><h5 id="原地逆置链表的方法非常经典,注意需要设置前后两个指针,建议多多练习"><a href="#原地逆置链表的方法非常经典,注意需要设置前后两个指针,建议多多练习" class="headerlink" title="原地逆置链表的方法非常经典,注意需要设置前后两个指针,建议多多练习"></a>原地逆置链表的方法非常经典,注意需要设置前后两个指针,建议多多练习</h5>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 链表 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-239】滑动窗口最大值</title>
<link href="/2021/05/23/leetcode-239/"/>
<url>/2021/05/23/leetcode-239/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/sliding-window-maximum/" target="_blank" rel="noopener">239. 滑动窗口最大值</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。</p><p>返回滑动窗口中的最大值。</p><p>示例 1:</p><pre><code>输入:nums = [1,3,-1,-3,5,3,6,7], k = 3输出:[3,3,5,5,6,7]解释:滑动窗口的位置 最大值--------------- -----[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7</code></pre><p>示例 2:</p><pre><code>输入:nums = [1], k = 1输出:[1]示例 3:输入:nums = [1,-1], k = 1输出:[1,-1]示例 4:输入:nums = [9,11], k = 2输出:[11]示例 5:输入:nums = [4,-2], k = 2输出:[4]</code></pre><p>提示:</p><ul><li>1 <= nums.length <= 105</li><li>-104 <= nums[i] <= 104</li><li>1 <= k <= nums.length</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。</p><p>在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快算出最值;但如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值。</p><p>回到这道题的场景,每个窗口前进的时候,要添加一个数同时减少一个数,所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了。</p><p>单调队列的最大的特性在于队列内的元素全部是单调递增或者单调递减的。这个主要通过每次在队尾插入新元素的时候做一些额外的操作来实现。</p><p>也就是将队列传统的push方法做一些小的改动就可以</p><pre><code>// 实现单调队列的push方法,注意传引用func (mq *MonotonicQueue) push(n int) { // 依旧是从队尾插入,但注意需要把前面比自己小的元素都删掉 for len(mq.q) > 0 && mq.q[len(mq.q)-1] < n { // 只要当前队尾元素比要插入的元素小,就删掉 mq.q = mq.q[:len(mq.q)-1] } // 直到所有小的元素都删掉后再插入元素 mq.q = append(mq.q, n)}</code></pre><p>此外,我们还需要补充max和pop方法</p><pre><code>// 获取单调队列的最大值func (mq MonotonicQueue) max() int { // 因为是单调队列,所以最大元素就是队首元素,直接取出来就好了 return mq.q[0]}// 实现单调队列的pop方法,注意传引用func (mq *MonotonicQueue) pop(n int) { // 因为队要删除的素可能在push的时候为了保持单调性已经被删掉,所以要确认有再pop if mq.q[0] == n { mq.q = mq.q[1:] }}</code></pre><p>至此,我们就实现了本题中单调队列所需的所有方法,然后就可以借助这个单调队列完成题目了,完整代码如下</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 用切片模拟单调队列type MonotonicQueue struct { q []int}// 实现单调队列的push方法,注意传引用func (mq *MonotonicQueue) push(n int) { // 依旧是从队尾插入,但注意需要把前面比自己小的元素都删掉 for len(mq.q) > 0 && mq.q[len(mq.q)-1] < n { // 只要当前队尾元素比要插入的元素小,就删掉 mq.q = mq.q[:len(mq.q)-1] } // 直到所有小的元素都删掉后再插入元素 mq.q = append(mq.q, n)}// 获取单调队列的最大值func (mq MonotonicQueue) max() int { // 因为是单调队列,所以最大元素就是队首元素,直接取出来就好了 return mq.q[0]}// 实现单调队列的pop方法,注意传引用func (mq *MonotonicQueue) pop(n int) { // 因为队要删除的素可能在push的时候为了保持单调性已经被删掉,所以要确认有再pop if mq.q[0] == n { mq.q = mq.q[1:] }}func maxSlidingWindow(nums []int, k int) []int { // 用单调队列维护一个滑动窗口 window := MonotonicQueue{} // 存放结果 res := make([]int, 0) // 先填满窗口的前k-1个元素 i := 0 for ;i < k-1; i++ { window.push(nums[i]) } // 开始遍历数组,依次加入新数字 for ;i < len(nums); i++ { // 窗口向前滑动,加入新数字 window.push(nums[i]) // 获取当前滑动窗口的最大值也就是单调队列的最大值 res = append(res, window.max()) // 左边界向前移动一位 // 注意因为当前新的右边界数字下标是i,而窗口大小是k,所以左边界的数字下标是i-k+1, window.pop(nums[i-k+1]) } // 返回结果 return res}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-N"><a href="#时间复杂度:O-N" class="headerlink" title="时间复杂度:O((N))"></a>时间复杂度:O((N))</h3><p>单独看 push 操作的复杂度确实不是 O((1)),但是算法整体的复杂度依然是 O((N)) 线性时间。要这样想,nums 中的每个元素最多被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度还是 O((N))</p><h3 id="空间复杂度:O-K"><a href="#空间复杂度:O-K" class="headerlink" title="空间复杂度:O((K))"></a>空间复杂度:O((K))</h3><p>空间复杂度就很简单了,就是窗口的大小 O((k))。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>滑动窗口问题是一个非常经典的问题,并且难度是Hard,但是通过引入一个单调队列,维护一个滑动窗口,本题就引刃而解。</p><h4 id="单调队列主要需要维护push、pop以及getMax三个方法,值得注意的是"><a href="#单调队列主要需要维护push、pop以及getMax三个方法,值得注意的是" class="headerlink" title="单调队列主要需要维护push、pop以及getMax三个方法,值得注意的是"></a>单调队列主要需要维护push、pop以及getMax三个方法,值得注意的是</h4><ol><li>push的时候需要把前面比他小的都删掉,保持队头始终是最大元素</li><li>pop的时候只需要检查队头即可,因为如果队头不是这个数字,说明这个数字已经被删掉了(这个元素既然先于队头元素出窗口,那么意味着他也是先于队头元素进入窗口,那么肯定已经被压扁,除非它刚好就是队头,所以只需要判断要删除的元素是不是队头即可)</li><li>这道题和一般的滑动窗口还有些不同,就是窗口的大小是固定的,所以可以不用left和right指针,正常的for循环依次遍历即可</li></ol><blockquote><p>定义单调队列的方法的时候对象名需要加上“*”,这样表示引用,否则默认是值传递,即使调用了方法,被调用对象也不会发生任何变化。</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> queue </tag>
<tag> 滑动窗口 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-236】二叉树的最近公共祖先</title>
<link href="/2021/05/22/leetcode-236/"/>
<url>/2021/05/22/leetcode-236/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/" target="_blank" rel="noopener">236. 二叉树的最近公共祖先</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。</p><p>百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”</p><p>示例 1:</p><pre><code>输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1输出:3解释:节点 5 和节点 1 的最近公共祖先是节点 3 。</code></pre><p>示例 2:</p><pre><code>输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4输出:5解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。</code></pre><p>示例 3:</p><pre><code>输入:root = [1,2], p = 1, q = 2输出:1</code></pre><p>提示:</p><ul><li>树中节点数目在范围 [2, 105] 内。</li><li>-109 <= Node.val <= 109</li><li>所有 Node.val 互不相同 。</li><li>p != q</li><li>p 和 q 均存在于给定的二叉树中。</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>对于二叉树问题,最常用的就是前中后序遍历。所以我们上来先把这个框架写出来:</p><pre><code>func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { left := lowestCommonAncestor(root.left, p, q) right := lowestCommonAncestor(root.right, p, q)}</code></pre><p>这里我们选择的是后序遍历,因为后续遍历是从上往下走,就好比是从p和q出发往上走,第一次相交的节点就是这个root,那么这个root必定是公共祖先。</p><p>接下来我们就要分情况讨论了。</p><ul><li><p>情况一:<br>如果根节点本身就是p或者q节点,那么另一个节点必定在其子树中,所以公共节点就是根节点。</p></li><li><p>情况二:<br>如果p和q都在以root为根的树中,那么left和right一定分别是p和q,此时他们的最近公共祖先就是根节点。这个是由后序遍历的特点决定的,在上文中已经有说明。</p></li><li><p>情况三:<br>如果p和q都在左子树或者右子树中,那么直接返回左子树或者右子树遍历的结果。</p></li></ul><p>想清楚这几种情况,就可以开始直接写题解了。另外插一句,就是在解题的时候我们其实很难把所有细节都想得明明白白,而是心中有了个大致的思路其实就可以开始动手写代码,先确定框架,再补充细节,先保证写出一个能够AC的代码,后面再想着怎么去进一步优化细节,提高时间复杂度和空间复杂度,而不是上来就要做一个完美的解法,先从无到有,在从有到优,这也比较符合实际生产环境中敏捷开发,快速迭代的思想。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { // base case if root == nil { return nil } // 如果根节点本身就是p或者q节点,那么另一个节点必定在其子树中,所以公共节点就是根节点 if root == p || root == q { return root } // 遍历左右子树 left := lowestCommonAncestor(root.Left, p, q) right := lowestCommonAncestor(root.Right, p, q) // 如果p和q都在以root为根的树中,那么left和right一定分别是p和q,此时他们的最近公共祖先就是根节点 if left != nil && right != nil { return root } // p和q都在右子树中 if left == nil { return right } // p和q都在左子树中 return left}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-N"><a href="#时间复杂度:O-N" class="headerlink" title="时间复杂度:O((N))"></a>时间复杂度:O((N))</h3><p>其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O((N))。</p><h3 id="空间复杂度:O-N"><a href="#空间复杂度:O-N" class="headerlink" title="空间复杂度:O((N))"></a>空间复杂度:O((N))</h3><p>其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O((N))。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>本题主要是解题思路非常有技巧性,如果想清楚了逻辑就很容易做了</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> tree </tag>
</tags>
</entry>
<entry>
<title>【leetcode-297】二叉树的序列化和反序列化</title>
<link href="/2021/05/16/leetcode-297/"/>
<url>/2021/05/16/leetcode-297/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/" target="_blank" rel="noopener">297. 二叉树的序列化与反序列化</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。</p><p>请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。</p><p>提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。</p><p>示例 1:</p><p><img src="https://assets.leetcode.com/uploads/2020/09/15/serdeser.jpg" alt></p><pre><code>输入:root = [1,2,3,null,null,4,5]输出:[1,2,3,null,null,4,5]</code></pre><p>示例 2:</p><pre><code>输入:root = []输出:[]</code></pre><p>示例 3:</p><pre><code>输入:root = [1]输出:[1]</code></pre><p>示例 4:</p><pre><code>输入:root = [1,2]输出:[1,2]</code></pre><p>提示:</p><ul><li>树中结点数在范围 [0, 104] 内</li><li>-1000 <= Node.val <= 1000</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题我们将用前序、后序、层序遍历三种方式进行解答。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><p>共用数据结构</p><pre><code>const SEP = ","const NULL = "#"// 序列化后的二叉树用一个字符串切片暂存type Codec struct { List []string}// 构造方法func Constructor() Codec { return Codec{}}</code></pre><h2 id="前序遍历"><a href="#前序遍历" class="headerlink" title="前序遍历"></a>前序遍历</h2><pre><code>// Serializes a tree to a single string.func (this *Codec) serialize(root *TreeNode) string { this.serializeList(root) // 将字符化的节点用分隔符拼接成字符串 return strings.Join(this.List, SEP)}// 前序遍历func (this *Codec) serializeList(root *TreeNode) { // 遍历根 if root == nil { this.List = append(this.List, NULL) return } // 注意别忘了append this.List = append(this.List, strconv.Itoa(root.Val)) // 遍历左右子树 this.serializeList(root.Left) this.serializeList(root.Right)}// Deserializes your encoded data to tree.func (this *Codec) deserialize(data string) *TreeNode { // 用一个字符串切片暂存 this.List = []string{} // 按照分隔符拆分切片 for _, v := range strings.Split(data, SEP) { this.List = append(this.List, v) } return this.deserializeList()}//func (this *Codec) deserializeList() *TreeNode { if len(this.List) == 0 { return nil } // 前序遍历,第一个节点必定是根节点 first := this.List[0] this.List = this.List[1:] if first == NULL { return nil } // 构造根节点 root := new(TreeNode) root.Val, _ = strconv.Atoi(first) // 遍历左右子树 root.Left = this.deserializeList() root.Right = this.deserializeList() return root}</code></pre><h2 id="后序遍历"><a href="#后序遍历" class="headerlink" title="后序遍历"></a>后序遍历</h2><pre><code>// Serializes a tree to a single string.func (this *Codec) serialize(root *TreeNode) string { this.serializeList(root) // 将字符化的节点用分隔符拼接成字符串 return strings.Join(this.List, SEP)}// 后序遍历与前序遍历比序列化只需变换一下次序就行func (this *Codec) serializeList(root *TreeNode) { // 遍历根,注意先判空,否则会报空指针异常 if root == nil { this.List = append(this.List, NULL) return } // 遍历左右子树 this.serializeList(root.Left) this.serializeList(root.Right) // 注意别忘了append this.List = append(this.List, strconv.Itoa(root.Val))}// Deserializes your encoded data to tree.func (this *Codec) deserialize(data string) *TreeNode { // 用一个字符串切片暂存 this.List = []string{} // 按照分隔符拆分切片 for _, v := range strings.Split(data, SEP) { this.List = append(this.List, v) } return this.deserializeList()}//func (this *Codec) deserializeList() *TreeNode { if len(this.List) == 0 { return nil } // 后序遍历,最后一个节点必定是根节点 first := this.List[len(this.List)-1] this.List = this.List[:len(this.List)-1] if first == NULL { return nil } // 构造根节点 root := new(TreeNode) root.Val, _ = strconv.Atoi(first) // 遍历左右子树,注意先右后左 root.Right = this.deserializeList() root.Left = this.deserializeList() return root}</code></pre><h2 id="层序遍历"><a href="#层序遍历" class="headerlink" title="层序遍历"></a>层序遍历</h2><pre><code>// Serializes a tree to a single string.func (this *Codec) serialize(root *TreeNode) string { // 层序遍历需要用一个队列记录遍历顺序 var queue []*TreeNode // 将根节点入队 queue = append(queue, root) // 记录节点 var list []string // 只要队列不为空 for len(queue) > 0 { // 取出队首元素 node := queue[0] queue = queue[1:] if node == nil { list = append(list, NULL) // 如果是空节点直接返回 continue } // 将节点值转成string加入字符串 list = append(list, strconv.Itoa(node.Val)) // 将当前节点的左右子节点依次加入队列 queue = append(queue, node.Left, node.Right) } // 最后把所有字符拼起来 return strings.Join(list, SEP)}// Deserializes your encoded data to tree.func (this *Codec) deserialize(data string) *TreeNode { // 先按照分隔符拆成node切片 nodes := strings.Split(data, SEP) // 根节点为空,直接返回 if nodes[0] == NULL { return nil } // 先初始化根节点 root := new(TreeNode) root.Val, _ = strconv.Atoi(nodes[0]) // 创建队列,加入根节点,开始层序遍历 var queue []*TreeNode queue = append(queue, root) // 只要队列不为空 for i := 0; len(queue) > 0; { // 取出队头 parent := queue[0] queue = queue[1:] // 构建左子树 i++ left := nodes[i] if left != NULL { // 不是空节点则创建新的子节点 node := new(TreeNode) node.Val, _ = strconv.Atoi(left) parent.Left = node // 将左儿子加入队列 queue = append(queue, parent.Left) } // 构建右子树 i++ right := nodes[i] if right != NULL { // 不是空节点则创建新的子节点 node := new(TreeNode) node.Val, _ = strconv.Atoi(right) parent.Right = node // 将左儿子加入队列 queue = append(queue, parent.Right) } } return root}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>每个节点均访问一次,所以时间复杂度就是O((n))</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>需要开辟一个栈或者队列空间,最坏情况下栈和队列的长度为节点个数。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><ul><li>注意前序遍历喝后序遍历的区别,后序遍历与前序遍历很像,只需要对遍历顺序进行一些调整即可</li><li>层序遍历本质就是队列,出队和入队</li><li>判断空节点是非常频繁的操作</li><li>本题无法使用中序,因为要进行反序列化首先要找到根节点,而中序遍历的根节点无法确定具体位置</li></ul><h4 id="本题注意点:"><a href="#本题注意点:" class="headerlink" title="本题注意点:"></a>本题注意点:</h4><ol><li>前序遍历反序列化字符串的时候用split打散之后要再遍历一遍注意把”#”去掉。 构造二叉树的时候遍历终点不是遇到空节点,而是字符串走到底,空节点也是需要处理的。</li><li>后序遍历序列化在append的时候顺序不变,但是反序列化构造左右子树的时候要先构造右子树,再构造左子树,这是一个坑。</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> tree </tag>
</tags>
</entry>
<entry>
<title>【leetcode-222】完全二叉树的节点个数</title>
<link href="/2021/04/18/leetcode-222/"/>
<url>/2021/04/18/leetcode-222/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/count-complete-tree-nodes/" target="_blank" rel="noopener">222. 完全二叉树的节点个数</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。</p><p>完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。</p><p>示例 1:</p><pre><code>输入:root = [1,2,3,4,5,6]输出:6</code></pre><p>示例 2:</p><pre><code>输入:root = []输出:0</code></pre><p>示例 3:</p><pre><code>输入:root = [1]输出:1</code></pre><p>提示:</p><ul><li>树中节点的数目范围是[0, 5 * 104]</li><li>0 <= Node.val <= 5 * 104</li><li>题目数据保证输入的树是完全二叉树</li></ul><p>进阶:遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?</p><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>完全二叉树是一类特殊的二叉树,它介于满二叉树和普通二叉树之间。如果是满二叉树的话,很简单,直接计算出它的<strong>高度</strong>就行了,然后节点数就等于2^<strong>高度</strong>-1。但如果不是一棵满二叉树,则可以直接按照普通二叉树的方法求解。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func countNodes(root *TreeNode) int { // 先通过比较左右子树的高度判断是否是一棵满二叉树 l, r := root, root hl, hr := 0, 0 for l != nil { l = l.Left hl++ } for r != nil { r = r.Right hr++ } // 左子树高度等于右子树高度,则是一棵满二叉树 // 满二叉树的节点数 = 2^高度 - 1 if hl == hr { return int(math.Pow(2, float64(hl))) - 1 } // 如果不是一棵满二叉树,则按普通二叉树的逻辑计算 return 1 + countNodes(root.Left) + countNodes(root.Right)}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-1"><a href="#时间复杂度:O-1" class="headerlink" title="时间复杂度:O((1))"></a>时间复杂度:O((1))</h3><p>首先计算树的高度的时间复杂度是logN,然后对树进行递归的话,表面上是N,但实际上是logN,因为最多只有其中一棵子树的递归会真正进行下去,而另一棵一定会触发hl=hr,然后直接返回,这是因为一棵完全二叉树的两棵子树,至少又一棵是满二叉树。<br>综上,时间复杂度就是logNlogN</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>递归的空间复杂度为<strong>递归的次数</strong> <em> <strong>每次递归的空间复杂度</strong>,<br>也就是 n </em> O((1)) = O((n))</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>一棵完全二叉树的两棵子树,至少有一棵是满二叉树。利用这个性质可以有效降低时间复杂度。</p>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> tree </tag>
<tag> 完全二叉树 </tag>
</tags>
</entry>
<entry>
<title>【leetcode】二叉搜索树系列</title>
<link href="/2021/04/18/leetcode-bst/"/>
<url>/2021/04/18/leetcode-bst/</url>
<content type="html"><![CDATA[<blockquote><p>本节是一个系列,包括二叉树几种常用的算法操作(判断、搜索、插入,删除)。除了删除操作之外,其他操作都比较简单,这里就不给出具体的解题思路了,直接上代码应该也能看懂。为了方便理解,这里给出的题目顺序是从难到易的。</p></blockquote><h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/search-in-a-binary-search-tree/" target="_blank" rel="noopener">700. 二叉搜索树中的搜索</a></p><p><a href="https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/" target="_blank" rel="noopener">701. 二叉搜索树中的插入操作</a></p><p><a href="https://leetcode-cn.com/problems/validate-binary-search-tree/" target="_blank" rel="noopener">98. 验证二叉搜索树</a></p><p><a href="https://leetcode-cn.com/problems/delete-node-in-a-bst/" target="_blank" rel="noopener">450. 删除二叉搜索树中的节点</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>见leetcode。</p><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>略。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><h3 id="搜索"><a href="#搜索" class="headerlink" title="搜索"></a>搜索</h3><pre><code>func searchBST(root *TreeNode, val int) *TreeNode { if root == nil { return nil } else if root.Val == val { return root } if root.Val < val { return searchBST(root.Right, val) } else { return searchBST(root.Left, val) }}</code></pre><h3 id="插入"><a href="#插入" class="headerlink" title="插入"></a>插入</h3><pre><code>func insertIntoBST(root *TreeNode, val int) *TreeNode { // 找到空位置插入新节点 if root == nil { return &TreeNode{val,nil, nil} } // val小,则插入左子树 if val < root.Val { root.Left = insertIntoBST(root.Left, val) } else { root.Right = insertIntoBST(root.Right, val) } return root}</code></pre><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><pre><code>var pre = math.MinInt32-1func isValidBST(root *TreeNode) bool { // 初始化前驱节点的值,设置为最小 pre = math.MinInt32-1 return ValidBST(root)}// 搜索二叉树按照左-根-右的顺序遍历得到一定是一个有序数组func ValidBST(root *TreeNode) bool { // 节点为空,默认符合条件 if root == nil { return true } // 判断左子树 if !ValidBST(root.Left) { return false } // 如果当前节点的值比前驱节点的值小,则不合法 if root.Val <= pre { return false } // 更新pre的值 pre = root.Val // 判断右子树 return ValidBST(root.Right)}</code></pre><h3 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h3><pre><code>func deleteNode(root *TreeNode, key int) *TreeNode { // 如果节点是空,直接返回 if root == nil { return nil } // 假设已经找到了要删除的节点,执行删除操作 if root.Val == key { // 如果左子树为空或者右子树为空,则用另一个孩子直接接替自己的位置 if root.Left == nil { return root.Right } if root.Right == nil { return root.Left } // 左右子树都不为空,需要找到左子树中最大或者右子树中最小的节点替换自己 // 这里我们选择右子树中最小的节点 minNode := getMin(root.Right) // 用最小节点替换自己 root.Val = minNode.Val // 替换完成后把用于替换的节点删掉,防止重复 root.Right = deleteNode(root.Right, minNode.Val) } else if root.Val > key { // 目标值小于根节点的值,去左子树中找 root.Left = deleteNode(root.Left, key) } else { // 右子树中找 root.Right = deleteNode(root.Right, key) } return root}// 二叉搜索树中最小的节点一定是最左侧的节点func getMin(root *TreeNode) *TreeNode { for root.Left != nil { root = root.Left } return root}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><p>略。</p><h3 id="二叉搜索树的中序遍历是一个单调序列,可用于判定二叉搜索树的合法性。遇到需要用到全局变量的,要记得每次重置一下全局变量,而且重置全局变量的操作不能写在递归函数中,而需要写在主函数中。"><a href="#二叉搜索树的中序遍历是一个单调序列,可用于判定二叉搜索树的合法性。遇到需要用到全局变量的,要记得每次重置一下全局变量,而且重置全局变量的操作不能写在递归函数中,而需要写在主函数中。" class="headerlink" title="二叉搜索树的中序遍历是一个单调序列,可用于判定二叉搜索树的合法性。遇到需要用到全局变量的,要记得每次重置一下全局变量,而且重置全局变量的操作不能写在递归函数中,而需要写在主函数中。"></a>二叉搜索树的中序遍历是一个单调序列,可用于判定二叉搜索树的合法性。遇到需要用到全局变量的,要记得每次重置一下全局变量,而且重置全局变量的操作不能写在递归函数中,而需要写在主函数中。</h3><h3 id="删除操作需要考虑多种情况,从最简单的情况开始"><a href="#删除操作需要考虑多种情况,从最简单的情况开始" class="headerlink" title="删除操作需要考虑多种情况,从最简单的情况开始"></a>删除操作需要考虑多种情况,从最简单的情况开始</h3><ol><li>目标节点的左右子树为空</li><li>目标节点的左右子树不为空</li></ol><blockquote><p>注意寻找最左侧节点时需要一直向左子树循环找,也就是用for, 而不是if !</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> tree </tag>
<tag> 二叉搜索树 </tag>
</tags>
</entry>
<entry>
<title>【Go 源码解析14】调度器</title>
<link href="/2021/04/05/go-sourcecode-gmp/"/>
<url>/2021/04/05/go-sourcecode-gmp/</url>
<content type="html"><![CDATA[<blockquote><p>Go 语言在并发编程方面有强大的能力,这离不开语言层面对并发编程的支持。本节会介绍 Go 语言运行时调度器的实现原理,其中包含调度器的设计与实现原理、演变过程以及与运行时调度相关的数据结构。</p></blockquote><p><img src="https://vegard-bear.github.io/images/%E8%B0%83%E5%BA%A6%E5%99%A8GMP.png" alt></p><blockquote><p>点击图片查看高清大图</p></blockquote>]]></content>
<categories>
<category> go </category>
</categories>
<tags>
<tag> go </tag>
<tag> 源码解析 </tag>
<tag> GMP </tag>
</tags>
</entry>
<entry>
<title>【leetcode-460】LFU 缓存</title>
<link href="/2021/04/04/leetcode-460/"/>
<url>/2021/04/04/leetcode-460/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/lfu-cache/" target="_blank" rel="noopener">460. LFU 缓存</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。</p><p>实现 LFUCache 类:</p><ul><li>LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象</li><li>int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。</li><li>void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。<br>注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。</li></ul><p>为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。</p><p>当一个键首次插入到缓存中时,它的使用计数器被设置为 1(由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。</p><p>示例:</p><pre><code>输入:["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"][[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]输出:[null, null, null, 1, null, -1, 3, null, -1, 3, 4]解释:// cnt(x) = 键 x 的使用计数// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)LFUCache lFUCache = new LFUCache(2);lFUCache.put(1, 1); // cache=[1,_], cnt(1)=1lFUCache.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1lFUCache.get(1); // 返回 1 // cache=[1,2], cnt(2)=1, cnt(1)=2lFUCache.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小 // cache=[3,1], cnt(3)=1, cnt(1)=2lFUCache.get(2); // 返回 -1(未找到)lFUCache.get(3); // 返回 3 // cache=[3,1], cnt(3)=2, cnt(1)=2lFUCache.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用 // cache=[4,3], cnt(4)=1, cnt(3)=2lFUCache.get(1); // 返回 -1(未找到)lFUCache.get(3); // 返回 3 // cache=[3,4], cnt(4)=1, cnt(3)=3lFUCache.get(4); // 返回 4 // cache=[3,4], cnt(4)=2, cnt(3)=3</code></pre><p>提示:</p><ul><li>0 <= capacity, key, value <= 104</li><li>最多调用 105 次 get 和 put 方法</li></ul><ul><li>进阶:你可以为这两种操作设计时间复杂度为 O((1)) 的实现吗?</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>LFU缓存淘汰算法是非常经典的算法,它其实是LRU算法的增强版本,关于LRU算法,可参考考<a href="https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/" target="_blank" rel="noopener">LRU 策略详解和实现</a></p><h3 id="核心思想"><a href="#核心思想" class="headerlink" title="核心思想"></a>核心思想</h3><p>在解这道题的时候首先需要重点关注LFU算法和LRU算法的主要不同:</p><p><strong>LRU算法只是简单地将数据按照时间进行排序,借助链表就很容易实现,因为缓存淘汰的时候只要将链表尾部的元素淘汰就行了;而LFU算法则是先把数据按照访问次数进行排序,优先删除访问次数少的数据,当访问次数相同时,则删除最早插入的数据</strong></p><h3 id="逐步细化"><a href="#逐步细化" class="headerlink" title="逐步细化"></a>逐步细化</h3><p>然后,我们根据这个LFU算法的特点对需求进行进一步拆解和细化:</p><ol><li>需要快速知道当前的最小频次(用一个变量minfreq表示)</li><li>需要访问频次freq到key的映射(采用哈希表进行存储)</li><li>可能有多个key拥有相同的频次freq,所以freq对key是一对多的关系(一个freq对应一个key列表)</li><li>希望freq对应的列表是存在时序的,这样能够快速删除最旧的key(key列表可以采用链表的结构进行存储)</li><li>希望能够快速删除key列表中的任何一个key,因为频次为freq的某个key被访问后,它的频次将会+1,也就是说需要从当前的freq列表中删除,加入到对应频次为freq+1的列表中。</li></ol><p>除此之外,LFU算法还应该保留LRU算法中的key到value的映射关系,用一个哈希表表示就可以。</p><h3 id="定义数据结构"><a href="#定义数据结构" class="headerlink" title="定义数据结构"></a>定义数据结构</h3><p>搞清楚了以上需求,我们就可以定义一下数据结构了:</p><pre><code>// 定义节点类型type Node struct { key int val int freq int}type LFUCache struct { // key 到val的映射, 一个Element代表一个双向链表节点 keyToVal map[int]*list.Element // freq到key列表的映射,一个List代表一个双向链表 freqTokeys map[int]*list.List // 记录最小的访问频次 minFreq int // 记录LFU缓存的最大容量 cap int}</code></pre><h3 id="关键逻辑"><a href="#关键逻辑" class="headerlink" title="关键逻辑"></a>关键逻辑</h3><p>然后就是实现get和put两个方法了:</p><h4 id="get"><a href="#get" class="headerlink" title="get"></a>get</h4><ul><li>get的逻辑很简单,返回key对应的value,然后增加key对应的freq就好了</li></ul><pre><code>// Get// 获取值func (this *LFUCache) Get(key int) int { // 不存在时则返回-1 ele, ok := this.keyToVal[key] if !ok { return -1 } // 增加key对应的freq return this.updateFreq(ele)}</code></pre><p>更新freq的操作是LFU算法的核心,我们这里单独封装一个updateFreq实现,这样get的逻辑就会显得比较清晰。</p><h4 id="put"><a href="#put" class="headerlink" title="put"></a>put</h4><p>对于put方法,逻辑略微复杂,这里我们通过画张图进行整理:</p><p><img src="https://vegard-bear.github.io/images/LFU_put.png" alt></p><p>根据图可以直接写出put的逻辑:</p><pre><code>// Put// 存入新的值func (this *LFUCache) Put(key int, value int) { // 特殊情况处理 if this.cap <= 0 { return } // 如果key已存在,修改对应的val即可 if ele, ok := this.keyToVal[key]; ok { data := ele.Value.(*Node) data.val = value // 调整key的freq次数 this.updateFreq(ele) return } // key不存在则需要插入 // 容量已满的话需要淘汰一个freq最小的key if this.cap == len(this.keyToVal) { this.removeMinFreqKey() } // 创建新的节点 newNode := &Node{ key: key, val: value, freq: 1, } // 更新freq to key的链表 newList, ok := this.freqTokeys[newNode.freq] if !ok { // 不存在则创建一个链表 newList = list.New() this.freqTokeys[newNode.freq] = newList } // 更新KeyToVal表 newEle := newList.PushBack(newNode) this.keyToVal[key] = newEle this.minFreq = newNode.freq}</code></pre><h4 id="updateFreq"><a href="#updateFreq" class="headerlink" title="updateFreq"></a>updateFreq</h4><p>更新freq的方法简单来说就是按照以下步骤走:</p><ol><li>找到key所在的freq列表</li><li>将key从当前freq列表中删除</li><li>更新key的freq = freq+1</li><li>将更新后的元素插入到对应freq+1的列表中</li><li>同步更新keyToVal列表</li></ol><p>至此在补充一下淘汰一个freq最小的key方法removeMinFreqKey就行了,完整代码如题解所示。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 定义节点类型type Node struct { key int val int freq int}type LFUCache struct { // key 到val的映射, 一个Element代表一个双向链表节点 keyToVal map[int]*list.Element // freq到key列表的映射,一个List代表一个双向链表 freqTokeys map[int]*list.List // 记录最小的访问频次 minFreq int // 记录LFU缓存的最大容量 cap int}// Constructor// 初始化LFU缓存func Constructor(capacity int) LFUCache { return LFUCache{ keyToVal: map[int]*list.Element{}, freqTokeys: map[int]*list.List{}, minFreq: 0, cap: capacity, }}// Get// 获取值func (this *LFUCache) Get(key int) int { // 不存在时则返回-1 ele, ok := this.keyToVal[key] if !ok { return -1 } // 增加key对应的freq return this.updateFreq(ele)}// updateFreq// 访问key的同时需要增加对应freq的次数func (this *LFUCache) updateFreq(ele *list.Element) int { // 这里需要根据输入先造一个节点 data := ele.Value.(*Node) // 处理freq to key的映射关系 // 先找到这个freq对应的节点链表,如果没有,直接返回-1 curList, ok := this.freqTokeys[data.freq] if !ok { return -1 } // FK表中删除原有的记录 curList.Remove(ele) // 如果当前要更新的频率已经是最小频率了,那么最小频率也顺带更新了 if curList.Len() == 0 { if data.freq == this.minFreq { this.minFreq++ } } // 更新key与freq的关系,当key对应的freq增加时候,则存储到对应的链表中 data.freq++ newList, ok := this.freqTokeys[data.freq] // 如果不存在链表则创建 if !ok { newList = list.New() this.freqTokeys[data.freq] = newList } // 在链表后追加,注意返回值 newEle := newList.PushBack(data) // 更新keyToVal表中元素的freq this.keyToVal[data.key] = newEle return data.val}// Put// 存入新的值func (this *LFUCache) Put(key int, value int) { // 特殊情况处理 if this.cap <= 0 { return } // 如果key已存在,修改对应的val即可 if ele, ok := this.keyToVal[key]; ok { data := ele.Value.(*Node) data.val = value // 调整key的freq次数 this.updateFreq(ele) return } // key不存在则需要插入 // 容量已满的话需要淘汰一个freq最小的key if this.cap == len(this.keyToVal) { this.removeMinFreqKey() } // 创建新的节点 newNode := &Node{ key: key, val: value, freq: 1, } // 更新freq to key的链表 newList, ok := this.freqTokeys[newNode.freq] if !ok { // 不存在则创建一个链表 newList = list.New() this.freqTokeys[newNode.freq] = newList } // 更新KeyToVal表 newEle := newList.PushBack(newNode) this.keyToVal[key] = newEle this.minFreq = newNode.freq}// removeMinFreqKey// 淘汰一个freq最小的keyfunc (this *LFUCache) removeMinFreqKey() { // 先根据最小频率找到对应链表 minList, ok := this.freqTokeys[this.minFreq] if !ok { return } // 链表中的第一个元素自然就是最长时间未被使用的元素 delE := minList.Front() minList.Remove(delE) data := delE.Value.(*Node) // 同步更新KeyToVal表 delete(this.keyToVal, data.key)}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O(1)"><a href="#时间复杂度:O(1)" class="headerlink" title="时间复杂度:O(1)"></a>时间复杂度:O(1)</h3><p>get 时间复杂度 O(1),put 时间复杂度 O(1)。由于两个操作从头至尾都只利用了哈希表的插入删除还有链表的插入删除,且它们的时间复杂度均为 O(1),所以保证了两个操作的时间复杂度均为 O(1)。</p><h3 id="空间复杂度:O(capacity)"><a href="#空间复杂度:O(capacity)" class="headerlink" title="空间复杂度:O(capacity)"></a>空间复杂度:O(capacity)</h3><p>其中capacity为LFU的缓存容量。哈希表中不会存放超过缓存容量的键值对。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>LFU 的逻辑不难理解,但是写代码实现并不容易,因为要同时维护多个哈希表。</p><ol><li>不要企图上来就实现算法的所有细节,而应该自顶向下,逐步求精,先写清楚主函数的逻辑框架,然后再一步步实现细节。</li><li>搞清楚映射关系,如果我们更新了某个 key 对应的 freq,那么就要同步修改 KF 表和 FK 表,这样才不会出问题。</li><li>画图,画图,画图,重要的话说三遍,把逻辑比较复杂的部分用流程图画出来,然后根据图来写代码,可以极大减少出错的概率。</li></ol>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> LFU </tag>
</tags>
</entry>
<entry>
<title>【leetcode-146】LRU 缓存机制</title>
<link href="/2021/03/29/leetcode-146/"/>
<url>/2021/03/29/leetcode-146/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/lru-cache/" target="_blank" rel="noopener">146. LRU 缓存机制</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>运用你所掌握的数据结构,设计和实现一个 LRU ((最近最少使用)) 缓存机制 。<br>实现 LRUCache 类:</p><ul><li>LRUCache((int capacity)) 以正整数作为容量 capacity 初始化 LRU 缓存</li><li>int get((int key)) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。</li><li>void put((int key, int value)) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。</li></ul><p>进阶:你是否可以在 O((1)) 时间复杂度内完成这两种操作?</p><p>示例:</p><p>输入</p><pre><code>["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"][[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]</code></pre><p>输出</p><pre><code>[null, null, null, 1, null, -1, null, -1, 3, 4]</code></pre><p>解释</p><pre><code>LRUCache lRUCache = new LRUCache(2);lRUCache.put(1, 1); // 缓存是 {1=1}lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}lRUCache.get(1); // 返回 1lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}lRUCache.get(2); // 返回 -1 (未找到)lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}lRUCache.get(1); // 返回 -1 (未找到)lRUCache.get(3); // 返回 3lRUCache.get(4); // 返回 4</code></pre><p>提示:</p><ul><li>1 <= capacity <= 3000</li><li>0 <= key <= 3000</li><li>0 <= value <= 104</li><li>最多调用 3 * 104 次 get 和 put</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>LRU缓存淘汰算法本质就是双向链表+哈希表,具体题解可参考<a href="https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/" target="_blank" rel="noopener">LRU 策略详解和实现</a></p><p>整体难度倒不是很大,主要是同时考察了双向链表和哈希表的性质,需要注意的细节和坑比较多:</p><ul><li>双向链表头尾两个节点是不做实际用途的,只是用于标记头尾</li><li>最近访问的在前面(靠近队头)</li><li>每次更新链表((插入和删除))的同时,需要注意同步更新哈希表</li><li>双向链表自带头节点和尾节点,删除节点的时候注意不能直接删除tail节点,而是删除tail.pre</li><li>对于key相同,value不同的情况在put的时候需要特别注意</li></ul><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 定义双向链表节点type LinkNode struct { // key用作哈希表中的键,val是节点的实际值 key, val int // 前驱和后趋节点 pre, next *LinkNode}type LRUCache struct { // 哈希表 m map[int]*LinkNode // 记录一下容量,防止超出 cap int // 用于标记头尾节点 head, tail *LinkNode}func Constructor(capacity int) LRUCache { // 初始化双向链表的数据 head := &LinkNode{0,0, nil, nil} tail := &LinkNode{0, 0, nil, nil} // 连接指针 head.next = tail tail.pre = head // 返回构造好的实例 return LRUCache{make(map[int]*LinkNode), capacity, head, tail}}func (this *LRUCache) Get(key int) int { // 先检查该元素是否存在 if v, ok := this.m[key]; ok { // 如果存在,直接将该节点移动到队头 this.moveToHead(v) return v.val } // 不存在直接返回-1 return -1}// 增加一个节点func (this *LRUCache) addNode(node *LinkNode) { // 修改新增节点前后指针 node.next = this.head.next node.pre = this.head // 将相邻节点指针指向新增节点 this.head.next = node node.next.pre = node // 注意需要同步更新map this.m[node.key] = node}// 删除一个节点func (this *LRUCache) removeNode(node *LinkNode)() { // 修改相邻节点指针 node.pre.next = node.next node.next.pre = node.pre // 注意需要在map中同步删除key才算完全删除 delete(this.m, node.key)}// 将节点移动到队头func (this *LRUCache) moveToHead(node *LinkNode) { // 先删除这个节点 this.removeNode(node) // 再在队头新增这个节点 this.addNode(node)}func (this *LRUCache) Put(key int, value int) { // 先检查该元素是否存在 if v, ok := this.m[key]; ok { // 如果存在,先将该节点移动到队头,表示最近访问 this.moveToHead(v) // 这里需要特别注意key相同,但是value不同的情况 // 这种情况意味着更新了key对应的value // 当然也可以先删掉原来的key,再重新插入,经测试,直接更新value值时间开销更小 if v.val != value { v.val = value } return } // 不存在则新增一个节点 node := &LinkNode{key, value, nil, nil} // 新增前需要考虑容量是否已满 if len(this.m) == this.cap { // 删除最后一个节点,要删除的实际有价值节点是tail.pre this.removeNode(this.tail.pre) } // 增加节点 this.addNode(node) // 注意需要更新map this.m[key] = node}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-1"><a href="#时间复杂度:O-1" class="headerlink" title="时间复杂度:O((1))"></a>时间复杂度:O((1))</h3><p>链表增删和哈希表查找的时间复杂度都是O((1)),因此对于put和get都是O((1))</p><h3 id="空间复杂度:O-capacity"><a href="#空间复杂度:O-capacity" class="headerlink" title="空间复杂度:O((capacity))"></a>空间复杂度:O((capacity))</h3><p>O((capacity)),因为哈希表和双向链表最多存储 capacity+1 个元素</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>LRU缓存淘汰算法原理不难,它同时考察了双向链表+哈希表两个知识点,因此需要对这两种数据结构的操作比较熟悉,一些细节问题尤其需要注意。</p><h4 id="本题主要难点如下:"><a href="#本题主要难点如下:" class="headerlink" title="本题主要难点如下:"></a>本题主要难点如下:</h4><ol><li>构造数据结构时注意需要哈希表+双向链表。</li><li>构造函数需要把所有数据结构都初始化了</li><li>双向链表的插入和删除操作,插入改四个指针,删除改两个。</li><li>无论是get还是put都要将被访问的节点放到队头。</li><li>哈希表和链表要记得同步更新。</li></ol><blockquote><p>对于双向链表的定义需要特别注意,双向链表节点中需要同时定义key和value,然后在外部通过头尾节点标记一个双向链表。</p></blockquote>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> LRU </tag>
</tags>
</entry>
<entry>
<title>【Go源码解析13】上下文context</title>
<link href="/2021/03/15/go-sourcecode-context/"/>
<url>/2021/03/15/go-sourcecode-context/</url>
<content type="html"><![CDATA[<p>上下文 context.Context 是 Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。</p><p><img src="https://vegard-bear.github.io/images/%E4%B8%8A%E4%B8%8B%E6%96%87context.png" alt></p><blockquote><p>点击图片查看高清大图</p></blockquote>]]></content>
<categories>
<category> go </category>
</categories>
<tags>
<tag> go </tag>
<tag> 源码解析 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-494】目标和</title>
<link href="/2021/03/15/leetcode-494/"/>
<url>/2021/03/15/leetcode-494/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/target-sum/" target="_blank" rel="noopener">494. 目标和</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。</p><p>返回可以使最终数组和为目标数 S 的所有添加符号的方法数。</p><p>示例:</p><pre><code>输入:nums: [1, 1, 1, 1, 1], S: 3输出:5解释:-1+1+1+1+1 = 3+1-1+1+1+1 = 3+1+1-1+1+1 = 3+1+1+1-1+1 = 3+1+1+1+1-1 = 3一共有5种方法让最终目标和为3。</code></pre><p>提示:</p><ul><li>数组非空,且长度不会超过 20 。</li><li>初始的数组的和不会超过 1000 。</li><li>保证返回的最终结果能被 32 位整数存下。</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这道题我们将给出两个题解决,第一种是常规的动态规划(第一反应应该想到的),第二种是将题意转化成【子集背包问题】,然后再套用背包算法框架解决。</p><p>动态规划首先需要确定状态和选择,在这道题中,状态就是遍历到第几个数(数组的下标),选择就是给每个数字选择“+”还是“-”号。此外还可以通过设置备忘录消除重叠子问题。</p><p>具体题解可参考<a href="https://leetcode-cn.com/problems/target-sum/solution/dong-tai-gui-hua-he-hui-su-suan-fa-dao-di-shui-shi/" target="_blank" rel="noopener">动态规划和回溯算法对比</a></p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><h2 id="传统动态规划"><a href="#传统动态规划" class="headerlink" title="传统动态规划"></a>传统动态规划</h2><pre><code>// 利用备忘录消除重叠子问题var memo = map[string]int{}func findTargetSumWays(nums []int, S int) int { memo = map[string]int{} // 如果数组为空,直接返回 if len(nums) == 0 { return 0 } // 从nums[0]开始选择,初始状态需要凑的数字为S return dp(nums, 0, S)}func dp(nums []int, i, rest int) int { // 定义base case // 如果已经遍历完所有的数字 if i == len(nums) { // 此时刚好凑成需要的数字 if rest == 0 { // 那么说明已经找到了一种符合要求的组合,直接返回1 return 1 } // 用了所有的数字仍然没刚好凑足数字,返回0 return 0 } // 如果已经计算过,直接返回 // 这里采用拼凑的方法得到key,memo[key]表示遍历到nums[i]开始算,有多少种组合可以凑成rest // 注意这里将int转成string不能直接用string(),否则会转成空 key := strconv.Itoa(i) + "," + strconv.Itoa(rest) if n, ok := memo[key]; ok { return n } // 推导状态转移方程,其实就是选择"+"还是"-" // 注意这里是将两个组合相加而不是选最优 result := dp(nums, i + 1, rest - nums[i]) + dp(nums, i + 1, rest + nums[i]) // 将计算过的结果添加到备忘录中 memo[key] = result return result}</code></pre><h2 id="背包问题解法"><a href="#背包问题解法" class="headerlink" title="背包问题解法"></a>背包问题解法</h2><pre><code>func findTargetSumWays(nums []int, S int) int { // 转化为背包问题,重新定义target sum := 0 for _, num := range nums { sum += num } target := (S + sum)/2 // 注意不管如何组合,最终的结果总是在[-sum,sum]间波动, // 也就是说一个合法的S必然满足 -sum<=S<=sum, 所以 S+sum 必然大于等于0 if target < 0 { return 0 } // 注意,这两种情况没法进行子集划分,不符合背包问题,需要排除 if sum < S || (S + sum)%2 == 1 { return 0 } // 定义dp数组,dp[i][j]表示对于前i个物品,恰好装满容量为j的背包有几种组合 n := len(nums) dp := make([][]int, n + 1) for i := 0; i <= n; i++ { dp[i] = make([]int, target + 1) } // 定义base case // dp[0][...]=0表示物品个数为0时没有能装满背包的方法 // dp[...][0]=1表示背包容量为0时,什么都不用做也算一种组合 for i := 0; i <= n; i++ { dp[i][0] = 1 } // 推导状态转移方程,注意这里的i是从1开始算的,那么nums[i-1]就表示第i个物品 // 注意,这道题和常规的背包问题有所不同,j是从0开始的 // 因为dp[...][0]不能简单的认为就是1,就是说可能存在x个物品,这x个物品的重量都为0, // 那么当背包的承重为0的时候,是可以在这x个物品中任选来实现的,所以在求dp数组的时候,j是要从0开始计算的 for i := 1; i <= n; i++ { for j := 0; j <= target; j++ { // 当前物品太大,放不进背包,肯定不放 if j - nums[i-1] < 0 { dp[i][j] = dp[i-1][j] } else { // 可以选择放或者不放,两种选择之和 dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]] } } } return dp[n][target]}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n-S"><a href="#时间复杂度:O-n-S" class="headerlink" title="时间复杂度:O((n*S))"></a>时间复杂度:O((n*S))</h3><p>n为数组的元素个数</p><h3 id="空间复杂度:O-n-S"><a href="#空间复杂度:O-n-S" class="headerlink" title="空间复杂度:O((n*S))"></a>空间复杂度:O((n*S))</h3><p>需要开辟一个大小为n*S的二维数组</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>回溯算法和动态规划其实有很多相似之处,对于大多数题目可以相互转化。对于第二种解法,可以考虑状态压缩,也就是将二维数组压缩成一维数组,可以进一步优化空间复杂度。</p><p>这道题关键是怎么转化为子集背包问题,尤其是开始几种特殊情况的判定很容易漏:</p><ol><li>背包都是整数</li><li>背包都是非负数</li></ol><h4 id="还需要注意的一点就是和背包问题不同的是,j-0的时候并不都是只有一种装法,这里可能存在0,0,0,0,0,0这种多个物品重量为0的情况,而给这些数加正负号都算是不同的组合,所以推导状态转移方程的时候需要从j-0开始。"><a href="#还需要注意的一点就是和背包问题不同的是,j-0的时候并不都是只有一种装法,这里可能存在0,0,0,0,0,0这种多个物品重量为0的情况,而给这些数加正负号都算是不同的组合,所以推导状态转移方程的时候需要从j-0开始。" class="headerlink" title="还需要注意的一点就是和背包问题不同的是,j=0的时候并不都是只有一种装法,这里可能存在0,0,0,0,0,0这种多个物品重量为0的情况,而给这些数加正负号都算是不同的组合,所以推导状态转移方程的时候需要从j=0开始。"></a>还需要注意的一点就是和背包问题不同的是,j=0的时候并不都是只有一种装法,这里可能存在0,0,0,0,0,0这种多个物品重量为0的情况,而给这些数加正负号都算是不同的组合,所以推导状态转移方程的时候需要从j=0开始。</h4>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 动态规划 </tag>
<tag> 背包 </tag>
</tags>
</entry>
<entry>
<title>【Go源码解析12】make和new</title>
<link href="/2021/03/11/go-sourcecode-make-new/"/>
<url>/2021/03/11/go-sourcecode-make-new/</url>
<content type="html"><![CDATA[<blockquote><p>当我们想要在 Go 语言中初始化一个结构时,可能会用到两个不同的关键字 — make 和 new。因为它们的功能相似,所以初学者可能会对这两个关键字的作用感到困惑1,但是它们两者能够初始化的变量却有较大的不同。</p></blockquote><p><img src="https://vegard-bear.github.io/images/make%E5%92%8Cnew.png" alt></p><blockquote><p>点击图片查看高清大图</p></blockquote>]]></content>
<categories>
<category> go </category>
</categories>
<tags>
<tag> go </tag>
<tag> 源码解析 </tag>
</tags>
</entry>
<entry>
<title>【leetcode-337】打家劫舍III</title>
<link href="/2021/03/11/leetcode-337/"/>
<url>/2021/03/11/leetcode-337/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/house-robber-iii/" target="_blank" rel="noopener">337. 打家劫舍 III</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。</p><p>计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。</p><p>示例 1:</p><pre><code>输入: [3,2,3,null,3,null,1] 3 / \ 2 3 \ \ 3 1输出: 7 解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.</code></pre><p>示例 2:</p><pre><code>输入: [3,4,5,1,3,null,1] 3 / \ 4 5 / \ \ 1 3 1输出: 9解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.</code></pre><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这是打家劫舍系列的第三题,但这次不是数组了,是二叉树了。</p><p>对于二叉树来说,相邻的房子的索引不再是i-1或者是i-1了,而是父节点和左右子节点,所以这里我们只需要在<a href="https://leetcode-cn.com/problems/house-robber/" target="_blank" rel="noopener">198. 打家劫舍</a>的基础上改变一下遍历的顺序就行了,按照二叉树的顺序遍历而不是线性表。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>// 定义备忘录var memo = map[*TreeNode]int{}func rob(root *TreeNode) int { // 定义base case if root == nil { return 0 } // 利用备忘录消除重叠子问题 if _, ok := memo[root]; ok { return memo[root] } // 做选择 // 选当前节点,那么只能隔一层选下下层 do := root.Val if root.Left != nil { do += rob(root.Left.Left) + rob(root.Left.Right) } if root.Right != nil { do += rob(root.Right.Left) + rob(root.Right.Right) } // 不选当前节点,可以选下一层 notDo := rob(root.Left) + rob(root.Right) // 选其中较大者 res := max(do, notDo) // 记录备忘录 memo[root] = res return res}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>n为树的节点数</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>需要开辟一个大小为n的memo数组。</p><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>这道题其实还是蛮有新意的,可以认为是二叉树数据结构的动态规划,其本质还是一个二叉树的前序遍历,只是在遍历的过程中进行选择,此外还加入了备忘录优化效率。</p><h3 id="当思路没有问题,但是提交后显示超出时间限制,可以考虑是否加入备忘录进行优化,消除重叠子问题,否则会导致递归层数太多,超出时间限制"><a href="#当思路没有问题,但是提交后显示超出时间限制,可以考虑是否加入备忘录进行优化,消除重叠子问题,否则会导致递归层数太多,超出时间限制" class="headerlink" title="当思路没有问题,但是提交后显示超出时间限制,可以考虑是否加入备忘录进行优化,消除重叠子问题,否则会导致递归层数太多,超出时间限制"></a>当思路没有问题,但是提交后显示超出时间限制,可以考虑是否加入备忘录进行优化,消除重叠子问题,否则会导致递归层数太多,超出时间限制</h3>]]></content>
<categories>
<category> leetcode </category>
</categories>
<tags>
<tag> leetcode </tag>
<tag> 动态规划 </tag>
<tag> tree </tag>
</tags>
</entry>
<entry>
<title>【leetcode-213】打家劫舍II</title>
<link href="/2021/03/11/leetcode-213/"/>
<url>/2021/03/11/leetcode-213/</url>
<content type="html"><![CDATA[<h1 id="题目链接"><a href="#题目链接" class="headerlink" title="题目链接"></a>题目链接</h1><p><a href="https://leetcode-cn.com/problems/house-robber-ii/" target="_blank" rel="noopener">213. 打家劫舍 II</a></p><h1 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h1><p>你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。</p><p>给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。</p><p>示例 1:</p><pre><code>输入:nums = [2,3,2]输出:3解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。</code></pre><p>示例 2:</p><pre><code>输入:nums = [1,2,3,1]输出:4解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。</code></pre><p>示例 3:</p><pre><code>输入:nums = [0]输出:0</code></pre><p>提示:</p><ul><li>1 <= nums.length <= 100</li><li>0 <= nums[i] <= 1000</li></ul><h1 id="解题思路"><a href="#解题思路" class="headerlink" title="解题思路"></a>解题思路</h1><p>这是打家劫舍系列的第二题,和<a href="https://leetcode-cn.com/problems/house-robber/" target="_blank" rel="noopener">198. 打家劫舍</a>唯一的区别就是房子不再是一排,而是一个圈了。</p><p>围成一个圈和一排的唯一区别就在于首尾两个房子也算是相邻的了,不能同时取到。那么很简单,我们要做的就是在之前的基础上单独处理以下首尾的情况就好了。</p><p>首尾不能同时取钱,可以分为三种情况:</p><ol><li>第一间房子和最后一间房子都不取钱。</li><li>取第一间房子,不取最后一间房子。</li><li>取最后一间房子,不取第一间房子。</li></ol><p>那么我们只要穷举这三种情况,进行最优选择就好了。这里需要注意的是,其实第二和第三种情况已经覆盖了第一种情况。</p><h1 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h1><pre><code>func rob(nums []int) int { n := len(nums) // 环形数组,对于只有一个元素的特殊情况需要单独考虑 if n == 1 { return nums[0] } // 因为是环形数组,首尾两端不能同时选 return max(dp(nums[1:]), dp(nums[:n-1]))}func dp(nums []int) int { n := len(nums) // 定义dp数组,dp[i]表示从第i个房子开始选择所能获得的最大金额 dp := make([]int, n + 2) // 确定base case // dp[n+1] = 0, 0 // 推导状态转移方程 for i := n - 1; i >=0; i-- { // 要么选第i个房子,要么不选第i个房子 dp[i] = max(dp[i+1], nums[i] + dp[i+2]) } return dp[0]}</code></pre><h1 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h1><h3 id="时间复杂度:O-n"><a href="#时间复杂度:O-n" class="headerlink" title="时间复杂度:O((n))"></a>时间复杂度:O((n))</h3><p>这里就一层循环,遍历了一下所有的房子,所以算法的总时间复杂度是 O((n))</p><h3 id="空间复杂度:O-n"><a href="#空间复杂度:O-n" class="headerlink" title="空间复杂度:O((n))"></a>空间复杂度:O((n))</h3><p>需要开辟一个大小为n的dp数组。</p><blockquote><p>值得注意的是,我们其实可以通过状态压缩进一步将空间复杂度压缩到常数级,也就是O((1)),不过写出来的代码就有点不好理解了。</p></blockquote><h1 id="反思"><a href="#反思" class="headerlink" title="反思"></a>反思</h1><p>打家劫舍系列很经典,比如这道题就是结合了环形数组。</p><h4 id="这道题的巧妙之处在于可以通过去掉首位元素模拟首尾元素不能重复取到的情况,然后复用线性条件下的rob方法。"><a href="#这道题的巧妙之处在于可以通过去掉首位元素模拟首尾元素不能重复取到的情况,然后复用线性条件下的rob方法。" class="headerlink" title="这道题的巧妙之处在于可以通过去掉首位元素模拟首尾元素不能重复取到的情况,然后复用线性条件下的rob方法。"></a>这道题的巧妙之处在于可以通过去掉首位元素模拟首尾元素不能重复取到的情况,然后复用线性条件下的rob方法。</h4>]]></content>
<categories>
<category> leetcode </category>
</categories>