-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlocal-search.xml
1411 lines (681 loc) · 533 KB
/
local-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>dlopen动态加载共享库出现符号未定义错误</title>
<link href="/2024/05/13/dlopen%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%85%B1%E4%BA%AB%E5%BA%93%E5%87%BA%E7%8E%B0%E7%AC%A6%E5%8F%B7%E6%9C%AA%E5%AE%9A%E4%B9%89%E9%94%99%E8%AF%AF/"/>
<url>/2024/05/13/dlopen%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%85%B1%E4%BA%AB%E5%BA%93%E5%87%BA%E7%8E%B0%E7%AC%A6%E5%8F%B7%E6%9C%AA%E5%AE%9A%E4%B9%89%E9%94%99%E8%AF%AF/</url>
<content type="html"><![CDATA[<h1 id="dlopen动态加载共享库出现符号未定义错误"><a href="#dlopen动态加载共享库出现符号未定义错误" class="headerlink" title="dlopen动态加载共享库出现符号未定义错误"></a>dlopen动态加载共享库出现符号未定义错误</h1><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>有两个动态库,libfunc.so 和 libbase.so。其中 libfunc.so 依赖 libbase.so,在 libfunc.so 中有一个函数 <code>void func()</code> 调用了 libbase.so 中的函数 <code>void base()</code>,如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// libbase.so</span><br><span class="hljs-comment">// base.h</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> _base_h_</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> _base_h_</span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">base</span><span class="hljs-params">()</span>;<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// _base_h_</span></span><br><br><span class="hljs-comment">// base.c</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"base.h"</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><stdio.h></span></span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">base</span><span class="hljs-params">()</span> {<br> <span class="hljs-built_in">printf</span>(<span class="hljs-string">"base\n"</span>);<br>}<br></code></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// libfunc.so</span><br><span class="hljs-comment">// func.h</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> _func_h_</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> _func_h_</span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">func</span><span class="hljs-params">()</span>;<br><br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// _func_h_</span></span><br><br><span class="hljs-comment">// func.c</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"func.h"</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><stdio.h></span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"base.h"</span></span><br><br><span class="hljs-type">void</span> <span class="hljs-title function_">func</span><span class="hljs-params">()</span> {<br> <span class="hljs-built_in">printf</span>(<span class="hljs-string">"func\n"</span>);<br> base();<br>}<br></code></pre></td></tr></table></figure><p>在 main.c 中通过 <code>dlopen</code> 函数动态加载 libfunc.so,并调用其中的 <code>func</code> 函数:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">// main.c</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><dlfcn.h></span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><stdio.h></span></span><br><br><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span> {<br> <span class="hljs-type">void</span> *handle = dlopen(<span class="hljs-string">"./libfunc.so"</span>, RTLD_LAZY);<br> <span class="hljs-keyword">if</span> (!handle) {<br> <span class="hljs-built_in">fprintf</span>(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"%s\n"</span>, dlerror());<br> <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;<br> }<br><br> <span class="hljs-type">void</span> (*func)() = dlsym(handle, <span class="hljs-string">"func"</span>);<br> <span class="hljs-keyword">if</span> (!func) {<br> <span class="hljs-built_in">fprintf</span>(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"%s\n"</span>, dlerror());<br> <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;<br> }<br><br> func();<br><br> dlclose(handle);<br><br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>}<br></code></pre></td></tr></table></figure><p>但是在编译后提示 <code>undefined symbol: base</code> 错误:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [14:53:36] dl ➜ gcc -O0 -g main.c -o test -ldl <br>code-server [15:01:03] dl ➜ ./test <br>func<br>./test: symbol lookup error: ./libfunc.so: undefined symbol: base<br></code></pre></td></tr></table></figure><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><p>由于 main.c 中并没有显式依赖 libfunc.so 和 libbase.so,所以在编译 main.c 的命令中没有进行显式链接这两个库。因此在运行时,<code>dlopen</code> 函数只加载了 libfunc.so,而没有加载 libbase.so,导致在 libfunc.so 中调用 libbase.so 中的函数时出现符号未定义错误。</p><p>那么我们尝试将这两个库都进行显式链接,看看能否解决问题:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:03:42] dl ➜ gcc -O0 -g main.c -o test -ldl -L. -lfunc -lbase<br>code-server [15:05:27] dl ➜ ./test <br>func<br>./test: symbol lookup error: ./libfunc.so: undefined symbol: base<br></code></pre></td></tr></table></figure><p>可以看到仍然提示 <code>base</code> 符号未定义。</p><h2 id="根因分析"><a href="#根因分析" class="headerlink" title="根因分析"></a>根因分析</h2><p>由于 main.c 通过 <code>dlopen()</code> 加载 libfunc.so,所以不存在显式的依赖关系,我们甚至不需要在 main.c 中包含 func.h 这个头文件。因此在链接过程中,链接器会认为当前的 libfunc.so 和 main.o 无关,并将 libfunc.so 给优化掉。既然 libfunc.so 被视为无关项,那 libbase.so 更没有理由被留下来了。</p><p>也就是说,即使我们在命令中显式地去链接 <code>-L. -lfunc -lbase</code> 也是无效的。我们可以通过 <code>readelf -d</code> 命令查看 test 可执行文件的依赖:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:13:40] dl ➜ readelf -d test | grep -i 'share'<br> 0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]<br> 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]<br></code></pre></td></tr></table></figure><p>因此当执行 <code>func()</code> 函数时,由于 libbase.so 没有被加载,所以会提示 <code>undefined symbol: base</code> 错误。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>libfunc.so 可以通过 <code>dlopen()</code> 被加载,但是 libbase.so 本身与 main.c 无关,因此我们没有理由通过 <code>dlopen()</code> 来加载 libbase.so。</p><p>那有没有什么办法可以让 libbase.so 被加载进来呢?</p><h3 id="1、-使用-LD-PRELOAD-强制预加载"><a href="#1、-使用-LD-PRELOAD-强制预加载" class="headerlink" title="1、 使用 LD_PRELOAD 强制预加载"></a>1、 使用 LD_PRELOAD 强制预加载</h3><p><code>LD_PRELOAD</code> 是一个环境变量,用于在运行程序时预先加载指定的共享库。它的作用是在运行目标程序时优先加载指定的共享库,覆盖系统默认的库,从而实现对目标程序的功能增强、修改或者监控等目的。使用 LD_PRELOAD 可以在不修改目标程序源代码的情况下,对目标程序的行为进行改变。</p><p>我们可以通过设置 <code>LD_PRELOAD</code> 环境变量来强制预加载 libbase.so:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:18:27] dl ➜ LD_PRELOAD=./libbase.so ./test <br>func<br>base<br></code></pre></td></tr></table></figure><p>可以看到此时程序正常运行,<code>base</code> 函数也被调用了。</p><p>但是这样有个问题,如果 libfunc.so 依赖了多个共享库,那么我们就需要将所有的共享库都通过 <code>LD_PRELOAD</code> 预加载进来这样就会变得非常麻烦。</p><p>因此是否有其他的方法来解决这个问题呢?</p><h3 id="2-Wl-–no-as-needed-选项"><a href="#2-Wl-–no-as-needed-选项" class="headerlink" title="2. -Wl,–no-as-needed 选项"></a>2. -Wl,–no-as-needed 选项</h3><p>在部分默认情况下,链接器会尽可能地链接被指定的库,但如果这个库的符号没有被使用到,链接器也不会链接它,可以认为,链接器在默认状态下使用了 <code>--as-needed</code> 选项对库进行链接。</p><p>这种情况导致了我们即使显式链接 libfunc.so 和 libbase.so,由于 main.c 并没有使用这两个库符号,所以链接器会将这两个库优化掉。</p><p>因此我们可以通过 <code>-Wl,--no-as-needed</code> 选项来关闭这个优化,使得可执行文件强制保持这个依赖关系:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:46:29] dl ➜ gcc -O0 -g main.c -o test -ldl -L. -Wl,--no-as-needed -lfunc -lbase <br>code-server [15:46:34] dl ➜ ./test <br>func<br>base<br></code></pre></td></tr></table></figure><p>此时程序正常运行,我们再通过 <code>readelf -d</code> 命令查看 test 可执行文件的依赖:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:47:53] dl ➜ readelf -d test | grep -i 'share'<br> 0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]<br> 0x0000000000000001 (NEEDED) Shared library: [libfunc.so]<br> 0x0000000000000001 (NEEDED) Shared library: [libbase.so]<br> 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]<br></code></pre></td></tr></table></figure><p>可以看到此时 test 可执行文件已经显式依赖了 libfunc.so 和 libbase.so 两个共享库。</p><p>此时还有一个问题,我们将这个依赖关系交给了应用去链接处理,当模块数量较多,且各个模块有各自的依赖时,应用的链接命令会变得非常复杂,并且应用本身并不想关心各个模块自己的依赖关系,它只想维护自己关心的模块即可。</p><p>是否还有其他的方式进行优化解决呢?</p><h3 id="3-构建-libfunc-so-时指定依赖"><a href="#3-构建-libfunc-so-时指定依赖" class="headerlink" title="3. 构建 libfunc.so 时指定依赖"></a>3. 构建 libfunc.so 时指定依赖</h3><p>既然应用不想关心这些依赖关系,那么只能由 libfunc.so 自己来管理自己的依赖关系了。我们可以在构建 libfunc.so 时指定依赖的库,这样在链接 libfunc.so 时,链接器会自动将这些依赖的库链接进来。</p><p>我们只需要在编译 libfunc.so 时显式链接它的所有依赖库即可。我们先看看不链接依赖库时,libfunc.so 的信息:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:58:46] dl ➜ readelf -d libfunc.so | grep -i 'share'<br> 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]<br></code></pre></td></tr></table></figure><p>可以看到此时 libfunc.so 只依赖了 libc.so.6 这个库。</p><p>我们在编译命令上加上 <code>-lbase</code> 选项,显式链接 libbase.so:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [15:59:03] dl ➜ gcc -fPIC -shared -O0 -g func.c -I. -o libfunc.so -L. -lbase <br>code-server [15:59:50] dl ➜ readelf -d libfunc.so | grep -i 'share'<br> 0x0000000000000001 (NEEDED) Shared library: [libbase.so]<br> 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]<br></code></pre></td></tr></table></figure><p>此时 libfunc.so 显式依赖了 libbase.so 这个库。</p><p>我们再来编译运行一下 test 试试:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs shell">code-server [16:01:03] dl ➜ gcc -O0 -g main.c -o test -ldl<br>code-server [16:01:07] dl ➜ readelf -d test | grep -i 'share' <br> 0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]<br> 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]<br>code-server [16:01:13] dl ➜ ./test <br>func<br>base<br></code></pre></td></tr></table></figure><p>可以看到此时可执行文件 test 并没有显式依赖 libfunc.so 和 libbase.so,通过 <code>dlopen</code> 加载 libfunc.so 时,libbase.so 也被加载进来了,因此程序正常运行。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在使用 <code>dlopen</code> 函数动态加载共享库时,如果共享库之间存在依赖关系,需要注意以下几点:</p><ol><li>如果共享库之间存在依赖关系,可以通过 <code>LD_PRELOAD</code> 环境变量来强制预加载依赖库。</li><li>如果共享库之间存在依赖关系,可以通过 <code>-Wl,--no-as-needed</code> 选项来关闭链接器的优化,强制保持这个依赖关系。</li><li>如果共享库之间存在依赖关系,可以在构建共享库时显式链接它的依赖库,这样在加载共享库时,依赖库也会被加载进来。</li></ol><p>在模块化开发中,我们应该尽量使用第三种方法进行处理,将依赖关系交给模块自己去管理,而不是交给应用去处理。</p><p>以上就是本文的全部内容,希望对你有所帮助。</p>]]></content>
<categories>
<category>踩坑日记</category>
</categories>
<tags>
<tag>原创</tag>
<tag>dlopen</tag>
<tag>linux</tag>
<tag>符号未定义</tag>
<tag>c/c++</tag>
</tags>
</entry>
<entry>
<title>Emby Notifier 项目说明</title>
<link href="/2024/04/29/Emby%20Notifier%E9%A1%B9%E7%9B%AE%E8%AF%B4%E6%98%8E/"/>
<url>/2024/04/29/Emby%20Notifier%E9%A1%B9%E7%9B%AE%E8%AF%B4%E6%98%8E/</url>
<content type="html"><![CDATA[<h1 id="Emby-Notifier"><a href="#Emby-Notifier" class="headerlink" title="Emby Notifier"></a>Emby Notifier</h1><blockquote><p>这是另一个项目 <a href="https://github.com/Ccccx159/watchdog_for_Emby/tree/main">watchdog_for_Emby</a> 的最新优化版本,取消了 nfo 文件的监视依赖,该版本不再需要手动设置媒体库路径,对通过网盘挂载生成的媒体库更加友好~</p></blockquote><h2 id="Emby-Server-版本-重要!!!"><a href="#Emby-Server-版本-重要!!!" class="headerlink" title="Emby Server 版本 (重要!!!)"></a>Emby Server 版本 (重要!!!)</h2><p><font color=red><strong>4.8.0.80 及更新版本的 Emby Server!!!</strong></font></p><p>本项目是基于 Emby Server 官方插件 Webhooks 实现的,在 4.8.0.80 版本以前需要激活 Emby Premiere 才能使用 Webhooks 插件。</p><p>在 4.8.0.80 版本,Webhooks 被集成到控制台 “通知” 功能中,免费用户也可使用,因此建议使用本项目的朋友更新 Emby Server 到指定版本。</p><p><mark>需要注意的是,群晖套件中心的 Emby Server 最新在线版本为 4.7.14.0,因此需要 Emby 官方网站下载相应平台的安装包进行手动安装。</mark></p><h2 id="修订版本"><a href="#修订版本" class="headerlink" title="修订版本"></a>修订版本</h2><table><thead><tr><th>版本</th><th>日期</th><th>修订说明</th></tr></thead><tbody><tr><td>v1.0.0</td><td>2024.04.29</td><td><li>新增项目</li></td></tr></tbody></table><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p><strong>Emby Notifier</strong> 是一个基于 Emby Server Webhooks 实现的自动通知工具。Emby Server 通过 Webhooks 插件,可以在影片刮削完成后,自动推送事件到指定的 URL。本项目通过监听 Emby Server 推送的 Webhooks 事件,获取影片的基本信息,通过 TMDB 的 API 查询影片的详细信息,然后通过 Telegram Bot 推送至指定频道。</p><h2 id="环境变量和服务端口"><a href="#环境变量和服务端口" class="headerlink" title="环境变量和服务端口"></a>环境变量和服务端口</h2><p>端口:8000</p><table><thead><tr><th>参数</th><th>要求</th><th>说明</th></tr></thead><tbody><tr><td>TMDB_API_TOKEN</td><td>必须</td><td>Your TMDB API Token</td></tr><tr><td>TVDB_API_KEY</td><td>必须</td><td>Your TVDB API Key</td></tr><tr><td>TG_BOT_TOKEN</td><td>必须</td><td>Your Telegram Bot Tokne</td></tr><tr><td>TG_CHAT_ID</td><td>必须</td><td>Your Telegram Channel’s Chat ID</td></tr><tr><td>LOG_LEVEL</td><td>可选</td><td>日志等级 [DEBUG, INFO, WARNING] 三个等级,默认 WARNING</td></tr><tr><td>LOG_EXPORT</td><td>可选</td><td>日志写文件标志 [True, False] 是否将日志输出到文件,默认 False</td></tr><tr><td>LOG_PATH</td><td>可选</td><td>日志文件保存路径,默认 /var/tmp/emby_notifier_tg</td></tr></tbody></table><h2 id="docker-Run"><a href="#docker-Run" class="headerlink" title="docker Run"></a>docker Run</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker run -d --name=emby-notifier-tg --restart=unless-stopped \<br> -e TMDB_API_TOKEN=Your_TMDB_API_Token \<br> -e TVDB_API_KEY=Your_TVDB_API_Key \<br> -e TG_BOT_TOKEN=Your_Telegram_Bot_Token \<br> -e TG_CHAT_ID=Your_Telegram_Chat_ID \<br> -p 8000:8000 \<br> b1gfac3c4t/emby_notifier_tg:latest<br> <br></code></pre></td></tr></table></figure><h2 id="docker-compose"><a href="#docker-compose" class="headerlink" title="docker-compose"></a>docker-compose</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span><br><span class="hljs-attr">services:</span><br> <span class="hljs-attr">emby_notifier_tg:</span><br> <span class="hljs-attr">build:</span><br> <span class="hljs-attr">context:</span> <span class="hljs-string">.</span><br> <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">dockerfile</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">b1gfac3c4t/emby_notifier_tg:latest</span><br> <span class="hljs-attr">environment:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TZ=Asia/Shanghai</span><br> <span class="hljs-comment"># 这里所有的环境变量都不要使用引号</span><br> <span class="hljs-comment"># 必填参数</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TMDB_API_TOKEN=<Your</span> <span class="hljs-string">TMDB</span> <span class="hljs-string">API</span> <span class="hljs-string">Token></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TVDB_API_KEY=<Your</span> <span class="hljs-string">TVDB</span> <span class="hljs-string">API</span> <span class="hljs-string">Key></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TG_BOT_TOKEN=<Your</span> <span class="hljs-string">Telegram</span> <span class="hljs-string">Bot</span> <span class="hljs-string">Tokne></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TG_CHAT_ID=<Your</span> <span class="hljs-string">Telegram</span> <span class="hljs-string">Channel's</span> <span class="hljs-string">Chat</span> <span class="hljs-string">ID></span><br> <span class="hljs-comment"># 可选参数</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_LEVEL=WARNING</span> <span class="hljs-comment"># [DEBUG, INFO, WARNING] 三个等级,默认 WARNING</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_EXPORT=False</span> <span class="hljs-comment"># [True, False0] 是否将日志输出到文件,默认 False</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_PATH=/var/tmp/emby_notifier_tg/</span> <span class="hljs-comment"># 默认 /var/tmp/emby_notifier_tg/</span><br> <span class="hljs-attr">network_mode:</span> <span class="hljs-string">"bridge"</span><br> <span class="hljs-attr">ports:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">"8000:8000"</span><br> <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span><br></code></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker-compose up -d<br></code></pre></td></tr></table></figure><h2 id="Emby-Server-设置"><a href="#Emby-Server-设置" class="headerlink" title="Emby Server 设置"></a>Emby Server 设置</h2><ol><li><p>打开 Emby Server 控制台,点击左侧菜单栏的 “设置” -> “通知” -> “添加 Webhooks”</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/%E6%B7%BB%E5%8A%A0%E9%80%9A%E7%9F%A5.png" alt="添加通知"></p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/%E6%B7%BB%E5%8A%A0webhooks.png" alt="添加Webhooks"></p></li><li><p>在弹出的对话框中,填写 Webhooks 的 URL,例如:<code>http://192.168.1.100:8000</code>,选择数据类型为 <code>application/json</code></p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/%E9%85%8D%E7%BD%AEnotifier.png" alt="配置Webhooks"></p></li><li><p>点击 “发送测试通知” 按钮,观察 Notifier 的日志输出,如果输出了测试通知的信息,说明 Webhooks 设置成功</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/%E6%8E%A5%E5%8F%97%E6%B5%8B%E8%AF%95%E6%B6%88%E6%81%AF.png" alt="接收测试事件通知"></p><p> Notifier 日志中出现以下信息,说明 Webhooks 设置成功</p> <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">[WARNING] : Unsupported event type: system.notificationtest<br></code></pre></td></tr></table></figure></li><li><p>选择通知事件:媒体库 -> 新媒体已添加,点击保存</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/%E9%80%89%E6%8B%A9%E4%BA%8B%E4%BB%B6.png" alt="选择通知事件"></p></li></ol><h2 id="媒体信息检索流程"><a href="#媒体信息检索流程" class="headerlink" title="媒体信息检索流程"></a>媒体信息检索流程</h2><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/emby-telegram-notification/-/raw/main/doc/Emby_Notifier.drawio.png"></p><h2 id="局限性"><a href="#局限性" class="headerlink" title="局限性"></a>局限性</h2><p>Emby Server 的新媒体添加事件的触发时机受限于对新增文件的监视方式和扫描媒体库的频率,如果 Emby Server 触发新媒体添加事件,则 Notifier 也就无法推送通知。</p><h2 id="效果展示"><a href="#效果展示" class="headerlink" title="效果展示"></a>效果展示</h2><p>电影:</p><p><img src="https://user-images.githubusercontent.com/35327600/209752390-4e45180b-d8cc-4378-bd98-c489638f7cb7.png"></p><p>剧集:</p><p><img src="https://user-images.githubusercontent.com/35327600/209752275-bad230b0-97a7-47e5-9a77-081afae7d6cf.png"></p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li>tmdb api 文档:<a href="https://developers.themoviedb.org/3">https://developers.themoviedb.org/3</a></li><li>telegram bot api 文档:<a href="https://core.telegram.org/bots/api">https://core.telegram.org/bots/api</a></li></ul>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Emby</tag>
<tag>Notification</tag>
<tag>Webhook</tag>
<tag>Telegram</tag>
</tags>
</entry>
<entry>
<title>ELF 可执行文件被识别为共享目标文件</title>
<link href="/2024/04/25/ELF%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6%E8%A2%AB%E8%AF%86%E5%88%AB%E4%B8%BA%E5%85%B1%E4%BA%AB%E7%9B%AE%E6%A0%87%E6%96%87%E4%BB%B6/"/>
<url>/2024/04/25/ELF%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6%E8%A2%AB%E8%AF%86%E5%88%AB%E4%B8%BA%E5%85%B1%E4%BA%AB%E7%9B%AE%E6%A0%87%E6%96%87%E4%BB%B6/</url>
<content type="html"><![CDATA[<h1 id="ELF-可执行文件被识别为共享目标文件"><a href="#ELF-可执行文件被识别为共享目标文件" class="headerlink" title="ELF 可执行文件被识别为共享目标文件"></a>ELF 可执行文件被识别为共享目标文件</h1><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>个人项目中有一个解析 ELF 文件类型的功能,在测试过程中,错误地将可执行文件 (ET_EXEC) 识别为共享目标文件 (ET_DYN)。</p><p>如下图所示,exec_test 必然是一个可执行文件:</p><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/43dad8fbc3f5c70f79ab0c2d3724eefe/2024-04-25_155005.png"></p><p>但是当我无论使用 <code>readelf -h</code> 查看 exec_test 的头部信息,还是使用 <code>file</code> 命令查看文件类型,都显示这是一个共享目标文件:</p><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/371fe11454fc9e9404b2ee5e147c24a7/2024-04-25_155806.png"></p><p>通过上面第一张图片中的编译指令,可以看到仅仅使用了最常规的 <code>-g -O0 -o</code> 选项,并没有使用额外的编译或者链接选项。</p><h2 id="系统环境"><a href="#系统环境" class="headerlink" title="系统环境"></a>系统环境</h2><ul><li>OS: Ubuntu 20.04.6 LTS x86_64</li><li>Compiler: g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0</li></ul><h2 id="ELF-文件简单介绍"><a href="#ELF-文件简单介绍" class="headerlink" title="ELF 文件简单介绍"></a>ELF 文件简单介绍</h2><p>一般来说,ELF 文件有 4 种:</p><ul><li>可重定位文件 (ET_REL)</li><li>可执行文件 (ET_EXEC)</li><li>共享目标文件 (ET_DYN)</li><li>核心转储文件 (ET_CORE)</li></ul><table><thead><tr><th align="left">文件类型</th><th align="left">描述</th></tr></thead><tbody><tr><td align="left">可重定位文件 (ET_REL)</td><td align="left"><li>待重定位文件是由编译器产生的中间文件,包含了程序的机器代码、符号表、重定位信息等。</li><li>它是源代码编译后生成的目标文件,但还没有链接成可执行文件或共享目标文件。</li><li>待重定位文件中的地址信息仍然是相对地址,需要在链接时进行地址重定位。</li></td></tr><tr><td align="left">可执行文件 (ET_EXEC)</td><td align="left"><li>可执行文件包含了可以直接在操作系统上运行的程序代码和数据。</li><li>它可以被操作系统加载到内存中,并执行其中的指令。</li><li>在Unix/Linux系统中,可执行文件通常没有文件扩展名,但是在Windows系统中通常使用 .exe 扩展名。</li></td></tr><tr><td align="left">共享目标文件 (ET_DYN)</td><td align="left"><li>共享目标文件是包含了可重用代码和数据的文件,可以被多个可执行文件动态链接和共享使用。</li><li>它以一种与操作系统和其他程序共享的形式存在,可以在程序运行时动态加载到内存中。</li><li>共享目标文件通常具有文件扩展名 .so(Unix/Linux)或 .dll(Windows)。</li></td></tr><tr><td align="left">核心转储文件 (ET_CORE)</td><td align="left"><li>Core 文件是在程序崩溃或异常退出时由操作系统自动生成的一种文件。</li><li>它包含了程序崩溃时的内存快照信息,包括堆栈信息、寄存器状态等。</li><li>Core 文件通常用于调试程序崩溃的原因,可以通过调试工具分析其内容以定位问题。</li></td></tr></tbody></table><p>也可见下图:</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bab723b24d954cd39cf89370b95a1cdf~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp"></p><h2 id="问题原因"><a href="#问题原因" class="headerlink" title="问题原因"></a>问题原因</h2><p>在查找了资料后,得知是由于高版本 gcc/g++ 默认使用 <code>-pie</code> 选项,导致生成的可执行文件被识别为共享目标文件。</p><p><code>-pie</code> 选项是 Position Independent Executable 的缩写,即生成位置无关可执行文件。这种可执行文件可以被加载到任意地址运行,而不需要进行地址重定位。这样可以提高程序的安全性,防止恶意程序利用地址重定位漏洞进行攻击。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>解决方法有两种:</p><ol><li>使用 <code>-no-pie</code> 选项,禁用生成位置无关可执行文件:</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">g++ -g -O0 -no-pie -o exec_test test.cc<br></code></pre></td></tr></table></figure><ol start="2"><li>使用 <code>-fno-pie</code> 选项,禁用生成位置无关可执行文件:</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">g++ -g -O0 -fno-pie -o exec_test test.cc<br></code></pre></td></tr></table></figure><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/d79f24ba7a2a8e259a4011aad40cafb8/2024-04-25_163212.png"></p><p>可以看到,在添加了 <code>-no-pie</code> 选项后,exec_test 被正确识别为可执行文件。</p>]]></content>
<categories>
<category>踩坑日记</category>
</categories>
<tags>
<tag>原创</tag>
<tag>ELF</tag>
<tag>gcc</tag>
<tag>共享目标文件</tag>
<tag>链接</tag>
</tags>
</entry>
<entry>
<title>19. 删除链表的倒数第 N 个结点</title>
<link href="/2024/04/18/leetcode/19.%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACN%E4%B8%AA%E7%BB%93%E7%82%B9/"/>
<url>/2024/04/18/leetcode/19.%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACN%E4%B8%AA%E7%BB%93%E7%82%B9/</url>
<content type="html"><![CDATA[<h1 id="19-删除链表的倒数第-N-个结点"><a href="#19-删除链表的倒数第-N-个结点" class="headerlink" title="19. 删除链表的倒数第 N 个结点"></a>19. 删除链表的倒数第 N 个结点</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:head = [1,2,3,4,5], n = 2<br>输出:[1,2,3,5]<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:head = [1], n = 1<br>输出:[]<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:head = [1,2], n = 1<br>输出:[1]<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>链表中结点的数目为 sz</li><li>1 <= sz <= 30</li><li>0 <= Node.val <= 100</li><li>1 <= n <= sz</li></ul><p>进阶:你能尝试使用一趟扫描实现吗?</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:遍历-哈希表"><a href="#解法一:遍历-哈希表" class="headerlink" title="解法一:遍历 + 哈希表"></a>解法一:遍历 + 哈希表</h3><p>常规思路,基于单链表的性质只能<strong>从头到尾</strong>逐个遍历,因此倒数第 n 个节点,首先需要知道一共有几个节点即链表长度 sz,然后找到倒数第 n 个节点前驱节点,即倒数第 n+1 个节点,才能删除倒数第 n 个节点。</p><p>因此,我们可以遍历一遍链表,并使用哈希表来存储节点索引和节点地址,最后索引为 sz - n 的节点即为倒数第 n 个节点的前驱节点。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">ListNode* <span class="hljs-title">removeNthFromEnd</span><span class="hljs-params">(ListNode* head, <span class="hljs-type">int</span> n)</span> </span>{<br> std::unordered_map<<span class="hljs-type">int</span>, ListNode*> <span class="hljs-type">hash_t</span>;<br> <span class="hljs-type">int</span> sz = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">while</span>(head->next != <span class="hljs-literal">nullptr</span>) {<br> sz++;<br> <span class="hljs-type">hash_t</span>[sz] = head;<br> head = head->next;<br> }<br> <span class="hljs-keyword">if</span> (<span class="hljs-type">hash_t</span>.<span class="hljs-built_in">size</span>() - n == <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-type">hash_t</span>[<span class="hljs-number">2</span>];<br> <span class="hljs-type">hash_t</span>[<span class="hljs-type">hash_t</span>.<span class="hljs-built_in">size</span>() - n]->next = <span class="hljs-type">hash_t</span>[<span class="hljs-type">hash_t</span>.<span class="hljs-built_in">size</span>() - n]->next->next;<br> <span class="hljs-keyword">return</span> <span class="hljs-type">hash_t</span>[<span class="hljs-number">1</span>];<br>}<br></code></pre></td></tr></table></figure><p>当链表长度很长时,用于额外存储的哈希表也会占用较大一部分内存。</p><h3 id="解法二:双指针"><a href="#解法二:双指针" class="headerlink" title="解法二:双指针"></a>解法二:双指针</h3><p>双指针法,使用两个指针 <code>fast</code> 和 <code>slow</code>,<code>fast</code> 先走 n 步,然后 <code>fast</code> 和 <code>slow</code> 同时走,当 <code>fast</code> 走到链表尾部时,<code>slow</code> 指向的节点即为倒数第 n 个节点的前驱节点。</p><p>为了避免删除头结点的特殊情况,可以在头结点前加一个哑结点 dummy。这样无论删除的是头节点还是其余子节点,都可以用 <code>dummy->next</code> 统一表达。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">ListNode* <span class="hljs-title">removeNthFromEnd</span><span class="hljs-params">(ListNode* head, <span class="hljs-type">int</span> n)</span> </span>{<br> ListNode _Dummy(<span class="hljs-number">0</span>, head);<br> <span class="hljs-keyword">auto</span> slow = &_Dummy, fast = &_Dummy;<br> <span class="hljs-keyword">while</span> (n-- > <span class="hljs-number">0</span>) {<br> fast = fast->next;<br> }<br> <span class="hljs-keyword">while</span>(fast->next != <span class="hljs-literal">nullptr</span>) {<br> slow = slow->next;<br> fast = fast->next;<br> }<br> slow->next = slow->next->next;<br> <span class="hljs-keyword">return</span> _Dummy.next;<br>}<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>链表特性:只能从头到尾遍历,无法回溯;删除某个节点,需要找到待删除节点的前驱节点;</li><li>删除子节点和删除头节点的操作不一致,可使用增加一个虚假头节点进行统一处理,简化逻辑;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
<tag>单链表</tag>
</tags>
</entry>
<entry>
<title>18. 四数之和</title>
<link href="/2024/04/17/leetcode/18.%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C/"/>
<url>/2024/04/17/leetcode/18.%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C/</url>
<content type="html"><![CDATA[<h1 id="18-四数之和"><a href="#18-四数之和" class="headerlink" title="18. 四数之和"></a>18. 四数之和</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):</p><p>0 <= a, b, c, d < n<br>a、b、c 和 d 互不相同<br>nums[a] + nums[b] + nums[c] + nums[d] == target<br>你可以按<strong>任意顺序</strong>返回答案 。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [1,0,-1,0,-2,2], target = 0<br>输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [2,2,2,2,2], target = 8<br>输出:[[2,2,2,2]]<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= nums.length <= 200</li><li>$-10^9 <= nums[i] <= 10^9$</li><li>$-10^9 <= target <= 10^9$</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="排序-双指针"><a href="#排序-双指针" class="headerlink" title="排序 + 双指针"></a>排序 + 双指针</h3><p>解题思路同 <a href="./15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.md">15. 三数之和</a>。</p><p>固定元素由 1 个变成 2 个,然后另外两个双指针和三数之和的解法一样。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><code class="hljs cpp">std::vector<std::vector<<span class="hljs-type">int</span>>> <span class="hljs-built_in">fourSum</span>(std::vector<<span class="hljs-type">int</span>>& nums, <span class="hljs-type">int</span> target) {<br> <span class="hljs-keyword">if</span> (nums.<span class="hljs-built_in">size</span>() < <span class="hljs-number">4</span>) <span class="hljs-keyword">return</span> {};<br> std::vector<std::vector<<span class="hljs-type">int</span>>> res;<br> <span class="hljs-built_in">sort</span>(nums.<span class="hljs-built_in">begin</span>(), nums.<span class="hljs-built_in">end</span>());<br> <span class="hljs-keyword">if</span> ((<span class="hljs-type">long</span>)nums[<span class="hljs-number">0</span>] + nums[<span class="hljs-number">1</span>] + nums[<span class="hljs-number">2</span>] + nums[<span class="hljs-number">3</span>] > target) <span class="hljs-keyword">return</span> res;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">3</span>; i++) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && nums[i] == nums[i - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>; <br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = i + <span class="hljs-number">1</span>; j < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">2</span>; j++) {<br> <span class="hljs-keyword">if</span> (j > i+<span class="hljs-number">1</span> && nums[j] == nums[j - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>;<br> <span class="hljs-type">int</span> m = j + <span class="hljs-number">1</span>, n = nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>;<br> <span class="hljs-type">long</span> max_ = (<span class="hljs-type">long</span>)nums[i] + nums[j] + nums[n] + nums[n - <span class="hljs-number">1</span>];<br> <span class="hljs-type">long</span> min_ = (<span class="hljs-type">long</span>)nums[i] + nums[j] + nums[m] + nums[m + <span class="hljs-number">1</span>];<br> <span class="hljs-keyword">if</span> (target > max_) {<br> <span class="hljs-keyword">continue</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (target < min_) {<br> <span class="hljs-keyword">break</span>;<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">while</span> (m < n) {<br> <span class="hljs-type">long</span> sum = (<span class="hljs-type">long</span>)nums[i] + nums[j] + nums[m] + nums[n];<br> <span class="hljs-keyword">if</span> (sum == target) {<br> res.<span class="hljs-built_in">push_back</span>({nums[i], nums[j], nums[m], nums[n]});<br> }<br> <span class="hljs-keyword">if</span> (sum <= target) {<br> <span class="hljs-type">int</span> x = nums[m];<br> <span class="hljs-keyword">while</span> (x == nums[++m] && m < n);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-type">int</span> x = nums[n];<br> <span class="hljs-keyword">while</span> (x == nums[--n] && m < n);<br> }<br> }<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>元素最大值可能溢出,需要转换为 <code>long</code> 类型;</li><li>可计算当前 i 和 j 对应的最大值和最小值进行优化剪枝,提前结束循环;s’s’s</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
<tag>整数溢出</tag>
</tags>
</entry>
<entry>
<title>17. 电话号码的字母组合</title>
<link href="/2024/04/17/leetcode/17.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/"/>
<url>/2024/04/17/leetcode/17.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88/</url>
<content type="html"><![CDATA[<h1 id="17-电话号码的字母组合"><a href="#17-电话号码的字母组合" class="headerlink" title="17. 电话号码的字母组合"></a>17. 电话号码的字母组合</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。</p><p>给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:digits = "23"<br>输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:digits = ""<br>输出:[]<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:digits = "2"<br>输出:["a","b","c"]<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>0 <= digits.length <= 4</li><li>digits[i] 是范围 [‘2’, ‘9’] 的一个数字。</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:迭代"><a href="#解法一:迭代" class="headerlink" title="解法一:迭代"></a>解法一:迭代</h3><p>将问题转化为:每增加一个数字,就在之前的结果集中的每个字符串后面加上这个数字对应的字母。</p><p>例如 digits = “23”:</p><ol><li>当 digits[0] == ‘2’ 时,res = [“a”, “b”, “c”]</li><li>当 digits[1] == ‘3’ 时,向res中追加 “def”,得到新的 res’ = [“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br> <span class="hljs-keyword">public</span>:<br> <span class="hljs-function">std::vector<std::string> <span class="hljs-title">letterCombinations</span><span class="hljs-params">(std::string digits)</span> </span>{<br> std::vector<std::string> res;<br> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> dig : digits) {<br> res = <span class="hljs-built_in">letComb</span>(dig, res);<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br><br> <span class="hljs-keyword">private</span>:<br> <span class="hljs-function">std::vector<std::string> <span class="hljs-title">letComb</span><span class="hljs-params">(<span class="hljs-type">char</span> dig,</span></span><br><span class="hljs-params"><span class="hljs-function"> std::vector<std::string>& last_res)</span> </span>{<br> std::vector<std::string> res;<br> <span class="hljs-comment">// 遍历当前数字对应的字母</span><br> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> ch : cell_map[dig]) {<br> <span class="hljs-comment">// 向上一次结果进行追加,并将追加结果填充到当前的新结果中</span><br> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> str : last_res) {<br> res.<span class="hljs-built_in">push_back</span>(str + ch);<br> }<br> <span class="hljs-keyword">if</span> (last_res.<span class="hljs-built_in">size</span>() == <span class="hljs-number">0</span>) {<br> res.<span class="hljs-built_in">push_back</span>(std::<span class="hljs-built_in">string</span>(<span class="hljs-number">1</span>, ch));<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br><br> std::unordered_map<<span class="hljs-type">char</span>, std::string> cell_map = {<br> {<span class="hljs-string">'2'</span>, <span class="hljs-string">"abc"</span>}, {<span class="hljs-string">'3'</span>, <span class="hljs-string">"edf"</span>}, {<span class="hljs-string">'4'</span>, <span class="hljs-string">"ghi"</span>}, {<span class="hljs-string">'5'</span>, <span class="hljs-string">"jkl"</span>},<br> {<span class="hljs-string">'6'</span>, <span class="hljs-string">"mno"</span>}, {<span class="hljs-string">'7'</span>, <span class="hljs-string">"pqrs"</span>}, {<span class="hljs-string">'8'</span>, <span class="hljs-string">"tuv"</span>}, {<span class="hljs-string">'9'</span>, <span class="hljs-string">"wxzy"</span>}};<br>};<br></code></pre></td></tr></table></figure><h3 id="解法二:回溯"><a href="#解法二:回溯" class="headerlink" title="解法二:回溯"></a>解法二:回溯</h3><p>这是一个典型的<strong>回溯穷举问题</strong>。</p><p>每次递归时,将当前数字对应的字母依次加入到当前字符串中,当字符串长度等于 digits 时,将当前字符串加入到结果集中,然后回溯,将当前字符串的最后一个字符删除。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">std::vector<std::string> <span class="hljs-title">letterCombinations</span><span class="hljs-params">(std::string digits)</span> </span>{<br> std::vector<std::string> res;<br> <span class="hljs-keyword">if</span> (digits.<span class="hljs-built_in">size</span>() == <span class="hljs-number">0</span>) {<br> <span class="hljs-keyword">return</span> res;<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-built_in">letComb</span>(res, <span class="hljs-number">0</span>, digits);<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br><br> <span class="hljs-keyword">private</span>:<span class="hljs-function">s</span><br><span class="hljs-function"> <span class="hljs-type">int</span> <span class="hljs-title">letComb</span><span class="hljs-params">(std::vector<std::string>& res, <span class="hljs-type">int</span> idx, std::string & digits)</span> </span>{<br> <span class="hljs-keyword">if</span> (idx == digits.<span class="hljs-built_in">size</span>()) {<br> res.<span class="hljs-built_in">push_back</span>(comb);<br> } <span class="hljs-keyword">else</span> {<br> std::string str = cell_map[digits[idx]];<br> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span> ch : str) {<br> comb.<span class="hljs-built_in">push_back</span>(ch);<br> <span class="hljs-built_in">letComb</span>(res, idx+<span class="hljs-number">1</span>, digits);<br> comb.<span class="hljs-built_in">pop_back</span>();<br> }<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> }<br><br> std::string comb;<br><br> std::unordered_map<<span class="hljs-type">char</span>, std::string> cell_map = {<br> {<span class="hljs-string">'2'</span>, <span class="hljs-string">"abc"</span>}, {<span class="hljs-string">'3'</span>, <span class="hljs-string">"edf"</span>}, {<span class="hljs-string">'4'</span>, <span class="hljs-string">"ghi"</span>}, {<span class="hljs-string">'5'</span>, <span class="hljs-string">"jkl"</span>},<br> {<span class="hljs-string">'6'</span>, <span class="hljs-string">"mno"</span>}, {<span class="hljs-string">'7'</span>, <span class="hljs-string">"pqrs"</span>}, {<span class="hljs-string">'8'</span>, <span class="hljs-string">"tuv"</span>}, {<span class="hljs-string">'9'</span>, <span class="hljs-string">"wxzy"</span>}};<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>迭代法注意在遍历完当前 digits[i] 后,需更新原始结果集,在下一次 i+1 迭代中需要使用更新后的结果集进行追加字符;</li><li>回溯条件:idx == digits.size(),将当前字符串加入到结果集中。</li><li>在将字符串加入结果集后进行回溯,即将字符串的最后一位删除,并加上下一个字符;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>回溯</tag>
</tags>
</entry>
<entry>
<title>16. 最接近的三数之和</title>
<link href="/2024/04/17/leetcode/16.%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/"/>
<url>/2024/04/17/leetcode/16.%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/</url>
<content type="html"><![CDATA[<h1 id="16-最接近的三数之和"><a href="#16-最接近的三数之和" class="headerlink" title="16. 最接近的三数之和"></a>16. 最接近的三数之和</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。</p><p>返回这三个数的和。</p><p>假定每组输入只存在恰好一个解。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [-1,2,1,-4], target = 1<br>输出:2<br>解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [0,0,0], target = 1<br>输出:0<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>3 <= nums.length <= 1000</li><li>-1000 <= nums[i] <= 1000</li><li>-10^4 <= target <= 10^4</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="排序-双指针"><a href="#排序-双指针" class="headerlink" title="排序 + 双指针"></a>排序 + 双指针</h3><p>解题思路同 <a href="./15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.md">15. 三数之和</a>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">threeSumClosest</span><span class="hljs-params">(std::vector<<span class="hljs-type">int</span>>& nums, <span class="hljs-type">int</span> target)</span> </span>{<br> <span class="hljs-built_in">sort</span>(nums.<span class="hljs-built_in">begin</span>(), nums.<span class="hljs-built_in">end</span>());<br> <span class="hljs-type">int</span> dis = INT_MAX;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">2</span>; i++) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && nums[i] == nums[i - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>;<br> <span class="hljs-type">int</span> m = i + <span class="hljs-number">1</span>, n = nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">while</span> (m < n) {<br> <span class="hljs-type">int</span> sum = nums[i]+nums[m]+nums[n];<br> <span class="hljs-keyword">if</span> (sum == target) {<br> <span class="hljs-keyword">return</span> target;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (sum < target) {<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">abs</span>(target - sum) < <span class="hljs-built_in">abs</span>(dis)) dis = target - sum;<br> <span class="hljs-type">int</span> x = nums[m];<br> <span class="hljs-keyword">while</span>(x == nums[++m] && m < n);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">abs</span>(target - sum) < <span class="hljs-built_in">abs</span>(dis)) dis = target - sum;<br> <span class="hljs-type">int</span> x = nums[n];<br> <span class="hljs-keyword">while</span>(x == nums[--n] && m < n);<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> target - dis;<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>使用 INT_MAX 初始化三元组和与 target 的差值,然后在遍历过程中不断更新这个差值;</li><li>三元组与 target 的插值 dis 类型为 int,存储真实差值,避免正负差异,dis = target - sum;</li><li>与 target 最接近的和即为 target - dis;</li><li>当 sum == target 时,直接返回 target,可以降低耗时,避免冗余枚举;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
<tag>排序</tag>
</tags>
</entry>
<entry>
<title>15. 三数之和</title>
<link href="/2024/04/17/leetcode/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/"/>
<url>/2024/04/17/leetcode/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/</url>
<content type="html"><![CDATA[<h1 id="15-三数之和"><a href="#15-三数之和" class="headerlink" title="15. 三数之和"></a>15. 三数之和</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。</p><p>请你返回所有和为 0 且不重复的三元组。</p><p>注意:答案中不可以包含重复的三元组。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [-1,0,1,2,-1,-4]<br>输出:[[-1,-1,2],[-1,0,1]]<br>解释:<br>nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。<br>nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。<br>nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。<br>不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。<br>注意,输出的顺序和三元组的顺序并不重要。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [0,1,1]<br>输出:[]<br>解释:唯一可能的三元组和不为 0 。<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:nums = [0,0,0]<br>输出:[[0,0,0]]<br>解释:唯一可能的三元组和为 0 。<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>3 <= nums.length <= 3000</li><li>-10^5 <= nums[i] <= 10^5</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:暴力枚举-去重"><a href="#解法一:暴力枚举-去重" class="headerlink" title="解法一:暴力枚举 + 去重"></a>解法一:暴力枚举 + 去重</h3><p>最容易想到的方法,直接枚举三个数的组合,当和为 0 时,将当前组合排序,然后遍历结果集,如果当前结果集中没有这个组合则加入结果集。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs cpp">std::vector<std::vector<<span class="hljs-type">int</span>>> <span class="hljs-built_in">threeSum</span>(std::vector<<span class="hljs-type">int</span>>& nums) {<br> std::vector<std::vector<<span class="hljs-type">int</span>>> res;<br> <span class="hljs-built_in">sort</span>(nums.<span class="hljs-built_in">begin</span>(), nums.<span class="hljs-built_in">end</span>());<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">2</span>; i++) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && nums[i] == nums[i - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = i + <span class="hljs-number">1</span>; j < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>; j++) {<br> <span class="hljs-keyword">if</span> (j > i + <span class="hljs-number">1</span> && nums[j] == nums[j - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> k = j + <span class="hljs-number">1</span>; k < nums.<span class="hljs-built_in">size</span>(); k++) {<br> <span class="hljs-keyword">if</span> (nums[i] + nums[j] + nums[k] == <span class="hljs-number">0</span>) {<br> res.<span class="hljs-built_in">push_back</span>({nums[i], nums[j], nums[k]});<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br>}<br></code></pre></td></tr></table></figure><p>但是时间复杂度为 $O(n^3)$,提交后显示超时。</p><h3 id="解法二:排序-双指针"><a href="#解法二:排序-双指针" class="headerlink" title="解法二:排序 + 双指针"></a>解法二:排序 + 双指针</h3><p>解法一在去重过程需要先排序,不妨在最开始先进行排序。</p><p>当排序后,我们在遍历三元组时,当下标移动时遇到相同元组,直接跳过即可,避免了每次向结果集查找去重的操作。</p><p>遍历三元组时,必然先固定一个元素,nums[i],然后继续向后遍历 nums[j] 和 nums[k]。但是这样一次枚举就会导致时间复杂度过高。</p><blockquote><p>可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 <code>a+b+c=0</code>。当第二重循环往后枚举一个元素 b′ 时,由于 b′>b,那么满足 a+b′+c′=0 的 c′ 一定有 <code>c′ < c</code> ,即 c′ 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。</p><p>有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针。</p></blockquote><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs cpp">std::vector<std::vector<<span class="hljs-type">int</span>>> <span class="hljs-built_in">threeSum</span>(std::vector<<span class="hljs-type">int</span>>& nums) {<br> std::vector<std::vector<<span class="hljs-type">int</span>>> res;<br> <span class="hljs-built_in">sort</span>(nums.<span class="hljs-built_in">begin</span>(), nums.<span class="hljs-built_in">end</span>());<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">2</span>; i++) {<br> <span class="hljs-keyword">if</span> (nums[i] > <span class="hljs-number">0</span>) <span class="hljs-keyword">break</span>;<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && nums[i] == nums[i - <span class="hljs-number">1</span>]) <span class="hljs-keyword">continue</span>;<br> <span class="hljs-type">int</span> m = i + <span class="hljs-number">1</span>, n = nums.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">while</span> (m < n) {<br> <span class="hljs-keyword">if</span> (nums[n] < <span class="hljs-number">0</span>) <span class="hljs-keyword">break</span>;<br> <span class="hljs-keyword">if</span> (nums[i]+nums[m]+nums[n] == <span class="hljs-number">0</span>) {<br> res.<span class="hljs-built_in">push_back</span>({nums[i], nums[m], nums[n]});<br> }<br> <span class="hljs-keyword">if</span> (nums[i]+nums[m]+nums[n] <= <span class="hljs-number">0</span>) {<br> <span class="hljs-type">int</span> x = nums[m];<br> <span class="hljs-keyword">while</span>(x == nums[++m] && m < n);<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-type">int</span> x = nums[n];<br> <span class="hljs-keyword">while</span>(x == nums[--n] && m < n);<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br>}<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>双指针,优化第二个数和第三个数遍历过程,将第三个数遍历过程变成一个从数组最右端开始向左移动的指针;</li><li>排序时间复杂度为 $O(n\log n)$,双指针时间复杂度为 $O(n^2)$,总时间复杂度为 $O(n^2)$。</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
<tag>排序</tag>
</tags>
</entry>
<entry>
<title>14. 最长公共前缀</title>
<link href="/2024/04/17/leetcode/14.%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%89%8D%E7%BC%80/"/>
<url>/2024/04/17/leetcode/14.%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%89%8D%E7%BC%80/</url>
<content type="html"><![CDATA[<h1 id="14-最长公共前缀"><a href="#14-最长公共前缀" class="headerlink" title="14. 最长公共前缀"></a>14. 最长公共前缀</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>编写一个函数来查找字符串数组中的最长公共前缀。</p><p>如果不存在公共前缀,返回空字符串 “”。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:strs = ["flower","flow","flight"]<br>输出:"fl"<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:strs = ["dog","racecar","car"]<br>输出:""<br>解释:输入不存在公共前缀。<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= strs.length <= 200</li><li>0 <= strs[i].length <= 200</li><li>strs[i] 仅由小写英文字母组成</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><p>最长公共前缀,表明这个 prefix 在每个 str 中都存在,因此只需要将 strs 中随便取一个 str 作为基准,逐位去和剩余的 str 进行比较即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">std::string <span class="hljs-title">longestCommonPrefix</span><span class="hljs-params">(std::vector<std::string>& strs)</span> </span>{<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < strs[<span class="hljs-number">0</span>].<span class="hljs-built_in">size</span>(); i++) {<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = <span class="hljs-number">1</span>; j < strs.<span class="hljs-built_in">size</span>(); j++) {<br> <span class="hljs-keyword">if</span> (i >= strs[j].<span class="hljs-built_in">size</span>() || strs[<span class="hljs-number">0</span>][i] != strs[j][i]) {<br> <span class="hljs-keyword">return</span> strs[<span class="hljs-number">0</span>].<span class="hljs-built_in">substr</span>(<span class="hljs-number">0</span>, i);<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> strs[<span class="hljs-number">0</span>];<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>逐位比较</li><li>边界条件:i >= strs[j].size() 或 strs[0][i] != strs[j][i] 时,返回结果</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>模拟</tag>
</tags>
</entry>
<entry>
<title>11. 盛最多水的容器</title>
<link href="/2024/04/17/leetcode/11.%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/"/>
<url>/2024/04/17/leetcode/11.%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/</url>
<content type="html"><![CDATA[<h1 id="11-盛最多水的容器"><a href="#11-盛最多水的容器" class="headerlink" title="11. 盛最多水的容器"></a>11. 盛最多水的容器</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。</p><p>找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。</p><p>返回容器可以储存的最大水量。</p><p>说明:你不能倾斜容器。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:[1,8,6,2,5,4,8,3,7]<br>输出:49 <br>解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:height = [1,1]<br>输出:1<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>n == height.length</li><li>2 <= n <= 10^5</li><li>0 <= height[i] <= 10^4</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="双指针"><a href="#双指针" class="headerlink" title="双指针"></a>双指针</h3><p>算法正确性的证明见官方题解原文: <a href="https://leetcode-cn.com/problems/container-with-most-water/solution/sheng-zui-duo-shui-de-rong-qi-by-leetcode-solution/">盛最多水的容器</a></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">maxArea</span><span class="hljs-params">(vector<<span class="hljs-type">int</span>>& height)</span> </span>{<br> <span class="hljs-type">int</span> res = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>, j = height.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>; i <= j;) {<br> res = <span class="hljs-built_in">max</span>(res, <span class="hljs-built_in">min</span>(height[i], height[j]) * (j-i));<br> <span class="hljs-keyword">if</span> (height[i] > height[j]) {<br> j--;<br> } <span class="hljs-keyword">else</span> {<br> i++;<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>双指针</li><li>移动较小的指针,缩小问题规模</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
</tags>
</entry>
<entry>
<title>10. 正则表达式匹配</title>
<link href="/2024/04/14/leetcode/10.%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%8C%B9%E9%85%8D/"/>
<url>/2024/04/14/leetcode/10.%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%8C%B9%E9%85%8D/</url>
<content type="html"><![CDATA[<h1 id="10-正则表达式匹配"><a href="#10-正则表达式匹配" class="headerlink" title="10. 正则表达式匹配"></a>10. 正则表达式匹配</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。</p><ul><li>‘.’ 匹配任意单个字符</li><li>‘*’ 匹配零个或多个前面的那一个元素</li><li>所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。</li></ul><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "aa", p = "a"<br>输出:false<br>解释:"a" 无法匹配 "aa" 整个字符串。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "aa", p = "a*"<br>输出:true<br>解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "ab", p = ".*"<br>输出:true<br>解释:".*" 表示可匹配零个或多个('*')任意字符('.')。<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= s.length <= 20</li><li>1 <= p.length <= 20</li><li>s 只包含从 a-z 的小写字母。</li><li>p 只包含从 a-z 的小写字母,以及字符 . 和 *。</li><li>保证每次出现字符 * 时,前面都匹配到有效的字符</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:动态规划"><a href="#解法一:动态规划" class="headerlink" title="解法一:动态规划"></a>解法一:动态规划</h3><blockquote><p>以下题解思路和图片,引用自:</p><p>作者:笨猪爆破组</p><p>链接:<a href="https://leetcode.cn/problems/regular-expression-matching/solutions/1/shou-hui-tu-jie-wo-tai-nan-liao-by-hyj8/">https://leetcode.cn/problems/regular-expression-matching/solutions/1/shou-hui-tu-jie-wo-tai-nan-liao-by-hyj8/</a></p></blockquote><h4 id="从左往右扫的话"><a href="#从左往右扫的话" class="headerlink" title="从左往右扫的话"></a>从左往右扫的话</h4><ul><li>字符后面是否跟着星号会影响结果,分析起来有点复杂。<br><img src="https://pic.leetcode-cn.com/073085fa67286871f76e8e9daa162bdb291a101b4314666c75379a7b0441cad6-image.png"></li></ul><h4 id="选择从右往左扫描"><a href="#选择从右往左扫描" class="headerlink" title="选择从右往左扫描"></a>选择从右往左扫描</h4><ul><li><p>星号的前面肯定有一个字符,星号也只影响这一个字符,它就像一个拷贝器。<br><img src="https://pic.leetcode-cn.com/5e7b1748039a2a779d7378bebc4926ef3e584e88cc22b67f3a4e18c0590bcc55-image.png"></p></li><li><p>s、p 串是否匹配,取决于:最右端是否匹配、剩余的子串是否匹配。</p></li><li><p>只是最右端可能是特殊符号,需要分情况讨论而已。</p></li></ul><h4 id="通用地表示出子问题"><a href="#通用地表示出子问题" class="headerlink" title="通用地表示出子问题"></a>通用地表示出子问题</h4><ul><li>大子串是否匹配,和剩余子串是否匹配,是规模不一样的同一问题。<br><img src="https://pic.leetcode-cn.com/e1bcac2ad07a3a5c959bf0fe5c8ceea9bbd033c3066e7ec7f384aedd98cd95aa-image.png"></li></ul><h5 id="情况1:s-i−1-和-p-j−1-是匹配的"><a href="#情况1:s-i−1-和-p-j−1-是匹配的" class="headerlink" title="情况1:s[i−1] 和 p[j−1] 是匹配的"></a>情况1:s[i−1] 和 p[j−1] 是匹配的</h5><ul><li>最右端的字符是匹配的,那么,大问题的答案 = 剩余子串是否匹配。<br><img src="https://pic.leetcode-cn.com/f817caaa40b0c39fc3ddabfa1383a8218ab364b8e49b30e5ce85cb30a3cdc503-image.png"></li></ul><h5 id="情况2:s-i−1-和-p-j−1-是不匹配的"><a href="#情况2:s-i−1-和-p-j−1-是不匹配的" class="headerlink" title="情况2:s[i−1] 和 p[j−1] 是不匹配的"></a>情况2:s[i−1] 和 p[j−1] 是不匹配的</h5><ul><li>右端不匹配,还不能判死刑——可能是 p[j−1] 为星号造成的不匹配,星号不是真实字符,它不匹配不算数。</li><li>如果 p[j−1]p[j-1]p[j−1] 不是星号,那就真的不匹配了。<br><img src="https://pic.leetcode-cn.com/fe763378879a0a52e9f17171e3bc1db18cfc83bf59f14efcd31ec9edb37adfac-image.png"></li></ul><ol><li>p[j−1]==”∗”,且 s[i−1] 和 p[j−2] 匹配<ul><li>p[j−1] 是星号,并且 s[i−1] 和 p[j−2] 匹配,要考虑三种情况:<ul><li>p[j−1] 星号可以让 p[j−2] 在 p 串中消失、出现 1 次、出现 >=2 次。</li><li>只要其中一种使得剩余子串能匹配,那就能匹配,见下图 a1、a2、a3。<br> <img src="https://pic.leetcode-cn.com/a1cc0caf806f7d7f5419d820e0e7be7a364c96656a98ca4d7f351661d6a62aa6-image.png"></li><li>a3 情况:假设 s 的右端是一个 a,p 的右端是 a * ,* 让 a 重复 >= 2 次<ul><li>星号不是真实字符,s、p是否匹配,要看 s 去掉末尾的 a,p 去掉末尾一个 a,剩下的是否匹配。</li><li>星号拷贝了 >=2 个 a,拿掉一个,剩下 >=1 个a,p 末端依旧是 a* 没变。</li><li>s 末尾的 a 被抵消了,继续考察 s(0,i-2) 和 p(0,i-1) 是否匹配。</li></ul></li></ul></li></ul></li><li>p[j−1]==”∗”,但 s[i−1] 和 p[j−2] 不匹配<ul><li>s[i−1] 和 p[j−2] 不匹配,还有救,p[j−1] 星号可以干掉 p[j−2],继续考察 s(0,i−1) 和 p(0,j−3)。<br> <img src="https://pic.leetcode-cn.com/dabf2195c460052e2719340de8f2d22f791694d4443424478201be3b5d601fe1-image.png"></li></ul></li></ol><h4 id="base-case"><a href="#base-case" class="headerlink" title="base case"></a>base case</h4><ul><li>p 为空串,s 不为空串,肯定不匹配。</li><li>s 为空串,但 p 不为空串,要想匹配,只可能是右端是星号,它干掉一个字符后,把 p 变为空串。</li><li>s、p 都为空串,肯定匹配。</li></ul><p><img src="https://pic.leetcode-cn.com/140597adfd5f03dd481e136163d98e7160cce4761c7cb8227010d828f24b7498-image.png"></p><h4 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">isMatch</span><span class="hljs-params">(std::string s, std::string p)</span> </span>{<br> <span class="hljs-type">int</span> s_len = s.<span class="hljs-built_in">size</span>(), p_len = p.<span class="hljs-built_in">size</span>(), i = <span class="hljs-number">0</span>, j = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">if</span> (s_len > <span class="hljs-number">0</span> && p_len <= <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> <span class="hljs-comment">// 申请 dp</span><br> std::vector<std::vector<<span class="hljs-type">bool</span>>> <span class="hljs-built_in">dp</span>(s_len + <span class="hljs-number">1</span>,<br> std::<span class="hljs-built_in">vector</span><<span class="hljs-type">bool</span>>(p_len + <span class="hljs-number">1</span>, <span class="hljs-literal">false</span>));<br><br> <span class="hljs-comment">// 边界</span><br> <span class="hljs-comment">// 1. s, p 均为空串</span><br> dp[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>] = <span class="hljs-literal">true</span>;<br> <span class="hljs-comment">// 2. s 为空串,p 不为空,则 p 必须以 * 结尾</span><br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = <span class="hljs-number">2</span>; j <= p_len; j++) {<br> <span class="hljs-keyword">if</span> (p[j - <span class="hljs-number">1</span>] == <span class="hljs-string">'*'</span>) dp[<span class="hljs-number">0</span>][j] = dp[<span class="hljs-number">0</span>][j - <span class="hljs-number">2</span>];<br> }<br> <span class="hljs-comment">// 3. s 不为空,p 为空,初始化时默认已经全部设为false了</span><br><br> <span class="hljs-comment">// 状态转移,i 和 j 表示的时长度,并非真实下标</span><br> <span class="hljs-keyword">for</span> (j = <span class="hljs-number">1</span>; j <= p_len; j++) {<br> <span class="hljs-keyword">for</span> (i = <span class="hljs-number">1</span>; i <= s_len; i++) {<br> <span class="hljs-keyword">if</span> (s[i<span class="hljs-number">-1</span>] == p[j<span class="hljs-number">-1</span>] || p[j<span class="hljs-number">-1</span>] == <span class="hljs-string">'.'</span>) { <span class="hljs-comment">// 当 s[i-1] 和 p[j-1] 匹配,则状态转移为 dp[i-1][j-1]</span><br> dp[i][j] = dp[i<span class="hljs-number">-1</span>][j<span class="hljs-number">-1</span>];<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (p[j<span class="hljs-number">-1</span>] == <span class="hljs-string">'*'</span>) { <span class="hljs-comment">// 当 s[i-1] 和 p[j-1] 不匹配时,考虑 p[j-1] 为 * 号</span><br> <span class="hljs-keyword">if</span> (s[i<span class="hljs-number">-1</span>] == p[j<span class="hljs-number">-2</span>] || p[j<span class="hljs-number">-2</span>] == <span class="hljs-string">'.'</span>) {<br> <span class="hljs-comment">// 当 s[i-1] 和 p[j-2] 匹配,则考虑 * 抵消 0 次,1 次和 多次</span><br> <span class="hljs-comment">// 0 次:i 不变,j-2</span><br> <span class="hljs-comment">// 1 次:i-1, j-2</span><br> <span class="hljs-comment">// >=2 次:i-1,j 不变</span><br> dp[i][j] = dp[i<span class="hljs-number">-1</span>][j<span class="hljs-number">-2</span>] || dp[i<span class="hljs-number">-1</span>][j] || dp[i][j<span class="hljs-number">-2</span>];<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-comment">// 当 s[i-1] 和 p[j-2] 不匹配时,由于 * 号可表示前置字符出现0次,因此 p 中去除这两个字符 j - 2 后继续匹配</span><br> dp[i][j] = dp[i][j<span class="hljs-number">-2</span>];<br> }<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> dp[s_len][p_len];<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>逆序思考问题,从右往左扫描,可以简化问题;</li><li>状态转移方程的推导,需要考虑多种情况,包括星号的作用;</li><li>边界条件的处理,需要考虑 s、p 为空串的情况;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>动态规划</tag>
</tags>
</entry>
<entry>
<title>09. 回文数</title>
<link href="/2024/04/14/leetcode/09.%E5%9B%9E%E6%96%87%E6%95%B0/"/>
<url>/2024/04/14/leetcode/09.%E5%9B%9E%E6%96%87%E6%95%B0/</url>
<content type="html"><![CDATA[<h1 id="09-回文数"><a href="#09-回文数" class="headerlink" title="09. 回文数"></a>09. 回文数</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。</p><p>回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。</p><p>例如,121 是回文,而 123 不是。</p><p>示例 1:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp">输入:x = <span class="hljs-number">121</span><br>输出:<span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cpp">输入:x = <span class="hljs-number">-121</span><br>输出:<span class="hljs-literal">false</span><br>解释:从左向右读, 为 <span class="hljs-number">-121</span> 。 从右向左读, 为 <span class="hljs-number">121</span>- 。因此它不是一个回文数。<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cpp">输入:x = <span class="hljs-number">10</span><br>输出:<span class="hljs-literal">false</span><br>解释:从右向左读, 为 <span class="hljs-number">01</span> 。因此它不是一个回文数。<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>-2^31 <= x <= 2^31 - 1</li></ul><p>进阶:你能不将整数转为字符串来解决这个问题吗?</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><p>根据题干可知:</p><ol><li>当 x 为负数时,由于负号的存在,必定不为回文数;</li><li>当 x 为非负个位整数时,必定是回文数;</li></ol><p>所以可以直接通过简单判断,将这两种情况直接返回:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (x < <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br><span class="hljs-keyword">if</span> (x >= <span class="hljs-number">0</span> && x <= <span class="hljs-number">9</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br></code></pre></td></tr></table></figure><p>对于剩余 x >= 10 的情况,比较好理解的方法就是将 x 转换为字符串,然后判断字符串是否为回文串即可。</p><h3 id="解法一:字符串为回文串"><a href="#解法一:字符串为回文串" class="headerlink" title="解法一:字符串为回文串"></a>解法一:字符串为回文串</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">isPalindrome</span><span class="hljs-params">(<span class="hljs-type">int</span> x)</span> </span>{<br> <span class="hljs-keyword">if</span> (x < <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> <span class="hljs-keyword">if</span> (x >= <span class="hljs-number">0</span> && x <= <span class="hljs-number">9</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br> std::string tmp = std::<span class="hljs-built_in">to_string</span>(x);<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < tmp.<span class="hljs-built_in">size</span>() / <span class="hljs-number">2</span>; i++) {<br> <span class="hljs-keyword">if</span> (tmp[i] != tmp[tmp.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span> - i]) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br> }<br>};<br></code></pre></td></tr></table></figure><h3 id="解法二:-反转一半数字"><a href="#解法二:-反转一半数字" class="headerlink" title="解法二: 反转一半数字"></a>解法二: 反转一半数字</h3><p>官方题解给出另一个解法,通过比较翻转数字,比较翻转结果和原始数字是否相等来判断是否为回文数。</p><p>但是官方也提到,这种解法存在部分数字反转后溢出的问题。不过换个角度想,根据回文数的定义,反转后应该等于这个数本身,这也就意味着,由于输入必然不会溢出,那么如果反转后数字出现溢出,那么这个数必然不是回文数。</p><p>即使进行逻辑优化,判断反转结果是否溢出仍然是不可避免的,并且并没有对时间复杂度产生优化。因此官方基于反转数字提出了一种更优化的解法,即<strong>反转一半数字</strong>。</p><p>根据回文数的特征,将后半部分的数字进行反转,应该要和前半部分数字相等(当数字位数为奇数时,这个逻辑仍然成立,只需去除中间位即可)。</p><p>这个解法的关键在于:<mark>如何判断当前已经反转了一半的数字</mark>。</p><blockquote><p>由于整个过程我们不断将原始数字除以 10,然后给反转后的数字乘上 10,所以,当原始数字小于或等于反转后的数字时,就意味着我们已经处理了一半位数的数字了。</p></blockquote><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">isPalindrome</span><span class="hljs-params">(<span class="hljs-type">int</span> x)</span> </span>{<br> <span class="hljs-keyword">if</span> (x < <span class="hljs-number">0</span> || (x != <span class="hljs-number">0</span> && x % <span class="hljs-number">10</span> == <span class="hljs-number">0</span>)) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> <span class="hljs-keyword">if</span> (x >= <span class="hljs-number">0</span> && x <= <span class="hljs-number">9</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br> <span class="hljs-type">int</span> y = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">while</span> (x > y) {<br> y = y * <span class="hljs-number">10</span> + x % <span class="hljs-number">10</span>;<br> x = x / <span class="hljs-number">10</span>;<br> }<br> <span class="hljs-keyword">return</span> (x == y || x == y / <span class="hljs-number">10</span>) ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>;<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>通过回文数特征,提取出 x < 0 ,0 <= x <= 9,以及 x 为 10 的倍数这几种特殊情况,快速判断返回;</li><li>将 x 是否为回文数问题,转换为 x 这个字符串是否为回文串问题;</li><li>根据回文数特征,回文数反转后等于数字本身,但是需注意直接反转存在溢出问题;</li><li>回文数后半部分反转后等于前半部分,通过判断 x <= 反转结果 y, 得到此时已经反转一半数字;</li><li>当数字为奇数位时,只需去除中间位,判断剩余部分 x == y / 10 即可;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>整数溢出</tag>
<tag>回文字符串</tag>
<tag>整数反转</tag>
</tags>
</entry>
<entry>
<title>08. 字符串转换整数 (atoi)</title>
<link href="/2024/04/14/leetcode/08.%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E6%8D%A2%E6%95%B4%E6%95%B0/"/>
<url>/2024/04/14/leetcode/08.%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E6%8D%A2%E6%95%B4%E6%95%B0/</url>
<content type="html"><![CDATA[<h1 id="08-字符串转换整数-atoi"><a href="#08-字符串转换整数-atoi" class="headerlink" title="08. 字符串转换整数 (atoi)"></a>08. 字符串转换整数 (atoi)</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。</p><p>函数 myAtoi(string s) 的算法如下:</p><ol><li>读入字符串并丢弃无用的前导空格</li><li>检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。</li><li>读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。</li><li>将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。</li><li>如果整数数超过 32 位有符号整数范围 [−2^31, 2^31 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^31 − 1 的整数应该被固定为 2^31 − 1 。</li><li>返回整数作为最终结果。</li></ol><p>注意:</p><p>本题中的空白字符只包括空格字符 ‘ ‘ 。<br>除前导空格或数字后的其余字符串外,<strong>请勿忽略</strong>任何其他字符。</p><p>示例 1:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs text">输入:s = "42"<br>输出:42<br>解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。<br>第 1 步:"42"(当前没有读入字符,因为没有前导空格)<br> ^<br>第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')<br> ^<br>第 3 步:"42"(读入 "42")<br> ^<br>解析得到整数 42 。<br>由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs text">输入:s = " -42"<br>输出:-42<br>解释:<br>第 1 步:" -42"(读入前导空格,但忽视掉)<br> ^<br>第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数)<br> ^<br>第 3 步:" -42"(读入 "42")<br> ^<br>解析得到整数 -42 。<br>由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs text">输入:s = "4193 with words"<br>输出:4193<br>解释:<br>第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)<br> ^<br>第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')<br> ^<br>第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)<br> ^<br>解析得到整数 4193 。<br>由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>0 <= s.length <= 200</li><li>s 由英文字母(大写和小写)、数字(0-9)、’ ‘、’+’、’-‘ 和 ‘.’ 组成</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><p>stoi 和 regex 秒了 0.0 只想偷懒……</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">myAtoi</span><span class="hljs-params">(std::string s)</span> </span>{<br> <span class="hljs-function">std::regex <span class="hljs-title">re</span><span class="hljs-params">(<span class="hljs-string">"^ *([\\+\\-]?[0-9]+)"</span>)</span></span>;<br> std::smatch m;<br> std::string sub;<br><br> <span class="hljs-keyword">if</span> (std::<span class="hljs-built_in">regex_search</span>(s, m, re) && m.<span class="hljs-built_in">size</span>() > <span class="hljs-number">1</span>) {<br> sub = m[<span class="hljs-number">1</span>].<span class="hljs-built_in">str</span>();<br> }<br><br> <span class="hljs-keyword">if</span> (sub.<span class="hljs-built_in">empty</span>()) {<br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> }<br><br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-keyword">if</span> (sub[<span class="hljs-number">0</span>] == <span class="hljs-string">'-'</span>) {<br> <span class="hljs-keyword">return</span> -std::<span class="hljs-built_in">stoi</span>(sub.<span class="hljs-built_in">substr</span>(<span class="hljs-number">1</span>));<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">return</span> std::<span class="hljs-built_in">stoi</span>(sub);<br> }<br> } <span class="hljs-built_in">catch</span> (std::exception&) {<br> <span class="hljs-keyword">return</span> sub[<span class="hljs-number">0</span>] == <span class="hljs-string">'-'</span> ? INT_MIN : INT_MAX;<br> }<br>}<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>题干意义不明,很多用例在题干理解错误的情况下,无法理解为什么会这样……例如 “word and 987” 正确结果为 0,再例如 “+-12” 正确结果为 0;</li><li>题干解析:<ul><li>字符串开头可以由若干个 “ “ 空格;</li><li>去除开头若干个空格后只能有 1 个或 0 个 “+” 或 “-“;</li><li>正负号后必须是数字,数字可以是多个 0 开头</li><li>其余任意情况均为非法字符串</li></ul></li><li>第一眼直接正则秒了,可惜效率和内存并不好,无所谓,AC 就行</li><li>正负溢出靠捕获 <code>stoi</code> 异常解决</li></ol><p>搞清楚题干意思后,遍历字符串也很方便,判断溢出条件可以通过比较字符串,或者在 INT_MAX / 10 和 INT_MIN / 10 之前进行比较判断,避免溢出</p>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>DFA</tag>
<tag>整数溢出</tag>
<tag>正则表达式</tag>
<tag>确定性有限状态机</tag>
</tags>
</entry>
<entry>
<title>07. 整数反转</title>
<link href="/2024/04/14/leetcode/07.%E6%95%B4%E6%95%B0%E5%8F%8D%E8%BD%AC/"/>
<url>/2024/04/14/leetcode/07.%E6%95%B4%E6%95%B0%E5%8F%8D%E8%BD%AC/</url>
<content type="html"><![CDATA[<h1 id="07-整数反转"><a href="#07-整数反转" class="headerlink" title="07. 整数反转"></a>07. 整数反转</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。</p><p>如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。</p><p>假设环境不允许存储 64 位整数(有符号或无符号)。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:x = 123<br>输出:321<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:x = -123<br>输出:-321<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:x = 120<br>输出:21<br></code></pre></td></tr></table></figure><p>示例 4:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:x = 0<br>输出:0<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>-2^31 <= x <= 2^31 - 1</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><p>数学计算令人头秃,选择字符串反转 T_T</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">reverse</span><span class="hljs-params">(<span class="hljs-type">int</span> x)</span> </span>{<br> <span class="hljs-type">int</span> smb = x < <span class="hljs-number">0</span> ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>;<br> string s = <span class="hljs-built_in">to_string</span>(x).<span class="hljs-built_in">substr</span>(smb);<br> string s_re;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = s.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>; i >= <span class="hljs-number">0</span>; i--) {<br> s_re += s[i];<br> }<br> string max = <span class="hljs-built_in">to_string</span>(INT_MAX);<br> s_re = <span class="hljs-built_in">string</span>(max.<span class="hljs-built_in">size</span>() - s_re.<span class="hljs-built_in">size</span>(), <span class="hljs-string">'0'</span>) + s_re;<br> <span class="hljs-keyword">if</span> (s_re >= max) {<br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> }<br> <span class="hljs-type">int</span> z = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < s.<span class="hljs-built_in">size</span>(); i++) {<br> z += (s[i] - <span class="hljs-string">'0'</span>) * std::<span class="hljs-built_in">pow</span>(<span class="hljs-number">10</span>, i);<br> }<br> <span class="hljs-keyword">return</span> smb == <span class="hljs-number">1</span> ? -z : z;<br>}<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>字符串翻转,遍历字符串 (s[i] - ‘0’) * 10 ^ i 计算结果;</li><li>判断溢出问题,去除符号位后,将判断条件优化为字符串 s_rev > INT_MIN 和 s_rev > INT_MAX</li><li><mark>根据题目限制,输入 x 的取值范围 [-2^31, 2^31 - 1],因此不存在两个数在翻转后可以等于边界值,所以优化溢出判断,仅需要判断 s_rev < INT_MAX 即可;</mark></li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>整数溢出</tag>
<tag>字符串反转</tag>
</tags>
</entry>
<entry>
<title>06. Z 字形变换</title>
<link href="/2024/04/14/leetcode/06.Z%E5%AD%97%E5%BD%A2%E5%8F%98%E6%8D%A2/"/>
<url>/2024/04/14/leetcode/06.Z%E5%AD%97%E5%BD%A2%E5%8F%98%E6%8D%A2/</url>
<content type="html"><![CDATA[<h1 id="06-Z-字形变换"><a href="#06-Z-字形变换" class="headerlink" title="06. Z 字形变换"></a>06. Z 字形变换</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。</p><p>比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">P A H N<br>A P L S I I G<br>Y I R<br></code></pre></td></tr></table></figure><p>之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:”PAHNAPLSIIGYIR”。</p><p>请你实现这个将字符串进行指定行数变换的函数:<code>string convert(string s, int numRows);</code></p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "PAYPALISHIRING", numRows = 3<br>输出:"PAHNAPLSIIGYIR"<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "PAYPALISHIRING", numRows = 4<br>输出:"PINALSIGYAHRPI"<br>解释:<br>P I N<br>A L S I G<br>Y A H R<br>P I<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "A", numRows = 1<br>输出:"A"<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= s.length <= 1000</li><li>s 由英文字母(小写和大写)、’,’ 和 ‘.’ 组成</li><li>1 <= numRows <= 1000</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:矩阵模拟"><a href="#解法一:矩阵模拟" class="headerlink" title="解法一:矩阵模拟"></a>解法一:矩阵模拟</h3><p>若模拟矩阵类型为 <code>vector<vector<char>></code> 则需要先计算矩阵列数 col。根据 Z 字形填充规则,得到填充周期为 (row+row-2) 个字符,一个周期为 (row - 1) 列,因此列数为 <code>(s.size() / (row + row - 2) + 1) * (row -1)</code>。由于字符数不一定是周期的整数倍,计算周期数是需要向上取整,将最后一个周期视作完整周期。</p><p>另外设置一个填充次数 cnt,每当填充 (row - 1) 个字符时,填充方向改变,cnt++ ,填充方向由 cnt 奇偶性进行判断,奇数时填充方向为向下,偶数时填充方向为向上。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-function">string <span class="hljs-title">convert2</span><span class="hljs-params">(string s, <span class="hljs-type">int</span> numRows)</span> </span>{<br> <span class="hljs-keyword">if</span> (s.<span class="hljs-built_in">size</span>() < <span class="hljs-number">2</span> || numRows == <span class="hljs-number">1</span> || s.<span class="hljs-built_in">size</span>() <= numRows) <span class="hljs-keyword">return</span> s;<br> <span class="hljs-type">int</span> col = s.<span class="hljs-built_in">size</span>() / <span class="hljs-number">2</span> + numRows - <span class="hljs-number">1</span>;<br> vector<vector<<span class="hljs-type">char</span>>> <span class="hljs-built_in">mat</span>(col, <span class="hljs-built_in">vector</span><<span class="hljs-type">char</span>>(numRows, <span class="hljs-string">'\0'</span>));<br> <span class="hljs-type">int</span> x = <span class="hljs-number">0</span>, y = <span class="hljs-number">0</span>, cnt = <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < s.<span class="hljs-built_in">size</span>(); i++) {<br> mat[x][y] = s[i];<br> <span class="hljs-keyword">if</span> (i % (numRows - <span class="hljs-number">1</span>) == <span class="hljs-number">0</span> && i != <span class="hljs-number">0</span>) {<br> cnt++;<br> }<br> <span class="hljs-keyword">if</span> (cnt % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>) {<br> x++;<br> y--;<br> } <span class="hljs-keyword">else</span> {<br> y++;<br> }<br><br> }<br> x = y = <span class="hljs-number">0</span>;<br> std::string res;<br> <span class="hljs-keyword">for</span>(y = <span class="hljs-number">0</span>; y < numRows; y++) {<br> <span class="hljs-keyword">for</span>(x = <span class="hljs-number">0</span>; x < col; x++) {<br> <span class="hljs-keyword">if</span> (mat[x][y] != <span class="hljs-string">'\0'</span>) {<br> res += mat[x][y];<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br>}<br></code></pre></td></tr></table></figure><h3 id="解法二:压缩矩阵"><a href="#解法二:压缩矩阵" class="headerlink" title="解法二:压缩矩阵"></a>解法二:压缩矩阵</h3><p>当 numRows 较大时,解法一存在大量的冗余空间,且使用 <code>vector<vector<char>></code> 需优先计算列数 col。</p><p>因此在解法二中,使用 <code>vector<string></code> 按行申请空间。由于最终读取时忽略空白字符,所以直接将当前行字符追加到字符串尾部即可,消除冗余的空白空间。</p><p><mark>由于不需要计算列数,因此在填充时仅需要记录当前行数 row 即可,当 row == 0 或 row == numRows - 1 时,填充方向翻转。翻转的方法极为巧妙:每次填充字符时,行数 row 存在两种情况,+1 或者 -1,所以设置一个标记 k = 1,在 row 符合翻转条件时,将 k 取反,即 k = -k,此时 只需要 <code>row += k</code> 即可完成行号的更新。</mark></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-function">string <span class="hljs-title">convert</span><span class="hljs-params">(string s, <span class="hljs-type">int</span> numRows)</span> </span>{<br> <span class="hljs-keyword">if</span> (s.<span class="hljs-built_in">size</span>() < <span class="hljs-number">2</span> || numRows == <span class="hljs-number">1</span> || s.<span class="hljs-built_in">size</span>() <= numRows) <span class="hljs-keyword">return</span> s;<br> <span class="hljs-function">vector<string> <span class="hljs-title">mat</span><span class="hljs-params">(numRows, <span class="hljs-string">""</span>)</span></span>;<br> <span class="hljs-type">int</span> row = <span class="hljs-number">0</span>, k = <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">for</span>(<span class="hljs-type">char</span> c:s) {<br> mat[row] += c;<br> row += k;<br> <span class="hljs-keyword">if</span> (row == <span class="hljs-number">0</span> || row == numRows - <span class="hljs-number">1</span>) {<br> k = -k;<br> }<br> }<br> string res;<br> <span class="hljs-keyword">for</span>(<span class="hljs-keyword">auto</span> &t:mat) {<br> res += t;<br> }<br> <span class="hljs-keyword">return</span> res;<br>}<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>矩阵模拟,计算填充模式下的周期,和找到坐标更新模式的拐点;</li><li>根据题意无视空白字符,因此可直接按行追加字符,无需留出空白位置,压缩矩阵,节省空间</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>矩阵模拟</tag>
<tag>压缩矩阵</tag>
</tags>
</entry>
<entry>
<title>05. 最长回文子串</title>
<link href="/2024/04/13/leetcode/05.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/"/>
<url>/2024/04/13/leetcode/05.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/</url>
<content type="html"><![CDATA[<h1 id="05-最长回文子串"><a href="#05-最长回文子串" class="headerlink" title="05. 最长回文子串"></a>05. 最长回文子串</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给<br>你一个字符串 s,找到 s 中最长的回文子串。</p><p>如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "babad"<br>输出:"bab"<br>解释:"aba" 同样是符合题意的答案。<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入:s = "cbbd"<br>输出:"bb"<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= s.length <= 1000</li><li>s 仅由数字和英文字母组成</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:暴力枚举"><a href="#解法一:暴力枚举" class="headerlink" title="解法一:暴力枚举"></a>解法一:暴力枚举</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">bool</span> <span class="hljs-title">check</span><span class="hljs-params">(string s)</span> </span>{<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>, j = s.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>; i < j; i++, j--) {<br> <span class="hljs-keyword">if</span> (s[i] != s[j]) {<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br> }<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>}<br><br><span class="hljs-function">string <span class="hljs-title">longestPalindrome2</span><span class="hljs-params">(string s)</span> </span>{<br> string sub;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < s.<span class="hljs-built_in">size</span>(); i++) {<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = i; j < s.<span class="hljs-built_in">size</span>(); j++) {<br> std::string temp = s.<span class="hljs-built_in">substr</span>(i, j - i + <span class="hljs-number">1</span>);<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">check</span>(temp)) {<br> sub = sub.<span class="hljs-built_in">size</span>() > temp.<span class="hljs-built_in">size</span>() ? sub : temp;<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> sub;<br>}<br></code></pre></td></tr></table></figure><h3 id="解法二:动态规划"><a href="#解法二:动态规划" class="headerlink" title="解法二:动态规划"></a>解法二:动态规划</h3><p>根据回文串的特点,s[i][j] 为回文串,则 s[i+1][j-1] 也为回文串。因此我们得到状态转移方程:</p> <figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs txt">dp[i][j] = dp[i+1][j-1] && s[i] == s[j]<br></code></pre></td></tr></table></figure><p>官方给出的动态规划解法,根据子串长度 len 进行遍历,右移左边界 i ,并根据 len 和 i 倒推右边界 j。此时存在额外需要处理的两处逻辑边界:</p><ol><li>当 len < 3 时, dp[i+1][j-1] 出现翻转,手动设置 dp[i][j] 为 true;</li><li>j = i + len - 1 存在下标溢出,需额外判断停止循环;</li></ol><p>此处给出优化解法,使用左右边界进行遍历,由于dp[i][j] 依赖于 dp[i+1][j-1],因此左边界 i 应该从大到小,右边界 j 从小到大,我们从右下角开始遍历,即从左边界开始遍历,右边界逐渐右移。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">string <span class="hljs-title">longestPalindrome3</span><span class="hljs-params">(string s)</span> </span>{<br> <span class="hljs-keyword">if</span> (s.<span class="hljs-built_in">size</span>() < <span class="hljs-number">2</span>) <span class="hljs-keyword">return</span> s;<br> <span class="hljs-type">int</span> maxl = s.<span class="hljs-built_in">size</span>(), start = <span class="hljs-number">0</span>, len = <span class="hljs-number">1</span>;<br> vector<vector<<span class="hljs-type">bool</span>>> <span class="hljs-built_in">dp</span>(maxl, <span class="hljs-built_in">vector</span><<span class="hljs-type">bool</span>>(maxl, <span class="hljs-literal">true</span>));<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = maxl - <span class="hljs-number">2</span>; i >= <span class="hljs-number">0</span>; i--) {<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = i+<span class="hljs-number">1</span>; j < maxl; j++) {<br> dp[i][j] = <span class="hljs-literal">false</span>;<br> <span class="hljs-keyword">if</span> (s[i] == s[j]) {<br> dp[i][j] = dp[i+<span class="hljs-number">1</span>][j<span class="hljs-number">-1</span>];<br> <span class="hljs-keyword">if</span> (dp[i][j] && len < j - i + <span class="hljs-number">1</span>) {<br> start = i;<br> len = j - i + <span class="hljs-number">1</span>;<br> }<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> s.<span class="hljs-built_in">substr</span>(start,len);<br>}<br></code></pre></td></tr></table></figure><h3 id="解法三:中心扩展"><a href="#解法三:中心扩展" class="headerlink" title="解法三:中心扩展"></a>解法三:中心扩展</h3><p>枚举回文字符串中心点,由于回文字符串长度可能为奇数 1 个中心点或偶数 2 个中心点,因此我们将中心点统一扩展为 2 个,奇数时两个中心点相同,遍历过程中每次都需计算奇偶两种情况。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">string <span class="hljs-title">longestPalindrome</span><span class="hljs-params">(string s)</span> </span>{<br> <span class="hljs-type">int</span> n = s.<span class="hljs-built_in">size</span>();<br> <span class="hljs-type">int</span> start = <span class="hljs-number">0</span>, mx = <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">auto</span> f = [&](<span class="hljs-type">int</span> l, <span class="hljs-type">int</span> r) {<br> <span class="hljs-keyword">while</span> (l >= <span class="hljs-number">0</span> && r < n && s[l] == s[r]) {<br> l--, r++;<br> }<br> <span class="hljs-keyword">return</span> r - l - <span class="hljs-number">1</span>;<br> };<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < n; ++i) {<br> <span class="hljs-type">int</span> a = <span class="hljs-built_in">f</span>(i, i);<br> <span class="hljs-type">int</span> b = <span class="hljs-built_in">f</span>(i, i + <span class="hljs-number">1</span>);<br> <span class="hljs-type">int</span> t = <span class="hljs-built_in">max</span>(a, b);<br> <span class="hljs-keyword">if</span> (mx < t) {<br> mx = t;<br> start = i - (t - <span class="hljs-number">1</span> >> <span class="hljs-number">1</span>);<br> }<br> }<br> <span class="hljs-keyword">return</span> s.<span class="hljs-built_in">substr</span>(start, mx);<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>暴力枚举可解,但是时间复杂度为 O(n^3),部分超长用例存在超时问题;</li><li>动态规划,找到状态转移方程,回文字符串形式为关键点;</li><li>动态规划解法中,根据状态转移方程优化遍历条件和顺序,减少边界判断;</li><li>中心扩展算法,奇偶差异性如何统一,减少冗余计算</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>动态规划</tag>
<tag>中心扩展</tag>
</tags>
</entry>
<entry>
<title>04. 寻找两个正序数组的中位数</title>
<link href="/2024/04/13/leetcode/04.%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/"/>
<url>/2024/04/13/leetcode/04.%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/</url>
<content type="html"><![CDATA[<h1 id="04-寻找两个正序数组的中位数"><a href="#04-寻找两个正序数组的中位数" class="headerlink" title="04. 寻找两个正序数组的中位数"></a>04. 寻找两个正序数组的中位数</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。</p><p>算法的时间复杂度应该为 O(log (m+n)) 。</p><p>示例 1:<br>输入:nums1 = [1,3], nums2 = [2]<br>输出:2.00000<br>解释:合并数组 = [1,2,3] ,中位数 2</p><p>示例 2:<br>输入:nums1 = [1,2], nums2 = [3,4]<br>输出:2.50000<br>解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5</p><p>提示:<br>nums1.length == m<br>nums2.length == n<br>0 <= m <= 1000<br>0 <= n <= 1000<br>1 <= m + n <= 2000<br>-106 <= nums1[i], nums2[i] <= 106</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:分别从两个数组中按照升序不断取元素,直到取到-m-n-x2F-2-个元素,然后取中位数"><a href="#解法一:分别从两个数组中按照升序不断取元素,直到取到-m-n-x2F-2-个元素,然后取中位数" class="headerlink" title="解法一:分别从两个数组中按照升序不断取元素,直到取到 m+n/2 个元素,然后取中位数"></a>解法一:分别从两个数组中按照升序不断取元素,直到取到 m+n/2 个元素,然后取中位数</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br> <span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">findMedianSortedArrays</span><span class="hljs-params">(vector<<span class="hljs-type">int</span>>& nums1, vector<<span class="hljs-type">int</span>>& nums2)</span> </span>{<br> <span class="hljs-type">int</span> n = nums1.<span class="hljs-built_in">size</span>(), m = nums2.<span class="hljs-built_in">size</span>();<br> <span class="hljs-type">int</span> total = n + m;<br> <span class="hljs-type">int</span> target1 = (total + <span class="hljs-number">1</span>) / <span class="hljs-number">2</span>, target2 = (total + <span class="hljs-number">2</span>) / <span class="hljs-number">2</span>;<br> <span class="hljs-type">int</span> i = <span class="hljs-number">0</span>, j = <span class="hljs-number">0</span>, cnt = <span class="hljs-number">0</span>;<br> <span class="hljs-type">double</span> median1 = <span class="hljs-number">0</span>, median2 = <span class="hljs-number">0</span>;<br><br> <span class="hljs-keyword">while</span> (i < n || j < m) {<br> <span class="hljs-type">int</span> num;<br> <span class="hljs-keyword">if</span> (i < n && (j >= m || nums1[i] < nums2[j])) {<br> num = nums1[i++];<br> } <span class="hljs-keyword">else</span> {<br> num = nums2[j++];<br> }<br> cnt++;<br> <span class="hljs-keyword">if</span> (cnt == target1) {<br> median1 = num;<br> }<br> <span class="hljs-keyword">if</span> (cnt == target2) {<br> median2 = num;<br> <span class="hljs-keyword">break</span>;<br> }<br> }<br><br> <span class="hljs-keyword">return</span> (median1 + median2) / <span class="hljs-number">2</span>;<br> }<br>};<br></code></pre></td></tr></table></figure><h3 id="解法二:归并排序"><a href="#解法二:归并排序" class="headerlink" title="解法二:归并排序"></a>解法二:归并排序</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-comment">/*</span><br><span class="hljs-comment"> 解题思路:</span><br><span class="hljs-comment"> 合并两数组,并利用std::sort进行排序,</span><br><span class="hljs-comment"> 对合并后的有序数组进行中位数求解,分两种情况:</span><br><span class="hljs-comment"> 1.数组元素个数为奇数,则直接取下标为【(n+1)/2】的元素为结果;</span><br><span class="hljs-comment"> 2.数组元素个数为偶数,则取下标为【len/2-1】和【len/2】元素的平均值作为结果</span><br><span class="hljs-comment"> */</span><br> <span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">findMedianSortedArrays</span><span class="hljs-params">(vector<<span class="hljs-type">int</span>>& nums1, vector<<span class="hljs-type">int</span>>& nums2)</span> </span>{<br> <span class="hljs-type">double</span> midNum = <span class="hljs-number">0.0</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i=<span class="hljs-number">0</span>; i<nums2.<span class="hljs-built_in">size</span>(); i++){<br> nums1.<span class="hljs-built_in">push_back</span>(nums2[i]);<br> }<br> <span class="hljs-built_in">sort</span>(nums1.<span class="hljs-built_in">begin</span>(), nums1.<span class="hljs-built_in">end</span>());<br> <span class="hljs-type">int</span> len = nums1.<span class="hljs-built_in">size</span>();<br> <span class="hljs-keyword">if</span> (<span class="hljs-number">0</span> == len%<span class="hljs-number">2</span>){<br> len = len/<span class="hljs-number">2</span>;<br> midNum = (nums1[len<span class="hljs-number">-1</span>] + nums1[len]) / <span class="hljs-number">2.0</span>;<br> }<br> <span class="hljs-keyword">else</span>{<br> midNum = nums1[len/<span class="hljs-number">2</span>]/<span class="hljs-number">1.0</span>;<br> }<br> <span class="hljs-keyword">return</span> midNum;<br> }<br>};<br></code></pre></td></tr></table></figure><h3 id="解法三:二分查找-分治"><a href="#解法三:二分查找-分治" class="headerlink" title="解法三:二分查找 (分治)"></a>解法三:二分查找 (分治)</h3><p>解法一、二的时间复杂度均为 O(m+n),尽管能 AC,但是并不满足题目要求。题目要求时间复杂度为 O(log(m+n)),因此需要使用<strong>二分查找</strong>进行降低时间复杂度。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">findMedianSortedArrays</span><span class="hljs-params">(vector<<span class="hljs-type">int</span>>& nums1, vector<<span class="hljs-type">int</span>>& nums2)</span> </span>{<br> <span class="hljs-type">int</span> m = nums1.<span class="hljs-built_in">size</span>(), n = nums2.<span class="hljs-built_in">size</span>();<br> function<<span class="hljs-type">int</span>(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>, <span class="hljs-type">int</span>)> f = [&](<span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j, <span class="hljs-type">int</span> k) {<br> <span class="hljs-keyword">if</span> (i >= m) {<br> <span class="hljs-keyword">return</span> nums2[j + k - <span class="hljs-number">1</span>];<br> }<br> <span class="hljs-keyword">if</span> (j >= n) {<br> <span class="hljs-keyword">return</span> nums1[i + k - <span class="hljs-number">1</span>];<br> }<br> <span class="hljs-keyword">if</span> (k == <span class="hljs-number">1</span>) {<br> <span class="hljs-keyword">return</span> <span class="hljs-built_in">min</span>(nums1[i], nums2[j]);<br> }<br> <span class="hljs-type">int</span> p = k / <span class="hljs-number">2</span>;<br> <span class="hljs-type">int</span> x = i + p - <span class="hljs-number">1</span> < m ? nums1[i + p - <span class="hljs-number">1</span>] : <span class="hljs-number">1</span> << <span class="hljs-number">30</span>;<br> <span class="hljs-type">int</span> y = j + p - <span class="hljs-number">1</span> < n ? nums2[j + p - <span class="hljs-number">1</span>] : <span class="hljs-number">1</span> << <span class="hljs-number">30</span>;<br> <span class="hljs-keyword">return</span> x < y ? <span class="hljs-built_in">f</span>(i + p, j, k - p) : <span class="hljs-built_in">f</span>(i, j + p, k - p);<br> };<br> <span class="hljs-type">int</span> a = <span class="hljs-built_in">f</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, (m + n + <span class="hljs-number">1</span>) / <span class="hljs-number">2</span>);<br> <span class="hljs-type">int</span> b = <span class="hljs-built_in">f</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, (m + n + <span class="hljs-number">2</span>) / <span class="hljs-number">2</span>);<br> <span class="hljs-keyword">return</span> (a + b) / <span class="hljs-number">2.0</span>;<br> }<br>};<br></code></pre></td></tr></table></figure><p>思路:两个升序数组求中位数,简化问题为求第 (m+n+1)/2 和第 (m+n+2)/2 个数的平均值,即找到两个正序序列的第 (m+n+1)/2 小的数和第 (m+n+2)/2 小的数。此时问题可归纳为:两个正序序列,如何找到第 k 小的数。二分法查找,每个数组中各取 k/2 的元素比较大小,较小部分的元素必然不是第 k 小的数,因此移动较小部分的指针,递归继续。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>第一直觉:归并排序,解法一和二本质相同,但时间复杂度无法满足题解要求</li><li>关键点:O(log(m+n)),二分查找</li><li>简化问题模型,根据中位数特性,将问题转化为求第 k 小的数,二分查找迭代计算</li><li>解法三使用lambda表达式,以及函数递归调用,会导致耗时和内存消耗增加,提交测试耗时和内存同解法一相似,可简化为普通函数和循环计算,避免递归次数过多导致栈溢出</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>二分查找</tag>
<tag>分治</tag>
<tag>归并排序</tag>
</tags>
</entry>
<entry>
<title>03. 无重复字符的最长子串</title>
<link href="/2024/04/13/leetcode/03.%E6%9C%80%E9%95%BF%E4%B8%8D%E9%87%8D%E5%A4%8D%E5%AD%90%E4%B8%B2/"/>
<url>/2024/04/13/leetcode/03.%E6%9C%80%E9%95%BF%E4%B8%8D%E9%87%8D%E5%A4%8D%E5%AD%90%E4%B8%B2/</url>
<content type="html"><![CDATA[<h1 id="03-无重复字符的最长子串"><a href="#03-无重复字符的最长子串" class="headerlink" title="03. 无重复字符的最长子串"></a>03. 无重复字符的最长子串</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。</p><p>示例 1:<br>输入: <code>s = "abcabcbb"</code><br>输出: <code>3</code><br>解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。</p><p>示例 2:<br>输入: <code>s = "bbbbb"</code><br>输出: <code>1</code><br>解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。</p><p>示例 3:<br>输入: <code>s = "pwwkew"</code><br>输出: <code>3</code><br>解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。<br>请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。</p><p>提示:<br>0 <= s.length <= 5 * 104<br>s 由英文字母、数字、符号和空格组成</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">lengthOfLongestSubstring</span><span class="hljs-params">(string s)</span> </span>{<br> <span class="hljs-type">int</span> m = <span class="hljs-number">0</span>, n = <span class="hljs-number">0</span>, l = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> (n = <span class="hljs-number">0</span>; n < s.<span class="hljs-built_in">size</span>(); n++) {<br> <span class="hljs-keyword">auto</span> pos = s.<span class="hljs-built_in">substr</span>(m, n - m).<span class="hljs-built_in">find</span>(s[n]);<br> <span class="hljs-keyword">if</span> (pos == std::string::npos) {<br> <span class="hljs-keyword">continue</span>;<br> } <span class="hljs-keyword">else</span> {<br> l = <span class="hljs-built_in">max</span>(l, (n - m));<br> m += (pos + <span class="hljs-number">1</span>);<br> }<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-built_in">max</span>(l, (n - m));<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>解决关键词:滑动窗口,双指针</li><li>右侧边界 n 滑动,从当前窗口 substr(m, n - m) 中查找 s[n],如果找到,则将左侧边界 m 滑动至 pos + 1,不断重复</li><li>对 std::string::substr 使用 find 方法时,需要额外加上 substr 在原始字符串的起始位置</li><li>substr 和 find 方法并非最优解,使用表存储字符出现的位置,查找时间复杂度为 O(n)。</li></ol><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">lengthOfLongestSubstring</span><span class="hljs-params">(string s)</span> </span>{<br> <span class="hljs-type">int</span> idx[<span class="hljs-number">128</span>];<br> <span class="hljs-built_in">memset</span>(idx, <span class="hljs-number">-1</span>, <span class="hljs-built_in">sizeof</span>(idx));<br> <span class="hljs-type">int</span> m = <span class="hljs-number">0</span>, n = <span class="hljs-number">0</span>, l = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span>(n = <span class="hljs-number">0</span>; n < s.<span class="hljs-built_in">size</span>(); n++) {<br> <span class="hljs-comment">// idx[s[n]] + 1 当前字符上一次出现的索引值 + 1</span><br> <span class="hljs-comment">// 确保 m 左侧边界已经越过重复字符 s[n],始终保持滑动窗口为 [m,n] 的闭区间</span><br> <span class="hljs-comment">// 即确保子串 s[m,n] 中没有重复字符</span><br> m = <span class="hljs-built_in">max</span>(m, idx[s[n]] + <span class="hljs-number">1</span>);<br> <span class="hljs-comment">// 因此子串长度为 n - m + 1</span><br> l = <span class="hljs-built_in">max</span>(l, n - m + <span class="hljs-number">1</span>);<br> <span class="hljs-comment">// 记录当前字符 s[n] 的索引值</span><br> idx[s[n]] = n;<br> }<br> <span class="hljs-keyword">return</span> l;<br> }<br>};<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>双指针</tag>
<tag>滑动窗口</tag>
</tags>
</entry>
<entry>
<title>02.两数相加</title>
<link href="/2024/04/13/leetcode/02.%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/"/>
<url>/2024/04/13/leetcode/02.%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/</url>
<content type="html"><![CDATA[<h1 id="02-两数相加"><a href="#02-两数相加" class="headerlink" title="02.两数相加"></a>02.两数相加</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给你两个 <strong>非空</strong> 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。</p><p>请你将两个数相加,并以相同形式返回一个表示和的链表。</p><p>你可以假设除了数字 0 之外,这两个数都不会以 0 开头。</p><p>示例 1:</p><p>输入:l1 = [2,4,3], l2 = [5,6,4]<br>输出:[7,0,8]<br>解释:342 + 465 = 807.<br>示例 2:</p><p>输入:l1 = [0], l2 = [0]<br>输出:[0]<br>示例 3:</p><p>输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]<br>输出:[8,9,9,9,0,0,0,1]</p><p>提示:</p><p>每个链表中的节点数在范围 [1, 100] 内<br>0 <= Node.val <= 9<br>题目数据保证列表表示的数字不含前导零</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">ListNode* <span class="hljs-title">addTwoNumbers</span><span class="hljs-params">(ListNode* l1, ListNode* l2)</span> </span>{<br> ListNode* h = <span class="hljs-keyword">new</span> <span class="hljs-built_in">ListNode</span>();<br> ListNode* p = h;<br> ListNode* m = l1;<br> ListNode* n = l2;<br> <span class="hljs-type">int</span> z = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">while</span> (m != <span class="hljs-literal">nullptr</span> || n != <span class="hljs-literal">nullptr</span>) {<br> <span class="hljs-type">int</span> x = <span class="hljs-number">0</span>, y = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">if</span> (m != <span class="hljs-literal">nullptr</span>) {<br> x = m->val;<br> m = m->next;<br> }<br> <span class="hljs-keyword">if</span> (n != <span class="hljs-literal">nullptr</span>) {<br> y = n->val;<br> n = n->next;<br> }<br> z = x + y + p->val;<br> p->val = z % <span class="hljs-number">10</span>;<br> <span class="hljs-keyword">if</span> (z < <span class="hljs-number">10</span> && m == <span class="hljs-literal">nullptr</span> && n == <span class="hljs-literal">nullptr</span>) <span class="hljs-keyword">break</span>;<br> p->next = <span class="hljs-keyword">new</span> <span class="hljs-built_in">ListNode</span>(z >= <span class="hljs-number">10</span> ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>);<br> p = p->next;<br> }<br> <span class="hljs-keyword">return</span> h;<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>第一直觉提取链表元素组装为两个整数,相加后转换为字符串再填充链表,但是<font color=red>存在数据溢出问题</font>,题目提示已给出了每个链表中的节点数在范围 [1, 100] 内,意味着单个链表表示最大的整数为 10^101 -1</li><li>关键 <strong>链表逆序排列</strong>,模拟加法从低到高逐位计算过程,同步遍历两个链表</li><li><strong>进位</strong>问题</li><li>优化点:较短链表处理完后,根据进位条件,将较长链表剩余部分直接拼接到结果链表上,减少循环次数</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>单链表</tag>
<tag>加法模拟</tag>
<tag>整数溢出</tag>
</tags>
</entry>
<entry>
<title>01.两数之和</title>
<link href="/2024/04/13/leetcode/01.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/"/>
<url>/2024/04/13/leetcode/01.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/</url>
<content type="html"><![CDATA[<h1 id="01-两数之和"><a href="#01-两数之和" class="headerlink" title="01.两数之和"></a>01.两数之和</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。</p><p>你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。</p><p>你可以按任意顺序返回答案。</p><p>示例 1:</p><p>输入:nums = [2,7,11,15], target = 9<br>输出:[0,1]<br>解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。<br>示例 2:</p><p>输入:nums = [3,2,4], target = 6<br>输出:[1,2]<br>示例 3:</p><p>输入:nums = [3,3], target = 6<br>输出:[0,1]</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">vector<<span class="hljs-type">int</span>> <span class="hljs-title">twoSum</span><span class="hljs-params">(vector<<span class="hljs-type">int</span>>& nums, <span class="hljs-type">int</span> target)</span> </span>{<br> std::unordered_map<<span class="hljs-type">int</span>, <span class="hljs-type">int</span>> hash_map;<br> <span class="hljs-keyword">for</span>(<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < nums.<span class="hljs-built_in">size</span>(); i++) {<br> <span class="hljs-keyword">if</span> (hash_map.<span class="hljs-built_in">find</span>(target - nums[i]) != hash_map.<span class="hljs-built_in">end</span>()) {<br> <span class="hljs-keyword">return</span> {hash_map[target - nums[i]], i};<br> }<br> hash_map[nums[i]] = i;<br> }<br> <span class="hljs-keyword">return</span> {};<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="关键"><a href="#关键" class="headerlink" title="关键"></a>关键</h2><ol><li>双循环暴力枚举可解,但是时间复杂度为O(n^2)。</li><li>空间换时间思路,基于<mark><strong>哈希表</strong>查找时间复杂度为O(1)</mark>,将遍历过的元素存入哈希表,将问题转换为:查找哈希表中是否存在 value 是否等于 target - nums[i] 的元素。</li><li>哈希表 key 为数组元素,value 为数组下标。</li><li>C++ 中典型的哈希表实现为 <code>std::unordered_map</code> 和 <code>std::unordered_set</code>,查找元素时间复杂度为 O(1)。</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>数组</tag>
<tag>哈希表</tag>
</tags>
</entry>
<entry>
<title>13. 罗马数字转整数</title>
<link href="/2024/04/12/leetcode/13.%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97%E8%BD%AC%E6%95%B4%E6%95%B0/"/>
<url>/2024/04/12/leetcode/13.%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97%E8%BD%AC%E6%95%B4%E6%95%B0/</url>
<content type="html"><![CDATA[<h1 id="13-罗马数字转整数"><a href="#13-罗马数字转整数" class="headerlink" title="13. 罗马数字转整数"></a>13. 罗马数字转整数</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>规则同题【12.整数转罗马数字】,只是转换方向相反,由罗马数字转换为整数。</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:从小到大遍历"><a href="#解法一:从小到大遍历" class="headerlink" title="解法一:从小到大遍历"></a>解法一:从小到大遍历</h3><p>由于罗马数字中,”V”、”X”、”L”、”C”、”D”、”M” 这几个字符可能存在前缀,因此当遇到这几个字符时,需要额外判断前一位是否为对应的前缀。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">romanToInt</span><span class="hljs-params">(std::string s)</span> </span>{<br> <span class="hljs-type">int</span> x = <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = s.<span class="hljs-built_in">size</span>() - <span class="hljs-number">1</span>; i >= <span class="hljs-number">0</span>; i--) {<br> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'I'</span>) {<br> x += <span class="hljs-number">1</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'V'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'I'</span>) {<br> x += <span class="hljs-number">4</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">5</span>;<br> }<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'X'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'I'</span>) {<br> x += <span class="hljs-number">9</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">10</span>;<br> }<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'L'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'X'</span>) {<br> x += <span class="hljs-number">40</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">50</span>;<br> }<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'C'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'X'</span>) {<br> x += <span class="hljs-number">90</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">100</span>;<br> }<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'D'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'C'</span>) {<br> x += <span class="hljs-number">400</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">500</span>;<br> }<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (s[i] == <span class="hljs-string">'M'</span>) {<br> <span class="hljs-keyword">if</span> (i > <span class="hljs-number">0</span> && s[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'C'</span>) {<br> x += <span class="hljs-number">900</span>;<br> i--;<br> } <span class="hljs-keyword">else</span> {<br> x += <span class="hljs-number">1000</span>;<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> x;<br> }<br>};<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>贪心</tag>
<tag>硬编码</tag>
</tags>
</entry>
<entry>
<title>12. 整数转罗马数字</title>
<link href="/2024/04/12/leetcode/12.%E6%95%B4%E6%95%B0%E8%BD%AC%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97/"/>
<url>/2024/04/12/leetcode/12.%E6%95%B4%E6%95%B0%E8%BD%AC%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97/</url>
<content type="html"><![CDATA[<h1 id="12-整数转罗马数字"><a href="#12-整数转罗马数字" class="headerlink" title="12. 整数转罗马数字"></a>12. 整数转罗马数字</h1><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><p>罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。</p><table><thead><tr><th align="left">字符</th><th align="left">数值</th></tr></thead><tbody><tr><td align="left">I</td><td align="left">1</td></tr><tr><td align="left">V</td><td align="left">5</td></tr><tr><td align="left">X</td><td align="left">10</td></tr><tr><td align="left">L</td><td align="left">50</td></tr><tr><td align="left">C</td><td align="left">100</td></tr><tr><td align="left">D</td><td align="left">500</td></tr><tr><td align="left">M</td><td align="left">1000</td></tr></tbody></table><p>例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。</p><p>通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:</p><ul><li>I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。</li><li>X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 </li><li>C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。</li></ul><p>给你一个整数,将其转为罗马数字。</p><p>示例 1:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入: num = 3<br>输出: "III"<br></code></pre></td></tr></table></figure><p>示例 2:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入: num = 4<br>输出: "IV"<br></code></pre></td></tr></table></figure><p>示例 3:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入: num = 9<br>输出: "IX"<br></code></pre></td></tr></table></figure><p>示例 4:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入: num = 58<br>输出: "LVIII"<br>解释: L = 50, V = 5, III = 3.<br></code></pre></td></tr></table></figure><p>示例 5:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs txt">输入: num = 1994<br>输出: "MCMXCIV"<br>解释: M = 1000, CM = 900, XC = 90, IV = 4.<br></code></pre></td></tr></table></figure><p>提示:</p><ul><li>1 <= num <= 3999</li></ul><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><h3 id="解法一:-贪心算法"><a href="#解法一:-贪心算法" class="headerlink" title="解法一: 贪心算法"></a>解法一: 贪心算法</h3><p>根据题意,罗马数字存在固定的组合,我们只需要从大到小枚举所有的组合即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br> <span class="hljs-keyword">private</span>:<br> std::string Roman[<span class="hljs-number">13</span>] = {<span class="hljs-string">"M"</span>, <span class="hljs-string">"CM"</span>, <span class="hljs-string">"D"</span>, <span class="hljs-string">"CD"</span>, <span class="hljs-string">"C"</span>, <span class="hljs-string">"XC"</span>, <span class="hljs-string">"L"</span>, <span class="hljs-string">"XL"</span>, <span class="hljs-string">"X"</span>, <span class="hljs-string">"IX"</span>, <span class="hljs-string">"V"</span>, <span class="hljs-string">"IV"</span>, <span class="hljs-string">"I"</span>}; <br> <span class="hljs-type">int</span> Value[<span class="hljs-number">13</span>] = {<span class="hljs-number">1000</span>, <span class="hljs-number">900</span>, <span class="hljs-number">500</span>, <span class="hljs-number">400</span>, <span class="hljs-number">100</span>, <span class="hljs-number">90</span>, <span class="hljs-number">50</span>, <span class="hljs-number">40</span>, <span class="hljs-number">10</span>, <span class="hljs-number">9</span>, <span class="hljs-number">5</span>, <span class="hljs-number">4</span>, <span class="hljs-number">1</span>};<br><br> <span class="hljs-keyword">public</span>:<br> <span class="hljs-function">std::string <span class="hljs-title">intToRoman</span><span class="hljs-params">(<span class="hljs-type">int</span> num)</span> </span>{<br> std::string res;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-number">13</span>; i++) {<br> <span class="hljs-keyword">while</span> (num >= Value[i]) {<br> num -= Value[i];<br> res += Roman[i];<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br> }<br>};<br></code></pre></td></tr></table></figure><p>用 if-else 优化速度和内存消耗:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function">std::string <span class="hljs-title">intToRoman</span><span class="hljs-params">(<span class="hljs-type">int</span> num)</span> </span>{<br> std::string res;<br> <span class="hljs-keyword">while</span> (num > <span class="hljs-number">0</span>) {<br> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">1000</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">1000</span>;<br> res += <span class="hljs-string">'M'</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">900</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">900</span>;<br> res += <span class="hljs-string">"CM"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">500</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">500</span>;<br> res += <span class="hljs-string">"D"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">400</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">400</span>;<br> res += <span class="hljs-string">"CD"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">100</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">100</span>;<br> res += <span class="hljs-string">"C"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">90</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">90</span>;<br> res += <span class="hljs-string">"XC"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">50</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">50</span>;<br> res += <span class="hljs-string">"L"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">40</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">40</span>;<br> res += <span class="hljs-string">"XL"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">10</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">10</span>;<br> res += <span class="hljs-string">"X"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">9</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">9</span>;<br> res += <span class="hljs-string">"IX"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">5</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">5</span>;<br> res += <span class="hljs-string">"V"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">4</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">4</span>;<br> res += <span class="hljs-string">"IV"</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (num - <span class="hljs-number">1</span> >= <span class="hljs-number">0</span>) {<br> num -= <span class="hljs-number">1</span>;<br> res += <span class="hljs-string">"I"</span>;<br> }<br> }<br> <span class="hljs-keyword">return</span> res;<br>}<br></code></pre></td></tr></table></figure><h3 id="解法二:硬编码"><a href="#解法二:硬编码" class="headerlink" title="解法二:硬编码"></a>解法二:硬编码</h3><p>官方题解给出了 <strong>硬编码</strong> 的解法,根据罗马数字的特点,可以将 13 个符号分成千位,百位,十位和个位,将每个位的所有情况硬编码成一个映射表,然后根据数字的每一位去查找对应 Roman 数字。</p><p><img src="https://assets.leetcode-cn.com/solution-static/12/3.png" alt="硬编码表"></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">const</span> string thousands[] = {<span class="hljs-string">""</span>, <span class="hljs-string">"M"</span>, <span class="hljs-string">"MM"</span>, <span class="hljs-string">"MMM"</span>};<br><span class="hljs-type">const</span> string hundreds[] = {<span class="hljs-string">""</span>, <span class="hljs-string">"C"</span>, <span class="hljs-string">"CC"</span>, <span class="hljs-string">"CCC"</span>, <span class="hljs-string">"CD"</span>, <span class="hljs-string">"D"</span>, <span class="hljs-string">"DC"</span>, <span class="hljs-string">"DCC"</span>, <span class="hljs-string">"DCCC"</span>, <span class="hljs-string">"CM"</span>};<br><span class="hljs-type">const</span> string tens[] = {<span class="hljs-string">""</span>, <span class="hljs-string">"X"</span>, <span class="hljs-string">"XX"</span>, <span class="hljs-string">"XXX"</span>, <span class="hljs-string">"XL"</span>, <span class="hljs-string">"L"</span>, <span class="hljs-string">"LX"</span>, <span class="hljs-string">"LXX"</span>, <span class="hljs-string">"LXXX"</span>, <span class="hljs-string">"XC"</span>};<br><span class="hljs-type">const</span> string ones[] = {<span class="hljs-string">""</span>, <span class="hljs-string">"I"</span>, <span class="hljs-string">"II"</span>, <span class="hljs-string">"III"</span>, <span class="hljs-string">"IV"</span>, <span class="hljs-string">"V"</span>, <span class="hljs-string">"VI"</span>, <span class="hljs-string">"VII"</span>, <span class="hljs-string">"VIII"</span>, <span class="hljs-string">"IX"</span>};<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function">string <span class="hljs-title">intToRoman</span><span class="hljs-params">(<span class="hljs-type">int</span> num)</span> </span>{<br> <span class="hljs-keyword">return</span> thousands[num / <span class="hljs-number">1000</span>] + hundreds[num % <span class="hljs-number">1000</span> / <span class="hljs-number">100</span>] + tens[num % <span class="hljs-number">100</span> / <span class="hljs-number">10</span>] + ones[num % <span class="hljs-number">10</span>];<br> }<br>};<br></code></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>罗马数字组合有限且确定,从大到小逐位枚举即可;</li></ol>]]></content>
<categories>
<category>leetcode</category>
</categories>
<tags>
<tag>贪心</tag>
<tag>硬编码</tag>
</tags>
</entry>
<entry>
<title>关于字符串转换整数 (atoi) 的思考</title>
<link href="/2024/04/11/%E5%85%B3%E4%BA%8E%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E6%95%B4%E6%95%B0%E7%AE%97%E6%B3%95%E9%A2%98%E7%9A%84%E6%80%9D%E8%80%83/"/>
<url>/2024/04/11/%E5%85%B3%E4%BA%8E%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E6%95%B4%E6%95%B0%E7%AE%97%E6%B3%95%E9%A2%98%E7%9A%84%E6%80%9D%E8%80%83/</url>
<content type="html"><![CDATA[<p>[TOC]</p><h1 id="关于字符串转换整数-atoi-的思考"><a href="#关于字符串转换整数-atoi-的思考" class="headerlink" title="关于字符串转换整数 (atoi) 的思考"></a>关于字符串转换整数 (atoi) 的思考</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近解了一道 LeetCode 中的算法题: <em>8.字符串转换整数 (atoi)</em> ,描述如下:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs txt">请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。<br><br>函数 myAtoi(string s) 的算法如下:<br><br>1. 读入字符串并丢弃无用的前导空格<br>2. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。<br>3. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。<br>4. 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。<br>5. 如果整数数超过 32 位有符号整数范围 [−2^31, 2^31 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^31 − 1 的整数应该被固定为 2^31 − 1 。<br>6. 返回整数作为最终结果。<br></code></pre></td></tr></table></figure><p>在 AC 后,看了官方和其他人的题解,了解到了一个不曾涉及到过的知识点:有限状态机 (Finite State Automaton) 和确定性有限自动机 (Deterministic Finite Automaton, DFA)。</p><p>因此在这里记录一下解决这个题目,并学习这个新知识点的过程。</p><h2 id="解法一、正则表达式-“-0-9-”"><a href="#解法一、正则表达式-“-0-9-”" class="headerlink" title="解法一、正则表达式 “^ *([\\-\\+]?[0-9]+)”"></a>解法一、正则表达式 “^ *([\\-\\+]?[0-9]+)”</h2><h3 id="提取有效整数字符串"><a href="#提取有效整数字符串" class="headerlink" title="提取有效整数字符串"></a>提取有效整数字符串</h3><p>在最开始看到这个题目的时候,厘清题干对数字有效性的要求后,可以将这个特征提取为以下正则表达式: </p><p><code>^ *([\\-\\+]?[0-9]+)</code></p><p>根据题干的第一点,我们可知,字符串可能存在前导空格,因此我们这里用 <code>^ *</code> 来匹配字符串起始位置的 0 个或多个 ‘ ‘ 空格。</p><p>然后题干第 2 点表明,可能存在正负符号,用于解释当前待转换字符串整数的正负性。这里需要注意,**’+/-‘ 符号只能为 0 个或 1 个**。因为根据第 3 点的描述,在读入正负号后,下一个字符如果为非数字字符,则剩余部分会被全部忽略。所以我们使用 <code>[\\-\\+]?</code> 来匹配 0 个或 1 个 ‘-‘ 或 ‘+’ 符号。</p><p>在去掉前导空格和读取正负符号后,只能是数字字符,并且应该是** 1 个或多个**时,当前数字才有效。所以用 <code>[0-9]+</code> 来匹配 1 个或多个数字字符。</p><p>通过这个正则表达式即可判断当前字符串是否能被转换为有效的整数,并且使用 <code>()</code> 的子表达式方法可以直接提取出整数的有效部分。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// 使用正则表达式和 std::stoi </span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">myAtoi</span><span class="hljs-params">(std::string s)</span> </span>{<br> <span class="hljs-function">std::regex <span class="hljs-title">re</span><span class="hljs-params">(<span class="hljs-string">"^ *([\\+\\-]?[0-9]+)"</span>)</span></span>;<br> std::smatch m;<br> std::string sub;<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">regex_search</span>(s, m, re)) {<br> <span class="hljs-keyword">if</span> (m.<span class="hljs-built_in">size</span>() > <span class="hljs-number">1</span>) {<br> sub = m[<span class="hljs-number">1</span>].<span class="hljs-built_in">str</span>();<br> }<br> }<br> <span class="hljs-keyword">if</span> (sub.<span class="hljs-built_in">size</span>() == <span class="hljs-number">0</span>) {<br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-keyword">return</span> std::<span class="hljs-built_in">stoi</span>(sub.<span class="hljs-built_in">c_str</span>());<br> } <span class="hljs-built_in">catch</span> (std::exception& e) {<br> <span class="hljs-keyword">return</span> sub[<span class="hljs-number">0</span>] == <span class="hljs-string">'-'</span> ? INT_MIN : INT_MAX;<br> }<br> }<br> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> }<br>};<br></code></pre></td></tr></table></figure><p>当然要转换成数字还有一个 signed int 类型的溢出问题。</p><h3 id="signed-int-溢出判断"><a href="#signed-int-溢出判断" class="headerlink" title="signed int 溢出判断"></a>signed int 溢出判断</h3><blockquote><p>溢出判断时每个解法都需要考虑的问题,后续几种解法中的溢出判断也都是这一章节中所提到的,因此在后续解法中不在重复描述。</p></blockquote><h4 id="std-stoi-和-out-of-range"><a href="#std-stoi-和-out-of-range" class="headerlink" title="std::stoi 和 out of range"></a>std::stoi 和 out of range</h4><p>最开始的时候,为了方便,直接使用 <code>std::stoi</code> 来进行转换,这个函数能直接转换带 ‘+’ 和 ‘-‘ 的整数,并且在溢出时会抛出 “out of range” 的异常,因此只要一个 <code>try-catch</code> 就可以解决,具体应用可见上一节中给出的代码示例。</p><h4 id="字符串比较"><a href="#字符串比较" class="headerlink" title="字符串比较"></a>字符串比较</h4><p>如果说使用 <code>std::stoi</code> 也太犯规了,那么换种思路,通过整数字符串 s 和 INT_MIN (-2147483648), INT_MAX (2147483647) 的字符串进行比较,也是一种判断溢出的方法。并且该方法在判断完成后再转换为 int 类型,一定不会造成溢出问题。</p><p>使用字符串比较需要注意两个点:</p><ol><li>整数字符串可能存在 ‘+/-‘ 符号,比较前需要统一,即两个字符串要么都有符号,要么都没有符号;</li><li>比较过程中需保持整数字符串 s 和 INT_MIN, INT_MAX 字符串长度一致,较短字符串前部追加 ‘0’ 字符;</li></ol><h4 id="乘法判断"><a href="#乘法判断" class="headerlink" title="乘法判断"></a>乘法判断</h4><p>由于 string 转换成 int 是必经之路,因此我们在逐位转换过程中直接比较乘法结果是否溢出即可。</p><p>一个整数字符串 s 转换成整数 (不考虑符号,因为不考虑符号的情况下判断溢出,永远只需要判断 ‘>’ 即可,可有效减少判断条件数量) 的过程,只需要重复计算 <code>num = num * 10 + s[i] - '0'</code>,这里我们限制只能使用 int 类型存储 res,因此我们需要在每次计算之前就进行判断,否则在计算过程中会提示溢出错误。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (溢出) {<br> <span class="hljs-keyword">return</span> (符号 == <span class="hljs-string">'-'</span>) ? INT_MIN : INT_MAX;<br>} <span class="hljs-keyword">else</span> {<br> num = num * <span class="hljs-number">10</span> + s[i] - <span class="hljs-string">'0'</span>;<br>}<br></code></pre></td></tr></table></figure><p>会溢出的情况有以下几种:</p><ol><li>当前为负数,且 num > INT_MAX / 10 (为什么是 INT_MAX / 10?因为此时的 num 不考虑符号,那么 INT_MAX / 10 和 -INT_MIN / 10 结果是一样的,都等于 ‘214748364’);</li><li>当前为负数,且 num == INT_MAX / 10 且 s[i] > ‘8’;</li><li>当前为正数,且 num > INT_MAX / 10;</li><li>当前为正数,且 num == INT_MAX / 10 且 s[i] > ‘7’;</li></ol><p>看似有 4 种情况,但是实际上,我们可以将问题优化为 2 种情况:</p><ol><li>num > INT_MAX / 10;</li><li>num == INT_MAX / 10 且 s[i] > ‘7’;</li></ol><p>为什么正数和负数的情况下,一个是 <code>s[i] > '7'</code>,一个是 <code>s[i] > '8'</code> 可以统一为 <code>s[i] > 7</code> 呢?</p><p>这是因为当负数 num == INT_MAX / 10 且 s[i] == ‘8’ 时,我们假定此时已经溢出直接返回 INT_MIN ,和未溢出继续计算 <code>num = num * 10 + s[i] - '0'</code> 的结果都是 INT_MIN。</p><p>但是继续计算的话,由于我们在此前将 num 符号去除了,也就意味着 num 作为一个非负数始终应该小于 INT_MAX,但是当 num == INT_MAX / 10 且 s[i] == ‘8 时,结果已经溢出了。</p><p>所以我们不妨将这种情况也视为已溢出,将溢出条件极大的简化。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">if</span> (num > INT_MAX / <span class="hljs-number">10</span> || (num == INT_MAX / <span class="hljs-number">10</span> && s[i] > <span class="hljs-string">'7'</span>)) {<br> <span class="hljs-keyword">return</span> (符号 == <span class="hljs-string">'-'</span>) ? INT_MIN : INT_MAX;<br>} <span class="hljs-keyword">else</span> {<br> num = num * <span class="hljs-number">10</span> + s[i] - <span class="hljs-string">'0'</span>;<br>}<br></code></pre></td></tr></table></figure><h2 id="解法二、确定性有限状态机-DFA"><a href="#解法二、确定性有限状态机-DFA" class="headerlink" title="解法二、确定性有限状态机 (DFA)"></a>解法二、确定性有限状态机 (DFA)</h2><h3 id="状态机"><a href="#状态机" class="headerlink" title="状态机"></a>状态机</h3><p><strong>状态机(State Machine)</strong>是一种抽象的计算模型,用于描述一个系统或算法在不同状态之间的转换和行为。它可以被用来建模各种问题,从简单的逻辑控制到复杂的软件系统。状态机通常由以下几个部分组成:</p><ol><li><p><strong>状态(State):</strong> 系统可能处于的不同状态。每个状态代表系统的某种特定行为或情况。例如,自动售货机可以处于”待命”、”投币”、”出售”等状态。</p></li><li><p><strong>事件(Event):</strong> 引起状态转换的外部或内部事件。当事件发生时,系统可以从当前状态转移到新的状态。例如,自动售货机可能会接收到”投币”、”选择商品”等事件。</p></li><li><p><strong>转移(Transition):</strong> 从一个状态到另一个状态的过渡,通常与事件相关联。转移描述了当某个特定事件发生时系统如何从一个状态切换到另一个状态。</p></li><li><p><strong>动作(Action):</strong> 在状态转换时执行的操作或行为。动作可以是更新内部状态、输出信息、执行计算等。例如,当自动售货机从”投币”状态转移到”出售”状态时,会扣除相应金额并输出商品。</p></li><li><p><strong>初始状态(Initial State):</strong> 系统在开始时所处的状态。</p></li><li><p><strong>终止状态(Final State):</strong> 系统的终止状态,表示系统已经完成了某个任务或达到了某个目标。</p></li></ol><p>状态机可以分为有限状态机(Finite State Machine,FSM)和无限状态机(Infinite State Machine)。在有限状态机中,状态的数量是有限的,状态之间的转换也是有限的;而在无限状态机中,状态的数量是无限的,状态之间的转换也可以是无限的。</p><p>状态机在计算机科学领域有着广泛的应用,例如在编译器设计、网络协议分析、游戏开发等方面都能看到它的身影。</p><h3 id="确定性有限状态机(Deterministic-Finite-Automaton,DFA)"><a href="#确定性有限状态机(Deterministic-Finite-Automaton,DFA)" class="headerlink" title="确定性有限状态机(Deterministic Finite Automaton,DFA)"></a>确定性有限状态机(Deterministic Finite Automaton,DFA)</h3><p>确定性有限状态机(Deterministic Finite Automaton,DFA)是有限状态机(Finite State Machine,FSM)的一个子集。DFA 是一种特殊类型的有限状态机,其特点是在给定状态和输入字符的情况下,只有一种确定的状态转移路径。</p><p>与一般的有限状态机相比,确定性状态机具有以下不同之处:</p><ol><li><p><strong>确定性:</strong> DFA 在任何给定时刻都有一个唯一的状态,且从当前状态和输入字符出发只能转移到一个确定的下一个状态。这种确定性使得 DFA 在状态转移和行为方面更加可预测和简单。</p></li><li><p><strong>状态转移表:</strong> DFA 可以使用状态转移表来描述状态之间的转移关系。在状态转移表中,每一行代表一个状态,每一列代表一个输入字符,表格中的每个元素表示从当前状态经过对应输入字符转移到的下一个状态。这种表格形式的表示使得 DFA 的状态转移过程变得直观和易于理解。</p></li><li><p><strong>非确定性:</strong> 与非确定性有限状态机(Non-deterministic Finite Automaton,NFA)相比,DFA 不允许存在一个状态在给定输入字符的情况下具有多个可能的转移路径。这种特性使得 DFA 在状态转移和行为方面更加确定和可靠。</p></li></ol><p>总的来说,确定性状态机是有限状态机中的一种特殊形式,它的确定性和简单性使得它在许多实际应用中得到了广泛的应用,例如在编译器设计、字符串匹配算法、网络协议分析等领域。</p><h3 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h3><blockquote><p>这里直接引用 LeetCode 官方题解中的图示,方便理解。</p></blockquote><h4 id="状态定义"><a href="#状态定义" class="headerlink" title="状态定义"></a>状态定义</h4><p>根据题干,我们可以定义这几个状态:</p><ol><li><strong>起始空格状态 (start):</strong> 从起始位置开始的空格状态;</li><li><strong>符号位状态 (signed):</strong> 正负号位状态;</li><li><strong>数字状态 (in_number):</strong> 数字状态;</li><li><strong>结束状态 (end):</strong> 结束状态;</li></ol><h4 id="事件"><a href="#事件" class="headerlink" title="事件"></a>事件</h4><p>在这个算法题中,事件即<strong>读取一个字符</strong>。</p><h4 id="状态转移表"><a href="#状态转移表" class="headerlink" title="状态转移表"></a>状态转移表</h4><p><img src="https://assets.leetcode-cn.com/solution-static/8/fig1.png" alt="状态转移图"></p><p>可以看出,每读取一个字符,对应的状态均是唯一确定的,因此根据上图,我们可以得到以下状态转移表:</p><table><thead><tr><th align="center">state</th><th align="center">‘ ‘</th><th align="center">+/-</th><th align="center">number</th><th align="center">other</th></tr></thead><tbody><tr><td align="center">start</td><td align="center">start</td><td align="center">signed</td><td align="center">in_number</td><td align="center">end</td></tr><tr><td align="center">signed</td><td align="center">end</td><td align="center">end</td><td align="center">in_number</td><td align="center">end</td></tr><tr><td align="center">in_number</td><td align="center">end</td><td align="center">end</td><td align="center">in_number</td><td align="center">end</td></tr><tr><td align="center">end</td><td align="center">end</td><td align="center">end</td><td align="center">end</td><td align="center">end</td></tr></tbody></table><p>我们可以直接使用一个 <code>std::unordered_map<std::string, std::vector<std::string>></code> 来表示这个状态转移表:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs cpp">std::unordered_map<std::string, std::vector<std::string>> stat_tb = {<br> {<span class="hljs-string">"start"</span>, {<span class="hljs-string">"start"</span>, <span class="hljs-string">"signed"</span>, <span class="hljs-string">"in_number"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"signed"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"in_number"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"in_number"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"in_number"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"end"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>}}<br>};<br></code></pre></td></tr></table></figure><h4 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h4><p>有了上面的状态转移表,我们只需要每读取一个字符,然后判断状态,并转移状态,在转换整数时结合上一章节中提到的溢出判断,即可完成字符串到整数的转换。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// 利用 DFA (确定性有限自动机) 解决该问题</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br> <span class="hljs-keyword">private</span>:<br> <span class="hljs-comment">// 设置状态转移表</span><br> std::unordered_map<std::string, std::vector<std::string>> stat_tb = {<br> {<span class="hljs-string">"start"</span>, {<span class="hljs-string">"start"</span>, <span class="hljs-string">"sign"</span>, <span class="hljs-string">"num"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"sign"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"num"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"num"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"num"</span>, <span class="hljs-string">"end"</span>}},<br> {<span class="hljs-string">"end"</span>, {<span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>, <span class="hljs-string">"end"</span>}},<br> };<br><br> <span class="hljs-comment">// 根据当前输入字符,获取状态</span><br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">getch</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> </span>{<br> <span class="hljs-keyword">if</span> (c == <span class="hljs-string">' '</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br> <span class="hljs-keyword">if</span> (c == <span class="hljs-string">'+'</span> || c == <span class="hljs-string">'-'</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isdigit</span>(c)) <span class="hljs-keyword">return</span> <span class="hljs-number">2</span>;<br> <span class="hljs-keyword">return</span> <span class="hljs-number">3</span>;<br> }<br><br> <span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">myAtoi</span><span class="hljs-params">(std::string s)</span> </span>{<br> <span class="hljs-type">int</span> num = <span class="hljs-number">0</span>, sign = <span class="hljs-number">1</span>, lim_a = INT_MAX / <span class="hljs-number">10</span>, lim_b = INT_MAX % <span class="hljs-number">10</span>;<br> std::string stat = <span class="hljs-string">"start"</span>;<br> <br> <span class="hljs-keyword">for</span> (<span class="hljs-type">char</span> c : s) {<br> <span class="hljs-comment">// 此处使用下标的方式读取当前字符输入状态转移结果,极大简化了判断条件</span><br> stat = stat_tb[stat][<span class="hljs-built_in">getch</span>(c)];<br> <span class="hljs-keyword">if</span> (stat == <span class="hljs-string">"sign"</span>) {<br> sign = c == <span class="hljs-string">'-'</span> ? <span class="hljs-number">-1</span> : <span class="hljs-number">1</span>;<br> }<br> <span class="hljs-keyword">if</span> (stat == <span class="hljs-string">"num"</span>) {<br> <span class="hljs-comment">// 溢出判断</span><br> <span class="hljs-keyword">if</span> (num > lim_a || (num == lim_a && c - <span class="hljs-string">'0'</span> > lim_b)) {<br> <span class="hljs-keyword">return</span> sign == <span class="hljs-number">-1</span> ? INT_MIN : INT_MAX;<br> }<br> num = num * <span class="hljs-number">10</span> + (c - <span class="hljs-string">'0'</span>);<br> }<br> <span class="hljs-keyword">if</span> (stat == <span class="hljs-string">"end"</span>) <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-keyword">return</span> num * sign;<br> }<br>};<br></code></pre></td></tr></table></figure><p>官方题解中使用 map 和 vector 的组合来存储状态转移表,并且通过下标的方式读取输入字符后状态转移的结果,这一设计非常巧妙,值得学习。</p><p>当然我们单纯使用 <code>if-else</code> 完成判断并转移状态,也是可以的,只不过代码会相对长一些,这也是我在了解状态机后手搓的代码:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Solution</span> {<br><span class="hljs-keyword">private</span>:<br> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">State</span> { Start, Signed, Number, End };<br><br><span class="hljs-keyword">public</span>:<br> <span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">myAtoi</span><span class="hljs-params">(std::string s)</span> </span>{<br> <span class="hljs-type">int</span> stat_ = Start, num_ = <span class="hljs-number">0</span>, sig_ = <span class="hljs-number">1</span>;<br> <span class="hljs-type">int</span> lim_a = INT_MAX / <span class="hljs-number">10</span>, lim_b = INT_MAX % <span class="hljs-number">10</span>;<br> <span class="hljs-keyword">for</span> (<span class="hljs-type">char</span> ch : s) {<br> <span class="hljs-keyword">switch</span> (stat_) {<br> <span class="hljs-keyword">case</span> Start: {<br> <span class="hljs-keyword">if</span> (ch == <span class="hljs-string">' '</span>) {<br> stat_ = Start;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (ch == <span class="hljs-string">'+'</span> || ch == <span class="hljs-string">'-'</span>) {<br> stat_ = Signed;<br> sig_ = (ch == <span class="hljs-string">'-'</span>) ? <span class="hljs-number">-1</span> : <span class="hljs-number">1</span>;<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isdigit</span>(ch)) {<br> stat_ = Number;<br> num_ = num_ * <span class="hljs-number">10</span> + (ch - <span class="hljs-string">'0'</span>);<br> } <span class="hljs-keyword">else</span> {<br> stat_ = End;<br> }<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-keyword">case</span> Signed: {<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isdigit</span>(ch)) {<br> stat_ = Number;<br> num_ = num_ * <span class="hljs-number">10</span> + (ch - <span class="hljs-string">'0'</span>);<br> } <span class="hljs-keyword">else</span> {<br> stat_ = End;<br> }<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-keyword">case</span> Number: {<br> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isdigit</span>(ch)) {<br> <span class="hljs-keyword">if</span> (num_ > lim_a || (num_ == lim_a && ch - <span class="hljs-string">'0'</span> > lim_b)) {<br> <span class="hljs-keyword">return</span> sig_ == <span class="hljs-number">-1</span> ? INT_MIN : INT_MAX;<br> } <span class="hljs-keyword">else</span> {<br> num_ = num_ * <span class="hljs-number">10</span> + (ch - <span class="hljs-string">'0'</span>);<br> }<br> } <span class="hljs-keyword">else</span> {<br> stat_ = End;<br> }<br> <span class="hljs-keyword">break</span>;<br> }<br> <span class="hljs-keyword">case</span> End: {<br> <span class="hljs-keyword">return</span> num_ * sig_;<br> }<br> }<br> }<br> <span class="hljs-keyword">return</span> num_ * sig_;<br> }<br>};<br></code></pre></td></tr></table></figure><blockquote><p>吐槽一句,使用 <code>std::unordered_map<std::string, std::vector<std::string>></code> 的代码看起来简洁很多,但是在实际运行时,和 使用 <code>if-else</code> 的代码相比,性能差距大的不是一点半点…使用上面第二种我手搓的代码,AC 显示耗时 0ms,但是官方题解需要 11 ms…所以也不能一味的追求代码的简短 囧。。</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>在工作业务中,如果并非追求极致的造轮子,合理应用<strong>正则表达式</strong>和标准库函数来解决业务问题,也不失为一种好的选择;</li><li><strong>溢出边界的判断条件</strong>优化,是简化代码的关键;</li><li>使用<strong>确定性有限状态机</strong>解决这类问题(有输入,根据输入可确定唯一状态),是一种非常清晰好理解,并且简洁的解决方案;</li></ol>]]></content>
<categories>
<category>程序员进阶</category>
</categories>
<tags>
<tag>原创</tag>
<tag>leetcode</tag>
<tag>确定有限状态机</tag>
<tag>DFA</tag>
</tags>
</entry>
<entry>
<title>Media-Download-Helper 项目说明</title>
<link href="/2024/03/08/Media-Download-Helper%20%E9%A1%B9%E7%9B%AE%E8%AF%B4%E6%98%8E/"/>
<url>/2024/03/08/Media-Download-Helper%20%E9%A1%B9%E7%9B%AE%E8%AF%B4%E6%98%8E/</url>
<content type="html"><![CDATA[<h1 id="Media-Download-Helper"><a href="#Media-Download-Helper" class="headerlink" title="Media-Download-Helper"></a>Media-Download-Helper</h1><p>完整部署教程请参考:<a href="https://github.com/Ccccx159/Media-Download-Helper/blob/v2.0.0/doc/Media-Download-Helper%20%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B.md">Media-Download-Helper 完整部署教程</a></p><h2 id="V2-0-0-版本说明"><a href="#V2-0-0-版本说明" class="headerlink" title="V2.0.0 版本说明"></a>V2.0.0 版本说明</h2><blockquote><p>鉴于公网 IP 难以获取,而且将内部服务直接暴露到公网环境,安全系数较低,部署难度较大等几个原因,因此在 v2.0.0 版本中,取消了 http server 模块,由 media download helper 直接向 Google Apps Script 主动发起 get 请求进行轮询获取磁力链接信息。去除了公网 ip 的依赖,提高安全性的同时也增加了部署的便利性。</p><p>当然,v2.0.0 并非完美的方案,它同样存在着弊端,就是轮询请求 Google Apps Script 部署的在线 Web 应用的间隔不好把控。目前版本设置的轮询间隔为 1 分钟,这也就意味着 Google Apps Script 的 web 应用一天需要相应 1440 次请求,如果可以,请考虑使用更长的轮询间隔,避免薅羊毛过狠导致后期不可用的情况出现。</p><p>还有一点,v2.0.0 版本中,Google Apps Script 仅仅通过脚本属性字段来缓存接收到的 post 数据,这里也存在着未知的风险。</p><p>如果条件允许的情况下,还是推举使用 v1.x.x 版本,由 google apps script 响应 post 请求时,直接将数据推送到 Media Download Helper 内置的 http server,避免了缓存和并发的问题。</p><p>当然,轻度使用场景下,v2.0.0 版本也是可以接受的。</p></blockquote><h2 id="开发目的"><a href="#开发目的" class="headerlink" title="开发目的"></a>开发目的</h2><p>玩个人影视服务器的朋友,应该都知道 Jeckett,Sonarr,Radarr 这些自动下载工具,只要我添加了对应的 PT 站,它们就能根据提交的请求进行检索下载。但是部分资源可能不在 PT 站,尤其是部分生肉,外网的磁力链接发布的更快一些,例如 TG 上的一些频道。Radarr 和 Sonarr 不支持直接使用磁力链接进行下载,在外也不方便使用这些内网的服务。</p><p>再者有部分朋友也不想弄 PT 和上面说的那些服务,只是偶尔有部分资源想下载下来观看一下,完整部署自动化追剧工具就显得有点大材小用了。</p><p>所以才想开发一个小工具,借助免费的工具就可以实现在外远程进行下载,同时还可以在下载完成后通过电报机器人推送影片信息,方便查看下载信息。</p><h2 id="项目简介"><a href="#项目简介" class="headerlink" title="项目简介"></a>项目简介</h2><p><strong>“Media Download Helper”</strong> 是一个基于电报机器人,Google Apps Script 和 qbittorrent 的多媒体下载助手。</p><p>用户可通过向 tg bot 发送影片的磁力链接,通过部署的 Google Apps Script 在线 web 应用转发至 Media Download Helper。Media Download Helper 将通过 qbittorrent web API 下载该磁力链接,并支持查重功能,根据磁链中的 hash 值判重,如果 qbittorrent 中已存在该 hash 值的 torrent,则不再重复下载,通过 tg bot 返回 “xxxxxx 已存在,请勿重复下载”给用户。</p><p>除了提供下载功能以外,Media Download Helper 还支持在下载完成后,根据磁力链接中的文件名,对 TMDB 数据库进行检索,将检索到的影片信息通过 tg bot 返回给用户,具体效果图可见<a href="#%E6%95%88%E6%9E%9C%E5%9B%BE">下方章节</a>。( 说句题外话,这个推送的效果,本来是用在我另一个辅助 Emby 私人服务器针对入库影片进行推送发布消息的,感兴趣的朋友也可以试用一下,给点建议,项目地址:<a href="https://github.com/Ccccx159/watchdog_for_Emby">https://github.com/Ccccx159/watchdog_for_Emby</a> )</p><h2 id="各模块交互图"><a href="#各模块交互图" class="headerlink" title="各模块交互图"></a>各模块交互图</h2><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/module.png" alt="模块交互图"></p><h2 id="依赖项"><a href="#依赖项" class="headerlink" title="依赖项"></a>依赖项</h2><p>仅在 Python 3.11 环境下测试通过:</p><ul><li>python 3.11 (External module: requests, python-qbittorrent, colorlog)</li><li>Telegram Bot and it’s Token (鉴于电报资源丰富,尤其 <a href="https://t.me/RarbgPro">@RarbgPro</a> 频道发布的公开磁链,以及强大的 bot 功能,因此将用户交互客户端选择为 Telegram)</li><li>Google Apps Script (构建在线脚本,并注册为电报机器人的 webhook)</li><li>Qbittorrent with WebUI (磁链下载工具)</li><li>TMDB API Token (影视资源数据库,用于下载完成后进行检索,并传递相关信息给用户)</li></ul><p><del>这里需要注意,<code>Simple Http Server</code> 是在局域网内部部署的服务,如果没有将其暴露到公网环境下,那么 <code>Google Apps Scrip</code> 的在线脚本则无法访问 <code>Simple Http Server</code> 。所以,在部署在线脚本时,需要将 <code>Simple Http Server</code> 暴露到公网环境下。(此处可能存在安全风险,建议使用 https 和 cdn 来提高安全系数)</del></p><p>完整部署教程请参考:<a href="https://github.com/Ccccx159/Media-Download-Helper/blob/v2.0.0/doc/Media-Download-Helper%20%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B.md">Media-Download-Helper 完整部署教程</a></p><h2 id="环境变量说明"><a href="#环境变量说明" class="headerlink" title="环境变量说明"></a>环境变量说明</h2><table><thead><tr><th align="center">环境变量</th><th align="left">说明</th></tr></thead><tbody><tr><td align="center"><code>QBIT_HOST</code></td><td align="left">Qbittorrent WebUI host 地址</td></tr><tr><td align="center"><code>QBIT_USER</code></td><td align="left">Qbittorrent WebUI username 用户名</td></tr><tr><td align="center"><code>QBIT_PASS</code></td><td align="left">Qbittorrent WebUI password 密码</td></tr><tr><td align="center"><code>BOT_TOKEN</code></td><td align="left">Telegram Bot Token</td></tr><tr><td align="center"><code>TMDB_API_TOKEN</code></td><td align="left">TMDB API Token</td></tr><tr><td align="center"><code>GOOGLE_APPS_SCRIPT_URL</code></td><td align="left">Googel Apps Script 部署的在线 web 应用网址</td></tr><tr><td align="center"><code>LOG_LEVEL</code></td><td align="left">(可选) 日志等级,默认 WARNING</td></tr><tr><td align="center"><code>LOG_EXPORT</code></td><td align="left">(可选) 日志导出到文件,默认 False</td></tr><tr><td align="center"><code>LOG_PATH</code></td><td align="left">(可选) 日志导出路径,默认 <code>/var/tmp/media_dlhelper_logs/</code></td></tr></tbody></table><h2 id="效果图"><a href="#效果图" class="headerlink" title="效果图"></a>效果图</h2><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/demo-suc.png" alt="效果图"></p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/demo-fail.png" alt="效果图"></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ul><li><a href="https://core.telegram.org/bots/api">Telegram Bot API 说明</a></li><li><a href="https://python-qbittorrent.readthedocs.io/en/latest/modules/api.html">Python Qbittorrent 接口文档</a></li><li><a href="https://github.com/qbittorrent/qBittorrent/wiki/">Qbittorrent 官方 API 文档</a></li><li><a href="https://www.dengnz.com/2020/11/23/telegram-%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%9A%84%E7%94%B3%E8%AF%B7%E5%92%8C%E8%AE%BE%E7%BD%AE%E5%9B%BE%E6%96%87%E6%95%99%E7%A8%8B/">Telegram 机器人的申请和设置图文教程</a></li><li><a href="https://www.dengnz.com/2018/09/05/telegram-bot-%e7%ac%ac%e4%b8%80%e4%b8%aa%e5%9b%9e%e5%a3%b0%e6%9c%ba%e5%99%a8%e4%ba%ba-%e4%bd%a0%e6%98%af%e6%b1%a4%e5%a7%86%e7%8c%ab%e5%90%a7%e7%ac%ac%e4%ba%8c%e9%9b%86/">Telegram Bot 第一个回声机器人 – 你是汤姆猫吧[第二集]</a></li><li><a href="https://developer.themoviedb.org/reference/intro/getting-started">TMDB API 说明文档</a></li></ul>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Google Apps Script</tag>
<tag>TMDB</tag>
<tag>qbittorrent</tag>
<tag>Telegram bot</tag>
</tags>
</entry>
<entry>
<title>Media-Download-Helper 完整部署教程</title>
<link href="/2024/03/08/Media-Download-Helper%20%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/"/>
<url>/2024/03/08/Media-Download-Helper%20%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/</url>
<content type="html"><![CDATA[<h1 id="Media-Download-Helper-完整部署说明"><a href="#Media-Download-Helper-完整部署说明" class="headerlink" title="Media-Download-Helper 完整部署说明"></a>Media-Download-Helper 完整部署说明</h1><blockquote><p>前排提示:由于使用到了 Telegram、Google 等服务,所以当前方案不建议国内用户使用!</p></blockquote><h2 id="基于-docker-安装-qbittorrent-WebUI"><a href="#基于-docker-安装-qbittorrent-WebUI" class="headerlink" title="基于 docker 安装 qbittorrent WebUI"></a>基于 docker 安装 qbittorrent WebUI</h2><p>使用 docker 安装 qbittorrent 非常简单,教程也非常非常多,而且能看到这篇教程的人,我估计大部分可能正在使用 qbittorrent,因此这里就不在赘述了。</p><p>这里提供一个参考链接:<a href="https://juejin.cn/s/n1%20docker%E5%AE%89%E8%A3%85qb">《n1 docker安装qb》</a>。</p><blockquote><p>如果对 docker,参考链接中的 docker compose 部分不熟悉的话,还是先去学习一下相关的基础知识吧。</p></blockquote><p>在安装完成后,我们会获得一个 WebUI 的地址,形如:<code>http://192.168.1.123:8080/</code>,我们记录下来,一同需要记录的还有登录的用户名 <code>QBIT_USERNAME</code> 和密码 <code>QBIT_PASSWORD</code>。</p><blockquote><p>安装完成后,可以先手动下载一个文件试试,验证一下是否能正常下载磁力链接,以及下载后的文件是否能正常获取和使用。避免后续出现下载问题,例如无法联网,无法解析磁力链接,下载无速度,找不到下载的文件等。</p></blockquote><h2 id="申请-Telegram-Bot-并获取对应-Token"><a href="#申请-Telegram-Bot-并获取对应-Token" class="headerlink" title="申请 Telegram Bot 并获取对应 Token"></a>申请 Telegram Bot 并获取对应 Token</h2><p>这一部分不做详细介绍,网上教程非常多,直接按照本章节标题进行 Google 即可。</p><p>如果懒得 Google,也可以参考这篇文章<a href="https://www.dengnz.com/2020/11/23/telegram-%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%9A%84%E7%94%B3%E8%AF%B7%E5%92%8C%E8%AE%BE%E7%BD%AE%E5%9B%BE%E6%96%87%E6%95%99%E7%A8%8B/">《Telegram 机器人的申请和设置图文教程》</a>,里面的教程非常详细,原作者也由对应的油管视频可供参考,只要按部就班抄作业即可。</p><p>在此章节中,我们获取到了一个 **”Bot_TOKEN”**,在后续的配置和部署中会使用到。</p><h2 id="部署-Google-Apps-Script-在线脚本"><a href="#部署-Google-Apps-Script-在线脚本" class="headerlink" title="部署 Google Apps Script 在线脚本"></a>部署 Google Apps Script 在线脚本</h2><p>默认情况下,我们发送给 Telegram Bot 的消息是直接发送给 Telegram 官方服务器的,在这种情况下,我们无法获取到 Bot 接收到的数据并做出相应的处理和相应。</p><p>但是 Telegram 给我们提供了一个为 bot 设置 webhook 的方法 <code>/setwebhook?url=xxxx</code>,这样我们就可以通过 webhook 获取到用户发送的消息。这里我们通过 <strong>Google Apps Script</strong> 来实现这个功能。</p><blockquote><p>这一部分也是受到<a href="https://www.dengnz.com/2020/11/23/telegram-%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%9A%84%E7%94%B3%E8%AF%B7%E5%92%8C%E8%AE%BE%E7%BD%AE%E5%9B%BE%E6%96%87%E6%95%99%E7%A8%8B/">《Telegram 机器人的申请和设置图文教程》</a>作者的系列教程<a href="https://www.dengnz.com/2018/09/05/telegram-bot-%e7%ac%ac%e4%b8%80%e4%b8%aa%e5%9b%9e%e5%a3%b0%e6%9c%ba%e5%99%a8%e4%ba%ba-%e4%bd%a0%e6%98%af%e6%b1%a4%e5%a7%86%e7%8c%ab%e5%90%a7%e7%ac%ac%e4%ba%8c%e9%9b%86/">《Telegram Bot 第一个回声机器人 – 你是汤姆猫吧[第二集]》</a>启发。原文中作者通过 Google Apps Script 实现了一个简单的回声机器人,并且在后续的系列文章中,扩展了不少 Telegram bot 的高级用法,感兴趣朋友的也可以参考参考。</p></blockquote><h3 id="步骤-1、创建-Google-Apps-Script-项目"><a href="#步骤-1、创建-Google-Apps-Script-项目" class="headerlink" title="步骤 1、创建 Google Apps Script 项目"></a>步骤 1、创建 Google Apps Script 项目</h3><p>打开 Google Apps Script 的<a href="https://script.google.com/home/start">官网</a>,点击右上角的 <code>新建</code> 按钮,创建一个新的项目。</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE.png" alt="新建项目"></p><h3 id="步骤-2、编辑脚本的代码"><a href="#步骤-2、编辑脚本的代码" class="headerlink" title="步骤 2、编辑脚本的代码"></a>步骤 2、编辑脚本的代码</h3><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E7%BC%96%E8%BE%91%E8%84%9A%E6%9C%AC%E4%BB%A3%E7%A0%81.png" alt="编写代码"></p><h3 id="步骤-3、部署项目"><a href="#步骤-3、部署项目" class="headerlink" title="步骤 3、部署项目"></a>步骤 3、部署项目</h3><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B21.png" alt="部署项目1"></p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B22.png" alt="部署项目2"></p><blockquote><p>授权时可能会出现风险提示界面,估计是挂了代理的缘故,按下图操作继续即可。</p></blockquote><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B23.png" alt="部署项目3"></p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B24.png" alt="部署项目4"></p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B25.png" alt="部署项目5"></p><h3 id="步骤-4、获取-Webhook-URL"><a href="#步骤-4、获取-Webhook-URL" class="headerlink" title="步骤 4、获取 Webhook URL"></a>步骤 4、获取 Webhook URL</h3><p>在上一步中,我们最终获取了一个 Web 应用的网址,我们可以直接将这个网址粘贴到浏览器地址栏进行访问,来验证部署是否成功,当显示 <code>{"status":"OK","magnet_urls":[]}</code> 时,表明已部署成功。 如下图所示:</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%83%A8%E7%BD%B2%E6%88%90%E5%8A%9F.png" alt="部署成功"></p><p>我们还需要对这个 Web Url 进行一些处理,打开这个 Url Encode 网站 <a href="https://www.urlencoder.org/">https://www.urlencoder.org/</a>,将这个 url 进行编码,然后将编码后的 url 保存下来,后续会用。按下图操作即可:</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E7%BC%96%E7%A0%81URL.png" alt="UrlEncode"></p><p>至此我们已经完成了 Google Apps Script 在线脚本的部署,我们在这里获得了一个 **”原始 URL”**,和一个 **”编码后 URL”**。</p><h2 id="步骤-5、注册-Telegram-bot-Webhook"><a href="#步骤-5、注册-Telegram-bot-Webhook" class="headerlink" title="步骤 5、注册 Telegram bot Webhook"></a>步骤 5、注册 Telegram bot Webhook</h2><p>在这一步中,我们需要将上一步中获取到的 <strong>“编码后 URL”</strong> 注册到我们的 Telegram Bot 中,这样我们就可以通过 webhook 获取到用户发送的消息。</p><p>我们通过 Telegram Bot 的 API 来实现这个功能,这里用到两个 API,一个是 <code>setWebhook</code>,一个是 <code>getWebhookInfo</code>。</p><h3 id="步骤-5-1、注册"><a href="#步骤-5-1、注册" class="headerlink" title="步骤 5.1、注册"></a>步骤 5.1、注册</h3><p>我们使用 <strong>“Bot TOKEN”<strong>、</strong>“编码后 URL”</strong> 两个参数,按照以下格式完成 API 的组装:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">将 {Bot TOKEN} 替换为你的 Bot TOKEN</span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">将 {编码后 URL} 替换为你的 编码后 URL</span><br>https://api.telegram.org/bot{Bot TOKEN}/setWebhook?url={编码后 URL}<br></code></pre></td></tr></table></figure><p>然后将这个组装后的 url 粘贴到浏览器地址栏中进行访问,如果返回 <code>{"ok":true,"result":true,"description":"Webhook was set"}</code>,则表示注册成功。如下图所示:</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E6%B3%A8%E5%86%8C%E6%88%90%E5%8A%9F.png" alt="注册成功"></p><h3 id="步骤-5-2、验证"><a href="#步骤-5-2、验证" class="headerlink" title="步骤 5.2、验证"></a>步骤 5.2、验证</h3><p>我们可以通过 <code>getWebhookInfo</code> API 来验证是否注册成功,组装后的 url 如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">将 {Bot TOKEN} 替换为你的 Bot TOKEN</span><br>https://api.telegram.org/bot{Bot TOKEN}/getWebhookInfo<br></code></pre></td></tr></table></figure><p>然后将这个组装后的 url 粘贴到浏览器地址栏中进行访问,可以在返回结果中看到注册的 url,如下图所示:</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/%E9%AA%8C%E8%AF%81%E6%88%90%E5%8A%9F.png" alt="验证成功"></p><p>可以将图中红框部分的 URL 与我们在 <strong>步骤 4</strong> 中获取到的 <strong>“原始 URL”</strong> 进行对比,如果一致,则表示注册成功。</p><h2 id="申请-TMDB-API-Token"><a href="#申请-TMDB-API-Token" class="headerlink" title="申请 TMDB API Token"></a>申请 TMDB API Token</h2><p>想要达到 README.md 中展示的效果,在下载完成后将详细的影片信息推送到 Tg bot,则还需要一个 TMDB 的 API Token。</p><p>这里也不做赘述了,推荐一个网上的教程吧:<a href="https://post.smzdm.com/p/a5op4w33/"> 《教程篇 篇三:影视刮削必备神器——TMDB API申请攻略》 </a>。虽然发布日期比较久远了,但是流程估计整体也差不多,作者写的也比较详细,可以参考一下。</p><p>最终我们会获得下图中的 <strong>“API 密钥 (API Key)”</strong> 和 **”API 读访问令牌 (API Read Access Token)”**,在最新的<a href="https://developer.themoviedb.org/reference/intro/getting-started">官方 API 手册</a>中,官方推荐的是使用后者进行查询操作。</p><p><img src="https://github.com/Ccccx159/Media-Download-Helper/raw/v2.0.0/doc/.assert_Media-Download-Helper-%E5%AE%8C%E6%95%B4%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/TMDB_API_Token.png" alt="TMDB API Token"></p><p>因此我们将这个 **”API 读访问令牌”**保存下来即可,这个 Token 有点长,一定复制全,记得注意保护敏感数据哦 ~</p><h2 id="基于-docker-部署-Media-Download-Helper"><a href="#基于-docker-部署-Media-Download-Helper" class="headerlink" title="基于 docker 部署 Media-Download-Helper"></a>基于 docker 部署 Media-Download-Helper</h2><blockquote><p>本项目不是一定需要 docker 才能部署,使用 docker 只是为了保证环境的一致性,想必对 Python 比较熟悉的朋友也是对 Python 版本和各种依赖包的版本管理比较头疼的,使用 docker 可以很好的解决这个问题。</p><p>不过在最后我还是会补充一个脚本,方便部分朋友直接运行或者调试 ^ ^</p></blockquote><h3 id="docker-compose-部署"><a href="#docker-compose-部署" class="headerlink" title="docker-compose 部署"></a>docker-compose 部署</h3><p>如果有 docker-compose 的环境,则可以很方便使用仓库中 dockerfile/docker-compose.yml 文件进行部署。</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span><br><span class="hljs-attr">services:</span><br> <span class="hljs-attr">media_dlhelper:</span><br> <span class="hljs-attr">build:</span><br> <span class="hljs-attr">context:</span> <span class="hljs-string">.</span><br> <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">dockerfile</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">b1gfac3c4t/media_dlhelper:v2.0.0</span><br> <span class="hljs-attr">environment:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TZ=Asia/Shanghai</span><br> <span class="hljs-comment"># 这里所有的环境变量都不要使用引号</span><br> <span class="hljs-comment"># 必填参数</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">QBIT_HOST=<your</span> <span class="hljs-string">qbittorrent</span> <span class="hljs-string">WebUI</span> <span class="hljs-string">base</span> <span class="hljs-string">API></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">QBIT_USER=<your</span> <span class="hljs-string">qbittorrent</span> <span class="hljs-string">username></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">QBIT_PASS=<your</span> <span class="hljs-string">qbittorrent</span> <span class="hljs-string">password></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">BOT_TOKEN=<your</span> <span class="hljs-string">telegram</span> <span class="hljs-string">bot</span> <span class="hljs-string">token></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">TMDB_API_TOKEN=<your</span> <span class="hljs-string">TMDB</span> <span class="hljs-string">API</span> <span class="hljs-string">token></span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">GOOGLE_APPS_SCRIPT_URL=</span> <span class="hljs-string"><google</span> <span class="hljs-string">apps</span> <span class="hljs-string">scrip</span> <span class="hljs-string">部署后的原始web应用网址></span><br> <span class="hljs-comment"># 可选参数</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_LEVEL=INFO</span> <span class="hljs-comment"># [DEBUG, INFO, WARNING] 三个等级,默认 WARNING</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_EXPORT=True</span> <span class="hljs-comment"># [True, False0] 是否将日志输出到文件,默认 False</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_PATH=/var/tmp/media_dlhelper_logs/</span> <span class="hljs-comment"># 默认 /var/tmp/media_dlhelper_logs/</span><br> <span class="hljs-attr">network_mode:</span> <span class="hljs-string">"bridge"</span><br></code></pre></td></tr></table></figure><p>只需要将上面的环境变量替换为自己的即可,然后执行 <code>docker-compose up -d</code> 即可完成部署。</p><h3 id="docker-run-部署"><a href="#docker-run-部署" class="headerlink" title="docker run 部署"></a>docker run 部署</h3><p>如果没有 docker-compose 的环境,也可以使用 docker run 的方式进行部署。直接执行下面的命令即可</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs bash">docker pull b1gfac3c4t/media_dlhelper:v2.0.0<br>docker run -d \<br> --name media_dlhelper \<br> -e TZ=Asia/Shanghai \<br> -e QBIT_HOST=<your qbittorrent WebUI base API> \<br> -e QBIT_USER=<your qbittorrent username> \<br> -e QBIT_PASS=<your qbittorrent password> \<br> -e BOT_TOKEN=<your telegram bot token> \<br> -e TMDB_API_TOKEN=<your TMDB API token> \<br> -e GOOGLE_APPS_SCRIPT_URL=<google apps scrip 部署后的原始web应用网址> \<br> -e LOG_LEVEL=INFO \<br> -e LOG_EXPORT=True \<br> -e LOG_PATH=/var/tmp/media_dlhelper_logs/ \<br> --network=bridge \<br> --restart=always \<br> b1gfac3c4t/media_dlhelper:v2.0.0<br></code></pre></td></tr></table></figure><h3 id="直接运行"><a href="#直接运行" class="headerlink" title="直接运行"></a>直接运行</h3><p>如果不想使用 docker,也可以直接运行项目中的 main.py 文件,为了方便,这里提供了一个运行脚本,只要在项目的根目录下创建一个 run.sh 文件,然后将下面的内容复制进去,替换环境变量的值即可:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-meta">#!/bin/bash</span><br><br><span class="hljs-built_in">export</span> QBIT_HOST=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> QBIT_USER=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> QBIT_PASS=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> BOT_TOKEN=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> TMDB_API_TOKEN=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> GOOGLE_APPS_SCRIPT_URL=<span class="hljs-string">''</span><br><span class="hljs-built_in">export</span> LOG_LEVEL=<span class="hljs-string">'DEBUG'</span><br><span class="hljs-built_in">export</span> LOG_EXPORT=<span class="hljs-string">'True'</span><br><span class="hljs-built_in">export</span> LOG_PATH=<span class="hljs-string">'./log'</span><br><br><span class="hljs-built_in">nohup</span> python3 main.py > /dev/null 2>&1 &<br></code></pre></td></tr></table></figure><p>保存后,直接运行 <code>sh run.sh</code> 即可。</p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Google Apps Script</tag>
<tag>TMDB</tag>
<tag>qbittorrent</tag>
<tag>Telegram bot</tag>
</tags>
</entry>
<entry>
<title>Moonlight Stream 和 SteamLink 远程串流失败问题解决</title>
<link href="/2024/02/22/Moonlight%20Stream%20%E5%92%8C%20SteamLink%20%E8%BF%9C%E7%A8%8B%E4%B8%B2%E6%B5%81%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3/"/>
<url>/2024/02/22/Moonlight%20Stream%20%E5%92%8C%20SteamLink%20%E8%BF%9C%E7%A8%8B%E4%B8%B2%E6%B5%81%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3/</url>
<content type="html"><![CDATA[<h1 id="Moonlight-Stream-和-SteamLink-远程串流失败问题解决"><a href="#Moonlight-Stream-和-SteamLink-远程串流失败问题解决" class="headerlink" title="Moonlight Stream 和 SteamLink 远程串流失败问题解决"></a>Moonlight Stream 和 SteamLink 远程串流失败问题解决</h1><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>在使用 Moonlight Stream 或者 SteamLink 进行远程串流时,如果远程 PC 重启,或者被其他设备使用远程桌面 (mstsc, rdp) 登录过,则会出现串流失败的问题:</p><ul><li><strong>SteamLink</strong>: 锁屏界面卡弹窗</li><li><strong>Moonlight Stream</strong>: 远程PC显示感叹号</li></ul><p>如下图所示:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/e79aa91c00f14e723488789aa484904b/1.png" alt="steamlink 连接失败"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/659e88a7a79c52a7b3ce62ea0225ceb3/4.jpg" alt="Moonlight 无法连接"></p><p>其实问题原因是重启或者远程登录都会导致远程 PC 被锁定,串流软件不具备解锁功能,换句话说,串流软件只能在远程 PC 处于解锁状态下才能正常工作。通俗点讲就是你需要在远程 PC 上输入锁屏密码解锁后,才能正常使用串流软件。</p><p>但是远程串流的场景大多情况下不具备手动解锁 PC 的条件,所以我们需要一种可以远程解锁的方法来解决这个问题。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>解决方法非常简单,本质上只需要借助 Windows 自带的一个命令 <code>tscon</code> 来注销当前的远程登录即可,先上代码:</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs bat">@<span class="hljs-built_in">echo</span> off<br><span class="hljs-comment">@REM 获取当前会话的名称</span><br><span class="hljs-built_in">setlocal</span><br><span class="hljs-comment"></span><br><span class="hljs-comment">@REM 获取对应的会话ID</span><br><span class="hljs-keyword">for</span> /f "tokens=<span class="hljs-number">3</span>" <span class="hljs-variable">%%i</span> <span class="hljs-keyword">in</span> ('query session ^| <span class="hljs-built_in">findstr</span> ">"') <span class="hljs-keyword">do</span> <span class="hljs-built_in">set</span> "sessionid=<span class="hljs-variable">%%i</span>"<br><span class="hljs-comment">@REM echo %sessionid%</span><br><span class="hljs-comment">@REM 如果会话ID存在,则关闭对应的会话</span><br><span class="hljs-keyword">if</span> <span class="hljs-keyword">defined</span> sessionid (<br> @<span class="hljs-variable">%windir%</span>\System32\tscon.exe <span class="hljs-variable">%sessionid%</span> /dest:console<br>) <span class="hljs-keyword">else</span> (<br> <span class="hljs-built_in">echo</span> No session found <span class="hljs-keyword">for</span> <span class="hljs-variable">%sessionname%</span><br>)<br><span class="hljs-built_in">endlocal</span><br></code></pre></td></tr></table></figure><blockquote><p>注意:鉴于会话 id 是动态的,所以我们需要通过 <code>query session</code> 命令来获取当前所有会话信息,并通过当前会话名称中包含 ‘>’ 字符这一特征匹配来获取当前的会话 id,然后再通过 <code>tscon</code> 命令来关闭对应的会话。</p></blockquote><p>看不懂没关系,按照下面的操作步骤来操作即可:</p><ol><li>手机端使用 RDP 软件登录远程 PC</li><li>在桌面新建记事本,复制上面的代码到记事本中,然后保存为 <code>unlock.bat</code> 文件</li><li>右键 <code>unlock.bat</code> 文件,选择以管理员身份运行,此时会自动注销并退出当前远程连接,这是正常的,如果没有退出,则说明有问题需要检查</li><li>使用手机,平板等设备打开串流软件,连接远程 PC 即可</li></ol><p><mark>第三步中,如果远程连接没有退出,则说明注销当前会话失败了,此时仍然无法正常进行远程串流。</mark></p><h2 id="题外话"><a href="#题外话" class="headerlink" title="题外话"></a>题外话</h2><p>如果你无法通过远程连接来注销会话并解锁 PC,则仍然无法解决问题。如果你想获取更多关于远程连接 PC 的内容,包括如何在公网环境下远程连接家里的 PC 等,可以参考我的另一篇文章,<a href="https://ccccx159.github.io/2023/03/21/%E4%BD%BF%E7%94%A8%E5%85%AC%E7%BD%91IPv6%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%E8%AE%BE%E5%A4%87/">《使用公网IPv6远程访问内网设备》</a>。</p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>mstsc</tag>
<tag>Moonlight Stream</tag>
<tag>SteamLink</tag>
<tag>bat</tag>
</tags>
</entry>
<entry>
<title>基于TelegramBot和Qbittorrent打造下载机器人</title>
<link href="/2024/02/22/%E5%9F%BA%E4%BA%8ETelegramBot%E5%92%8CQbittorrent%E6%89%93%E9%80%A0%E4%B8%8B%E8%BD%BD%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
<url>/2024/02/22/%E5%9F%BA%E4%BA%8ETelegramBot%E5%92%8CQbittorrent%E6%89%93%E9%80%A0%E4%B8%8B%E8%BD%BD%E6%9C%BA%E5%99%A8%E4%BA%BA/</url>
<content type="html"><![CDATA[<h1 id="基于-TelegramBot-和-Qbittorrent-打造下载机器人"><a href="#基于-TelegramBot-和-Qbittorrent-打造下载机器人" class="headerlink" title="基于 TelegramBot 和 Qbittorrent 打造下载机器人"></a>基于 TelegramBot 和 Qbittorrent 打造下载机器人</h1><p>[] 创建 telegram bot<br>[] 编写 Google apps script<br>[] bot 注册 webhook<br>[] 本地搭建 http server<br>[] 调用 qbittorrent web API 下载并监控<br>[] 下载完成后根据文件名向TMDB查询信息,并通过bot发送至telegram</p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>TelegramBot</tag>
<tag>qbittoorrent</tag>
<tag>Google Apps Script</tag>
<tag>TMDB</tag>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title>记一次 UnRaid U盘损坏修复的过程</title>
<link href="/2023/12/28/%E8%AE%B0%E4%B8%80%E6%AC%A1%20UnRaid%20U%E7%9B%98%E6%8D%9F%E5%9D%8F%E4%BF%AE%E5%A4%8D%E7%9A%84%E8%BF%87%E7%A8%8B%E3%80%82%E3%80%82%E3%80%82/"/>
<url>/2023/12/28/%E8%AE%B0%E4%B8%80%E6%AC%A1%20UnRaid%20U%E7%9B%98%E6%8D%9F%E5%9D%8F%E4%BF%AE%E5%A4%8D%E7%9A%84%E8%BF%87%E7%A8%8B%E3%80%82%E3%80%82%E3%80%82/</url>
<content type="html"><![CDATA[<h1 id="记一次-unRaid-U盘损坏修复的过程。。。"><a href="#记一次-unRaid-U盘损坏修复的过程。。。" class="headerlink" title="记一次 unRaid U盘损坏修复的过程。。。"></a>记一次 unRaid U盘损坏修复的过程。。。</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>前段时间 unRAID 系统提示 flash 写失败了,没当回事,以为只是偶发的一次写错误,应该问题不大。</p><p>过了几天之后,在整理磁盘空间的时候,突然发现备份目录下有大概十几个的 boot 目录备份包,然而这些备份的压缩包本不应该出现在这里,而且这些压缩包的大小也存在异常,这就意味着 unRAID 已经有超过10天备份失败了。</p><p>这里说一下我预设的定时备份任务:每天凌晨 3 点自动打包 /boot 目录,然后通过 rclone 将阿里云盘挂载到本地,通过文件名中的时间戳判断并删除 7 天之前的备份文件,之后将新的备份文件上传到阿里云盘上。</p><p>出现了这样的问题,不想折腾都不行了。。。</p><h2 id="尝试修复过程"><a href="#尝试修复过程" class="headerlink" title="尝试修复过程"></a>尝试修复过程</h2><h3 id="尝试-1-——-手动打包"><a href="#尝试-1-——-手动打包" class="headerlink" title="尝试 1 —— 手动打包"></a>尝试 1 —— 手动打包</h3><p>因为之前周末电工师傅来换空开,直接拉闸断电了,怀疑会不会是这个导致了异常,于是尝试手动拷贝 /boot 目录,并进行手动打包。</p><p>拷贝倒是成功了,flash 中的 /boot 目录直接能拷贝到硬盘的共享目录中,但是打包同样失败了,提示文件损坏,而且文件名是乱码。。。</p><h3 id="尝试-2-——-删除-Nvidia-Drive-Plugin"><a href="#尝试-2-——-删除-Nvidia-Drive-Plugin" class="headerlink" title="尝试 2 —— 删除 Nvidia Drive Plugin"></a>尝试 2 —— 删除 Nvidia Drive Plugin</h3><p>上次重启后,英伟达驱动莫名其妙掉了,然后尝试重新下载也提示下载失败,然而本来也是闲置的功能,而且本着能不折腾就不折腾的主旨,也是放任没管。</p><p>猜测打包失败会不会和 Nvidia Drive Plugin 下载驱动失败导致的问题,ssh 进入后台后查看 /boot/config/plugin 中这个插件的目录,确实发现了几个大小异常的驱动包,和几个乱码的文件。</p><p><code>rm ./* -rf</code> 回车一敲,以为此次修复完美谢幕。删除后确实打包成功了,但是尝试重启后,直接无法启动了!出现了一堆错误日志,并且 web gui 无法启动,设置了自动挂载的阵列也没有挂载,docker 无法和 docker.sock 通信。。。什么鬼啊!</p><h3 id="尝试-3-——-重新制作-U-盘"><a href="#尝试-3-——-重新制作-U-盘" class="headerlink" title="尝试 3 —— 重新制作 U 盘"></a>尝试 3 —— 重新制作 U 盘</h3><p>排查错误不如我直接重做 U 盘,又快又不用动脑子,结果上官网一看 6.10.3 的下载链接已经从官网上下架了。。。我特意存储的备份系统镜像也在 unRaid 的阵列中。。。</p><p>翻箱倒柜找,终于在回收站里找到了之前下载的官方进行包。下载写盘器、写 U 盘、拷贝config、插 U 盘、开机,一气呵成。我嘞个豆。。。怎么还全是 ERROR 啊!!!</p><h3 id="尝试-4-——-解决-ERROR"><a href="#尝试-4-——-解决-ERROR" class="headerlink" title="尝试 4 —— 解决 ERROR"></a>尝试 4 —— 解决 ERROR</h3><p>不得已,只能解决 ERROR 了。</p><p>还好 unRAID 提示的还算清楚,仔细一看,嗨,这不是 docker 容器配置的 xml 解析失败了么,提示有格式错误。打开 U盘 中的 xml 检查了一下,确实有几个文件内容中出现了随机乱码的情况,导致格式错误。</p><p>将之前阿里云网盘上正确备份的文件下载下来,检查无误后替换错误文件,重启,这下总算是正常启动了。</p><h2 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h2><p>虽然系统正常了,但是问题的根本原因其实并没有找到。</p><p>目前仍然不清楚导致 docker 容器配置 xml 文件内容出现随机乱码的原因,怀疑的方向有几个:一是每天读 flash 进行备份,增大了 flash 出错的风险可能,毕竟 flash 不像硬盘那样耐*;二是拉闸断电导致的异常,不过这个我也没敢尝试复现,真出问题了,代价不小;三是 Nvidia Drive Plugin 出错,导致备份时出错,异常情况下导致 xml 出错。</p><p>由于之前备份都失败了,所以并不能确定 xml 出错的时间。几个猜测的点都是比较难去重现的,只能说后续使用上尽量谨慎一些吧。</p><p>Nvidia 的驱动插件,本来也是闲置的,所以不考虑重新安装了,而且万一后续有虚拟机直通显卡的需求,还得卸载。</p><p>备份策略也做了一定的修改,由原来的直接 tar 命令压缩 /boot 目录,修改为将 /boot/config 目录拷贝至硬盘后再进行压缩备份。备份间隔也从每天,修改为每周。当有较大更新时,则手动触发备份。</p><h2 id="折腾"><a href="#折腾" class="headerlink" title="折腾"></a>折腾</h2><p>中国有句古话,“来都来了”。</p><p>嘿嘿,来都来了,不折腾一下,那不得抓耳挠腮,浑身奇痒难耐啊。所以去老毛子的论坛翻了翻,正好有 6.12.6 的包和 hack 文件。于是乎直接下载,替换,完美启动(老脸一红)!</p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>unRAID</tag>
<tag>flash</tag>
</tags>
</entry>
<entry>
<title>Git —— Issue 模板</title>
<link href="/2023/12/27/Git%20%E2%80%94%E2%80%94%20Issue%20%E6%A8%A1%E6%9D%BF/"/>
<url>/2023/12/27/Git%20%E2%80%94%E2%80%94%20Issue%20%E6%A8%A1%E6%9D%BF/</url>
<content type="html"><![CDATA[<h1 id="Git-——-Issue-模板"><a href="#Git-——-Issue-模板" class="headerlink" title="Git —— Issue 模板"></a>Git —— Issue 模板</h1><h2 id="1-什么是-Issue-模板"><a href="#1-什么是-Issue-模板" class="headerlink" title="1. 什么是 Issue 模板"></a>1. 什么是 Issue 模板</h2><p>Issue 模板是在创建 Issue 时,预先填写好的内容,可以是一些提示性的文字,也可以是一些表单,用户在创建 Issue 时,可以根据模板填写内容,这样可以让 Issue 的内容更加规范,也方便了 Issue 的管理。</p><h2 id="2-如何使用-Issue-模板"><a href="#2-如何使用-Issue-模板" class="headerlink" title="2. 如何使用 Issue 模板"></a>2. 如何使用 Issue 模板</h2><h3 id="2-1-创建-Issue-模板"><a href="#2-1-创建-Issue-模板" class="headerlink" title="2.1 创建 Issue 模板"></a>2.1 创建 Issue 模板</h3><p>在项目的根目录下创建一个名为 <code>.gitlab</code> 的文件夹,然后在 <code>.gitlab</code> 文件夹下创建一个名为 <code>issue_templates</code> 的文件夹,然后在 <code>issue_templates</code> 文件夹下创建一个名为 <code>bug_report.md</code> 的文件,这个文件就是 Issue 模板了。</p><h3 id="2-2-编写-Issue-模板"><a href="#2-2-编写-Issue-模板" class="headerlink" title="2.2 编写 Issue 模板"></a>2.2 编写 Issue 模板</h3><p>在 <code>bug_report.md</code> 文件中,可以写一些提示性的文字,也可以写一些表单,例如:</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><code class="hljs markdown">---<br>name: Bug report<br>about: Create a report to help us improve<br>title: ''<br>labels: ''<br>assignees: ''<br><br>---<br><span class="hljs-section"># 🐞 Bug Reporter</span><br><br><span class="hljs-section">## 📋 Pre-Check</span><br><br><span class="hljs-bullet">-</span> [<span class="hljs-string"> </span>] I have searched the [<span class="hljs-string">issues</span>](<span class="hljs-link">https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/sample-gitlab/-/issuess</span>) of this repository and believe that this is not a duplicate.<br><br><span class="hljs-section">## 🐛 Bug Summary</span><br><br><span class="hljs-quote">> A clear and concise description of what the bug is.</span><br><br><br><span class="hljs-section">## 🐛 Bug Reproduce</span><br><br><span class="hljs-quote">> Steps to reproduce the behavior:</span><br><br><span class="hljs-bullet">1.</span> Go to '...'<br><span class="hljs-bullet">2.</span> Click on '....'<br><span class="hljs-bullet">3.</span> Scroll down to '....'<br><span class="hljs-bullet">4.</span> See error<br><br><span class="hljs-section">## 🐛 Expected Behavior</span><br><br><span class="hljs-quote">> A clear and concise description of what you expected to happen.</span><br><br><span class="hljs-section">## 🐛 Possible Solution</span><br><br><span class="hljs-quote">> Describe the solution you thought of.</span><br><br><span class="hljs-section">## 🐛 Context</span><br><br><span class="hljs-quote">> How has this issue affected you? What are you trying to accomplish? Providing context helps us come up with a solution that is most useful in the real world.</span><br><br><span class="hljs-section">## 🐛 Your Environment</span><br><br><span class="hljs-quote">> Include as many relevant details about the environment you experienced the bug in.</span><br><br>| | |<br>| ---------------- | ----------- |<br>| OS | x86? |<br>| Compiler | gcc-4.8.5 ? |<br>| Lib Ver(s) | |<br>| Lib Url | |<br>| Other Dependency | |<br><br><span class="hljs-section">## 🐛 Additional context</span><br><br><span class="hljs-quote">> Add any other context about the problem here.</span><br><br><br></code></pre></td></tr></table></figure><h3 id="2-3-使用-Issue-模板"><a href="#2-3-使用-Issue-模板" class="headerlink" title="2.3 使用 Issue 模板"></a>2.3 使用 Issue 模板</h3><p>在项目的 Issues 页面,点击 New issue 按钮,就可以看到 Issue 模板了,用户在创建 Issue 时,可以根据模板填写内容,这样可以让 Issue 的内容更加规范,也方便了 Issue 的管理。</p><h2 id="3-参考资料"><a href="#3-参考资料" class="headerlink" title="3. 参考资料"></a>3. 参考资料</h2><ul><li><a href="https://docs.github.com/en/free-pro-team@latest/github/building-a-strong-community/creating-issue-templates-for-your-repository">Creating issue templates for your repository</a></li></ul>]]></content>
<categories>
<category>程序员进阶</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title>Git —— Commit Message 规范介绍</title>
<link href="/2023/12/26/Git%20%E2%80%94%E2%80%94%20Commit%20Message%20%E8%A7%84%E8%8C%83%E4%BB%8B%E7%BB%8D/"/>
<url>/2023/12/26/Git%20%E2%80%94%E2%80%94%20Commit%20Message%20%E8%A7%84%E8%8C%83%E4%BB%8B%E7%BB%8D/</url>
<content type="html"><![CDATA[<h1 id="Git-——-Commit-Message-规范介绍"><a href="#Git-——-Commit-Message-规范介绍" class="headerlink" title="Git —— Commit Message 规范介绍"></a>Git —— Commit Message 规范介绍</h1><h2 id="为什么要规范-Commit-Message"><a href="#为什么要规范-Commit-Message" class="headerlink" title="为什么要规范 Commit Message"></a>为什么要规范 Commit Message</h2><p>日常开发中,我们经常会使用到 Git 进行代码管理,而 Git 中最常用的命令就是 <code>git commit</code>,我们通过 commit 命令将修改后的代码提交到本地仓库,然后再通过 <code>git push</code> 命令将本地仓库的代码推送到远程仓库。</p><p>git 规定提交时必须要写提交信息,作为改动说明,保存在 commit 历史中,方便回溯。规范的 log 不仅有助于他人 review, 还可以有效的输出 CHANGELOG,甚至对于项目的研发质量都有很大的提升,尤其是一些长期持续迭代维护,且多版本长期并存的项目。</p><p>优秀的规范化 Commit Message 应该具备以下优点:</p><ol><li><p>清晰明了:commit message 应该清晰明了,说明本次提交的目的,具体做了什么操作。这样可以让团队成员更好地理解每次提交的内容。</p></li><li><p>便于追溯:规范的 commit message 可以帮助程序员对提交历史进行追溯,了解发生了什么情况。</p></li><li><p>提高研发效率:一旦约束了 commit message,意味着我们将慎重的进行每一次提交,不能再一股脑的把各种各样的改动都放在一个 git commit 里面,这样一来整个代码改动的历史也将更加清晰。</p></li><li><p>自动化工具的友好性:规范的 commit message 可以被自动化工具用于生成发布日志或自动化版本号管理。</p></li><li><p>降低代码维护成本:如果 commit message 写得不清楚,例如使用 “fix bug” 这样笼统的描述,可能会导致后续代码维护成本特别大,有时自己都不知道自己的 “fix bug” 修复的是什么问题</p></li></ol><p>因此,规范化的 commit message 对于团队协作和项目管理是非常重要的。</p><h2 id="Commit-Message-规范"><a href="#Commit-Message-规范" class="headerlink" title="Commit Message 规范"></a>Commit Message 规范</h2><p>在 Git 中 Commit Message 的规范又很多种,其中比较受欢迎的有以下这些:</p><ol><li><p>Angular 规范:这是目前使用最广的写法,比较合理和系统化,并且有配套工具可以辅助生成(VS CODE 插件:git-commit-plugin)。</p></li><li><p>Conventional Commits 规范:这是一个基于语义化版本 (semver) 和简单的消息格式的轻量级约定,一传达代码更改的意图。</p></li></ol><p>这里我们着重介绍一下 Angular 规范。</p><h3 id="Angular-规范"><a href="#Angular-规范" class="headerlink" title="Angular 规范"></a>Angular 规范</h3><p>Angular 规范是一种广泛应用于软件开发中的提交信息(commit message)规范,其目的在于提供一种统一、清晰的提交信息格式,以便于开发者和维护者理解代码的变动情况。</p><p>Angular 规范将提交信息分为三个部分:Header、Body 和 Footer。</p><ol><li><p><strong>Header</strong>:Header 是提交信息的头部,包含三个字段:type、scope 和 subject。</p><ul><li><strong>type</strong>:type 字段用于指明<mark>本次提交的类型</mark>,例如:feat(新功能)、fix(修补bug)、docs(文档)、style(格式)、refactor(重构)、perf(性能优化)、test(增加测试)、chore(构建过程或辅助工具的变动)等。</li><li><strong>scope</strong>:scope 字段用于指明<mark>本次提交影响的范围</mark>,例如:数据层、控制层、视图层等。这个字段是可选的。</li><li><strong>subject</strong>:subject 字段是对<mark>本次提交目的的简短描述</mark>,不超过50个字符。</li></ul></li><li><p><strong>Body</strong>:Body 是提交信息的主体部分,用于<mark>详细描述本次提交的内容和原因</mark>。这个部分是可选的。</p></li><li><p><strong>Footer</strong>:Footer 是提交信息的尾部,通常用于<mark>记录不兼容变动和关闭 Issue</mark>。这个部分也是可选的。</p></li></ol><p>Angular 规范的提交信息格式如下:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">type</span>></span>(<span class="hljs-tag"><<span class="hljs-name">scope</span>></span>): <span class="hljs-tag"><<span class="hljs-name">subject</span>></span><br><span class="hljs-tag"><<span class="hljs-name">BLANK</span> <span class="hljs-attr">LINE</span>></span><br><span class="hljs-tag"><<span class="hljs-name">body</span>></span><br><span class="hljs-tag"><<span class="hljs-name">BLANK</span> <span class="hljs-attr">LINE</span>></span><br><span class="hljs-tag"><<span class="hljs-name">footer</span>></span><br></code></pre></td></tr></table></figure><p>以上就是 Angular 规范的基本内容。遵循这种规范可以帮助我们更好地管理代码提交的内容,使得每次提交的目的和主要改动都能清晰地体现在 Commit Message 中,便于后续的代码维护和版本管理。具体的规范可能会因团队和项目的不同而有所差异。所以,虽然 Git 本身没有强制的 Commit Message 规范,但在实际使用中,我们通常会根据实际需求来约定一些规范,以提高代码的可读性和维护性。</p><h2 id="Commit-Message-规范工具"><a href="#Commit-Message-规范工具" class="headerlink" title="Commit Message 规范工具"></a>Commit Message 规范工具</h2><p>在实际开发中,我们通常会使用一些工具来帮助我们规范 Commit Message,这样可以避免我们手动编写 Commit Message,提高开发效率。</p><p>这里介绍一个 VS Code 插件:<strong>git-commit-plugin</strong>,它可以帮助我们快速生成符合 Angular 规范的 Commit Message。这是它的仓库地址:<a href="https://github.com/RedJue/git-commit-plugin">https://github.com/RedJue/git-commit-plugin</a>。我们也可以直接在 VS Code 的扩展商店中直接下载安装。</p><blockquote><p>注意:商店中有两个名为 git-commit-plugin 的插件,我们选择作者为 redjue 的那一个。另一个为fork的版本,已经不再维护。</p></blockquote><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/0cd57b334faac82c88858d17cb3ff3e3/1703558434900.png" alt="git-commit-plugin"></p><p>安装完成后,我们点击 VS Code 左侧面板的存储库图标,可以看到存储库的右边出现了一个 git 图标,点击它就可以打开 git-commit-plugin 的界面。</p><ol><li><p>打开 git-commit-plugin 界面:</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/153afa2bf93c4a55d9e4c5420aa43ee0/1703558938449.png"></p></li><li><p>选择提交类型,这里以 fix 为例:</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/1ec1f6e85b71bd74e244632584d64eb3/1703559027194.png"></p></li><li><p>按照 Angular 规范依次完成 scope,subject,body 和 footer 字段</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/45e369bb0b664b9ee79ae550bc0524cb/1703559610753.png"></p></li><li><p>完成每项内容的填写后,点击 Complete,插件会将填写的内容,按照规范自动生成在左侧源代码管理面板中的提交信息中:</p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/cd6eb2280db4b9e0e125bc83329c0bd6/1703559528891.png"></p><p> <img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/df8e94be63ac8bdc9528e84a2bf2cd83/1703559648902.png"></p><p> 注意此处 VS Code 会提示“当前行比72超出39个字符”,这是因为 VS Code 限制了提交消息每行的最大长度。我们在上一步编辑 Body 时,受限于插件的界面,将所有内容都写在了一行中,因此在这里我们需要将 Body 部分的内容进行换行,达到美观和规范的目的。</p></li><li><p>最后我们点击提交按钮将本次修改提交到本地仓库,再点击同步就可以将本地仓库的提交记录推送到远程仓库了。</p></li></ol><h2 id="Commit-Message-模板的配置和使用"><a href="#Commit-Message-模板的配置和使用" class="headerlink" title="Commit Message 模板的配置和使用"></a>Commit Message 模板的配置和使用</h2><p>在某些环境下,可能仅有的就是一个命令行终端,我们需要在命令行中手动输入 Commit Message。这时候我们就需要一个 Commit Message 模板来帮助我们快速生成符合规范的 Commit Message。</p><p>以 Linux 端为例,git 提供了一个 commit.template 的配置项,用来指定期望使用的 Commit Message 模板。</p><ol><li><p>首先我们在项目的根目录创建一个 .gitmessage 文件,用来存放 Commit Message 模板:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">touch</span> .gitmessage<br></code></pre></td></tr></table></figure></li><li><p>然后我们编辑 .gitmessage 文件,将以下内容复制到 .gitmessage 文件中:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># head: <type>(<scope>): <subject></span><br><span class="hljs-comment"># - type: ✨ feat, 🐞 fix, 📃 docs, 🌈 style, 🦄 refactor, 🎈 perf, 🧪 test, 🔧 build, 🐎 ci, 🐳 chore, ↩ revert</span><br><span class="hljs-comment"># - scope: can be empty (eg. if the change is a global or difficult to assign to a single component)</span><br><span class="hljs-comment"># - subject: start with verb (such as 'change'), 50-character line</span><br><span class="hljs-comment">#</span><br><br><span class="hljs-comment"># body: 72-character wrapped. This should answer:</span><br><span class="hljs-comment"># * Why was this change necessary?</span><br><span class="hljs-comment"># * How does it address the problem?</span><br><span class="hljs-comment"># * Are there any side effects?</span><br><span class="hljs-comment">#</span><br><br><span class="hljs-comment"># footer: </span><br><span class="hljs-comment"># - Include a link to the ticket, if any.</span><br><span class="hljs-comment"># - BREAKING CHANGE</span><br><span class="hljs-comment">#</span><br><br></code></pre></td></tr></table></figure></li><li><p>在项目路径下,我们通过 <code>git config</code> 命令指定 commit.template 的路径:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git config commit.template .gitmessage<br></code></pre></td></tr></table></figure><blockquote><p>注意:这里仅对当前项目进行了模板配置,如果想要对所有项目使用该模板,则将该文件放置到用户目录下,然后在上述命令中添加 –global 参数即可。 </p><p><code>git config --global commit.template ~/.gitmessage</code></p></blockquote></li></ol><p>此时通过命令 <code>git commit</code> (<strong>不能带参数 -m !</strong>)即可显示模板,并在模板中快速完成 Commit Message 的编辑。</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/2f3bac498fc45f31f266dc5c6edb3135/1703569918312.png"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/a0b698669c2690308be04db208c5dd77/1703570281939.png"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/bd892785244176b2fc593c84e84a81a5/1703570593953.png"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/793f74f73b0aa895fcc4abf8ab95eb4a/1703571607092.png"></p>]]></content>
<categories>
<category>程序员进阶</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title>Git —— 常用指令</title>
<link href="/2023/07/31/Git%20%E2%80%94%E2%80%94%20%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4/"/>
<url>/2023/07/31/Git%20%E2%80%94%E2%80%94%20%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4/</url>
<content type="html"><![CDATA[<h1 id="Git-——-常用命令"><a href="#Git-——-常用命令" class="headerlink" title="Git —— 常用命令"></a>Git —— 常用命令</h1><ol><li><p>克隆</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash">git <span class="hljs-built_in">clone</span> <git-repository-url> [local-path] <br><span class="hljs-comment"># option</span><br><span class="hljs-comment"># 1. --recursive # 递归克隆子模块</span><br>git <span class="hljs-built_in">clone</span> <git-repository-url> --recursive [local-path]<br><span class="hljs-comment"># 2. --depth=1 # 浅克隆</span><br>git <span class="hljs-built_in">clone</span> <git-repository-url> --depth=1 [local-path]<br><span class="hljs-comment"># 3. --branch <branch-name> / -b <branch-name> # 克隆指定分支</span><br>git <span class="hljs-built_in">clone</span> <git-repository-url> --branch <branch-name> [local-path]<br></code></pre></td></tr></table></figure></li><li><p>提交</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1. 添加文件</span><br>git add <file-path><br><span class="hljs-comment"># 2. 提交</span><br>git commit -m <span class="hljs-string">"commit message"</span><br><span class="hljs-comment"># commit option</span><br><span class="hljs-comment"># 1. --amend # 修改上一次提交记录,注意!仅允许在本地仓库修改,不允许在远程仓库修改,且会覆盖上一次提交的记录</span><br>git commit --amend -m <span class="hljs-string">"commit message"</span><br><span class="hljs-comment"># 2. --no-edit # 仅修改上一次提交的文件,不修改提交记录</span><br>git commit --amend --no-edit<br><span class="hljs-comment"># 3. -a # 跳过 git add,添加所有存在跟踪记录的修改文件</span><br>git commit -a -m <span class="hljs-string">"commit message"</span><br><span class="hljs-comment"># 4. -v # 显示修改的文件</span><br>git commit -v -m <span class="hljs-string">"commit message"</span><br></code></pre></td></tr></table></figure></li><li><p>推送至远程仓库</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 注意!不建议直接在主干分支中进行修改和更新。</span><br><span class="hljs-comment"># 注意!建议在本地创建分支进行修改,并创建远程分支进行提交,修改完成后在主界面上提交merge request,由管理员审核后合并到主干分支</span><br><span class="hljs-comment"># 注意!建议先拉取远程仓库更新,并将更新合并到本地分支后,确定无conflict后再推送</span><br>git push <remote-name> <branch-name><br></code></pre></td></tr></table></figure></li><li><p>创建/切换分支</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1.查看远程分支</span><br>git branch -r<br><span class="hljs-comment"># 2.查看本地分支</span><br>git branch -l<br><span class="hljs-comment"># 3.创建分支</span><br>git branch <branch-name><br>git add .<br>git commit -m <span class="hljs-string">"commit message"</span><br><span class="hljs-comment"># 4.切换分支</span><br>git checkout <branch-name><br></code></pre></td></tr></table></figure></li><li><p>将远程仓库更新合并到本地分支</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 使用 git fetch 获取远程仓库更新</span><br>git fetch origin <branch-name><br><span class="hljs-comment"># 比较本地分支与远程分支的差异</span><br>git <span class="hljs-built_in">log</span> -p FETCH_HEAD <branch-name><br>git diff FETCH_HEAD <branch-name><br><span class="hljs-comment"># 修改存在差异和冲突的文件</span><br><span class="hljs-comment"># 将远程分支合并到本地分支</span><br>git merge FETCH_HEAD<br><span class="hljs-comment"># 如果存在冲突,解决冲突后,再次提交</span><br>git commit -am <span class="hljs-string">"commit message"</span><br><span class="hljs-comment"># 将本地分支推送至远程分支</span><br>git push origin <branch-name><br></code></pre></td></tr></table></figure></li><li><p>删除本地已合并分支</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git branch --merged main | grep -v <span class="hljs-string">'^[ *]*main$'</span> | xargs git branch -d<br></code></pre></td></tr></table></figure></li><li><p>删除远程仓库中已不存在的本地分支</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash">git branch -a<br><span class="hljs-comment"># </span><br>git remote show origin<br><br>git remote prune origin<br><br></code></pre></td></tr></table></figure></li><li><p>http/https 免密</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git config --global credential.helper store<br></code></pre></td></tr></table></figure></li><li><p>ssh 免密</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh-keygen -t rsa -C <span class="hljs-string">"your_email"</span><br><span class="hljs-comment"># copy id_ras.pub to git</span><br></code></pre></td></tr></table></figure></li><li><p>stash</p></li><li><p>git fsck –full</p></li><li></li></ol>]]></content>
<categories>
<category>程序员进阶</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title>开启 Cloudflare CDN 代理,实现 IPv4 to IPv6 转换</title>
<link href="/2023/06/01/%E5%BC%80%E5%90%AF%20Cloudflare%20CDN%20%E4%BB%A3%E7%90%86%EF%BC%8C%E5%AE%9E%E7%8E%B0%20IPv4%20to%20IPv6%20%E8%BD%AC%E6%8D%A2/"/>
<url>/2023/06/01/%E5%BC%80%E5%90%AF%20Cloudflare%20CDN%20%E4%BB%A3%E7%90%86%EF%BC%8C%E5%AE%9E%E7%8E%B0%20IPv4%20to%20IPv6%20%E8%BD%AC%E6%8D%A2/</url>
<content type="html"><![CDATA[<h1 id="开启-Cloudflare-CDN-代理,实现-IPv4-to-IPv6-转换"><a href="#开启-Cloudflare-CDN-代理,实现-IPv4-to-IPv6-转换" class="headerlink" title="开启 Cloudflare CDN 代理,实现 IPv4 to IPv6 转换"></a>开启 Cloudflare CDN 代理,实现 IPv4 to IPv6 转换</h1><p>通过公网IPv6地址实现远程访问专栏系列文章:</p><ol><li><a href="https://ccccx159.github.io/2023/03/21/%E4%BD%BF%E7%94%A8%E5%85%AC%E7%BD%91IPv6%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%E8%AE%BE%E5%A4%87/">《使用公网IPv6远程访问内网设备》</a></li><li><a href="https://ccccx159.github.io/2023/03/21/DDNS%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90IPv6%E5%9C%B0%E5%9D%80/">《DDNS动态域名解析IPv6地址》</a></li><li><a href="https://ccccx159.github.io/2023/06/01/%E5%BC%80%E5%90%AF%20Cloudflare%20CDN%20%E4%BB%A3%E7%90%86%EF%BC%8C%E5%AE%9E%E7%8E%B0%20IPv4%20to%20IPv6%20%E8%BD%AC%E6%8D%A2/">《开启 Cloudflare CDN 代理,实现 IPv4 to IPv6 转换》</a></li></ol><blockquote><p><font color="blue">温馨提示:</font><br>本文存在一部分付费内容,但是付费仅限于域名的购买,如果已经有域名的朋友,请放心大胆食用本文,因为剩余内容均为免费使用。没有域名的朋友,可以移步上一篇文章<a href="https://ccccx159.github.io/2023/03/21/DDNS%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90IPv6%E5%9C%B0%E5%9D%80/">《DDNS动态域名解析IPv6地址》</a>,里面详细介绍了如何在腾讯云购买便宜好用的域名。</p></blockquote><h2 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h2><p>在前两篇文章中,我们详细介绍了如何开启 IPv6 来实现远程访问内网设备,以及如何使用域名和搭建 DDNS 服务实现通过域名来进行远程访问。我们先简单回顾一下,首先需要开启本地网络运营商分发 IPv6 地址的功能,并且开启内网设备的 IPv6 网络权限,因为 IPv6 地址是公网地址,所以此时我们可以直接使用内网设备具体的 IPv6 地址进行直接访问。但是由于IPv6 地址实在是太长了,难以记忆,因此我们通过域名(domain)来绑定 IP 地址,方便记忆。运营商提供的 IP 地址是动态地址,在一定时间后、或者重新拨号联网后会发生变化,针对这一情况,我们在本地搭建了 DDNS 服务,用于监测当前的 IP 地址是否发生变化,如果发生变化,则将新 IP 发送给 DNS 解析服务商,更新域名的 DNS 解析记录。</p><p>但是我们仍然遗留了一个问题,部分网络环境没有 IPv6 解析能力,比如我们公司的网络,在这种情况下,我们无法仅仅使用前两篇文章的内容来进行远程访问了。那么是否有方法能在 IPv4 Only 的环境来访问 IPv6 的站点呢?答案是有的,即<mark>套用 CDN 进行流量回源,简单来说就是在源站和客户端之间建立一个中间服务节点,用来 IPv6 和 IPv4 流量的双向转换</mark>。</p><p>当然答案并不是唯一的,有能力的大神,完全可以自建用于中继转换的服务,但是有免费,简单的轮子,我们当然首选直接拿来用啦。</p><h2 id="二、什么是-CDN"><a href="#二、什么是-CDN" class="headerlink" title="二、什么是 CDN"></a>二、什么是 CDN</h2><p>全称:Content Delivery Network 或 Content Ddistribute Network,即内容分发网络,顾名思义,它是一个分布式节点网络(也称为边缘位置服务器),它有助于根据用户的位置,内容源服务器和边缘服务器向最终用户的地点传送内容(网页、视频、图像等)。CDN节点具有缓存内容的缓存功能,并且可以从地理上靠近最终用户的位置向用户提供内容。CDN节点由CDN提供商部署在多个地理位置,并且可以跨越多个ISP(因特网服务提供商)网络。</p><p>简单来说,它是一个边缘位置服务器,再简单点,它就是一个服务器,用来干什么,用来传递(中转)内容。也就是说我们在访问源站的过程中,实质上是先访问了 CDN 中的边缘服务器,然后由它向源站请求内容后再由它向我们传送了响应内容。</p><p>如果无法理解,那也没关系,看完本文内容,会用即可。</p><h2 id="三、为什么选择-Cloudflare(简称“CF”)"><a href="#三、为什么选择-Cloudflare(简称“CF”)" class="headerlink" title="三、为什么选择 Cloudflare(简称“CF”)"></a>三、为什么选择 Cloudflare(简称“CF”)</h2><p>我们先说一下 Cloudflare 开启 CDN 后最大的缺点:慢!如果使用 Cloudflare 提供的默认边缘节点,有可能会让你的访问速度变得奇慢无比,因为 Cloudflare 的服务器大部分在境外,所以国内访问这些境外的边缘节点的速度你自然懂的。</p><p>但是为什么我们还是选择 Cloudflare 呢?有这几个让人无法拒绝的理由:</p><ol><li>提供了免费的 DNS 解析和 CDN 代理,DNS 支持泛解析;</li><li>CDN 支持 IPv4 和 IPv6 双栈流量的互相转换;</li><li>可以使用第三方开源的 Cloudflare 边缘节点 IP 优选脚本,通过 host 劫持来提高访问速度;</li><li>启用 CDN 后,我们可以隐藏真实 IP 地址,提高个人网络安全;</li><li>国内的 CDN 收费,并且需要绑定实名备案的云服务器,部分 CDN 不支持 IPv6 回源(腾讯云默认 CDN 不支持,需要购买额外的 ECDN 支持 IPv6 的回源);</li></ol><p>其实仅凭第四点就薄纱国内的 CDN 服务了。CF 速度慢可以通过付费和优选 IP 解决,既然都要付费,那为什么不付给更良心的 CF 呢?</p><h2 id="四、将域名托管至-CF"><a href="#四、将域名托管至-CF" class="headerlink" title="四、将域名托管至 CF"></a>四、将域名托管至 CF</h2><p>前文中,我们在腾讯云购买了域名,并使用 dnspod 进行域名解析。那我们在使用 CF 前,首先要做的就是将域名托管到 CF。CF 使用需要注册账号,这一步就不做过多赘述了,网站支持简体中文,我相信按照说明注册账号应该都能顺利完成。</p><p>在注册完成后,我们点击主页中的“<strong>添加站点</strong>”按钮,导入我们购买的域名:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/483cec301c56b56f697ccb21605e50ce/1685951169889.jpg"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/37a856c85d1a6fe44f380261327cbb6f/1685953812251.jpg"></p><p>这里我们选择免费计划即可,如果有额外需求的,可以按需选择付费计划:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/e64117b1d2645418d6777759462d8143/1685954149112.png"></p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/b2ca18a9f53b7c773ed56b598e340aed/1685954472920.png"></p><p>完成到这一步时,我们已经完成了托管过程中在 CF 的界面所有操作,接下来我们去腾讯云的控制台,修改域名的名称服务器:</p><p>进入我的域名界面:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/1ffede4f3fc54987e9195931b3ddbca4/1685955178290.png"></p><p>在“修改DNS服务器”界面中,完成名称服务器的修改:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/795c0d5460d21f80df505c4a368f94c7/1685954836479.png"></p><h2 id="四、开启-CDN"><a href="#四、开启-CDN" class="headerlink" title="四、开启 CDN"></a>四、开启 CDN</h2><p>在域名托管的过程中,CF 会自动将原有的域名解析记录导入,我们进入 CF 的域名详情页面,选择左侧的 DNS 选项,打开当前域名的 DNS 解析记录界面:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/d39ea2b19a4fb646e050a8100fec5d31/image.png"></p><p>可以看到我这边已经添加了几条解析记录,下面我们从零开始介绍,如何添加解析记录并开启代理。</p><ol><li><p>手动添加一条 DNS 泛解析记录,并关闭代理:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/7c7b5244b0822b67697fc111b1385e26/image.png"></p></li><li><p>本地尝试 Ping 域名,确认 DNS 解析生效:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/237a1e5abb6c40d63531a4418c43906b/image.png"></p><p>可以看到 CF 的 DNS 解析已经生效了,域名被正确解析到了我们填写的 IP 上。</p></li><li><p>修改 DNS 解析记录,开启 CDN 代理:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/88df5e9aab35e19c32c3ff9ecb895f63/image.png"></p></li><li><p>再次尝试 Ping 域名,观察其返回的 IP 是否已经更新为代理的边缘节点 IP:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/731154f5637920912d564a89d9f572cd/image.png"></p></li><li><p>关闭本地电脑的 IPv6 网络,重新 Ping 域名,观察是否能正常 Ping 通,且返回的 IP 为 IPv4 地址:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/c0f48015f6d7c6824e1af70cfcf1ac56/image.png"></p></li><li><p>修改 OpenWrt 中的 DDNS 信息:</p><p>需要先在 CF 的个人资料中获取一个 API Key,用于更新 DNS 解析记录:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/a53419986eba8d4b3b27de92b501c857/image.png"></p><p>然后再去 OpenWrt 的“动态 DNS”插件中添加/修改 DDNS 服务配置信息:</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/1a4bb932f2720abbc81a808122e1c664/image.png"></p><p><font color=red>注意:当开启 CDN 代理时,这个插件可能会有 “warn” 级别日志,因为它默认使用了 nslookup 获取域名指向的 IP,在开启代理后 nslookup 获取到的是 CF 边缘节点的 IP 地址,和我们真实的 IP 地址并不相同,并且会获取到多个 IP 导致脚本执行过程中有一步骤 expand_ipv6 会报错。但是这两个问题是没有什么关系的,唯一的影响就是每次检查 IP 的时候,都会强制更新一次 DNS 解析记录,即使真实的 IP 没有发生变化。</font></p></li></ol><p>经过以上6步,我们已经成功给域名套上了 CDN,所有对域名的请求将通过 CF 的边缘节点进行分发和返回,并且我们可以看到,当本地的 IPv6 网络被关闭时,CF 自动给我们分配了 IPv4 的边缘节点,实现了无 IPv6 网络环境下对 IPv6 源站的访问。</p><h2 id="五、Cloudflare-IP-优选"><a href="#五、Cloudflare-IP-优选" class="headerlink" title="五、Cloudflare IP 优选"></a>五、Cloudflare IP 优选</h2><p>在上面开启 CDN 代理的操作步骤中,第2步未开启代理时,单次 ping 的响应时间是15 ms,而第4、5步中 ping 的响应时间则直接上涨到了 200 ms,可见 CF 开启 CDN 代理,对我们访问的速度影响还是比较大的。因此我们需要对 CDN 边缘节点的接入 IP 进行优选。</p><p>此处推荐一个第三方开源的 IP 优选脚本:<a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a>。详细的使用方法和文档在其 github 主页中都有详细介绍,本文就不再赘述了。</p><h2 id="六、Cloudflare-开启-CDN-后的局限性"><a href="#六、Cloudflare-开启-CDN-后的局限性" class="headerlink" title="六、Cloudflare 开启 CDN 后的局限性"></a>六、Cloudflare 开启 CDN 后的局限性</h2><p>是否只要套用的 CDN,就万事大吉了呢?实则不然,Cloudflare CDN 仅仅能代理 HTTP 和 HTTPS 流量,而我们实际使用过程中,往往还有不同的协议流量,例如 SSH 访问服务器后台(不建议将 SSH 服务暴露到公网)、微软的RDP(mstsc)等,无法通过被代理的域名进行访问。</p><p>解决办法倒也不算麻烦,只要单独为特殊的流量设置独立子域名,并关闭代理即可。比如添加一个用于微软 RDP(mstsc)的子域名解析记录:rdp.yourDomain.com,并指定对应的 IP 地址。同时在添加一个 <code>rdp.yourDomain.com</code> 的 DDNS 服务即可。但是这种方式由于没有 CDN 的代理,也就意味着将直接访问 IPv6 地址,当处于无 IPv6 能力的环境下时,将不可访问。</p><p>当然 Cloudflare 提供了更安全的付费服务 <a href="https://developers.cloudflare.com/spectrum/">Cloudflare Spectrum</a> 来解决这个问题</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/e489644ba5f788f159e905142f4fca6d/image.png"></p><p>对于 HTTP 和 HTTPS 流量的代理,也存在一定的局限性。由于国内无法使用标准的 80 和 443 端口,因此我们不得不使用非标准端口来进行 HTTP(s) 通信。而 Cloudflare 支持转发的端口存在限制,仅支持以下端口的转发:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">HTTP 端口:80、8080、8880、2052、2082、2086、2095<br>HTTPS 端口:443、2053、2083、2087、2096、8443<br></code></pre></td></tr></table></figure><p>因此势必需要设置端口转发,将 CF 端口的流量转发到部署的服务指定的端口。</p><p><img src="https://gitlab.ccccxyz.top:8443/xu4nch3n/notebooks/uploads/242211644d7144a59450b84bdc80a411/image.png"></p><h2 id="七、总结"><a href="#七、总结" class="headerlink" title="七、总结"></a>七、总结</h2><p>到这里,IPv6 远程访问的系列专题基本告一段落了。我们通过三篇文章,详细地介绍了如何开启 IPv6 网络,如何通过域名进行远程访问,以及如何在无 IPv6 网络环境下通过 CDN 代理访问 IPv6 源站。</p><p>虽然这个专题主要介绍的都是 IPv6,但是 IPv4 网络也同样使用,无非就是域名的 DNS 解析类型从 “AAAA” 转变为 “A” 记录。</p><p>尽管 Cloudflare 免费计划无法做到尽善尽美,但是我们可以略微绕个弯进行规避后,一般的个人家用场景基本足够使用了,更别说还有 Frp,ZeroTier 这些优秀的穿透工具可以辅助使用。对 Frp 和 ZeroTier 感兴趣的朋友,推荐观看司波图的这期视频:<a href="https://www.bilibili.com/video/BV1dr4y147aq/?spm_id_from=333.999.0.0&vd_source=d8c59e2abebb7aaa6127921565c34c80">独享带宽,教你搭建只属于自己的内网穿透服务器(基于frp与zerotier moon服务器)</a></p><p>希望这个专题系列能给有需要的人带去帮助~</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://developers.cloudflare.com/dns/">Cloudflare DNS 官方文档</a></li><li><a href="https://developers.cloudflare.com/spectrum/">Cloudflare spectrum 官方文档</a></li><li><a href="https://developers.cloudflare.com/api/">Cloudflare API 官方文档</a></li><li><a href="https://www.v2ex.com/t/870627">《家里只有 IPv6 公网地址,怎么操作才能使其他 IPv4 网络也访问到?》</a></li><li><a href="https://github.com/XIU2/CloudflareSpeedTest">XIU2/CloudflareSpeedTest</a></li></ol>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>cloudflare</tag>
<tag>CDN</tag>
<tag>IP</tag>
</tags>
</entry>
<entry>
<title>fail2ban 防止暴力破解密码</title>
<link href="/2023/05/31/fail2ban%20%E9%98%B2%E6%AD%A2%E6%9A%B4%E5%8A%9B%E7%A0%B4%E8%A7%A3%E5%AF%86%E7%A0%81/"/>
<url>/2023/05/31/fail2ban%20%E9%98%B2%E6%AD%A2%E6%9A%B4%E5%8A%9B%E7%A0%B4%E8%A7%A3%E5%AF%86%E7%A0%81/</url>
<content type="html"><![CDATA[<h1 id="fail2ban-防止暴力破解密码"><a href="#fail2ban-防止暴力破解密码" class="headerlink" title="fail2ban 防止暴力破解密码"></a>fail2ban 防止暴力破解密码</h1><h2 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h2><p>在五一劳动节期间发现家中搭建的 unRAID 服务器的 CPU 直接被拉满了,怀疑是被暴力破解入侵了,并植入了挖矿程序。经过两天的斗智斗勇,发现在系统盘中被创建了一个名为 wireguard.go 的定时脚本,会定时下载运行 Xmrig 的挖矿程序。。。打死我也没想到伪装名用的是 wireguard。由于部分服务被我暴露到公网上,所以不可避免的会被扫描到,然后被暴力破解了 sshd 的 root 密码,从而导致了以上惨剧。因此设置高位端口和复杂密码的必要性可想而知,一旦自建服务器被黑,如果只是植入挖矿程序,还算良心,万一隐私信息的暴露,或者被勒索,那就杯具了。</p><p>由于端口的有限性,尽管设置了高位端口,但还是有可能会有意者扫描到,因此防止暴力破解也是一个非常关键的安全手段。群晖自带了多次登录失败封禁 ip 的功能,但是 unRAID 本身没有这个功能,我们可以借助安装 fail2ban 这个防入侵软件来保护我们的服务器。</p><h2 id="二、fail2ban-简介"><a href="#二、fail2ban-简介" class="headerlink" title="二、fail2ban 简介"></a>二、fail2ban 简介</h2><p>fail2ban 的官方开源仓库:<a href="https://github.com/fail2ban/fail2ban">https://github.com/fail2ban/fail2ban</a></p><p>这里引用 fail2ban 开源仓库的文档内容:</p><blockquote><p>Fail2Ban 扫描日志文件 <code>/var/log/auth.log</code> 并禁止 IP 地址进行过多失败的登录尝试。它通过更新系统防火墙规则以在可配置的时间内拒绝来自这些 IP 地址的新连接来实现这一点。Fail2Ban 开箱即用,可以读取许多标准日志文件,例如 sshd 和 Apache 的日志文件,并且可以轻松配置为读取您选择的任何日志文件,以获取您希望的任何错误。</p><p>尽管 Fail2Ban 能够降低不正确的身份验证尝试率,但它无法消除弱身份验证带来的风险。如果您真的想保护服务,请将服务设置为仅使用两个因素或公共/私人身份验证机制。</p></blockquote><p>简单来说,fail2ban 通过扫描日志文件获取相关错误,并通过改写 iptable 来阻止对应 IP 的连接,支持多种服务的保护。</p><p>fail2ban 提供了 docker 版本,因此安装配置也是相对简单的,下面就简要介绍一下如何在 unRAID 中安装 docker 版本的 fail2ban。</p><h2 id="三、docker-安装"><a href="#三、docker-安装" class="headerlink" title="三、docker 安装"></a>三、docker 安装</h2><p>这里以 unRAID 为例,简单介绍一下安装和配置的过程,由于 unRAID 的 APP 市场中存在现有的 docker 应用,因此我们直接拉取市场应用,并填写相关配置即可。当然你也可以基于其 github 发布的源码或者 release 包自行编写 dockerfile 构建镜像。</p><ol><li>点击 unRAID 的 APP 按钮,进入应用市场,在搜索栏中输入 fail2ban,会出来两个应用,我们选择发布者为 linuxserver 的应用,相对安全。另一个是第三方个人构建的 docker 镜像:<br><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/b997e4a75c8c7a7692f7fcdad88c7f02/1685515906781.jpg"></li><li>点击安装后会跳出一个提示界面,提示当前应用将运行在特权(privilege)模式下,可能存在安全风险。由于 fail2ban 是开源的,因此放心大胆的安装吧<br><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/5e62abe2e324bb0e600864bd51a8476a/1685516101870.png"></li><li>进入后是 docker 的配置界面,其实就是 docker run 的参数,填充完参数后完成创建<br><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/7354da94922920c9ead8d4d012049fc6/1685517579501.png">如果仅仅是为了读取系统的日志,其实我们什么都不需要填写和修改,直接创建即可。但是我们也可以看到,模板中还提供了可选的、其他服务的日志监控路径的挂载配置,上图未截全,如果有对应需求的朋友可以自行配置。</li><li>此时根据<a href="https://github.com/linuxserver/fail2ban-confs/blob/master/README.md">linuxserver的说明文档</a>,还要手动去创建和设置一个配置文件 jail.local,否则重启容器后会丢失配置,而且默认的监控服务是被禁用的,我们直接拷贝 ta 提供的完整配置,然后手动修改其中的部分内容,并保存为 jail.local: <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><code class="hljs shell">[DEFAULT]<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">"bantime.increment"</span> allows to use database <span class="hljs-keyword">for</span> searching of previously banned ip<span class="hljs-string">'s to increase a</span></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">default ban time</span></span><br>bantime.increment = true<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">"bantime.maxtime" is the max number of seconds using the ban time can reach (doesn'</span>t grow further)</span><br>bantime.maxtime = 5w<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">"bantime.factor"</span> is a coefficient to calculate exponent growing of the formula or common multiplier</span><br>bantime.factor = 24<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">"bantime"</span> is the number of seconds that a host is banned.</span><br>bantime = 1h<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">A host is banned <span class="hljs-keyword">if</span> it has generated <span class="hljs-string">"maxretry"</span> during the last <span class="hljs-string">"findtime"</span></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">seconds.</span><br>findtime = 24h<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">"maxretry"</span> is the number of failures before a host get banned.</span><br>maxretry = 5<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">Prevents banning LAN subnets</span><br>ignoreip = 127.0.0.1/8 ::1<br> 10.0.0.0/8<br> 172.16.0.0/12<br> 192.168.0.0/16<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">The ban action <span class="hljs-string">"iptables-multiport"</span> (default) should work <span class="hljs-keyword">for</span> most</span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">The ban action <span class="hljs-string">"iptables-allports"</span> can be used <span class="hljs-keyword">if</span> multiport causes issues</span><br><span class="hljs-meta prompt_">#</span><span class="language-bash">banaction = %(banaction_allports)s</span><br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">Read https://github.com/sebres/PoC/blob/master/FW.IDS-DROP-vs-REJECT/README.md before changing block <span class="hljs-built_in">type</span></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">The block <span class="hljs-built_in">type</span> <span class="hljs-string">"REJECT --reject-with icmp-port-unreachable"</span> (default behavior) should respond to, but <span class="hljs-keyword">then</span> instantly reject connection attempts</span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">The block <span class="hljs-built_in">type</span> <span class="hljs-string">"DROP"</span> should not respond to connection attempts, resulting <span class="hljs-keyword">in</span> a <span class="hljs-built_in">timeout</span></span><br><span class="hljs-meta prompt_">#</span><span class="language-bash">banaction = iptables-multiport[blocktype=DROP]</span><br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">Add additional actions</span><br>action = %(action_)s<br> apprise-api[host="127.0.0.1", tag="fail2ban"]<br> cloudflare[cfuser="YOUR-EMAIL", cftoken="YOUR-TOKEN"]<br><br>abuseipdb_apikey = YOUR-API-KEY<br><br>[unraid-sshd]<br><span class="hljs-meta prompt_"># </span><span class="language-bash">configuration inherits from jail.d/unraid-sshd.conf</span><br>enabled = true<br>chain = INPUT<br>action = %(known/action)s<br> abuseipdb[abuseipdb_apikey="%(abuseipdb_apikey)s", abuseipdb_category="18,22"]<br><br>[unraid-webgui]<br><span class="hljs-meta prompt_"># </span><span class="language-bash">configuration inherits from jail.d/unraid-webgui.conf</span><br>enabled = true<br>chain = INPUT<br>port = http,https,YOUR-UNRAID-MY-SERVERS-WAN-PORT<br>action = %(known/action)s<br> abuseipdb[abuseipdb_apikey="%(abuseipdb_apikey)s", abuseipdb_category="18,21"]<br><br>[unifi-controller-auth]<br><span class="hljs-meta prompt_"># </span><span class="language-bash">configuration inherits from jail.d/unifi-controller-auth.conf</span><br>enabled = true<br>chain = DOCKER-USER<br>action = %(known/action)s<br> abuseipdb[abuseipdb_apikey="%(abuseipdb_apikey)s", abuseipdb_category="18,21"]<br><br>[vaultwarden-auth]<br><span class="hljs-meta prompt_"># </span><span class="language-bash">configuration inherits from jail.d/vaultwarden-auth.conf</span><br>enabled = true<br>chain = DOCKER-USER<br>action = %(known/action)s<br> abuseipdb[abuseipdb_apikey="%(abuseipdb_apikey)s", abuseipdb_category="18,21"]<br><br></code></pre></td></tr></table></figure></li></ol><p> 配置文件中,我们需要手动修改部分内容:</p><ul><li><p>删除第31行行首的 “#” 取消注释,按照其说明,默认的封禁 action 是 iptables-multiport,但是不知道为什么,我这边使用默认的封禁 action 时无法生效,因此我将这行注释取消,使用 iptables-allports 封禁所有端口</p></li><li><p>在 39 ~ 40 行中定义了三种 action,后面两种是推送消息的,如果有需要可以自行配置一下,如果不需要则在第 40 和 41 行的行首分别加上 “#” 即可</p></li><li><p>从 45 行开始,就是各个待监控的服务,每个服务都以 <code>[xxxxxx]</code> 开头,默认配置中添加了 <code>[unifi-controller-auth]</code> 和 <code>[vaultwarden-auth]</code> 的监控,如果不需要可以直接删除,并且默认的监控配置中,action 还额外增加了一条 abuseipdb 的配置,如果没有的,也可以直接将 abuseipdb 这行删除,保留默认的 <code>action = %(known/action)s</code> 即可。</p></li><li><p>如果需要添加其他的服务监控,可以从 docker 本身配置中复制过来,路径为 <code>/mnt/user/appdata/fail2ban/fail2ban/jail.d</code> 下面有各个服务独立的 conf 文件,我们以 gitlab.conf 为例。直接复制粘贴到 jail.local 中即可,将 enable 修改为 true,logpath 中的 /gitlab/xxxx 修改为 docker 配置界面中的挂载路径即可:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_">#</span><span class="language-bash"><span class="hljs-comment"># Version 2022/08/06</span></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">Fail2Ban jail configuration <span class="hljs-keyword">for</span> gitlab</span><br><br>[gitlab]<br><br>enabled = false<br>port = http,https<br>logpath = %(logs_path)s/gitlab/gitlab-rails/application.log<br></code></pre></td></tr></table></figure></li></ul><ol start="5"><li>将 jail.local 拷贝到 unRAID 的目录中 <code>/mnt/user/appdata/fail2ban/fail2ban/jail.d</code>,重启 docker。</li></ol><h2 id="四、fail2ban-client-命令和效果测试"><a href="#四、fail2ban-client-命令和效果测试" class="headerlink" title="四、fail2ban-client 命令和效果测试"></a>四、fail2ban-client 命令和效果测试</h2><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/1c02f915b01c6f05f89b579cfd94109e/1685520145020.png"></p><p>可以看到在输错几次密码后,ssh 连接直接被拒绝了。我们进 fail2ban 的控制台中看一下封禁状态 <code>fail2ban-client status <服务名称></code>:</p><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/e6fcd2c4d2085ef6739a64c2331afc1a/1685520332045.png"></p><p>我这边开了7个监控服务,刚才被封禁的是 unraid-sshd,我们在看一下详细的监控服务状态:</p><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/8667dfceba63606dcbd1bb3eea6dfee2/1685520362023.png"></p><p>列出了当前服务失败次数和总次数,以及当前封禁的 ip 数量和总 ip 数量,并列举出了被封禁的详细 ip。<br>我们可以通过 <code>fail2ban-client set <服务名称> unbanip <被封禁的 IP 地址></code> 来取消封禁:</p><p><img src="https://gitlab.b1gfac3c4t.top:1594/xu4nch3n/notebooks/uploads/b82877dd944756d2e25844094fd5a41a/1685520388282.png"></p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>
<tag>原创</tag>
<tag>fail2ban</tag>
<tag>防破解</tag>
</tags>
</entry>
<entry>
<title>Git —— submodule 操作说明</title>
<link href="/2023/05/19/Git%20%E2%80%94%E2%80%94%20submodule%20%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E/"/>
<url>/2023/05/19/Git%20%E2%80%94%E2%80%94%20submodule%20%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E/</url>
<content type="html"><![CDATA[<h1 id="Git-——-submodule-操作说明"><a href="#Git-——-submodule-操作说明" class="headerlink" title="Git —— submodule 操作说明"></a>Git —— submodule 操作说明</h1><h2 id="1-add-添加子模块"><a href="#1-add-添加子模块" class="headerlink" title="1. add 添加子模块"></a>1. add 添加子模块</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">git submodule add -b master https://github.com/coolsnowwolf/lede.git ./lede</span><br>git submodule add -b <branch-name]> <git-repository-url> [local-path]<br><span class="hljs-meta prompt_"># </span><span class="language-bash">本地提交</span><br>git commit -m "add submodule xxxx"<br><span class="hljs-meta prompt_"># </span><span class="language-bash">推送到远程仓库</span><br>git push<br></code></pre></td></tr></table></figure><h2 id="2-checkout-子模块检出"><a href="#2-checkout-子模块检出" class="headerlink" title="2. checkout 子模块检出"></a>2. checkout 子模块检出</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">有两种方式:</span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">1. 使用 --recursive 参数,跟随主仓库递归 <span class="hljs-built_in">clone</span></span><br>git clone <your main repository url> --recursive # 此时 clone 下来的主项目会直接 clone 远程仓库中记录的 commit id 版本的子模块<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">2. 单独 checkout 子模块</span><br>git clone <your main repository url> # 不带 --recursive 递归参数时,submodule 无法被一起 clone 下来<br>git submodule update --init --recursive # 将 submodule 更新到远程仓库中记录的 commit id 版本<br></code></pre></td></tr></table></figure><h2 id="3-update-更新-x2F-切换子模块-commit-id-和当前分支"><a href="#3-update-更新-x2F-切换子模块-commit-id-和当前分支" class="headerlink" title="3. update 更新/切换子模块 commit id 和当前分支"></a>3. update 更新/切换子模块 commit id 和当前分支</h2><p>这里存在一个较大的坑,默认检出的子模块并不属于任何分支,而是一个 “detached head” ,虽然可以提交更改,但是并没有本地分支跟踪提交的更改,这意味着<font color=red>下次更新子模块会丢失这些更改</font>。</p><p>因此在对子模块进行开发修改前,请先切换其所属分支和对应的 commit id。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">默认添加的 submodule 的 commit <span class="hljs-built_in">id</span> 是 add 时默认分支当前的一个 commit <span class="hljs-built_in">id</span>,当子模块原始仓库更新后,期望切换到指定的 commit <span class="hljs-built_in">id</span> 版本,或者像要切换分支</span><br>git pull<br>git submodule update # 更新本地仓库,避免出现冲突<br>cd <submodule dir><br>git checkout <branch name> # 切换分支<br>git pull # 拉取新分支源码<br>git checkout <commit id> # 更新子模块版本<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">回到主仓库目录,提交子模块的引用版本修改</span><br>cd ..<br>git add . # 暂存 submodule 的引用版本修改<br>git commit -m "update submodule xxx from xxx to xxx" # 提交<br>git push # 推送到远程仓库<br><br></code></pre></td></tr></table></figure><h2 id="4-commit-提交子模块"><a href="#4-commit-提交子模块" class="headerlink" title="4. commit 提交子模块"></a>4. commit 提交子模块</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell">git pull <br>git submodule update # 确保提交前已将本地仓库更新到远程仓库最新版本,避免提交出现冲突<br>cd <submodulde dir><br>git add .<br>git commit -am "submodule modify"<br>git push # 将子模块提交的更改推送至远程仓库<br></code></pre></td></tr></table></figure><p>由于子模块和主模块是独立的两个仓库,主模块仅仅应用了子模块的 url 和 commit id。因此当子模块推送更改后,生成新的 commit id,但是主模块对子模块的引用配置并未发生更改,因此需要在主模块中同步进行提交更改。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd ../ # 回到主模块目录<br>git add .<br>git commit -am "submodule reference modify"<br>git push # 推送主模块对子模块的引用记录更改到远程仓库<br></code></pre></td></tr></table></figure><p>可以看到对于子模块的修改,我们需要分别提交和推送子、主模块的更改,当然我们也可以将 “推送至远程仓库” 这一步合并:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd <main module dir> # 进入主模块目录<br><span class="hljs-meta prompt_"># </span><span class="language-bash">使用 --recurse-submodules=on-demand 选项,可以在推送主模块更改时,自动推送未推送的子模块</span><br>git push --recurse-submodules=on-demand<br></code></pre></td></tr></table></figure><p>如果出现子模块提交了更改记录,但是未推送到远程仓库,主模块提交了子模块引用记录的变更,并完成了推送到远程仓库的操作。此时拉取主模块没问题,但是在拉取子模块时,会出现 “not our ref” 的报错。这是因为主模块引用了一个远程仓库未记录的 commit id 版本的子模块。需要在提交了变更记录的子模块中完成 push 即可。为了避免<mark>忘记推送子模块修改,仅推送了主模块的引用记录变更</mark>,可以将主模块的推送命令修改为:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">使用 --recurse-submodule=check 选项可以自动检查子模块未 push 的错误</span><br>git push --recurse-submodule=check<br></code></pre></td></tr></table></figure><p>当使用 “–recurse-submodule=check” 选项时,如果子模块存在未 push 情况,则当前 push 操作会报警;并且如果子模块存在 push 失败的情况时,也同样会报错。可以直接将其写入 git 配置,减少重复劳动:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">git config push.recurseSubmodules check<br></code></pre></td></tr></table></figure><h2 id="5-modify-修改-submodule-远程仓库-url"><a href="#5-modify-修改-submodule-远程仓库-url" class="headerlink" title="5. modify 修改 submodule 远程仓库 url"></a>5. modify 修改 submodule 远程仓库 url</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd <main module dir> # 进入主模块目录<br><span class="hljs-meta prompt_"># </span><span class="language-bash">修改主模块中 .gitmodules 中的 url</span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">使用 <span class="hljs-built_in">sync</span> 命令同步修改至 .git/config 中</span><br>git submodule sync<br>git commit -am "modify submodule url" # 提交修改<br>git push # 推送到主模块远程仓库<br></code></pre></td></tr></table></figure><p>如果时别人修改了子模块 url,则拉取主模块的更新后,使用 sync 命令同步到本地 .git/config 中即可:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd <main module dir> # 进入主模块目录<br>git pull # 拉取主模块更新,即获取 .gitmodule 中 url 的修改<br>git submodule sync # 将 .gitmodule 中的修改同步到本地仓库的配置中 .git/config<br></code></pre></td></tr></table></figure><h2 id="6-deinit-移除已有的-submodule"><a href="#6-deinit-移除已有的-submodule" class="headerlink" title="6. deinit 移除已有的 submodule"></a>6. deinit 移除已有的 submodule</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">git submodule deinit <submodule name><br>git rm <submodule dir><br>git commit -am "remove submodule xxx"<br>git push<br></code></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://ldjhust.github.io/2018/08/22/Standard-Operation-of-Git-Submodule.html">《Git - 使用git submodule的规范操作》</a></li><li><a href="https://knightyun.github.io/2021/03/21/git-submodule">《Git submodule 知识总结》</a></li><li><a href="https://juejin.cn/post/6844903920645455879">《来说说坑爹的 git submodule》</a></li></ol>]]></content>
<categories>
<category>程序员进阶</category>
</categories>
<tags>
<tag>原创</tag>
<tag>Git</tag>
<tag>submodule</tag>
</tags>
</entry>
<entry>
<title>WinRAR去广告</title>
<link href="/2023/05/18/WinRAR%E5%8E%BB%E5%B9%BF%E5%91%8A/"/>
<url>/2023/05/18/WinRAR%E5%8E%BB%E5%B9%BF%E5%91%8A/</url>
<content type="html"><![CDATA[<h1 id="WinRAR-去广告。。。"><a href="#WinRAR-去广告。。。" class="headerlink" title="WinRAR 去广告。。。"></a>WinRAR 去广告。。。</h1><p>搜索引默认提供的 winrar 官网由国内公司代理。。。免费版中的广告就是被这个国内代理商插入的。。。真的就是中国人专坑中国人。。。</p><p>直接去源网站 <a href="https://www.rarlab.com/">https://www.rarlab.com/</a> 下载正式发布的英文版或者繁体中文版,纯净无广告。。。</p><p>无语至极。。。</p><p>不想重新安装的,也可以使用以下方法:</p><ol><li>在 WinRAR 的安装目录下新建一个 txt 文件,并重命名为 “<strong>rarreg.key</strong>“</li><li>用记事本,notepad++等软件打开 “<strong>rarreg.key</strong>“,并粘贴以下内容后保存即可 <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs shell">RAR registration data <br>Federal Agency for Education <br>1000000 PC usage license <br>UID=b621cca9a84bc5deffbf <br>6412612250ffbf533df6db2dfe8ccc3aae5362c06d54762105357d <br>5e3b1489e751c76bf6e0640001014be50a52303fed29664b074145 <br>7e567d04159ad8defc3fb6edf32831fd1966f72c21c0c53c02fbbb <br>2f91cfca671d9c482b11b8ac3281cb21378e85606494da349941fa <br>e9ee328f12dc73e90b6356b921fbfb8522d6562a6a4b97e8ef6c9f <br>fb866be1e3826b5aa126a4d2bfe9336ad63003fc0e71c307fc2c60 <br>64416495d4c55a0cc82d402110498da970812063934815d81470829275<br></code></pre></td></tr></table></figure></li></ol><p>完成上面两个步骤后,在打开 rar 文件就没有广告弹窗了。</p><p>参考:<a href="https://blog.csdn.net/qq_39313596/article/details/85169627">《WinRAR去除 屏蔽广告弹窗方法》</a></p>]]></content>
<categories>
<category>折腾之路</category>
</categories>
<tags>