-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
3673 lines (3252 loc) · 719 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>vi基本操作</title>
<url>/2019/03/06/vi%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/</url>
<content><![CDATA[<p>vi 是 Linux 常用的编辑器,本文记录了 vi 的基本操作。</p>
<a id="more"></a>
<h2 id="三种模式"><a href="#三种模式" class="headerlink" title="三种模式"></a>三种模式</h2><ol>
<li><p>命令模式<br>用 vi 打开一个文件即进入命令模式</p>
</li>
<li><p>输入模式<br>a i o 进入输入模式</p>
</li>
</ol>
<ul>
<li>a 光标后输入</li>
<li>i 光标前输入</li>
<li>o 光标下一行输入</li>
<li>A 光标所在行的行尾输入</li>
<li>O 光标所在行的上一行新建一行</li>
<li>esc退回到命令模式</li>
</ul>
<ol start="3">
<li>末行模式</li>
</ol>
<ul>
<li>: 进入末行模式</li>
<li>esc返回命令模式</li>
</ul>
<h2 id="光标移动"><a href="#光标移动" class="headerlink" title="光标移动"></a>光标移动</h2><ol>
<li><p>行内跳转<br>home 或者 $ 跳转行首<br>end 或者 ^ 跳转行尾</p>
</li>
<li><p>行间跳转<br>末行模式输入 <code>set nu</code> 显示行数</p>
<ul>
<li><p>命令模式<br><code>#gg</code> 跳转到#行,#代表数字<br><code>G</code> 跳转到行尾<br><code>gg</code> 跳转到行首</p>
</li>
<li><p>末行模式<br><code>:#</code> 跳转到#行,#代表数字</p>
</li>
</ul>
</li>
</ol>
<h2 id="复制"><a href="#复制" class="headerlink" title="复制"></a>复制</h2><ul>
<li>命令模式<br><code>#yy</code> 从光标所在行开始,往下复制#行</li>
<li>末行模式<br><code>:#y</code> 复制第#行<br><code>:m,ny</code> 复制从第m行到第n行</li>
</ul>
<h2 id="粘贴"><a href="#粘贴" class="headerlink" title="粘贴"></a>粘贴</h2><ul>
<li><code>p</code> 光标后粘贴</li>
<li><code>P</code> 光标前粘贴</li>
</ul>
<h2 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h2><ul>
<li><p>命令模式<br><code>x或者del</code> 删除光标所在字符<br><code>#dd</code> 删除从光标所在行开始,往下数#行</p>
</li>
<li><p>末行模式<br><code>:#d </code> 删除第#行<br><code>:m,nd</code> 删除从第m行到第n行</p>
</li>
</ul>
<h2 id="剪切"><a href="#剪切" class="headerlink" title="剪切"></a>剪切</h2><p> 删除 + 粘贴 </p>
<h2 id="查找"><a href="#查找" class="headerlink" title="查找"></a>查找</h2><p>命令模式<br> <code>/word</code> 从上往下查找word,小写n,查找下一个匹配的<br> <code>?word</code> 从下往上查找word,大写N,查找上一个匹配的</p>
<h2 id="替换"><a href="#替换" class="headerlink" title="替换"></a>替换</h2><p>末行模式<br> <code>:s/old/new</code> 将光标所在行,满足的第一个old替换成new<br> <code>:s#old#new</code> </p>
<p> <code>:s/old/new/g</code> 光标所在行的所有old替换成new<br> <code>:s#old#new#g</code> </p>
<p> <code>:m,ns/old/new</code> 第m行到第n行,每行第一个满足的old替换成new</p>
<p> <code>:%s/old/new/g</code> 全文替换<br> <code>:%s#old#new#g</code></p>
<h2 id="写入文件"><a href="#写入文件" class="headerlink" title="写入文件"></a>写入文件</h2><p>末行模式<br> <code>:r /root/test.txt</code> 在光标下一行写入文件/root/test.txt</p>
<h2 id="保存退出"><a href="#保存退出" class="headerlink" title="保存退出"></a>保存退出</h2><ul>
<li>末行模式<br>:wq<br>:x</li>
<li>命令模式<br>ZZ</li>
</ul>
<h2 id="其他退出"><a href="#其他退出" class="headerlink" title="其他退出"></a>其他退出</h2><ul>
<li>强制退出<br>:q!</li>
<li>强制保存退出<br>:wq!</li>
<li>正常退出<br>:q</li>
</ul>
]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>vi</tag>
</tags>
</entry>
<entry>
<title>学习Linux基本命令</title>
<url>/2018/12/06/%E5%AD%A6%E4%B9%A0Linux%E5%91%BD%E4%BB%A4%EF%BC%88%E4%B8%80%EF%BC%89/</url>
<content><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文主要介绍了常用的 Linux 命令。</p>
<a id="more"></a>
<h2 id="Linux系统"><a href="#Linux系统" class="headerlink" title="Linux系统"></a>Linux系统</h2><ul>
<li><code>pwd</code> 打印当前工作目录</li>
<li><code>cd</code> 改变目录</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cd /usr/bin 绝对路径从根目录出发,到达目标目录</span><br><span class="line">cd ./usr 相对路径从工作目录出发,到达目标目录</span><br><span class="line">cd .. 到达父目录</span><br><span class="line">cd(cd ~) 到达家目录,如果未root用户,pwd会打印出 /root,其上一层为 根目录/</span><br><span class="line">cd / (cd -) 回到根目录</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>
<ul>
<li><code>ls</code> 列出目录内容</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ls -l 使用长格式显示结果</span><br><span class="line">ls -t 按修改时间排序</span><br><span class="line">ls -r 以相反的顺序显示</span><br><span class="line">ls -S 按文件大小对结果进行排序</span><br><span class="line">ls -R [文件夹] 列出文件树</span><br><span class="line">......</span><br></pre></td></tr></table></figure>
<ul>
<li><code>file</code> 确定文件类型</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">file filename</span><br></pre></td></tr></table></figure>
<ul>
<li><code>less</code> 查看文件内容</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">less /etc/passwd</span><br></pre></td></tr></table></figure>
<ul>
<li><code>touch</code> 新建文件</li>
</ul>
<h2 id="操作文件与目录"><a href="#操作文件与目录" class="headerlink" title="操作文件与目录"></a>操作文件与目录</h2><ul>
<li><code>mkdir</code> 创建目录</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mkdir dir1 创建单个目录</span><br><span class="line">mkdir dir1 dir2 dir3 创建多个目录</span><br><span class="line">mkdir -p dir{1..9} 创建多个目录a1到a9</span><br><span class="line">mkdir -p a{1..3}/b{1..3}创建多个目录a1到a3,并且在每个目录下创建b1到b3</span><br></pre></td></tr></table></figure>
<ul>
<li><code>cp</code> 复制文件或目录</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cp file1 file2 将文件file1复制到file2中,file2内容将会被覆盖</span><br><span class="line">cp -r dir1 dir2 复制目录时一定要加 -r,如果dir2目录存在,则会复制到dir2目录下和mv是一样的道理</span><br><span class="line">cp file1 file2 dir1 将多个文件复制到一个目录下</span><br></pre></td></tr></table></figure>
<p><code>cp</code>命令选项</p>
<p><code>cp</code>在覆盖已存在的文件时默认情况下是 <code>cp -i</code>,即需要用户确认,我们可以这样 <code>\cp</code> 即可无需确认</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">-i 在覆盖一个已存在的文件前,提示用户进行确认。</span><br><span class="line">-r 递归复制目录及其内容。复制目录时需要这个选项</span><br><span class="line">-u 将文件从一个目录复制到另一个目录时,只会复制目标目录不存在的文件或是目标目录相应文件的更新文件</span><br><span class="line">-v 复制文件时显示信息性消息</span><br></pre></td></tr></table></figure>
<ul>
<li><code>mv</code> 重命名或移动文件和目录</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mv item1 item2 将文件或目录item1移动或重命名为item2</span><br><span class="line">mv item1 item2 item3 dir1 将多个条目移动到dir1目录下</span><br></pre></td></tr></table></figure>
<p><code>mv</code>命令选项与<code>cp</code>大致相同,<code>mv</code>没有<code>-r</code>选项</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">-i 在覆盖一个已存在的文件前,提示用户进行确认。</span><br><span class="line">-u 将文件从一个目录移动到另一个目录时,只会移动目标目录不存在的文件或是目标目录相应文件的更新文件</span><br><span class="line">-v 移动时显示信息性消息</span><br></pre></td></tr></table></figure>
<ul>
<li><code>rm</code> 删除文件或目录</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">rm -r item1 item2 item3 删除item1,item2,item3,删除目录时需要-r</span><br><span class="line">rm *.html 删除以.html结尾的文件</span><br></pre></td></tr></table></figure>
<p><code>rm</code>命令选项</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">-i 删除前提示用户确认</span><br><span class="line">-r 递归删除目录及其内容。删除目录时需要这个选项</span><br><span class="line">-f 忽略不存在的文件,并无需提示确认</span><br><span class="line">-v 删除时显示信息性消息</span><br></pre></td></tr></table></figure>
<ul>
<li><code>ln</code> 创建硬链接和符号链接</li>
</ul>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ln file hard-link-name 创建file文件的硬链接</span><br><span class="line">ln -s file sym-link-name 创建file文件的符号链接,符号链接指向源文件,与源文件内容保持一致</span><br></pre></td></tr></table></figure>
<p><code>file</code>为相对于<code>sym-link-name</code>的文件,即为相对路径,当然也可以是绝对路径</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ln -s ../file sym-link-name file在当前目录的父目录中,即file相对于sym-link-name的位置</span><br></pre></td></tr></table></figure>
<h1 id="读写文件"><a href="#读写文件" class="headerlink" title="读写文件"></a>读写文件</h1><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">echo "I am fine" 打印 I am fine</span><br><span class="line">echo "I am fine" > /root/test.txt 将 I am fine写入/root/test.txt中</span><br><span class="line">echo "I am fine" >> /root/test.txt 将 I am fine追加到/root/test.txt末尾</span><br><span class="line">grep "关键字" test.txt 在test.txt中查找含有关键字的行并打印</span><br><span class="line">grep -v "关键字" test.txt 在test.txt中查找不含有关键字的行并打印</span><br><span class="line">grep ^"关键字" test.txt 在test.txt中查找以关键字开头的行并打印</span><br><span class="line">grep $"关键字" test.txt 在test.txt中查找以关键字结尾的行并打印</span><br></pre></td></tr></table></figure>
<h2 id="管道"><a href="#管道" class="headerlink" title="管道"></a>管道</h2><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">ls -l | grep "关键字" > /root/test.txt 列出当前目录文件信息并交给grep过滤,最后写入/root/test.txt</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux 命令</tag>
</tags>
</entry>
<entry>
<title>前端性能优化—图像优化</title>
<url>/2021/05/29/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%961/</url>
<content><![CDATA[<p>网站作为一种信息传递的媒介,在如今各类的 Web 项目中,图像资源的使用占比越来越大,因此我们应当注意图像资源的使用方式。如果网站中的图像资源未进行恰当的优化,当网站访问量较大时会产生很大的带宽挑战,同时也会造成大尺寸图像请求时间过长等问题。</p>
<p>图像优化问题主要分为两个方面:图像的选取和使用和图像的加载和显示。本篇文章主要讨论图像的选取和使用。</p>
<a id="more"></a>
<h2 id="图像基础"><a href="#图像基础" class="headerlink" title="图像基础"></a>图像基础</h2><p>图像文件可分为两类:矢量图和位图,每种类型都有自己的优缺点和适用场景。</p>
<h3 id="矢量图"><a href="#矢量图" class="headerlink" title="矢量图"></a>矢量图</h3><p>矢量图中的图形元素被定义为一个对象,包括颜色、大小、形状及屏幕位置等信息。</p>
<p><strong>矢量图适合于如文本、logo、控件图标及二维码等形状简单的几何图形</strong>。</p>
<p>矢量图的优点是能够在任何缩放比例之下呈现同样清晰的展示效果。</p>
<p>矢量图的缺点是对细节的展示效果不够丰富。对于足够复杂的图像,如果要达到照片的效果,通过 SVG 绘制会使得文件大的离谱,即便如此也很难达到照片的真实效果。</p>
<h3 id="位图"><a href="#位图" class="headerlink" title="位图"></a>位图</h3><p>位图是通过对矩阵中的栅格进行编码来表示的图像,图像的栅格像素点越多,且每个像素点所能表达的颜色范围越广,则位图图像整体的显示效果就会越逼真。</p>
<p>位图的优点是能提供较为真实复杂的细节体验,但位图会受屏幕分辨率的影响。</p>
<p>常见的位图有:JPEG、GIF、PNG、WebP</p>
<h3 id="有损压缩和无损压缩"><a href="#有损压缩和无损压缩" class="headerlink" title="有损压缩和无损压缩"></a>有损压缩和无损压缩</h3><p>图像资源优化的根本思想是压缩,压缩是降低源文件大小的有效方式。图像压缩可分为有损压缩和无损压缩。</p>
<p>具体在选择压缩方式时,我们需要结合具体的业务需求考虑。如果业务上对图像的质量要求较高,则考虑使用无损压缩。</p>
<h2 id="图像格式"><a href="#图像格式" class="headerlink" title="图像格式"></a>图像格式</h2><h3 id="JPEG"><a href="#JPEG" class="headerlink" title="JPEG"></a>JPEG</h3><p>JPEG使用的是一种有损压缩算法。</p>
<p><strong>用途</strong>:用作<strong>背景图、轮播图或者一些商品的 banner 图</strong>。但是由于有损压缩,当<strong>处理 Logo 或者图标</strong>时,需要较强的线条感或者强烈的颜色对比的时候,使用 JPEG 可能<strong>会出现边界模糊的不加体验</strong>,另外JPEG<strong>不支持透明度</strong>。</p>
<p>JPEG包含多种压缩模式,其中常见的有<strong>基于基线的和渐进式</strong>的。</p>
<ul>
<li>基线模式:图像加载顺序是自上而下的,当网络较差时,图象是自上而下加载显示的</li>
<li>渐进式:将图像文件分为多次扫描,首先展示一个低质量模糊的图像,最后扫描到的图像信息不断增多,每次扫描过后所展示的图像清晰度也会不断提升</li>
</ul>
<p>优缺点:渐进式解码速度要比基线慢,另外渐进式压缩得到的图像文件也不一定是最小的。</p>
<p>在实际生活中,我们不难发现,目前渐进式的 JPEG 已经慢慢取代了基线 JPEG 了。在应用时,我们可以使用一些第三方工具来创建渐进式的图像,例如 imagemin、libjpeg、imageMagick。以下是使用 gulp 创建渐进式 JPEG 的代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> gulp = <span class="built_in">require</span>(<span class="string">"gulp"</span>)</span><br><span class="line"><span class="keyword">const</span> imagemin = <span class="built_in">require</span>(<span class="string">"gulp-imagemin"</span>)</span><br><span class="line">gulp.task(<span class="string">"images"</span>, <span class="function">() =></span> {</span><br><span class="line"> gulp.src(<span class="string">"images/*.jpg"</span>)</span><br><span class="line"> .pipe(imagemin({</span><br><span class="line"> progressive: <span class="literal">true</span></span><br><span class="line"> }))</span><br><span class="line"> .pipe(gulp.dest(<span class="string">"dist"</span>))</span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>在执行后见流程之后,gulp 会调用 imagemin 的方法把 images 文件夹下所有的 jpg 图像全部进行渐进式编码处理。</p>
<h3 id="GIF"><a href="#GIF" class="headerlink" title="GIF"></a>GIF</h3><p>gif 主要是动画图片,但是相比于视频文件,gif 在解码阶段十分耗时,所以出于对性能的考虑,我们应该尽量谨慎选用 gif。</p>
<h3 id="PNG"><a href="#PNG" class="headerlink" title="PNG"></a>PNG</h3><p>PNG 是一种无损压缩的高保真图片格式,相比于 JPEG,PNG支持透明度,对线条处理更加细腻,并增强了色彩的表现,不过缺点就是文件体积太大。</p>
<p>优化 PNG:</p>
<p>对于 PNG 图像,我们可以使用 imagemin-pngcrush 来进行优化:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> imagemin = <span class="built_in">require</span>(<span class="string">"imagemin"</span>)</span><br><span class="line"><span class="keyword">const</span> imageminPngcrudh = <span class="built_in">require</span>(<span class="string">"imagemin-pngcrush"</span>)</span><br><span class="line">imagemin([<span class="string">"images/*.png"</span>], <span class="string">"build/images"</span>, {</span><br><span class="line"> plugins: [imageminPngcrush()]</span><br><span class="line">}).then(<span class="function">()=></span><span class="built_in">console</span>.log(<span class="string">"图像优化完成"</span>))</span><br></pre></td></tr></table></figure>
<h3 id="WebP"><a href="#WebP" class="headerlink" title="WebP"></a>WebP</h3><p>前面的三种图像文件格式,在呈现位图方面各有优劣:GIF 能呈现动画;JPEG 虽然不支持透明度,但是图像文件的压缩比高;PNG 虽然文件尺寸较大,但支持透明且色彩表现力强。</p>
<p>开发者在使用位图时对于这样的现状就需要先考虑选型。假如有一个统一的图像文件格式,具有之前格式的所有优点就好了。WebP 由此产生。</p>
<p>根据 WebP 官方网站给出的实验数据,当使用 WebP 有损文件时,文件尺寸会比 JPEG 小 25%-34%,而使用 WebP 无损文件时,文件尺寸会比 PNG 小 26%。</p>
<p>但是 WebP 存在一定的兼容性问题</p>
<img src="/2021/05/29/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%961/1.png" class="" title="兼容性问题">
<p>从图中可以看出,除了 IE 浏览器不支持外,其他大部分浏览器都已经支持 WebP。</p>
<p>如何使用 WebP?</p>
<p>我们可以借助工具将原有的 jpg 或者 png 转换为 WebP 格式:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">loader: [{</span><br><span class="line"> test: <span class="regexp">/\.(jpe?g|png)$/</span>I,</span><br><span class="line"> loaders:[</span><br><span class="line"> <span class="string">"file-loader"</span>,</span><br><span class="line"> <span class="string">"webp-loader?{quality: 13}"</span></span><br><span class="line"> ]</span><br><span class="line">}]</span><br></pre></td></tr></table></figure>
<p>这里值得注意的是,尽量不要使用低质量的 JPEG 格式进行转换,建议使用高质量的 JPEG 图像进行转换。</p>
<p>兼容性处理?</p>
<p>目前 WebP 不适用于所有浏览器,因此在使用时需要做兼容性处理。</p>
<p>通常处理的思路有两种:</p>
<ul>
<li><p>一种是在前端通过 userAgent 判断浏览器版本,然后根据版本选择加载不同的图像。</p>
</li>
<li><p>另一种是可以通过 <code><picture></code> 标签来选择显示图像的格式,在 <code><picture></code> 标签中添加多个 <code><source></code> 标签元素,以及一个包含旧图像格式的 <code><img></code> 标签,当浏览器在解析 DOM 的时候,便会对 <code><picture></code> 标签中的多个图像源依次进行检测。如果都不支持,就会使用 <code><img></code> 标记兼容显示出旧的图像格式。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">picture</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">source</span> <span class="attr">srcset</span>=<span class="string">"/path/image.webp"</span> <span class="attr">type</span>=<span class="string">"image/webp"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">img</span> <span class="attr">src</span>=<span class="string">"/path/image.jpg"</span> <span class="attr">alt</span>=<span class="string">""</span>></span></span><br><span class="line"><span class="tag"></<span class="name">picture</span>></span></span><br></pre></td></tr></table></figure>
<p>tips: <code><picture></code> 标签的 <code><source></code> 标签里面还可以有 media 属性,可以根据不同的 media 来显示不同大小的图像,因此 <code><picture></code> 的常见使用场景:</p>
<ul>
<li>艺术指导(Art direction) —— 针对不同 <code>media</code> 条件裁剪或修改图像</li>
<li>遇到所有浏览器都不支持的特定格式时,提供不同的图像格式</li>
</ul>
</li>
</ul>
<h3 id="SVG"><a href="#SVG" class="headerlink" title="SVG"></a>SVG</h3><p>前面介绍的几种图像都是位图,而 SVG 是矢量图。SVG是基于 XML 语法描述图像形状的文件格式,适合用来表示 Logo 等图标图像。</p>
<h3 id="Base64"><a href="#Base64" class="headerlink" title="Base64"></a>Base64</h3><p>Base64 是一种编码方式,它通过将图像的编码直接写入 HTML 或者 CSS 中实现图像的展示。</p>
<p>使用该方式编码展示的图像,无需发送 HTTP 请求,浏览器会自动解析并展示该编码图像。由于 Base64 编码原理的特点,一般经过 Base64 编码后的图像大小会膨胀四分之三。因此,只有对于小图而言,Base64 才能发挥它真正的作用。因此在考虑使用 Base64 编码时,要考虑以下几个条件:</p>
<ul>
<li>图像文件的实际尺寸是否很小</li>
<li>图像是否真的无法以雪碧图的形式进行引入</li>
<li>图像文件的更新频率是否很低,以避免在使用 Base64 时,增加不必要的维护成本</li>
</ul>
<h2 id="格式选择建议"><a href="#格式选择建议" class="headerlink" title="格式选择建议"></a>格式选择建议</h2><ul>
<li>尽量使用矢量图,凡是用到图标的场景,应尽可能使用矢量图</li>
<li>对于位图使用的场景,首选 webp 格式</li>
<li>考虑到新技术的兼容性问题,使用 picture 标签进行适配,包含动画时,使用 GIF;需要展示细节并且需要透明度时,使用 PNG;追求更高图像压缩比时,使用 JPEG。此外对于不同缩放比的响应式场景,可以使用不同尺寸的图像,让浏览器根据实际情况进行调用。</li>
</ul>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul>
<li>适合用矢量图的地方首选矢量图</li>
<li>使用位图时首选webp,对不支持的浏览器场景进行兼容处理</li>
<li>尽量为位图图像格式找到最佳质量设置</li>
<li>删除图像文件中多余的元数据</li>
<li>对图像文件进行必要的压缩</li>
<li>为图像提供多种缩放尺寸的响应式资源</li>
<li>对工程化通用图像处理流程尽量自动化</li>
</ul>
]]></content>
<categories>
<category>前端基础</category>
</categories>
<tags>
<tag>性能优化</tag>
<tag>图像</tag>
</tags>
</entry>
<entry>
<title>V8 垃圾回收机制</title>
<url>/2021/04/02/V8-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/</url>
<content><![CDATA[<p>V8 的垃圾回收策略是基于分代式垃圾回收机制。在所有垃圾回收的算法中,没有一种能胜任所有的场景。因为在我们的实际应用中,对象的生存周期长短不一,不同的算法只能针对特定的情况具有最好的效果。</p>
<p>因此目前的垃圾回收算法一般是按照对象的存活时间将内存进行分代,然后对不同的内存代采用不同的垃圾回收算法。</p>
<a id="more"></a>
<h2 id="V8-的内存分代"><a href="#V8-的内存分代" class="headerlink" title="V8 的内存分代"></a>V8 的内存分代</h2><p>在 V8 中主要将内存分为新生代和老生代,新生代中的对象存活时间较短,老生代中的对象存活时间较长或常驻内存,新生代中的对象有机会晋升到老生代。</p>
<img src="/2021/04/02/V8-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/1.png" class="" title="新生代老生代表示">
<p>V8 堆整体的大小就是新生代的内存空间加上老生代的内存空间。在默认情况下,如果一直分配内存,在 64 位操作系统和 32 位操作系统下分别只能使用约 1.4 GB 和 0.7 GB 的大小。</p>
<p>对于新生代而言,在 64 位和 32 位操作系统下内存的最大值为 32MB 和 16MB;对于老生代而言,在 64 位和 32 位操作系统下内存的最大值为 1400MB 和 700MB</p>
<h2 id="V8-的主要垃圾回收算法"><a href="#V8-的主要垃圾回收算法" class="headerlink" title="V8 的主要垃圾回收算法"></a>V8 的主要垃圾回收算法</h2><p>根据不同的分代,V8 在新生代中使用 Scavenge 算法进行垃圾回收,而在老生代中使用 Mark-Sweep 和 Mark-Compact 进行垃圾回收。</p>
<h3 id="Scavenge-算法"><a href="#Scavenge-算法" class="headerlink" title="Scavenge 算法"></a>Scavenge 算法</h3><p>Scavenge 算法是新生代中的对象进行垃圾回收的算法,其主要采用了 Cheney 算法,算法的核心思想是:</p>
<p><strong>将堆一分为二</strong>,每一部分空间称为 semispace,<strong>然后采用复制的方式进行垃圾回收</strong>。在这两个 semispace 中,只有一个处于使用中,另一个处于闲置状态。处于使用中的空间称为 From 空间,处于闲置中的空间称为 To 空间。当我们在分配对象的时候,首先在 From 空间中进行分配。当进行垃圾回收的时候,检查 From 空间中的存活对象,将存活对象复制到 To 空间,而非存活对象的空间将会被释放。完成复制之后,From 空间变为 To 空间, To 空间变为 From 空间,即进行角色互换。</p>
<p><strong>Scavenge 算法的优点是时间效率较高,缺点是只能利用一半的内存。由于该算法只复制存活的对象,因此对于生存周期较短的场景(新生代),存活的对象较少,非常适合应用该算法进行垃圾回收。</strong></p>
<p>当一个对象在新生代中经过多次复制依然存活,它将被认为是生存周期较长的对象。这些生命周期较长的对象会被移动到老生代中,采用新的算法进行管理。<strong>对象从新生代移动到老生代称为晋升</strong>。</p>
<p>因此我们在将 From 空间的对象移动到 To 空间之前需要进行检查,在一定条件下需要将存活周期较长的对象移动到老生代中,也就是完成对象晋升。</p>
<p>对象晋升的主要条件有两个:</p>
<ul>
<li><p>对象是否经历过 Scavenge 回收</p>
<p>在默认情况下, V8 的对象分配主要集中在 From 空间,对象从 From 复制到 To 空间的时候,会检查它的内存地址来判断该对象是否经历过一次 Scavenge 回收。如果经历过,会将该对象复制到老生代空间中;否则复制到 To 空间。</p>
</li>
<li><p>To 空间的内存占比超过 25%</p>
<p>当要从 From 空间复制一个对象到 To 空间的时候,如果 To 空间已经使用了超过 25%,则这个对象直接晋升到老生代空间中。</p>
</li>
</ul>
<h3 id="Mark-Sweep-和-Mark-Compact"><a href="#Mark-Sweep-和-Mark-Compact" class="headerlink" title="Mark-Sweep 和 Mark-Compact"></a>Mark-Sweep 和 Mark-Compact</h3><p>对于老生代中的对象,由于存活对象占比较大,再采用 Scavenge 算法会造成两个问题:</p>
<ul>
<li>存活对象较多,复制存活对象的效率将会很低</li>
<li>浪费一半的空间</li>
</ul>
<p>因此 V8 在老生代中主要采用 Mark-Sweep 和 Mark-Compact 相结合的方法进行垃圾回收。</p>
<p>Mark-Sweep 实际上就是标记清除的意思,它分为标记和清除两个阶段。该算法会遍历堆中的所有对象,并标记存活的对象,在随后的清除过程中,清除未被标记的对象。可以看出 Scavenge 中只复制活着的对象,而 Mark-Sweep 中只清理死亡的对象。活对象在新生代中占较少一部分,死亡对象在老生代中占较少一部分,这是两种回收方式能高效处理的原因。</p>
<p>如图所示,黑色部分标记为死亡的对象。</p>
<img src="/2021/04/02/V8-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/2.png" class="" title="标记清除">
<p><strong>Mark-Sweep 算法最大的问题是,在进行一次垃圾回收之后,内存空间会出现不连续的状态</strong>。这种内存碎片会对后续的内存分配造成问题。例如我们要给一个大对象分配内存的时候,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收机制,而这次回收是没有必要的。</p>
<p>因此,为了 Mark-Sweep 解决内存碎片的问题,Mark-Compact 算法被提出来了。Mark-Compact 是标记整理的意思,是在 Mark-Sweep 的基础上演变而来的。<strong>Mark-Compact 在标记对象为死亡之后,在整理的过程中,将活的对象往一端移动,移动完成之后直接清理掉边界外的内存</strong>。</p>
<p>由于 Mark-Compact 需要移动对象,因此它的执行效率不可能很快,所以在取舍上, <strong>V8 主要采用 Mark-Sweep 算法,在空间不足以给从新生代中晋升过来的对象分配空间的时候才使用 Mark-Compact</strong>。</p>
<h3 id="Incremental-Marking"><a href="#Incremental-Marking" class="headerlink" title="Incremental Marking"></a>Incremental Marking</h3><p>为了避免出现 JS 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的三种基本算法都需要将应用逻辑暂停下来,待执行完回收之后再恢复应用程序的执行,这被称为”全停顿“。</p>
<p>在 V8 的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置较小,且其中存活的对象较少,所以即使它是全停顿也影响不大。但是在老生代中,空间配置较大,存活对象较多,全堆垃圾回收的标记、清理、整理等操作所造成的停顿就会较大,需要设法改善。</p>
<p>为了降低全堆垃圾回收带来的停顿时间,V8 采用了增量标记,也就是将原本需要一口气完成标记的过程拆分为许多小步进行,每做完一小步就让 JS 应用逻辑执行一小会,标记与应用程序交替执行直到标记完成。</p>
]]></content>
<categories>
<category>JavaScript 基础</category>
</categories>
<tags>
<tag>JS</tag>
<tag>垃圾回收</tag>
</tags>
</entry>
<entry>
<title>JS继承的实现方式及比较</title>
<url>/2021/03/01/JS%E7%BB%A7%E6%89%BF%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%8F%8A%E6%AF%94%E8%BE%83/</url>
<content><![CDATA[<p>继承是面向对象语言中的重要概念,许多面向对象的语言都支持类的继承。本文介绍几种 JavaScript 中常用的继承实现方法以及各自的特点。</p>
<a id="more"></a>
<h2 id="1-简单的原型继承"><a href="#1-简单的原型继承" class="headerlink" title="1. 简单的原型继承"></a>1. 简单的原型继承</h2><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.name = <span class="string">"super"</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params"></span>) </span>{}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 利用原型链实现继承</span></span><br><span class="line">SubType.prototype = <span class="keyword">new</span> SuperType()</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance1 = <span class="keyword">new</span> SubType()</span><br><span class="line"><span class="built_in">console</span>.log(instance1.name) <span class="comment">// super</span></span><br></pre></td></tr></table></figure>
<p>简单的原型继承存在以下两个问题:</p>
<ul>
<li><p>包含引用类型值的原型属性会被所有实例共享,在通过原型来实现继承时,原型实际上也会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性。思考一下代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.names = [<span class="string">"sillywa"</span>, <span class="string">"xinda"</span>]</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params"></span>) </span>{}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 利用原型链实现继承</span></span><br><span class="line">SubType.prototype = <span class="keyword">new</span> SuperType()</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance1 = <span class="keyword">new</span> SubType()</span><br><span class="line">instance1.names.push(<span class="string">"hahah"</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance1.names) <span class="comment">// ["sillywa", "xinda", "hahah"]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance2 = <span class="keyword">new</span> SubType()</span><br><span class="line"><span class="built_in">console</span>.log(instance2.names) <span class="comment">// ["sillywa", "xinda", "hahah"]</span></span><br></pre></td></tr></table></figure>
<p>这个例子中,SuperType构造函数定义了一个 names 属性,该属性为一个数组(引用类型)。SuperType的每个实例都会有自己的 names 属性。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此它也拥有自己的 names 属性——就跟专门创建了一个 SubType.prototype.names 属性一样。但是结果就是 SubType 的所有实例共享一个 names 属性。</p>
</li>
<li><p>简单的原型继承的另一个问题是:在创建子类类型的实例时,不能向超类类型的构造函数中传递参数。</p>
</li>
</ul>
<p>因此在继承上我们经常不会单独使用原型继承。</p>
<h2 id="2-借用构造函数继承(经典继承)"><a href="#2-借用构造函数继承(经典继承)" class="headerlink" title="2. 借用构造函数继承(经典继承)"></a>2. 借用构造函数继承(经典继承)</h2><p>这种继承的思想是在子类的构造函数内部调用超类的构造函数,该方法使用 call() 和 apply() 方法在新创建的对象上执行构造函数。如下所示:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params">age, name</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.colors = [<span class="string">"blue"</span>, <span class="string">"red"</span>]</span><br><span class="line"> <span class="built_in">this</span>.age = age</span><br><span class="line"> <span class="built_in">this</span>.name = name</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params"></span>) </span>{</span><br><span class="line"> SuperType.call(<span class="built_in">this</span>, ...arguments)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance1 = <span class="keyword">new</span> SubType(<span class="number">23</span>, <span class="string">"sillywa"</span>)</span><br><span class="line">instance1.colors.push(<span class="string">"yellow"</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance1.colors, instance1.name)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance2 = <span class="keyword">new</span> SubType(<span class="number">12</span>, <span class="string">"xinda"</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance2.colors, instance2.name)</span><br></pre></td></tr></table></figure>
<p>借用构造函数继承也有一些缺点,比如方法都只能在构造函数中定义,没有办法实现方法的复用。例如:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params">name</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.name = name</span><br><span class="line"> <span class="built_in">this</span>.sayName = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.name</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params">name, age</span>) </span>{</span><br><span class="line"> SuperType.call(<span class="built_in">this</span>, name)</span><br><span class="line"> <span class="built_in">this</span>.age = age</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 每次实例化一个对象,都会重新实例化 sayName 方法</span></span><br><span class="line"><span class="keyword">var</span> instance1 = <span class="keyword">new</span> SubType(<span class="string">"sillywa"</span>, <span class="number">24</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance1)</span><br><span class="line"><span class="built_in">console</span>.log(instance1.sayName())</span><br></pre></td></tr></table></figure>
<h2 id="3-组合式继承"><a href="#3-组合式继承" class="headerlink" title="3. 组合式继承"></a>3. 组合式继承</h2><p>组合继承结合了原型继承和借用构造函数继承的优点,其背后的思想是,使用原型链实现对原型方法的继承,使用构造函数实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的服用,又通过构造函数实现了每个实例都有自己的属性。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params">name</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.name = name</span><br><span class="line"> <span class="built_in">this</span>.colors = [<span class="string">"red"</span>, <span class="string">"yellow"</span>]</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 方法写在原型上</span></span><br><span class="line">SuperType.prototype.sayName = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.name</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params">name, age</span>) </span>{</span><br><span class="line"> <span class="comment">// 通过 构造函数继承属性</span></span><br><span class="line"> SuperType.call(<span class="built_in">this</span>, name)</span><br><span class="line"> <span class="built_in">this</span>.age = age</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 通过原型继承方法</span></span><br><span class="line">SubType.prototype = <span class="keyword">new</span> SuperType()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 重写了 SubType 的 prototype 属性,因此其 constructor 也被重写了,需要手动修正</span></span><br><span class="line">SubType.prototype.constructor = SubType</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义子类自己的方法</span></span><br><span class="line">SubType.prototype.sayAge = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.age</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>测试案例:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> instance1 = <span class="keyword">new</span> SubType(<span class="string">"sillywa"</span>, <span class="number">23</span>)</span><br><span class="line">instance1.colors.push(<span class="string">"blue"</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance1.colors) <span class="comment">//["red", "yellow", "blue"]</span></span><br><span class="line"><span class="built_in">console</span>.log(instance1.sayName()) <span class="comment">// sillywa</span></span><br><span class="line"><span class="built_in">console</span>.log(instance1.sayAge()) <span class="comment">// 23</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> instance2 = <span class="keyword">new</span> SubType(<span class="string">"xinda"</span>, <span class="number">90</span>)</span><br><span class="line"><span class="built_in">console</span>.log(instance2.colors) <span class="comment">// ["red", "yellow"]</span></span><br><span class="line"><span class="built_in">console</span>.log(instance2.sayName()) <span class="comment">// xinda</span></span><br><span class="line"><span class="built_in">console</span>.log(instance2.sayAge()) <span class="comment">// 90</span></span><br></pre></td></tr></table></figure>
<p>组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。</p>
<h2 id="4-原型式继承"><a href="#4-原型式继承" class="headerlink" title="4. 原型式继承"></a>4. 原型式继承</h2><p>借助原型可以通过已有的对象创建新对象,同时还不必因此创建自定义类型。为达到这个目的,可以定义如下函数:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">create</span>(<span class="params">o</span>) </span>{</span><br><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">F</span>(<span class="params"></span>)</span>{}</span><br><span class="line"> F.prototype = o</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> F()</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在 object 函数内部,首先创建了一个临时性构造函数 F,将 F 的 prototype 属性指向传入的对象 o,并返回 F 的一个实例,则该实例继承 o 的所有属性和方法。从本质上讲,create() 对传入的对象执行了一次浅复制。看以下代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> person = {</span><br><span class="line"> name: <span class="string">"sillywa"</span>,</span><br><span class="line"> firends: [<span class="string">"Johe"</span>]</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> person1 = create(person)</span><br><span class="line">person1.name = <span class="string">"coder"</span></span><br><span class="line">person1.firends.push(<span class="string">"Kobe"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> person2 = create(person)</span><br><span class="line">person2.firends.push(<span class="string">"Cury"</span>)</span><br><span class="line"><span class="built_in">console</span>.log(person2.firends) <span class="comment">// ["Johe", "Kobe", "Cury"]</span></span><br></pre></td></tr></table></figure>
<p>ES5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与 create() 方法的行为相同。</p>
<p>Object.create() 方法的第二个参数与 Object.defineProterties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> person = {</span><br><span class="line"> name: <span class="string">"sillywa"</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> person1 = <span class="built_in">Object</span>.create(person, {</span><br><span class="line"> name: {</span><br><span class="line"> value: <span class="string">"John"</span></span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"><span class="built_in">console</span>.log(person1.name) <span class="comment">// John</span></span><br></pre></td></tr></table></figure>
<h2 id="5-寄生式继承"><a href="#5-寄生式继承" class="headerlink" title="5. 寄生式继承"></a>5. 寄生式继承</h2><p>寄生式继承的思路与继承构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正地是它做了所有工作一样返回对象。以下是寄生式继承的代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createAnother</span>(<span class="params">original</span>) </span>{</span><br><span class="line"> <span class="keyword">var</span> clone = <span class="built_in">Object</span>.create(original)</span><br><span class="line"> clone.sayHi = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"Hi"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> clone</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="6-组合寄生式继承"><a href="#6-组合寄生式继承" class="headerlink" title="6. 组合寄生式继承"></a>6. 组合寄生式继承</h2><p>前面说过,组合继承是 JavaScript 最常用的继承模式,不过它也有自己的缺点,组合继承最大的问题是,无论什么情况下都会调用两次超类的构造函数。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params">name</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.name = name</span><br><span class="line"> <span class="built_in">this</span>.colors = []</span><br><span class="line">}</span><br><span class="line">SuperType.prototype.sayName = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.name</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params">name, age</span>) </span>{</span><br><span class="line"> <span class="comment">// 第一次调用父类的构造函数</span></span><br><span class="line"> SuperType.call(<span class="built_in">this</span>,name)</span><br><span class="line"> <span class="built_in">this</span>.age = age</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 第二次调用父类的构造函数</span></span><br><span class="line">SubType.prototype = <span class="keyword">new</span> SuperType()</span><br><span class="line">SubType.prototype.constructor = SubType</span><br><span class="line">SubType.prototype.sayAge = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.age</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>组合寄生式继承就是为了解决这一问题,将第二次调用构造函数改为使用 Object.create() 函数来实现:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SuperType</span>(<span class="params">name</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.name = name</span><br><span class="line"> <span class="built_in">this</span>.colors = []</span><br><span class="line">}</span><br><span class="line">SuperType.prototype.sayName = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.name</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">SubType</span>(<span class="params">name, age</span>) </span>{</span><br><span class="line"> <span class="comment">// 第一次调用父类的构造函数</span></span><br><span class="line"> SuperType.call(<span class="built_in">this</span>,name)</span><br><span class="line"> <span class="built_in">this</span>.age = age</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 关键代码</span></span><br><span class="line">SubType.prototype = <span class="built_in">Object</span>.create(SuperType.prototype)</span><br><span class="line">SubType.prototype.constructor = SubType</span><br><span class="line">SubType.prototype.sayAge = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.age</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>JavaScript 基础</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title>防抖和节流</title>
<url>/2021/02/28/%E9%98%B2%E6%8A%96%E5%92%8C%E8%8A%82%E6%B5%81/</url>
<content><![CDATA[<p>前面在实现 Vue 里面的滚动加载时,我们监听页面的滚动事件,然后不断获取元素距离页面顶部的距离。</p>
<a id="more"></a>
<p>代码如下:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">window</span>.onscroll = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> el = <span class="built_in">document</span>.querySelector(<span class="string">".el"</span>)</span><br><span class="line"> <span class="keyword">const</span> viewPortHeight = <span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight || <span class="built_in">document</span>.body.clientHeight </span><br><span class="line"> <span class="keyword">const</span> top = el.getBoundingClientRect() && el.getBoundingClientRect().top</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'top'</span>, top)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>但是在执行过程中,当我们轻轻滚动一下,浏览器会打印出很多 top 的值,说明函数执行的频率相当高。</p>
<p>但是我们不希望页面的滚动事件频繁的执行,毕竟浏览器的性能是有限的,所以对于这种情况就需要我们进行优化。</p>
<h2 id="防抖(debounce)"><a href="#防抖(debounce)" class="headerlink" title="防抖(debounce)"></a>防抖(debounce)</h2><p>基于以上需求,首先提出一种思路:<strong>在第一次触发事件的时候,不立即执行函数,而是等待一个时间期限 delay</strong>然后:</p>
<ul>
<li>如果在这个时间 delay 内没有再次触发该事件,那么久执行函数</li>
<li>如果在这个事件 delay 内再次触发该事件,那么当前的计时器取消,重新开始计时</li>
</ul>
<p>实现效果:短时间内大量触发同一事件,只执行一次函数</p>
<p>实现:根据以上分析,我们肯定需要使用 setTimeout 这个函数,同时还需要保存计时器,以方便后续取消计时。那么函数的实现方法如下:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">fn 为需要防抖的函数</span></span><br><span class="line"><span class="comment">delay 为时间期限</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">debounce</span>(<span class="params">fn, delay</span>)</span>{</span><br><span class="line"> <span class="comment">// 初始化计时器</span></span><br><span class="line"> <span class="keyword">let</span> timer = <span class="literal">null</span></span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>{</span><br><span class="line"> <span class="keyword">if</span>(timer) {</span><br><span class="line"> <span class="comment">// 如果正在进行一个计时过程,说明在 delay 事件内重复触发该事件,所以取消当前的计时</span></span><br><span class="line"> <span class="built_in">clearTimeout</span>(timer)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 新建一个计时器</span></span><br><span class="line"> timer = <span class="built_in">setTimeout</span>(fn, delay)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>然后可以配合之前的代码进行使用:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">window</span>.onscroll = debounce(showTop, <span class="number">1000</span>)</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">showTop</span>(<span class="params">el</span>)</span>{</span><br><span class="line"> <span class="keyword">const</span> viewPortHeight = <span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight || <span class="built_in">document</span>.body.clientHeight </span><br><span class="line"> <span class="keyword">const</span> top = el.getBoundingClientRect() && el.getBoundingClientRect().top</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'top'</span>, top)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>此时运行代码并滚动,会发现必须停止滚动 1000ms以后,才会打印出 top 的值。</p>
<p>这样我们就是先了防抖函数,现在给出防抖的定义:</p>
<blockquote>
<p>短时间内连续触发相同的事件,防抖就是让在某个时间期限内事件处理函数只执行一次</p>
</blockquote>
<h2 id="节流(throttle)"><a href="#节流(throttle)" class="headerlink" title="节流(throttle)"></a>节流(throttle)</h2><p>思考上面方案就可以发现一个问题:<strong>在限定时间内不断触发事件,只要不停止触发,理论上就永远不会执行函数输出结果。</strong></p>
<p>但是如果我们希望:<strong>即使用户不断触发事件,也能在某个时间间隔之后给出反馈呢</strong>。</p>
<p>其实这就类似于<strong>定时开放的函数,也就是让函数执行一次后,在某个时间段内暂时失去效果,即使触发事件,函数也不会执行,过了这段时间再重新激活(类似于技能冷却)。</strong></p>
<p>实现效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。</p>
<p>实现:这里可以借助 setTimeout 来实现,并加上一个状态位 valid 表示函数是否可执行。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">throttle</span>(<span class="params">fn ,delay</span>) </span>{</span><br><span class="line"> <span class="comment">// 初始化 valid = true 表示函数可执行</span></span><br><span class="line"> <span class="keyword">let</span> valid = <span class="literal">true</span></span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">if</span>(!valid) {</span><br><span class="line"> <span class="comment">// 如果函数不可执行,直接 return</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果函数可执行, 放入任务队列等待执行</span></span><br><span class="line"> <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> fn()</span><br><span class="line"> <span class="comment">// 函数执行完成,valid 设为 true 表示函数可执行</span></span><br><span class="line"> valid = <span class="literal">true</span></span><br><span class="line"> }, delay)</span><br><span class="line"> <span class="comment">// valid 设为 false,表示正在等待执行该函数,函数暂时不可用</span></span><br><span class="line"> valid = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>需要注意的是,节流函数并不止上面这种方案,也可以直接将 setTimeout 返回的标记当作条件判断当前定时器是否存在,如果存在表示还在冷却,并且在执行 fn 之后消除定时器表示激活。</p>
<h2 id="应用场景举例"><a href="#应用场景举例" class="headerlink" title="应用场景举例"></a>应用场景举例</h2><ol>
<li>搜索框 input 事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值,就当作用户输入完成,然后开始搜索</li>
<li>页面 resize 事件,常用于需要做页面适配的时候。需要根据最终呈现的页面情况进行 DOM 渲染,这种情况一般用防抖,因为只需要判断最后一次的变化情况</li>
<li>页面滚动实现懒加载,可以使用防抖,因此在页面滚动过程中最终都会无法滚动,从而执行函数。</li>
</ol>
]]></content>
<categories>
<category>JavaScript 基础</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title>Vue中实现滚动加载</title>
<url>/2021/02/28/Vue%E4%B8%AD%E5%AE%9E%E7%8E%B0%E6%BB%9A%E5%8A%A8%E5%8A%A0%E8%BD%BD/</url>
<content><![CDATA[<p>最近在做一个 vue 商城的项目,项目中要求首先加载第一页商城的商品列表,当用户滚动查看商品时,在快到达列表底部的时候提前加载第一页商品列表,即诸如淘宝、天猫、京东商城的滚动懒加载。</p>
<a id="more"></a>
<p>分析需求:关键是如何判断在滚动的时候到达列表底部。我们可以在列表底部放一个 div ,判断该 div 出现在可视区域中的时候,即滚动到列表底部。那么如何判断一个元素是否在可视区域中出现呢?</p>
<h2 id="判断元素是否出现在可视区域"><a href="#判断元素是否出现在可视区域" class="headerlink" title="判断元素是否出现在可视区域"></a>判断元素是否出现在可视区域</h2><p>首先我们来总结一下几个关键的概念</p>
<h3 id="偏移量"><a href="#偏移量" class="headerlink" title="偏移量"></a>偏移量</h3><p>偏移量(offset dimension),元素的可见大小由其高度、宽度决定,包括所有的内边距、滚动条和边框大小,<strong>不包含外边距</strong>。以下是获取元素偏移量的方法:</p>
<ul>
<li><p>offsetHeight = content + padding + border + scrollX</p>
<p>元素在垂直方向上占用的空间大小,以像素计算。包含元素的高度、边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含:before或:after等伪类元素的高度。</p>
<p>如果元素被隐藏(例如 元素或者元素的祖先之一的元素的style.display被设置为none),则返回0</p>
<p>这个属性会被四舍五入为整数,如果需要一个浮点数值,请使用 <code>element.getBoundingClientRect()</code></p>
</li>
<li><p>offsetWidth = content + padding + border + scrollY</p>
<p>元素在水平方向上占用的空间大小,以像素计算。包含元素的宽度、边框、内边距和元素的竖直滚动条(如果存在且渲染的话),不包含:before或:after等伪类元素的高度。</p>
</li>
<li><p>offsetLeft</p>
<p>元素的左外边框至包含元素的左内边框之间的像素值。</p>
</li>
<li><p>offsetTop</p>
<p>元素的上外边框至包含元素的上内边框之间的像素距离。</p>
</li>
</ul>
<p>如下图所示:</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/11/7/166ec314e1bda0b5?w=532&h=307&f=png&s=27678" alt="偏移量图示"></p>
<h3 id="客户区域大小"><a href="#客户区域大小" class="headerlink" title="客户区域大小"></a>客户区域大小</h3><p>客户区域大小有以下两个属性:</p>
<ul>
<li><p>clientWidth = content + padding</p>
<p>clientWidth是元素内容区域宽度加上左右内边距宽度</p>
</li>
<li><p>clientHeight = content + padding</p>
<p>clientWidth是元素内容区域高度加上左右内边距高度</p>
</li>
</ul>
<p>可以通过如下方法来确定浏览器视口大小:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> viewPort = {</span><br><span class="line"> width: <span class="built_in">document</span>.body.clientWidth || <span class="built_in">document</span>.documentElement.clientWidth,</span><br><span class="line"> height: <span class="built_in">document</span>.body.clientHeight || <span class="built_in">document</span>.documentElement.clientHeight</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>客户区大小不包括滚动条、边框、外边距。</p>
<h3 id="滚动大小"><a href="#滚动大小" class="headerlink" title="滚动大小"></a>滚动大小</h3><ul>
<li>scrollHeight:在没有滚动条的情况下,元素内容的总高度,即 clientHeight</li>
<li>scrollWidth</li>
<li>scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。</li>
<li>scrollTop</li>
</ul>
<p>scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。</p>
<p>scrollLeft 和 scrollTop属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位 置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值 会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会 大于 0,且表示元素左侧不可见内容的像素宽度。这两个属性都是可以设置的,因此将元素的 scrollLeft 和 scrollTop 设置为 0,就可以重置元素的滚动位置。</p>
<h3 id="确定元素大小"><a href="#确定元素大小" class="headerlink" title="确定元素大小"></a>确定元素大小</h3><p>getBoundingClientRect:一般来 说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight 相等。</p>
<h3 id="实现判断元素在可视区内"><a href="#实现判断元素在可视区内" class="headerlink" title="实现判断元素在可视区内"></a>实现判断元素在可视区内</h3><ol>
<li><p>方法一</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">isInViewPort</span>(<span class="params">el</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> viewPortHeight = <span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.body.clientHeight || <span class="built_in">document</span>.documentElement.clientHeight</span><br><span class="line"> <span class="keyword">const</span> elOffsetTop = el.offsetTop</span><br><span class="line"> <span class="keyword">const</span> dScrollTop = <span class="built_in">document</span>.documentElement.scrollTop</span><br><span class="line"> <span class="keyword">const</span> top = elOffsetTop - dScrollTop</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> top <= viewPortHeight</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
</li>
</ol>
<ol start="2">
<li><p>方法二</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">isInViewPort</span> (<span class="params">el</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> viewPortHeight = <span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight || <span class="built_in">document</span>.body.clientHeight </span><br><span class="line"> <span class="keyword">const</span> top = el.getBoundingClientRect() && el.getBoundingClientRect().top</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'top'</span>, top)</span><br><span class="line"> <span class="keyword">return</span> top <= viewPortHeight</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
</li>
</ol>
<h2 id="Vue-滚动加载的实现"><a href="#Vue-滚动加载的实现" class="headerlink" title="Vue 滚动加载的实现"></a>Vue 滚动加载的实现</h2><p>设计一个 FooterLine 组件,判断该组件是否将要出现在视图中,如将要出现则进行加载。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><template></span><br><span class="line"> <div class="footer"></span><br><span class="line"> <p>加载中...</p></span><br><span class="line"> </div></span><br><span class="line"></template></span><br><span class="line"></span><br><span class="line"><script></span><br><span class="line"> export default {</span><br><span class="line"> name: "FooterLine", </span><br><span class="line"> }</span><br><span class="line"></script></span><br><span class="line"></span><br><span class="line"><style scoped></span><br><span class="line">.footer {</span><br><span class="line"> font-size: 12px;</span><br><span class="line"> position: relative;</span><br><span class="line"> text-align: center;</span><br><span class="line"> margin:12px;</span><br><span class="line">}</span><br><span class="line">.footer p::after, .footer p::before {</span><br><span class="line"> content:"";</span><br><span class="line"> position: absolute;</span><br><span class="line"> width:40%;</span><br><span class="line"> height: 1px;</span><br><span class="line"> background: #bab6b6;</span><br><span class="line"> top: 6px;</span><br><span class="line"> right: 0;</span><br><span class="line">}</span><br><span class="line"> .footer p::before {</span><br><span class="line"> left: 0;</span><br><span class="line"> }</span><br><span class="line"></style></span><br></pre></td></tr></table></figure>
<p>接下来监听页面的滚动事件,当 isInViewPort 函数返回 true 时,表明组件即将进入页面,触发函数加载数据:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">mounted() {</span><br><span class="line"> <span class="keyword">const</span> line = <span class="built_in">document</span>.querySelector(<span class="string">".footer"</span>)</span><br><span class="line"> <span class="built_in">window</span>.onscroll = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span>(isInViewPort(line)) {</span><br><span class="line"> <span class="built_in">this</span>.$emit(<span class="string">"arrive-bottom"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">},</span><br></pre></td></tr></table></figure>
<p>这样实现之后我们发现,当页面不断滚动的时候,控制台会不断打印出 top 值,并且当 top <= viewPortHeight 时会不断发送 http 请求数据,显然这对页面的性能影响很严重,并且也可能导致不断发送重复请求。经过思考,我们可以使用 vue 提供的 watch 来解决该问题。具体思路是:</p>
<ul>
<li>首先定义一个数据 IsEmit= false,使用 watch 监听该数据的更改</li>
<li>监听页面的滚动事件,当滚动到接近底部的时候,将 IsEmit 修改为 true</li>
<li>watch 监听到 IsEmit 的修改,并且 IsEmit 修改为 true 时,触发函数加载数据。这样即使继续滚动也不会不断发送重复请求</li>
</ul>
<p>具体代码实现如下:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> {</span><br><span class="line"> name: <span class="string">"FooterLine"</span>,</span><br><span class="line"> data() {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> IsEmit: <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> watch: {</span><br><span class="line"> IsEmit(newValue) {</span><br><span class="line"> <span class="keyword">if</span>(newValue) {</span><br><span class="line"> <span class="built_in">this</span>.$emit(<span class="string">"arrive-bottom"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> mounted() {</span><br><span class="line"> <span class="keyword">const</span> line = <span class="built_in">document</span>.querySelector(<span class="string">".footer"</span>)</span><br><span class="line"> <span class="built_in">window</span>.onscroll = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span>(<span class="built_in">this</span>.isInViewPort(line)) {</span><br><span class="line"> <span class="built_in">this</span>.IsEmit = <span class="literal">true</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">this</span>.IsEmit = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> methods: {</span><br><span class="line"> isInViewPort (el) {</span><br><span class="line"> <span class="keyword">const</span> viewPortHeight = <span class="built_in">window</span>.innerHeight || <span class="built_in">document</span>.documentElement.clientHeight || <span class="built_in">document</span>.body.clientHeight </span><br><span class="line"> <span class="keyword">const</span> top = el.getBoundingClientRect() && el.getBoundingClientRect().top</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'top'</span>, top)</span><br><span class="line"> <span class="keyword">return</span> top <= viewPortHeight</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上述方法利用 vue 的特性结局了不会重复发送 http 请求的问题,但是依然没有解决控制台不断打印 top 值的问题,即<strong>在页面滚动过程中,滚动事件执行的过于频繁,但是我们并不希望这么频繁的执行</strong>。</p>
<p>基于以上问题,我们可以利用防抖函数和节流函数来解决。</p>
]]></content>
<categories>
<category>前端基础</category>
</categories>
<tags>
<tag>vue</tag>
</tags>
</entry>
<entry>
<title>this全面解析</title>
<url>/2020/09/30/this%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90/</url>
<content><![CDATA[<p>this 是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。与<a href="/2018/12/07/%E8%AF%8D%E6%B3%95%E4%BD%9C%E7%94%A8%E5%9F%9F/" title="词法作用域">词法作用域</a>不同,this 是在运行时进行绑定的,并不是在编写时,它的上下文取决于函数调用的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。</p>
<a id="more"></a>
<h2 id="0-关于-this"><a href="#0-关于-this" class="headerlink" title="0.关于 this"></a>0.关于 this</h2><p>关于 this 主要有两种误解,一种是认为 this 指向函数自身,另一种是 this 指向函数的作用域。</p>
<h3 id="0-1-指向自身"><a href="#0-1-指向自身" class="headerlink" title="0.1 指向自身"></a>0.1 指向自身</h3><p>思考以下代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">num</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"foo: "</span> + num);</span><br><span class="line"> <span class="comment">// 记录 foo 被调用的次数</span></span><br><span class="line"> <span class="built_in">this</span>.count++;</span><br><span class="line">}</span><br><span class="line">foo.count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">0</span>; i < <span class="number">5</span>; i++) {</span><br><span class="line"> foo(i);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// foo被调用了多少次?</span></span><br><span class="line"><span class="built_in">console</span>.log(foo.count); <span class="comment">// 0 -- 为什么?</span></span><br></pre></td></tr></table></figure>
<p>执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,跟对象却并不相同。</p>
<p>实际上,如果深入探索的话,就会发现这段代码在无意中创建了一个全局变量 count,它的值为 NaN。</p>
<p>如果要让上面的代码实现我们的功能,我们可以用 foo 来代替 this 来引用函数对象:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">num</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"foo: "</span> + num);</span><br><span class="line"> <span class="comment">// 记录 foo 被调用的次数</span></span><br><span class="line"> foo.count++;</span><br><span class="line">}</span><br><span class="line">foo.count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">0</span>; i < <span class="number">5</span>; i++) {</span><br><span class="line"> foo(i);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(foo.count);</span><br></pre></td></tr></table></figure>
<p>另一种方法是强制 this 指向 foo 函数对象:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">num</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"foo: "</span> + num);</span><br><span class="line"> <span class="comment">// 记录 foo 被调用的次数</span></span><br><span class="line"> <span class="built_in">this</span>.count++;</span><br><span class="line">}</span><br><span class="line">foo.count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">0</span>; i < <span class="number">5</span>; i++) {</span><br><span class="line"> foo.call(foo, i);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(foo.count); <span class="comment">// 5</span></span><br></pre></td></tr></table></figure>
<h3 id="0-2-它的作用域"><a href="#0-2-它的作用域" class="headerlink" title="0.2 它的作用域"></a>0.2 它的作用域</h3><p>第二种常见的误解是,this指向函数的作用域。需要明确的是,this在任何情况下都不指向函数的词法作用域。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">var</span> a = <span class="number">2</span>;</span><br><span class="line"> <span class="built_in">this</span>.bar();</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">bar</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line">foo(); <span class="comment">//ReferenceError: a in not defined</span></span><br></pre></td></tr></table></figure>
<p><strong>因此在学习 this 之前,我们必须明白,this 既不指向函数自身也不指向函数的词法作用域,this 实际上是在函数被调用时发生绑定的。</strong></p>
<h2 id="1-调用位置"><a href="#1-调用位置" class="headerlink" title="1.调用位置"></a>1.调用位置</h2><p>在理解this的绑定规则之前,首先要理解调用位置,即函数在代码中被调用的位置。最重要的是要分析调用栈,我们关心的调用位置就是当前正在执行的函数的前一个调用中。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">baz</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 当前调用栈是: baz</span></span><br><span class="line"> <span class="comment">// 因此调用位置是全局作用域</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"baz"</span>);</span><br><span class="line"> bar(); <span class="comment">// <-- bar的调用位置</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">bar</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 当前调用栈是:baz->bar</span></span><br><span class="line"> <span class="comment">// 因此调用位置在 baz 中</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"bar"</span>);</span><br><span class="line"> foo(); <span class="comment">// <-- foo的调用位置</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 当前调用栈是:baz->bar->foo</span></span><br><span class="line"> <span class="comment">// 因此调用位置在 bar 中</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"foo"</span>);</span><br><span class="line">}</span><br><span class="line">baz(); <span class="comment">// <-- baz的调用位置</span></span><br></pre></td></tr></table></figure>
<p>注意我们是如何分析出真正的调用位置的,因为它决定了 this 的绑定。</p>
<h2 id="2-绑定规则"><a href="#2-绑定规则" class="headerlink" title="2.绑定规则"></a>2.绑定规则</h2><p>我们首先需要找到调用位置,然后判断需要应用下面四条规则中的哪一条。首先会介绍四条规则,然后说明多条规则都可以使用时的优先级。</p>
<h3 id="2-1-默认绑定"><a href="#2-1-默认绑定" class="headerlink" title="2.1 默认绑定"></a>2.1 默认绑定</h3><p>默认绑定就是简单的独立函数调用,可以把这条规则看作是无法应用其它规则时的默认规则。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> a = <span class="number">2</span>;</span><br><span class="line">foo(); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>在代码中,foo是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。在非严格默认下,默认绑定的 this 指向全局对象,严格模式下为 undefined。</p>
<h3 id="2-2-隐式绑定"><a href="#2-2-隐式绑定" class="headerlink" title="2.2 隐式绑定"></a>2.2 隐式绑定</h3><p>另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span>,</span><br><span class="line"> foo: foo</span><br><span class="line">};</span><br><span class="line">obj.foo(); <span class="comment">//2</span></span><br></pre></td></tr></table></figure>
<p>当 foo 被调用时,它前面加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定的规则会把函数调用中的 this 绑定到这个上下文对象。</p>
<p>对象属性链中只有上一层或者说最后一层在调用位置中起作用。举例来说:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj2 = {</span><br><span class="line"> a: <span class="number">42</span>,</span><br><span class="line"> foo: foo</span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> obj1 = {</span><br><span class="line"> a: <span class="number">2</span>,</span><br><span class="line"> obj2: obj2</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">obj1.obj2.foo(); <span class="comment">// 42</span></span><br></pre></td></tr></table></figure>
<p><strong><em>隐式丢失</em></strong></p>
<p>一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span>,</span><br><span class="line"> foo: foo</span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = obj.foo; <span class="comment">// 函数别名!</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> a = <span class="string">"oops,global"</span>;</span><br><span class="line"></span><br><span class="line">bar(); <span class="comment">// "oops,global"</span></span><br></pre></td></tr></table></figure>
<p>虽然 bar 是 obj.foo 的一个引用,但是实际上它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。</p>
<p>一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">doFoo</span>(<span class="params">fn</span>) </span>{</span><br><span class="line"> <span class="comment">// fn 其实引用的是 foo</span></span><br><span class="line"> fn(); <span class="comment">// <--调用位置</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span>,</span><br><span class="line"> foo: foo</span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> a = <span class="string">"oops,global"</span>;</span><br><span class="line"></span><br><span class="line">doFoo(obj.foo); <span class="comment">// "oops,global"</span></span><br></pre></td></tr></table></figure>
<p>传递参数其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。</p>
<p>同样把函数传入语言内置的函数结果也是一样的。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span>,</span><br><span class="line"> foo: foo</span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> a = <span class="string">"oops,global"</span>;</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(obj.foo, <span class="number">1000</span>); <span class="comment">// "oops,global"</span></span><br></pre></td></tr></table></figure>
<p>经过上面的分析我们知道,回调函数丢失 this 绑定是非常常见的。</p>
<h3 id="2-3-显示绑定"><a href="#2-3-显示绑定" class="headerlink" title="2.3 显示绑定"></a>2.3 显示绑定</h3><p>就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接绑定到对象上。</p>
<p>如果我们不想在对象内部包含函数的引用,而想在某个对象上强制调用函数,这是我们需要使用函数的 call() 和 apply() 方法。</p>
<p>它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为可以直接指定 this 的绑定对象,因此称之为显示绑定。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a:<span class="number">2</span></span><br><span class="line">};</span><br><span class="line">foo.call(obj); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>显示绑定的另一种情况就是硬绑定。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> foo.call(obj);</span><br><span class="line">}</span><br><span class="line"><span class="built_in">setTimeout</span>(bar, <span class="number">1000</span>); <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 硬绑定的 bar 不可能再修改它的 this</span></span><br><span class="line">bar.call(<span class="built_in">window</span>); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>因为我们把 bar 函数内部调用了 foo,而 foo 的 this 已经被强制绑定在 obj 上,因此无论之后如何调用 bar 函数,它总会手动在 obj 上调用 foo。</p>
<p>硬绑定的另一种应用场景就是创建一个包裹函数,负责接收参数并返回值:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">something</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a, something);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.a + something;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> foo.apply(obj, <span class="built_in">arguments</span>);</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> b = bar(<span class="number">3</span>); <span class="comment">//2 3</span></span><br><span class="line"><span class="built_in">console</span>.log(b); <span class="comment">// 5</span></span><br></pre></td></tr></table></figure>
<p>另一种方法是创建一个可以重复使用的辅助函数:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">something</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a, something);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.a + something;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">bind</span>(<span class="params">fn, obj</span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> fn.apply(obj, <span class="built_in">arguments</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> bar = bind(foo, obj);</span><br><span class="line"><span class="keyword">var</span> b = bar(<span class="number">3</span>); <span class="comment">// 2 3</span></span><br><span class="line"><span class="built_in">console</span>.log(b); <span class="comment">// 5</span></span><br></pre></td></tr></table></figure>
<p>ES5 中提供了 Function.prototype.bind 函数,它的用法如下:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">unction foo(something) {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a, something);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.a + something;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = foo.bind(obj);</span><br><span class="line"><span class="keyword">var</span> b = bar(<span class="number">3</span>); <span class="comment">// 2 3</span></span><br><span class="line"><span class="built_in">console</span>.log(b); <span class="comment">// 5</span></span><br></pre></td></tr></table></figure>
<p>bind() 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用原始函数。</p>
<h3 id="2-4-new绑定"><a href="#2-4-new绑定" class="headerlink" title="2.4 new绑定"></a>2.4 new绑定</h3><p>在传统的面向对象的语言中,“构造函数”是类中的一些的特殊方法,使用 new 初始化类时会调用类中的构造函数。Javascript 中也有一个 new 操作符,但是 Javascript 中 new 的机制实际上和面向对象的语言完全不同。在 Javascript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。</p>
<p>使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:</p>
<ol>
<li>创建一个全新的对象。</li>
<li>这个对象会被执行 [[Prototype]] 连接。</li>
<li>这个新对象会被绑定到函数调用的 this。</li>
<li>如果函数没有返回其它对象,那么 new 表达式中的函数调用会自动返回这个新对象。</li>
</ol>
<p>思考下面代码:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">a</span>) </span>{</span><br><span class="line"> <span class="built_in">this</span>.a = a;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> bar = <span class="keyword">new</span> foo(<span class="number">2</span>);</span><br><span class="line"><span class="built_in">console</span>.log(bar.a); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>使用 new 来调用 foo() 时,我们会构造一个新对象并把它绑定到 foo() 调用中的 this 上。 new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。</p>
<h2 id="3-判断-this"><a href="#3-判断-this" class="headerlink" title="3.判断 this"></a>3.判断 this</h2><p>学习了上面四条规则,我们可以根据下面的顺序来判断 this 绑定的对象:</p>
<ol>
<li>函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。</li>
<li>函数是否通过 call、apply 显示绑定或者硬绑定?如果是的话,this 绑定的是指定对象。</li>
<li>函数是否在某个上下文中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。</li>
<li>如果都不是,使用默认绑定。严格模式下绑定到 undefined,否则绑定到全局对象。</li>
</ol>
<h2 id="4-绑定例外"><a href="#4-绑定例外" class="headerlink" title="4.绑定例外"></a>4.绑定例外</h2><p>在某些场景下 this 的绑定行为会出乎意料,你认为应该应用其它绑定规则时,实际上应用的可能是默认绑定的规则。</p>
<h3 id="4-1-被忽略的-this"><a href="#4-1-被忽略的-this" class="headerlink" title="4.1 被忽略的 this"></a>4.1 被忽略的 this</h3><p>如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定的规则。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> a = <span class="number">2</span>;</span><br><span class="line">foo.call(<span class="literal">null</span>); <span class="comment">//2</span></span><br></pre></td></tr></table></figure>
<p>一种常见的做法是使用 apply(…) 来“展开”一个数组,并当作参数传入一个函数。类似地,bind(…)可以对参数进行柯里化,这种方法有时非常有用:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params">a ,b</span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"a: "</span> + a + <span class="string">", b: "</span> + b);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 把数组展开成参数</span></span><br><span class="line">foo.apply(<span class="literal">null</span>, [<span class="number">2</span>, <span class="number">3</span>]); <span class="comment">//a: 2, b: 3</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用 bind 进行柯里化</span></span><br><span class="line"><span class="keyword">var</span> bar = foo.bind(<span class="literal">null</span>, <span class="number">2</span>);</span><br><span class="line">bar(<span class="number">3</span>); <span class="comment">// a: 2, b: 3</span></span><br></pre></td></tr></table></figure>
<h3 id="4-2-间接引用"><a href="#4-2-间接引用" class="headerlink" title="4.2 间接引用"></a>4.2 间接引用</h3><p>另一个需要注意的是你可能有意或者无意地创建一个函数的”间接引用“,在这种情况下,调用这个函数会应用默认绑定规则。</p>
<p>间接引用最容易在赋值的时候发生:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> a = <span class="number">2</span>;</span><br><span class="line"><span class="keyword">var</span> o = {<span class="attr">a</span>: <span class="number">3</span>, <span class="attr">foo</span>: foo};</span><br><span class="line"><span class="keyword">var</span> p = {<span class="attr">a</span>: <span class="number">4</span>};</span><br><span class="line">o.foo(); <span class="comment">// 3</span></span><br><span class="line">(p.foo = o.foo)(); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。</p>
<h2 id="5-this-词法"><a href="#5-this-词法" class="headerlink" title="5.this 词法"></a>5.this 词法</h2><p>我们之前介绍的四条规则已经可以包含所有的正常函数。但是在 ES6 中介绍了一种无法使用这些规则的特殊类型函数:箭头函数。</p>
<p>箭头函数不使用 this 的四种标准规则,而是根据外层作用域来决定 this。</p>
<p>我们来看看箭头函数的词法作用域:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="params">a</span> =></span> {</span><br><span class="line"> <span class="comment">// this继承自 foo()</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj1 = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> obj2 = {</span><br><span class="line"> a: <span class="number">3</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = foo.call(obj1);</span><br><span class="line">bar.call(obj2); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
<p>对比正常的函数:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj1 = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> obj2 = {</span><br><span class="line"> a: <span class="number">3</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">var</span> bar = foo.call(obj1);</span><br><span class="line">bar.call(obj2); <span class="comment">// 3</span></span><br></pre></td></tr></table></figure>
<p>foo() 内部的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar 引用箭头函数的 this 也会绑定到 obj1,箭头函数的绑定无法修改。(new 也不行)</p>
<p>箭头函数最常用于回调函数,例如事件处理器或者定时器:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">foo</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 这里的 this 在词法上继承 foo</span></span><br><span class="line"> <span class="built_in">console</span>.log(<span class="built_in">this</span>.a);</span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> obj = {</span><br><span class="line"> a: <span class="number">2</span></span><br><span class="line">};</span><br><span class="line">foo.call(obj); <span class="comment">// 2</span></span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>JavaScript 基础</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>this</tag>
</tags>
</entry>
<entry>
<title>图的结构及遍历</title>
<url>/2019/12/24/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E5%9B%BE/</url>
<content><![CDATA[<h2 id="图的基本概念"><a href="#图的基本概念" class="headerlink" title="图的基本概念"></a>图的基本概念</h2><p>在树型结构中,结点间具有分支层次关系,每一层上的结点只能和上一层中的至多一个结点相关,但可能和下一层的多个节点相关。树的关系也叫一对多的关系,而在图状结构中,任意两个结点之间都可能相关,即结点的邻接关系可以是任意的。图的结构是<strong>任意两个数据对象之间都可能存在某种特定关系</strong>的数据结构,是一种多对多的关系。</p>
<a id="more"></a>
<h3 id="图的定义和术语"><a href="#图的定义和术语" class="headerlink" title="图的定义和术语"></a>图的定义和术语</h3><p>图(Graph)是由两个集合构成,一个是非空但有限的顶点集合 V,另一个是描述顶点之间关系——边的集合 E(可以是空集)。因此图可以表示为 G=(V,E)。每条边是一顶点对 (v,w) 且 v,w∈V。通常用 |V| 表示定点数量 |E| 表示边的数量。</p>
<p>关于图的定义,与以前的线性表和树比较,还有几点需要注意:</p>
<ul>
<li><p>在线性表中,一般叫数据对象为元素;在树中,将数据对象成为结点;而在图中,我们把数据对象称为顶点(Vertex)。</p>
</li>
<li><p>线性表中可以没有数据对象,此时叫空表;没有数据对象的树称为空树;而在图中,我们至少要求有一个顶点,但边集可以是空。</p>
</li>
</ul>
<h3 id="图的抽象数据类型"><a href="#图的抽象数据类型" class="headerlink" title="图的抽象数据类型"></a>图的抽象数据类型</h3><p>类型名称:图(Graph)。</p>
<p>数据对象集:一个非空顶点集合 Vertex 和一个边集合 Edge,每条边用对应的一对顶点表示。</p>
<p>操作集:对于任意的图 G∈Graph,顶点 V∈Vertex,边 E∈Edge,以及任一访问顶点的函数 Visit(),我们主要关心下列操作:</p>
<p>1.<code>Graph CreateGraph(int VertexNum)</code>:构造一个有 VertexNum 个顶点但没有边的图;</p>
<p>2.<code>void InsertEdge(Graph G, Edge E)</code>:在 G 中增加新边 E;</p>
<p>3.<code>void DeleteEdge(Graph G, Edge E)</code>:从 G 中删除边 E;</p>
<p>4.<code>bool IsEmpty(Graph G)</code>:判断图是否为空;</p>
<p>5.<code>void DFS(Graph G, Vertex V, (*Visit)(Vertex))</code>:在图 G 中,从顶点 V 出发进行深度优先遍历;</p>
<p>6.<code>void BFS(Graph G, Vertex V, (*Visit)(Vertex))</code>:在图 G 中,从顶点 V 出发进行广度优先遍历。</p>
<h2 id="图的存储结构"><a href="#图的存储结构" class="headerlink" title="图的存储结构"></a>图的存储结构</h2><h3 id="邻接矩阵"><a href="#邻接矩阵" class="headerlink" title="邻接矩阵"></a>邻接矩阵</h3><p>所谓邻接矩阵的存储结构,就是用矩阵表示图中各顶点之间的邻接关系和权值。以下是一个无向图的临界矩阵表示:</p>
<img src="/2019/12/24/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E5%9B%BE/1.png" class="" title="无向图的临界矩阵">
<p>从图的邻接矩阵存储方法容易看出这种表示具有以下特点:</p>
<p>1.无向图的邻接矩阵一定是个对称矩阵。因此在具体存放邻接矩阵时只需要存放上三角或者下三角的元素即可。所需要存储的元素个数是:|V|*(|V|-1)/2。</p>
<p>2.对于无向图,邻接矩阵的第 i 行(或第 i 列)非 0 元素的个数正好是第 i 个顶点的度(Degree)。</p>
<p>3.对于有向图,邻接矩阵第 i 行(或第 i 列)非 0 元素的个数正好是第 i 个顶点的出度(或入度)。</p>
<p>4.用临界矩阵方法存储图,很容易确定图中任意两点之间是否有边相连,只需要考察邻接矩阵对应的元素即可;确定一个顶点的所有邻接点,也只需要邻接矩阵对应的一行(或一列);但是要确定图中有多少边,则必须按行(或按列)对每个元素进行检测,所花费的时间代价是 O(|V|^2)。这是用邻接矩阵来存储图的局限性。</p>
<p>以下是邻接矩阵的 C 语言描述:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MaxVertexNum 100 <span class="comment">/* 最大顶点数 */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> INFINITY 65535 <span class="comment">/* 初始值设为双字节无符号整数的最大值 */</span></span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> Vertex; <span class="comment">/* 用顶点下标表示顶点,为整形 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> WeightType; <span class="comment">/* 边的权值 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">char</span> DataType; <span class="comment">/* 顶点存储的数据类型设为字符型 */</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 图的定义 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">GNode</span> {</span></span><br><span class="line"> <span class="keyword">int</span> Nv; <span class="comment">/* 顶点数 */</span></span><br><span class="line"> <span class="keyword">int</span> Ne; <span class="comment">/* 边数 */</span></span><br><span class="line"> WeightType G[MaxVertexNum][MaxVertexNum]; <span class="comment">/* 邻接矩阵 */</span></span><br><span class="line"> DataType Data[MaxVertexNum]; <span class="comment">/* 每个顶点的数据 */</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">GNode</span>* <span class="title">PtrToGNode</span>;</span></span><br><span class="line"><span class="keyword">typedef</span> PtrToGNode MGraph;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 边的定义 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ENode</span> {</span></span><br><span class="line"> Vertex V1, V2; <span class="comment">/* 有向边<V1,V2> */</span></span><br><span class="line"> WeightType Weight; <span class="comment">/* 权重 */</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">ENode</span>* <span class="title">PtrToENode</span>;</span></span><br><span class="line"><span class="keyword">typedef</span> PtrToENode Edge;</span><br></pre></td></tr></table></figure>
<p>有了图的结构和类型定义之后,先创建一个包含全部顶点但是没有边的图,再逐条插入边,从而创建一个无向网图的数据结构。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function">MGraph <span class="title">CreateGraph</span><span class="params">(<span class="keyword">int</span> VertexNum)</span> </span>{</span><br><span class="line"> MGraph Graph = (MGraph)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct GNode));</span><br><span class="line"> Graph->Nv = VertexNum;</span><br><span class="line"> Graph->Ne = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 初始化邻接矩阵 */</span></span><br><span class="line"> <span class="comment">/* 注意顶点默认从 0 编号 到 Graph->Nv - 1 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < Graph->Nv;i++) {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> j = <span class="number">0</span>;j < Graph->Nv;j++) {</span><br><span class="line"> Graph->G[i][j] = INFINITY;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Graph;</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">InsertEdeg</span><span class="params">(MGraph Graph, Edge E)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 插入边<V1,V2> */</span></span><br><span class="line"> Graph->G[E->V1][E->V2] = E->Weight;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 如果是无向图,还需要插入边<V2,V1> */</span></span><br><span class="line"> Graph->G[E->V2][E->V1] = E->Weight;</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"><span class="function">MGraph <span class="title">BuildGraph</span><span class="params">()</span> </span>{</span><br><span class="line"> MGraph Graph;</span><br><span class="line"> <span class="keyword">int</span> VertexNum;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 读入顶点数 */</span></span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &VertexNum);</span><br><span class="line"></span><br><span class="line"> Graph = CreateGraph(VertexNum);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 读入边数 */</span></span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &Graph->Ne);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(Graph->Ne != <span class="number">0</span>) {</span><br><span class="line"></span><br><span class="line"> Edge E = (Edge)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct ENode)); <span class="comment">/* 建立边结点 */</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/*依次读入每一条边的数据 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>; i < Graph->Ne;i++) {</span><br><span class="line"></span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d %d %d"</span>,&E->V1, &E->V2, &E->Weight);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 将该边插入图中 */</span></span><br><span class="line"> InsertEdeg(Graph, E);</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/* 如果顶点有数据,读入顶点数据 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < Graph->Nv;i++) {</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%c"</span>,&Graph->Data[i]);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Graph;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>邻接矩阵是一种表示各类图的简洁的数据结构。但是我们发现,不论图中边的数量或多或少,我们都花费了 O(|V|^2) 的存储空间,这对于稠密图来说是一种高效的存储方法。但是如果面对的是一个稀疏图,则邻接矩阵中的大多数项为 0 或 无穷,形成了所谓的稀疏矩阵,就会浪费很多空间。因此对于稀疏图,我们考虑另一种存储方法。</p>
<h3 id="邻接表"><a href="#邻接表" class="headerlink" title="邻接表"></a>邻接表</h3><p>邻接表是一种图的顺序存储与链式存储相结合的存储方法。</p>
<img src="/2019/12/24/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E5%9B%BE/2.png" class="" title="图的邻接表">
<p>图的邻接表存储具有以下特点:</p>
<p>1.方便查找任一顶点的所有邻接点。</p>
<p>2.节约稀疏图的空间。需要 N 个头指针 + 2E 个结点(每个结点至少两个域)。</p>
<p>3.对于无向图来说方便计算任一顶点的度,对于有向图来说只能计算出度。</p>
<p>4.不方便检查任一对顶点间是否存在边。</p>
<p>以下是图的邻接表存储的代码实现:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MaxVertexNum 100 <span class="comment">/* 最大顶点数设为100 */</span></span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> Vertex; <span class="comment">/* 用顶点下标表示顶点,为整形 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> WeightType; <span class="comment">/* 边的权值设为整形 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">char</span> DataType; <span class="comment">/* 顶点存储的数据类型设为字符型 */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 边的定义 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ENode</span> {</span></span><br><span class="line"> Vertex V1,V2; <span class="comment">/* 有向边<V1,V2> */</span></span><br><span class="line"> WeightType Weight; <span class="comment">/* 权重 */</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">ENode</span>* <span class="title">Edge</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 邻接点的定义 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">AdjVNode</span>* <span class="title">PtrToAdjVNode</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">AdjVNode</span> {</span></span><br><span class="line"> Vertex AdjV; <span class="comment">/* 邻接点的下标 */</span></span><br><span class="line"> WeightType Weight; <span class="comment">/* 邻接点边的权重 */</span></span><br><span class="line"> PtrToAdjVNode Next; <span class="comment">/* 下一个邻接点 */</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 顶点表头节点的定义 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">Vnode</span> {</span></span><br><span class="line"> PtrToAdjVNode FirstEdge; <span class="comment">/* 边表头节点指针 */</span></span><br><span class="line"> DataType Data; <span class="comment">/* 头结点的值 */</span></span><br><span class="line"> <span class="comment">/* 很多情况下顶点无数据,此时Data不用出现 */</span></span><br><span class="line">} AdjVList[MaxVertexNum]; <span class="comment">/* AdjVList是邻接表的类型 */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 图的定义 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">GNode</span>* <span class="title">PtrToGNode</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">GNode</span> {</span></span><br><span class="line"> <span class="keyword">int</span> Nv; <span class="comment">/* 顶点数 */</span></span><br><span class="line"> <span class="keyword">int</span> Ne; <span class="comment">/* 边数 */</span></span><br><span class="line"> AdjVList G; <span class="comment">/* 邻接表 */</span></span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> PtrToGNode LGraph;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 初始化一个有 VertexNum个顶点,但是没有边的图 */</span></span><br><span class="line"><span class="function">LGraph <span class="title">CreateGraph</span><span class="params">(<span class="keyword">int</span> VertexNum)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 将边<V1,V2>插入图中 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">InsertEdge</span><span class="params">(LGraph Graph, Edge E)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 根据输入构建图 */</span></span><br><span class="line"><span class="function">LGraph <span class="title">BuildGraph</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function">LGraph <span class="title">CreateGraph</span><span class="params">(<span class="keyword">int</span> VertexNum)</span> </span>{</span><br><span class="line"> LGraph Graph = (LGraph)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct GNode));</span><br><span class="line"> Graph->Nv = VertexNum;</span><br><span class="line"> Graph->Ne = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 初始化邻接表的表头指针 */</span></span><br><span class="line"> <span class="comment">/* 注意这里默认定点编号从 0 开始到 (Graph->Nv - 1) 结束 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < Graph->Nv;i++) {</span><br><span class="line"> Graph->G[i].FirstEdge = <span class="literal">NULL</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Graph;</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">InsertEdge</span><span class="params">(LGraph Graph, Edge E)</span> </span>{</span><br><span class="line"> <span class="comment">/* 插入有向边 <V1,V2> */</span></span><br><span class="line"> PtrToAdjVNode NewNode;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 构建一个邻接点,并将该邻接点插入链表头部 */</span></span><br><span class="line"> NewNode = (PtrToAdjVNode)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct AdjVNode));</span><br><span class="line"> NewNode->AdjV = E->V2;</span><br><span class="line"> NewNode->Weight = E->Weight;</span><br><span class="line"></span><br><span class="line"> NewNode->Next = Graph->G[E->V1].FirstEdge;</span><br><span class="line"> Graph->G[E->V1].FirstEdge = NewNode;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 如果是无向图还要插入<V2,V1> */</span></span><br><span class="line"> NewNode = (PtrToAdjVNode)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct AdjVNode));</span><br><span class="line"> NewNode->AdjV = E->V1;</span><br><span class="line"> NewNode->Weight = E->Weight;</span><br><span class="line"></span><br><span class="line"> NewNode->Next = Graph->G[E->V2].FirstEdge;</span><br><span class="line"> Graph->G[E->V2].FirstEdge = NewNode;</span><br><span class="line">}</span><br><span class="line"><span class="function">LGraph <span class="title">BuildGraph</span><span class="params">()</span> </span>{</span><br><span class="line"> LGraph Graph;</span><br><span class="line"> <span class="keyword">int</span> VertexNum;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 输入顶点数 */</span></span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &VertexNum);</span><br><span class="line"> Graph = CreateGraph(VertexNum);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 读入边数 */</span></span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &Graph->Ne);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(Graph->Ne != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">/* 构建边并读入 */</span></span><br><span class="line"> Edge E = (Edge)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct ENode));</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < Graph->Ne;i++) {</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d %d %d"</span>, &E->V1, &E->V2, &E->Weight);</span><br><span class="line"> InsertEdge(Graph, E);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 如果顶点有数据,读入数据 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>;i < Graph->Nv;i++)</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%c"</span>, &(Graph->G[i].Data));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Graph;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<h2 id="图的遍历"><a href="#图的遍历" class="headerlink" title="图的遍历"></a>图的遍历</h2><p>图的遍历就是从图中任一顶点出发,对图中所有顶点访问一次且仅访问一次的次序序列。</p>
<h3 id="深度优先搜索(Depth-First-Search-DFS)"><a href="#深度优先搜索(Depth-First-Search-DFS)" class="headerlink" title="深度优先搜索(Depth First Search,DFS)"></a>深度优先搜索(Depth First Search,DFS)</h3><p>深度优先搜索类似于树的先序遍历,是树的先序遍历的推广。假设初始状态所有顶点都没被访问过,则深度优先搜索从图中的任一顶点出发,设为v0 ,访问此顶点,然后从v0的邻接点中的一个出发递归地进行同样的深度优先搜索,直至图中所有节点都被访问。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* 邻接矩阵存储的图 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Visit</span><span class="params">(Vertex V)</span> </span>{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"正在访问顶点 %d"</span>, V);</span><br><span class="line">}</span><br><span class="line"><span class="comment">/* Visited[]已经为全局变量,且初始化为false */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">DFS</span><span class="params">(LGraph Graph, Vertex V, <span class="keyword">void</span> (*Visit)(Vertex))</span> </span>{</span><br><span class="line"></span><br><span class="line"> Visit(V); <span class="comment">/* 访问第V个顶点 */</span></span><br><span class="line"> Visited[V] = <span class="literal">true</span>; <span class="comment">/* 将V标记为已访问 */</span></span><br><span class="line"></span><br><span class="line"> PtrToAdjVNode W;</span><br><span class="line"> <span class="keyword">for</span>(W = Graph->G[V].FirstEdge;W;W = W->Next) { <span class="comment">/* 对V的每个邻接点W */</span></span><br><span class="line"> <span class="keyword">if</span>(! Visited[W->AdjV]) { <span class="comment">/* 如果W未被访问 */</span></span><br><span class="line"> DFS(Graph, W->AdjV, Visit); <span class="comment">/* 则递归访问之 */</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="广度优先搜索(Breadth-First-Search-BFS)"><a href="#广度优先搜索(Breadth-First-Search-BFS)" class="headerlink" title="广度优先搜索(Breadth First Search,BFS)"></a>广度优先搜索(Breadth First Search,BFS)</h3><p>广度优先搜索类似于树的层次遍历。从顶点v0出发,在访问了v0之后,依次访问v0各个未被访问的邻接点,然后分别从这些邻接点出发,访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。直至图中所有已被访问的顶点的邻接点都被访问到。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* 邻接矩阵存储的图 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Visit</span><span class="params">(Vertex V)</span> </span>{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"正在访问顶点 %d"</span>, V);</span><br><span class="line">}</span><br><span class="line"><span class="comment">/* IsEdge(Graph, V, W)检查<V, W>是否图Graph中的一条边,即W是否V的邻接点。 */</span></span><br><span class="line"><span class="comment">/* 此函数根据图的不同类型要做不同的实现,关键取决于对不存在的边的表示方法。*/</span></span><br><span class="line"><span class="comment">/* 例如对有权图, 如果不存在的边被初始化为INFINITY, 则函数实现如下: */</span></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">IsEdge</span><span class="params">(MGraph Graph, Vertex V, Vertex W)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Graph->G[V][W] < INFINITY ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">/* Visited[]已经为全局变量,且初始化为false */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">BFS</span><span class="params">(MGraph Graph, Vertex S, <span class="keyword">void</span>(* Visit)(Vertex))</span> </span>{</span><br><span class="line"> <span class="comment">/* 以S为出发点对邻接矩阵存储的图进行BFS搜索 */</span></span><br><span class="line"> Vertex V,W;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 访问 S 顶点 */</span></span><br><span class="line"> Visit(S);</span><br><span class="line"> Visited[S] = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> Queue Q;</span><br><span class="line"> Q = CreateQueue(MaxSize); <span class="comment">/* 创建一个空队列 */</span></span><br><span class="line"></span><br><span class="line"> AddQ(Q, S); <span class="comment">/* 将S入队 */</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(!IsEmpty(Q)) {</span><br><span class="line"> V = DeleteQ(Q); <span class="comment">/* 弹出V */</span></span><br><span class="line"> <span class="keyword">for</span>(W = <span class="number">0</span>;W < Graph->Nv;W++) { <span class="comment">/* 对图中的每一个顶点 W */</span></span><br><span class="line"> <span class="comment">/* 如果W没有访问过且是V的邻接点 */</span></span><br><span class="line"> <span class="keyword">if</span>(!Visited[W] && IsEdge(Graph, V, W))</span><br><span class="line"> <span class="comment">/* 访问 W 顶点 */</span></span><br><span class="line"> Visit(W);</span><br><span class="line"> Visited[W] = <span class="literal">true</span>; <span class="comment">/* 将 W 标记为已访问 */</span></span><br><span class="line"> AddQ(Q, W); <span class="comment">/* 将 W 入队列 */</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>若有 N 个顶点、E 条边,DFS 和 BFS 的时间复杂度为:</p>
<ul>
<li><p>用邻接表存储图,为 O(N+E);</p>
</li>
<li><p>用邻接矩阵存储图,为 O(N^2)。</p>
</li>
</ul>
]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>图</tag>
<tag>C语言</tag>
</tags>
</entry>
<entry>
<title>散列查找</title>
<url>/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/</url>
<content><![CDATA[<p>散列查找解决的一个基本问题是:如何快速搜索到需要的关键词?</p>
<p>我们知道查找的本质是已知一个对象,找到该对象的位置。因此如果我们在安排位置时,通过一个”散列函数“来计算出对象的位置进行存放,当要查找这个对象时,再通过相同的”散列函数“即可直接计算出对象的位置。</p>
<p>因此其时间复杂度几乎是:O(1),即查找时间与问题规模无关!</p>
<a id="more"></a>
<p>那么问题就来了,我们如何构造出一个比较好的散列函数,如果多个关键词通过某个散列函数计算出了相同的位置,我们如何解决这种冲突。</p>
<p>所以散列查找法的两项基本工作就是:</p>
<ul>
<li><p>计算位置:构造散列函数确定关键词的存储位置</p>
</li>
<li><p>解决冲突:应用某种策略解决多个关键字位置相同的问题</p>
</li>
</ul>
<h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p><strong>散列表(哈希表)</strong>:</p>
<p><strong>类型名称</strong>:符号表(SymbolTable)</p>
<p><strong>数据对象集</strong>:符号表是“名字(Name)—属性(Attribute)”对的集合</p>
<p><strong>操作集</strong>:对于一个具体的符号表Table∈SymbolTable,一个给定的名字Name∈NameType,属性Attr∈AttributeType,以及正整数TableSize,符号表的基本操作有:</p>
<p><code>SymbolTable CreateTable(int TableSize)</code>:创建空的符号表,其最大长度为TableSize;</p>
<p><code>bool IsIn(SymbolTable Table,NameType Name)</code>:查找指定 Name 是否在符号表 Table 中;</p>
<p><code>AttributeType Find(SymbolTable Table,NameType Name)</code>:获取符号表 Table 中指定名字 Name 对应的属性;</p>
<p><code>bool Modify(SymbolTable Table,NameType Name,AttributeType Attr)</code>:将Table 中指定名字 Name 的属性修改为 Attr;</p>
<p><code>bool Insert(SymbolTable Table,NameType Name,AttributeType Attr)</code>:向 Table 中插入一个新名字 Name 及其属性 Attr;</p>
<p><code>bool Delete(SymbolTable Table,NameType Name)</code>:从 Table 中删除一个名字 Name 及其属性。</p>
<p><strong>散列(Hashing)的基本思想是:</strong></p>
<p>1.以关键字key为自变量,通过一个确定的函数h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。</p>
<p>2.可能不同的关键字会映射到同一个散列地址上,即h(key_i )=h(key_j ),key_i≠key_j,称为冲突——因此需要某种冲突解决策略。</p>
<p>例:有 n=11 个对象的集合 {18,23,11,20,2,7,27,30,42,15,34}。符号表的大小 TableSize = 17(通常为素数),选取散列函数如下:</p>
<p>h(key) = key mod TableSize (求余)</p>
<p>用这个散列函数对 11 个对象建立查找表,如下所示:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/1.png" class="" title="散列表">
<ul>
<li><p>存放:<br>如果新插入35,h(35)=1,该位置已有对象,冲突</p>
</li>
<li><p>查找:<br>key = 22,h(22) = 5,该地址为空,不在表中<br>key = 30,h(30) = 13,该地址存放的是30,找到</p>
</li>
</ul>
<blockquote>
<p>定于 装填因子:设散列表空间大小为 m,填入表中的元素个数是 n,则称 a=n/m 为散列表的装填因子。</p>
</blockquote>
<h2 id="散列函数的构造方法"><a href="#散列函数的构造方法" class="headerlink" title="散列函数的构造方法"></a>散列函数的构造方法</h2><p>一个好的散列函数应该考虑以下两个因素:</p>
<p>1.计算简单,以便提高转换速度;</p>
<p>2.关键词对应的地址空间分布均匀,以尽量减少冲突。</p>
<h3 id="数字关键词的散列函数构造"><a href="#数字关键词的散列函数构造" class="headerlink" title="数字关键词的散列函数构造"></a>数字关键词的散列函数构造</h3><p>1.直接定址法</p>
<p>如果我们要统计人口的年龄分布情况(0——120),那么对于年龄这个关键词可以直接作为地址,即 h(key) = key。</p>
<p>如果要统计的是 1990 年以后出任的人口分布情况,那么对于出生年份这个关键词可以减去 1990 作为地址,即 h(key) = key-1990。</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/2.png" class="" title="直接定址法">
<p>总之,取关键词的某个线性函数值为散列地址,即</p>
<p>h(key) = a X key + b (a,b为常数)</p>
<p>2.除留余数法</p>
<p>现实生活中比较常用的方法是除留余数法。假设散列表长为 TableSize,选择一个正整数 p ≤ TableSize,散列函数构造为:</p>
<p>h(key) = key mod p</p>
<p>这里 p 一般取为小于等于散列表长 TableSize 的某个最大的素数比较好。</p>
<p>3.数字分析法</p>
<p>分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址</p>
<p>比如:取手机号 key 的后 4 位作为地址:</p>
<p>散列函数位:h(key) = atoi(key+7)</p>
<h3 id="字符串关键词的散列函数构造"><a href="#字符串关键词的散列函数构造" class="headerlink" title="字符串关键词的散列函数构造"></a>字符串关键词的散列函数构造</h3><p>1.一个简单的散列函数——ASCII码加和法</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/3.png" class="" title="ASCII码加和法">
<p>2.简单的改进——前3个字符移位法</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/4.png" class="" title="前3个字符移位法">
<p>3.好的散列函数——移位法</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/5.png" class="" title="好的散列函数——移位法">
<h2 id="处理冲突的方法"><a href="#处理冲突的方法" class="headerlink" title="处理冲突的方法"></a>处理冲突的方法</h2><p>在前面的散列函数构造过程中,我们努力使散列地址均匀分布在整个地址空间,但实际应用中,冲突只能尽量减少,而不能完全避免。接下来我们讨论在冲突发生时,如何有效地解决它。常用的处理冲突的方法有开放地址法(Open Addressing)和链地址法(Linear Probing)。</p>
<h3 id="开放地址法"><a href="#开放地址法" class="headerlink" title="开放地址法"></a>开放地址法</h3><p>一旦产生了冲突(该地址已有其它元素),就按照某种规则去寻找另一空地址。</p>
<p>若发生了第 i 次冲突,试探的下一个地址将增加 di,基本公式是:</p>
<p>hi(key) = (h(key) + di) mod TableSize (1 <= i < TableSize)</p>
<p>di决定了不同的解决冲突方案:线性探测、平方探测、双散列。</p>
<p>1.线性探测</p>
<p><strong>以增量序列1,2,3…TableSize-1循环试探下一个存储地址。</strong></p>
<p>设关键词序列为 {47,7,29,11,9,84,54,20,30}</p>
<p>散列表长 TableSize=13(装填因子 9/13=0.69);</p>
<p>散列函数为:h(key) = key mod 11。</p>
<p>用线性探测法处理冲突,列出一次插入后的散列表,并估算查找性能。</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/6.png" class="" title="线性探测">
<p>注意”聚集“现象。</p>
<p>散列表查找性能分析</p>
<ul>
<li><p>成功平均查找长度(ASLs)</p>
</li>
<li><p>不成功平均查找长度(ASLu)</p>
</li>
</ul>
<p>散列表如下:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/7.png" class="" title="散列表">
<p>分析:</p>
<p>ASLs:查找表中关键词的平均查找比较次数(其冲突次数加 1)</p>
<p>ASLs = (1+7+1+1+2+1+4+2+4)/9 = 2.56</p>
<p>ASLu:不在散列表中的关键词的平均查找次数(不成功)</p>
<p>一般方法:将不在散列表中的关键词分为若干类。如根据 h(key) 分类</p>
<p>ASLu = (3+2+1+2+1+1+1+9+8+7+6)/11 = 3.73</p>
<p>2.平方探测法——二次探测</p>
<p><strong>以增量序列 1,-1,4,-4,9,-9,…,q^2,-q^2,且 q <= [TableSize/2] 循环试探下一个存储地址。</strong></p>
<p>设关键词序列为 {47,7,29,11,9,84,54,20,30},散列表长度 TableSize = 11,散列函数为:h(key) = key mod 11。用平方探测法处理冲突,列出依次插入后的散列表,并估算ASLs。</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/8.png" class="" title="key">
<p>ASLs = (1+1+2+1+1+3+1+4+4)/9 = 2</p>
<p><strong>是否有空间,平方探测(二次探测)就能找到?</strong></p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/9.png" class="" title="key">
<p><strong>有证明表明,如果散列表的长度 TableSize 是某个 4k+3(k是正整数)形式的素数时,平方探测法就可以检测到整个散列表空间。</strong>这一点很重要,使我们能够放心使用平方探测法的理论保证。</p>
<p>在开放地址的散列表中,不能进行标准的删除操作,因为相应的单元可能引起过冲突,数据对象绕过它存在了别处。为此开放地址散列表需要“惰性删除”,即需要增加一个“删除标记”,而并不是真正的删除它。这样可以不影响查找,但额外的存储负担增加了代码的复杂性。</p>
<p>以下是开放地址法的类型声明:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/10.png" class="" title="开放地址法的类型声明">
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MAXTABLESIZE 100000 <span class="comment">/* 允许开辟的最大散列表长度 */</span></span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> ElementType; <span class="comment">/* 关键词类型 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> Index; <span class="comment">/* 散列地址类型 */</span></span><br><span class="line"><span class="keyword">typedef</span> Index Position; <span class="comment">/* 数据所在位置与散列地址是同一类型 */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 散列单元的状态,分别对应:有合法元素、空单元、有已删除元素 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">enum</span> { Legitimate, Empty, Deleted } EntryType;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">HashEntry</span> <span class="title">Cell</span>;</span> <span class="comment">/* 散列表单元类型 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">HashEntry</span> {</span></span><br><span class="line"> ElementType Data; <span class="comment">/* 存放的元素 */</span></span><br><span class="line"> EntryType Info; <span class="comment">/* 单元状态 */</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">TblNode</span>* <span class="title">HashTable</span>;</span> <span class="comment">/* 散列表类型 */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">TblNode</span> {</span> <span class="comment">/* 散列表节点定义 */</span></span><br><span class="line"> <span class="keyword">int</span> TableSize; <span class="comment">/* 散列表的最大长度 */</span></span><br><span class="line"> Cell *Cells; <span class="comment">/* 存放散列单元的数组 */</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>如下代码给出了散列表的初始化函数。首先申请散列表需要的空间,再将每个单元的 info 设置为 Empty,表示为空。注意需要确定一个不下于 TableSize 的素数,用作真正的散列表的地址空间大小,这个功能由 NextPrime 实现。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">NextPrime</span><span class="params">(<span class="keyword">int</span> N)</span> </span>{</span><br><span class="line"> <span class="comment">/* 返回大于N且不超过MAXTABLESIZE的最小素数 */</span></span><br><span class="line"> <span class="keyword">int</span> i;</span><br><span class="line"> <span class="keyword">int</span> p = (N%<span class="number">2</span>) ? N+<span class="number">2</span> : N+<span class="number">1</span>; <span class="comment">/* 从大于N的第一个奇数开始 */</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(p <= MAXTABLESIZE) {</span><br><span class="line"> <span class="keyword">for</span>(i = (<span class="keyword">int</span>)<span class="built_in">sqrt</span>(p); i><span class="number">2</span>; i--)</span><br><span class="line"> <span class="keyword">if</span>(!(p%i)) <span class="keyword">break</span>; <span class="comment">/* p不是素数 */</span></span><br><span class="line"> <span class="keyword">if</span>(i==<span class="number">2</span>) <span class="keyword">break</span>; <span class="comment">/* for循环正常结束,说明p是素数 */</span></span><br><span class="line"> <span class="keyword">else</span> p+=<span class="number">2</span>; <span class="comment">/* 否则试探下一个奇数 */</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> p;</span><br><span class="line">}</span><br><span class="line"><span class="function">HashTable <span class="title">CreateTable</span><span class="params">(<span class="keyword">int</span> TableSize)</span> </span>{</span><br><span class="line"></span><br><span class="line"> HashTable H = (HashTable)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct TblNode));</span><br><span class="line"> <span class="comment">/* 保证散列表的最大长度是素数 */</span></span><br><span class="line"> H->TableSize = NextPrime(TableSize);</span><br><span class="line"> <span class="comment">/* 声明单元数组 */</span></span><br><span class="line"> H->Cells = (Cell*)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(Cell)*H->TableSize);</span><br><span class="line"> <span class="comment">/* 初始化单元数组为空单元 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>; i < H->TableSize;i++) {</span><br><span class="line"> H->Cells[i].Info = Empty;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> H;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>以下代码是平方探测法的查找函数。首先调用 Hash 函数计算地址,以确定关键词所在的散列表地址。用 while 循环控制直至明确查找成功或者找到空位置表示查找失败,遇到冲突则继续查找。</p>
<p>注意关键词 key 的类型 ElementType 不一定为整形,也可能被定义为字符串,若是字符串,则 while 的判断条件要用 C 语言的 strcmp 函数来替换。若找到关键词,函数直接返回结点的地址,若找不到则返回一个空的单元。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function">Position <span class="title">Find</span><span class="params">(HashTable H, ElementType Key)</span> </span>{</span><br><span class="line"> Position CurrentPos,OldPos;</span><br><span class="line"> <span class="keyword">int</span> CNum = <span class="number">0</span>; <span class="comment">/* 记录冲突次数 */</span></span><br><span class="line"> CurrentPos = OldPos = Hash(Key, H->TableSize); <span class="comment">/* 计算出其位置 */</span></span><br><span class="line"> <span class="comment">/* 当该单元为非空并且不是要找的元素时,发生冲突 */</span></span><br><span class="line"> <span class="keyword">while</span>(H->Cells[CurrentPos].Info != Empty && H->Cells[CurrentPos].Data != Key) {</span><br><span class="line"> <span class="comment">/* 字符串类型需调用 strcmp 函数 */</span></span><br><span class="line"> <span class="comment">/* 统计冲突次数 */</span></span><br><span class="line"> <span class="keyword">if</span>((++CNum % <span class="number">2</span>)) { <span class="comment">/* 奇数次冲突 */</span></span><br><span class="line"> <span class="comment">/* 增量为 [(CNum+1)/2]^2 */</span></span><br><span class="line"> CurrentPos = OldPos + (CNum+<span class="number">1</span>) * (CNum+<span class="number">1</span>) /<span class="number">4</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(CurrentPos >= H->TableSize)</span><br><span class="line"> CurrentPos = CurrentPos%H->TableSize; <span class="comment">/* 调整为合法地址 */</span></span><br><span class="line"></span><br><span class="line"> } <span class="keyword">else</span> { <span class="comment">/* 偶数次冲突 */</span></span><br><span class="line"> <span class="comment">/* 增量为 -(CNum/2)^2 */</span></span><br><span class="line"> CurrentPos = OldPos - CNum * CNum /<span class="number">4</span>;</span><br><span class="line"> <span class="keyword">while</span>(CurrentPos < <span class="number">0</span>) <span class="comment">/* 调整为合法地址 */</span></span><br><span class="line"> CurrentPos += H->TableSize;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> CurrentPos;</span><br><span class="line"> <span class="comment">/* 此时 CurrentPos 或者是 Key 的位置,或者是一个空单元的地址(表示找不到) */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>以下是插入函数,先检查 Key 是否已经存在,该单元的状态只要不是合法的,就可以在此插入。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">Insert</span><span class="params">(HashTable H, ElementType Key)</span> </span>{</span><br><span class="line"> Position p = Find(H,Key); <span class="comment">/* 首先检查Key值是否存在 */</span></span><br><span class="line"> <span class="keyword">if</span>(H->Cells[p].Info != Legitimate) { <span class="comment">/* 如果这个单元没有被占用,说明Key可以插入在此 */</span></span><br><span class="line"> H->Cells[p].Info = Legitimate;</span><br><span class="line"> H->Cells[p].Data = Key;</span><br><span class="line"> <span class="comment">/* 字符串类型需调用strcpy函数 */</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"键值已存在"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>开放地址法的删除操作只需要该改变单元的状态 Info 即可。</p>
<p>3.双散列探测法</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/11.png" class="" title="双散列探测法">
<p>4.再散列(Rehashing)</p>
<p>当散列表元素太多时(即装填因子太大时),查找效率就会下降;实用的装填因子一般取<strong>0.5——0.85</strong>。</p>
<p>当装填因子过大时,解决的方法就是加倍扩大散列表,这个过程叫做**”再散列”**。</p>
<h3 id="分离链接法"><a href="#分离链接法" class="headerlink" title="分离链接法"></a>分离链接法</h3><p>分离链接法就是将相应位置上冲突的所有关键词存储在同一个单链表里面。</p>
<p>设关键字序列为{47,7,29,11,16,92,22,8,3,50,37,89,94,21},散列函数取:h(key) = key mod 11;用分离链接法处理冲突,结果如下:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/12.png" class="" title="分离链接法">
<p>表中有 9 个结点只需查找 1 次,5 个结点需要查找 2 次,查找成功的平均查找次数:</p>
<p>ASLs = (9+5*2)/14 = 1.36</p>
<p>以下是分离链接法的代码实现:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> KEYLENGTH 15 <span class="comment">/* 关键字符串的最大长度 */</span></span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">char</span> ElementType[KEYLENGTH+<span class="number">1</span>]; <span class="comment">/* 关键词类型用字符串 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> Index; <span class="comment">/* 散列地址类型 */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">LNode</span>* <span class="title">PtrToNode</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">LNode</span> {</span></span><br><span class="line"> ElementType Data;</span><br><span class="line"> PtrToNode Next;</span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> PtrToNode List;</span><br><span class="line"><span class="keyword">typedef</span> PtrToNode Position;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">TblNode</span>* <span class="title">HashTable</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">TblNode</span> {</span></span><br><span class="line"> <span class="keyword">int</span> TableSize;</span><br><span class="line"> List Heads; <span class="comment">/* 指向链表头结点的数组 */</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>散列表结构包括一个 TableSize 记录表的最大长度以及一个节点数组对应的单链表,它们在初始化时动态分配空间,并设置相应的初值。</p>
<p>以下是散列表的初始化函数 CreateTable。首先申请散列表的头结点空间;然后确定一个不小于 TableSize 的素数,用作真正的散列表的地址空间大小;最后动态分配散列表的地址列表数组并初始化空的头结点。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function">HashTable <span class="title">CreateTable</span><span class="params">(<span class="keyword">int</span> TableSize)</span> </span>{</span><br><span class="line"> HashTable H = (HashTable)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct TblNode));</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 保证散列的最大长度是素数 */</span></span><br><span class="line"> H->TableSize = NextPrime(TableSize);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 分配链表表头节点数组 */</span></span><br><span class="line"> H->Heads = (List)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct LNode)*H->TableSize);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 初始化表头节点 */</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>; i<H->TableSize; i++) {</span><br><span class="line"> H->Heads[<span class="number">0</span>].Data[<span class="number">0</span>] = <span class="string">'\0'</span>;</span><br><span class="line"> H->Heads.Next = <span class="literal">NULL</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> H;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>以下是查找 Find 函数。首先调用 Hash 函数计算地址,得到关键字所在的 Heads 中单元的下标 Pos;P 则指向 Heads[Pos] 链表中真正的第一个元素。因为关键字是字符串,所以 while 循环条件判断要用 strcmp 函数来比较 Data 与 Key 的值。若找到了关键词,函数直接返回结点的地址,若找不到则返回空地址。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function">Position <span class="title">Find</span><span class="params">(HashTable H, ElementType Key)</span> </span>{</span><br><span class="line"></span><br><span class="line"> Position Pos = Hash(Key,H->TableSize); <span class="comment">/* 找到该Key的位置 */</span></span><br><span class="line"></span><br><span class="line"> Index P = H->Heads[Pos].Next; <span class="comment">/* 从该链表的第一个节点开始 */</span></span><br><span class="line"> <span class="comment">/* 当未到表尾,并且Key未找到时 */</span></span><br><span class="line"> <span class="keyword">while</span>(P && <span class="built_in">strcmp</span>(P->Data,Key))</span><br><span class="line"> P=P->Next;</span><br><span class="line"> <span class="comment">/* 此时P指向找到的节点或者NULL */</span></span><br><span class="line"> <span class="keyword">return</span> P;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>以下是插入函数 Insert。该函数首先调用 Find 函数,如果找到了关键词则不需要插入,返回插入不成功的信息;如果找不到关键词才需要插入。插入时,先申请一个新结点 NewCell,然后计算 Key 的地址 Pos,插入成为单链表 Heads[Pos] 的第一个结点。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">Insert</span><span class="params">(HashTable H, ElementType Key)</span> </span>{</span><br><span class="line"> Position P = Find(H, Key);</span><br><span class="line"> <span class="keyword">if</span>(!P) {</span><br><span class="line"></span><br><span class="line"> Position NewCell = (Position)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct LNode));</span><br><span class="line"> <span class="built_in">strcpy</span>(NewCell->Data, Key);</span><br><span class="line"> Index Pos=Hash(Key,H->TableSize);</span><br><span class="line"> <span class="comment">/* 将NewCell插入为H->Heads[Pos]链表的第一个节点 */</span></span><br><span class="line"> NewCell->Next = H->Heads[Pos].Next;</span><br><span class="line"> H->Heads[Pos].Next =NewCell;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"关键词已存在"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>释放 CreateTable 所占用的内存空间可以调用如下 DestroyTable 函数:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">DestoryTable</span><span class="params">(HashTable H)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> i;</span><br><span class="line"> Position P, temp;</span><br><span class="line"> <span class="comment">/* 释放链表的每个节点 */</span></span><br><span class="line"> <span class="keyword">for</span>(i=<span class="number">0</span>; i<H->TableSize; i++) {</span><br><span class="line"> P = H->Heads[i].Next;</span><br><span class="line"> <span class="keyword">while</span>(P) {</span><br><span class="line"> temp = P->Next;</span><br><span class="line"> <span class="built_in">free</span>(P);</span><br><span class="line"> P = temp;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">free</span>(H->Heads); <span class="comment">/* 释放头结点数组 */</span></span><br><span class="line"> <span class="built_in">free</span>(H); <span class="comment">/* 释放散列表节点 */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>分离链接法的删除操作与链表的删除操作相似。不过需要先通过 Hash 函数得到链表的头结点,再在该链表中进行删除即可。</p>
<h2 id="散列表的性能分析"><a href="#散列表的性能分析" class="headerlink" title="散列表的性能分析"></a>散列表的性能分析</h2><p>在上面的介绍中,我们已经用 ASL 来度量散列表的查找效率。查找过程中,关键词比较的次数,取决于产生冲突的多少。产生的冲突多,查找效率就高;产生的冲突多,查找效率就低.因此,影响产生冲突多少的因素,也就是影响查找效率的因素。主要有以下三个因素:</p>
<ul>
<li><p>散列函数是否均匀</p>
</li>
<li><p>处理冲突的方法</p>
</li>
<li><p>散列表的装填因子</p>
</li>
</ul>
<h3 id="线性探测法的查找性能"><a href="#线性探测法的查找性能" class="headerlink" title="线性探测法的查找性能"></a>线性探测法的查找性能</h3><p>可以证明,线性探测法的期望探测次数满足下列公式:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/13.png" class="" title="线性探测法的查找性能">
<h3 id="平方探测法和双散列探测法的查找性能"><a href="#平方探测法和双散列探测法的查找性能" class="headerlink" title="平方探测法和双散列探测法的查找性能"></a>平方探测法和双散列探测法的查找性能</h3><p>可以证明,平方探测法和双散列探测法的期望探测次数满足下列公式:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/14.png" class="" title="平方探测法和双散列探测法的查找性能">
<p>下图表示了上面几种探测法的期望探测次数与装填因子之间的关系:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/15.png" class="" title="平方探测法和双散列探测法的查找性能图表">
<p>由图可知,当装填因子 < 0.5 的时候,各种探测法的期望探测次数都不大,也比较接近。随着装填因子的增大,线性探测法的期望探测次数增加较快,不成功查找和插入操作的期望探测次数明显比成功查找的期望探测次数要大。合理的装填因子应该不超过0.85。</p>
<h3 id="分离链接法的查找性能"><a href="#分离链接法的查找性能" class="headerlink" title="分离链接法的查找性能"></a>分离链接法的查找性能</h3><p>我们把分离链接法中的每个链表的平均长度定义成装填因子,因此装填因子有可能超过 1。</p>
<p>不难证明,其期望探测次数为:</p>
<img src="/2019/12/16/%E6%95%A3%E5%88%97%E6%9F%A5%E6%89%BE/16.png" class="" title="分离链接法的查找性能">
<h2 id="散列的特点"><a href="#散列的特点" class="headerlink" title="散列的特点"></a>散列的特点</h2><p>1.选择合适的 h(key),散列法的查找效率期望是常数 O(1),它几乎与关键字的空间的大小 N 无关,也适合于关键字直接比较计算量大的问题;</p>
<p>2.它是以较小的装填因子为前提,因此,散列方法是一个以空间换时间;</p>
<p>3.散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合于范围查找,或最大值最小值查找。</p>
<h3 id="开放地址法的特点"><a href="#开放地址法的特点" class="headerlink" title="开放地址法的特点"></a>开放地址法的特点</h3><p>1.散列表是一个数组,存储效率高,随即查找;</p>
<p>2.散列表有聚集现象</p>
<h3 id="分离链接法的特点"><a href="#分离链接法的特点" class="headerlink" title="分离链接法的特点"></a>分离链接法的特点</h3><p>1.散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低;</p>
<p>2.关键字删除不需要懒惰删除法,因此没有存储垃圾;</p>
<p>3.太小的装填因子可能导致空间浪费,大的装填因子又会付出更多的时间代价。不均匀的链表长度导致时间效率严重下降。</p>
]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>C语言</tag>
<tag>散列查找</tag>
<tag>散列表</tag>
<tag>哈希表</tag>
</tags>
</entry>
<entry>
<title>堆,哈夫曼树及集合</title>
<url>/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/</url>
<content><![CDATA[<h2 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h2><p>前面介绍过队列,它是一种先进先出的数据结构,队列中没有哪一个元素是有特权的,前面的元素未处理完,后面的只能等待。而本文章介绍的堆(Heap)正是考虑了适合于特权需求的数据结构,因此,堆也通常被称为“优先队列”(Priority Queue)。</p>
<a id="more"></a>
<h3 id="堆的定义和表示"><a href="#堆的定义和表示" class="headerlink" title="堆的定义和表示"></a>堆的定义和表示</h3><blockquote>
<p>堆是特殊的队列,从中取出元素是依照元素的优先级大小,而不是元素进入队列的先后顺序。</p>
</blockquote>
<p>那么我们应该如何组织优先队列的存储结构呢?</p>
<p>如采用数组或者链表实现优先队列</p>
<ul>
<li><p>数组<br>插入:元素总是插入尾部:O(1)<br>删除:查找最大或最小值:O(N)<br>从数组中删除需要移动元素:O(N)</p>
</li>
<li><p>链表<br>插入:元素总是插入在链表头部:O(1)<br>删除:查找最大或最小值:O(N)<br>删除结点:O(1)</p>
</li>
<li><p>有序数组<br>插入:找到合适的位置:O(N)或O(logN)<br>移动元素并插入:O(N)<br>删除:删除最后一个元素:O(1)</p>
</li>
<li><p>有序链表<br>插入:找到合适的位置:O(N)<br>插入元素:O(1)<br>删除:删除首元素或者最后一个元素:O(1)</p>
</li>
</ul>
<p>上面 4 种方式,其最坏时间复杂度都达到了 O(N),而我们知道二叉搜索树的插入和删除操作代价为 O(logN)。因此我们可以利用树型结构来组织数据。</p>
<p><strong>堆最常用放入结构是用二叉树表示,不特指的话,它是一颗完全二叉树。</strong>由于完全二叉树的排列及其规则,因此我们可以使用数组来实现堆的存储。</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/1.png" class="" title="堆的完全二叉树表示">
<p>堆中的元素是按照完全二叉树的层序存储的,还需要注意的是所用数组的起始单元为 1,这样做的目的是更容易从子结点找到父结点。根据完全二叉树的性质,对于下标为 i 的结点,其父结点的下标为 [i/2]。反过来,找结点 i 的左右子结点也非常方便,分别为 2i 和 2i + 1。</p>
<p>堆的两个特性:</p>
<ul>
<li><p>结构性:用数组表示的完全二叉树</p>
</li>
<li><p>有序性:任一结点的关键字是其子树所有结点的最大值或最小值<br>在最大堆(MaxHeap)中,任一结点的值大于或等于其子结点的值,那么根元素是整个堆中最大的;<br>在最小堆(MinHeap)中,任一结点的值小于或等于其子结点的值,那么根元素是整个堆中最小的。</p>
</li>
</ul>
<p>注意:从根节点到任意节点路径上结点序列的有序性!</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/2.png" class="" title="堆示例">
<h3 id="堆的抽象数据类型描述"><a href="#堆的抽象数据类型描述" class="headerlink" title="堆的抽象数据类型描述"></a>堆的抽象数据类型描述</h3><p>以最大堆为例介绍堆的抽象数据类型描述:</p>
<p>类型名称:最大堆(MaxHeap)</p>
<p>数据对象集:完全二叉树,每个结点的元素值不小于其子结点的元素值</p>
<p>操作集:最大堆 H∈MaxHeap,元素 item∈ElementType,主要操作有:</p>
<ul>
<li><p><code>MaxHeap CreateHeap(int MaxSize)</code>:创建长度为 MaxSize 的空最大堆</p>
</li>
<li><p><code>bool IsFull(MaxHeap H)</code>:判断最大堆是否已满</p>
</li>
<li><p><code>bool Insert(MaxHeap H, ElementType X)</code>:将元素 X 插入最大堆</p>
</li>
<li><p><code>bool IsEmpty(MaxHeap H)</code>:判断堆是否为空</p>
</li>
<li><p><code>ElementType DeleteMax(MaxHeap H)</code>:删除并返回最大元素</p>
</li>
</ul>
<p>因此用 C 语言描述最大堆如下:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="keyword">int</span> ElementType;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">HNode</span>* <span class="title">MaxHeap</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">HNode</span> {</span></span><br><span class="line"> ElementType *Data; <span class="comment">/* 存储元素的数组 */</span></span><br><span class="line"> <span class="keyword">int</span> Size; <span class="comment">/* 堆中当前元素个数 */</span></span><br><span class="line"> <span class="keyword">int</span> Capacity; <span class="comment">/* 堆的最大容量 */</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<h3 id="最大堆的创建"><a href="#最大堆的创建" class="headerlink" title="最大堆的创建"></a>最大堆的创建</h3><p>注意到根据用户的输入 MaxSize 创建最大堆时,数组应该有 MaxSize + 1 个元素,因为数组起始单元为 1,元素值存在第 1——MaxSize 个单元中。通常第 0 个单元是无用的,但是如果事先知道堆中所有元素的取值范围,也可以给第 0 个单元赋一个特殊的值 MAXDATA,这个值比堆中任何一个元素都要大。这人 MAXDATA 的“哨兵”作用会在插入操作中用到。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function">MaxHeap <span class="title">CreateHeap</span><span class="params">(<span class="keyword">int</span> MaxSize)</span> </span>{</span><br><span class="line"> MaxHeap H = (MaxHeap)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(struct HNode));</span><br><span class="line"> H->Data = (ElementType*)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(ElementType)*(MaxSize+<span class="number">1</span>));</span><br><span class="line"> H->Size = <span class="number">0</span>;</span><br><span class="line"> H->Capacity = MaxSize;</span><br><span class="line"> H->Data[<span class="number">0</span>] = MAXDATA; </span><br><span class="line"> <span class="keyword">return</span> H;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="最大堆的插入"><a href="#最大堆的插入" class="headerlink" title="最大堆的插入"></a>最大堆的插入</h3><p>最大堆中插入一个新元素以后,新增结点既要保证最大堆仍是一个完全二叉树,结点之间的元素值大小也要满足最大堆的性质,因此需要移动元素。</p>
<p>完成一个元素的最大堆插入操作,只要从完全二叉树的新增结点开始,顺着其父结点到根结点的路径,将路径上各点依次与新元素值进行比较,当一结点的值小于新元素的值,就下移这个结点的元素,直到有结点的值大于新元素的值或者根结点也下移为止,空出的结点位置就是新元素插入点。</p>
<p>插入过程可以用一句话简单描述:从新增的最后一个结点的父结点开始,用要插入的元素向下过滤上层结点。实际上,由于堆元素之间的部分有序性,最大堆从根结点到任一叶结点的路径都是递降的有序序列。插入过程的调整就是继续保证这个序列的有序性。</p>
<p>如下给出了最大堆的插入操作算法。注意到如果新插入的 X 比原先堆中所有的元素都大,那么它将一直向上比较到根结点都不会停止。对于这种情况,我们可以加一个特殊判断,当 i 值取 1 时,直接跳出循环,但是这种程序不够优美。因此之前我们定义了一个“哨兵”,即事先知道堆中所有元素的取值范围,这样可以给 H->Data[0] 赋一个特殊的值 MAXDATA,这个值比堆中所有元素都要大,这样当 i 为 1 时 H->Data[i/2] < X 这个条件肯定不满足,跳出循环。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">IsFull</span><span class="params">(MaxHeap H)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> H->Size == H->Capacity;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">Insert</span><span class="params">(MaxHeap H, ElementType X)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(IsFull(H)){</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"堆已满"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">int</span> i = ++(H->Size); <span class="comment">/* i指向插入后堆中最后一个元素 */</span></span><br><span class="line"> <span class="keyword">for</span>( ; H->Data[i/<span class="number">2</span>] < X; i/=<span class="number">2</span>)</span><br><span class="line"> H->Data[i] = H->Data[i/<span class="number">2</span>]; <span class="comment">/* 上滤 X */</span></span><br><span class="line"> H->Data[i] = X; <span class="comment">/* 找到位置将 X 插入 */</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>算法的时间复杂度为 O(logN)。</p>
<h3 id="最大堆的删除"><a href="#最大堆的删除" class="headerlink" title="最大堆的删除"></a>最大堆的删除</h3><p>最大堆的删除实际上是取出根结点的最大值元素,同时删除堆的一个结点。删除后仍要是一颗完全二叉树,结点元素的大小仍要满足最大堆的性质。因此删除的结点应该是数组的最后一个单元。即取走根结点之后,最后一个结点必须重新放置。确定最后一个结点放置在哪里是最大堆删除的关键。</p>
<p>因此我们可以将堆中的最后一个元素当成假设的根结点,依次与下层的子结点进行比较,如果小于子结点的值,从子结点中选择较大的元素上移一层,直到在某一点上,比较结果是大于两个子结点的值,此时的空结点就是元素要放置的位置。</p>
<p>删除过程可用一句简单的话描述:从根节点开始,用最大堆中最后一个元素向上过滤下层结点。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">IsEmpty</span><span class="params">(MaxHeap H)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> H->Size == <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function">ElementType <span class="title">Delete</span><span class="params">(MaxHeap H)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(IsEmpty(H)) {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"堆为空"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> ElementType MaxItem = H->Data[<span class="number">1</span>]; <span class="comment">/* 取出根结点存放最大值 */</span></span><br><span class="line"> <span class="comment">/* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */</span></span><br><span class="line"> ElementType X = H->Data[(H->Size)--]; <span class="comment">/* 注意堆的规模要减1 */</span></span><br><span class="line"> <span class="keyword">int</span> Parent,Child;</span><br><span class="line"> <span class="keyword">for</span>(Parent=<span class="number">1</span>; <span class="number">2</span>*Parent <= H->Size; Parent=Child) {</span><br><span class="line"> Child = <span class="number">2</span>*Parent; <span class="comment">/* 先将最大儿子设为左儿子 */</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 如果存在右儿子,并且右儿子的值大于左儿子,则将最大儿子设为右儿子 */</span></span><br><span class="line"> <span class="keyword">if</span>((Child+<span class="number">1</span>) <= H->Size && H->Data[Child+<span class="number">1</span>] > H->Data[Child])</span><br><span class="line"> Child++;</span><br><span class="line"> <span class="keyword">if</span>(X >= H->Data[Child]) <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">else</span> H->Data[Parent] = H->Data[Child];</span><br><span class="line"> }</span><br><span class="line"> H->Data[Parent] = X;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> MaxItem;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>其时间复杂度也为 O(logN)。</p>
<h3 id="最大堆的建立"><a href="#最大堆的建立" class="headerlink" title="最大堆的建立"></a>最大堆的建立</h3><p>建立最大堆是指如何将已经存在的 N 个元素按照最大堆的要求存放在一个一位数组里面。主要有如下两种方法:</p>
<ul>
<li><p>通过插入操作,将 N 个元素依次插入到一个初始为空的堆中去,其时间复杂度显然是 O(NlogN)。</p>
</li>
<li><p>在线性时间复杂度下建立最大堆。<br>将 N 个元素按照输入顺序存入二叉树中,这一步只需要满足完全二叉树的结构特性;接着调整各结点的位置,以满足最大堆的有序特性。</p>
</li>
</ul>
<p>我们主要介绍第二种方法:</p>
<p>首先将 N 个元素读入数组,接着从第 [N/2] 个结点(这是最后面一个有儿子的结点)开始,对包括此节点在内的其它前面各节点 [N/2]-1,[N-2]-2,…逐一向下进行过滤,直到根结点过滤完毕,最大堆也就建立起来了。</p>
<p>首先实现向下过滤的函数:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PercDown</span><span class="params">(MaxHeap H, <span class="keyword">int</span> p)</span> </span>{</span><br><span class="line"> <span class="comment">/* 对堆中的第 p 个结点向下过滤,与删除操作类似 */</span></span><br><span class="line"> ElementType X = H->Data[p];</span><br><span class="line"> <span class="keyword">int</span> Parent,Child;</span><br><span class="line"> <span class="keyword">for</span>(Parent = p; <span class="number">2</span>*Parent <= H->Size; Parent=Child) {</span><br><span class="line"> Child = <span class="number">2</span>*Parent; <span class="comment">/* 先将最大儿子设为左儿子 */</span></span><br><span class="line"> <span class="comment">/* 如果存在右儿子,并且右儿子的值大于左儿子,则将最大儿子设为右儿子 */</span></span><br><span class="line"> <span class="keyword">if</span>((Child+<span class="number">1</span>) <= H->Size && H->Data[Child+<span class="number">1</span>] > H->Data[Child])</span><br><span class="line"> Child++;</span><br><span class="line"> <span class="keyword">if</span>(X >= H->Data[Child]) <span class="keyword">break</span>; <span class="comment">/* 找到合适的位置 */</span></span><br><span class="line"> <span class="keyword">else</span> H->Data[Parent] = H->Data[Child]; <span class="comment">/* 下滤 */</span></span><br><span class="line"> }</span><br><span class="line"> H->Data[Parent] = X;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>接着从第 [N/2] 个结点(这是最后面一个有儿子的结点)开始,对包括此节点在内的其它前面各节点 [N/2]-1,[N-2]-2,…逐一向下进行过滤,直到根结点过滤完毕。</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">BuildHeap</span><span class="params">(MaxHeap H)</span> </span>{</span><br><span class="line"> <span class="comment">/* 调整堆中的元素,使其满足有序性 */</span></span><br><span class="line"> <span class="comment">/* 这里假设所有 H->Size 个元素已经存在 H->Data[] 中 */</span></span><br><span class="line"> <span class="keyword">int</span> p;</span><br><span class="line"> <span class="comment">/* 从最后一个有孩子的父结点开始,到根结点1 */</span></span><br><span class="line"> <span class="keyword">for</span>(p = H->Size/<span class="number">2</span>; p>=<span class="number">1</span>; p--) {</span><br><span class="line"> PercDown(H,p);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>该算法的时间复杂度为 O(N)。证明如下:</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/3.png" class="" title="时间复杂度证明">
<h2 id="哈夫曼树"><a href="#哈夫曼树" class="headerlink" title="哈夫曼树"></a>哈夫曼树</h2><p>首先看一个简单的例子,要求编写一个程序将百分制成绩转化成五分制成绩。首先给出一个简单的示例:</p>
<figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span>(score < <span class="number">60</span>) grade = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span>(score < <span class="number">70</span>) grade = <span class="number">2</span>;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span>(score < <span class="number">80</span>) grade = <span class="number">3</span>;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span>(score < <span class="number">90</span>) grade = <span class="number">4</span>;</span><br><span class="line"><span class="keyword">else</span> grade = <span class="number">5</span>;</span><br></pre></td></tr></table></figure>
<p>其判定树如下:</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/4.png" class="" title="判定树1">
<p>如果考虑学生的成绩分布概率:</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/5.png" class="" title="分布概率">
<p>则该判定树的查找效率为:0.05x1 + 0.15x2 + 0.4x3 + 0.3x4 + 0.1x4 = 3.15</p>
<p>如果根据概率修改判定树:</p>
<img src="/2019/12/15/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E2%80%94%E6%A0%913/6.png" class="" title="判定树2">
<p>则查找效率变为:0.05x3 + 0.15x3 + 0.4x2 + 0.3x2 + 0.1x2 = 2.2</p>
<p>由此可见,同一问题采用不同的判定逻辑,计算效率是不一样的。那么是否能够找到最好的比较判定逻辑,使运算效率达到最高?即如何根据结点不同的查找频率构造更有效的搜索树?</p>
<h3 id="哈夫曼树的定义"><a href="#哈夫曼树的定义" class="headerlink" title="哈夫曼树的定义"></a>哈夫曼树的定义</h3><blockquote>
<p>带权路径长度:结点的带权路径长度是指从根结点到该结点之间的路径长度与该结点上所带权值的乘积。</p>
</blockquote>
<p>设一棵树有 n 个叶子结点,每个叶结点带有权值 Wk,从根结点到每个叶结点的长度为 lk,则每个叶结点的带权路径长度之和就是这棵树的带权路径长度(Weighted Path Length,WPL),它可以表示为:</p>
<p>WPL = W1xl1 + W2xl2 + W3xl3 + … + Wkxlk</p>