-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.json
463 lines (463 loc) · 310 KB
/
index.json
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
[
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/lldb-%E5%8D%81%E5%88%86%E9%92%9F%E5%BF%AB%E9%80%9F%E6%95%99%E7%A8%8B/",
"title": "LLDB 快速教程",
"tags": [],
"description": "",
"content": "概述 macOS 默认使用 LLDB 来进行 C/C++ 程序的调试, LLDB 能够逐行调试程序,使开发者能够了解程序的变量值以及堆栈是如何变化的,一旦学会之后使用起来也比 printf 更加方便和简单,赶紧学起来吧。\nLLDB 实现原理 在此之前,请考虑如何实现一个能够监听其他程序(被监听者称为 Client)运行情况的程序(监听者称为 Server)。\n 第一种方式是 Server 拷贝 Client 的代码来模拟 Client 运行,并且在运行的过程中,Server 通过在模拟过程中使用额外的指令从而能够查看和修改 Client 的运行堆栈和数据信息,其中,Valgrind 就是这样实现的。这种方式的优点是无需预先编译 Client 程序,缺点是因为需要运行额外的指令所以 Server 的运行会比 Client 慢很多(Valgrind 大概会会原程序慢 20-50 倍)。\n 第二种方式是使用操作系统的 ptrace 系统调用,这也是 LLDB 的实现方式。ptrace 系统调用可以让 A 进程监听和控制 B 进程的内存和寄存器。ptrace 系统调用有以下几个主要功能:\n 捕获 exec 系统调用并阻止程序的运行。 查询 CPU 的寄存器来获取当前的指令,数据和栈地址。 监听 clone/fork 事件来判断是否创建新的线程。 读取或者修改 Client 内存变量。 也就是说利用 ptrace 系统调用,Client 运行的每一行代码的情况 Server 都能知道。\n常用指令 这里我们先简单列出来 LLDB 的常见指令,接下来的例子会介绍如何使用(其中括号中的为指令的缩写,例如 break main 可以缩写为 b main):\nbreak (b) - 设置断点,也就是程序暂停的地方 run (r) - 启动目标程序,如果遇到断点则暂停 step (s) - 进入下一条指令中的函数内部 backtrace (bt) - 显示当前的有效函数 frame (f) - 默认显示当前栈的内容,可以通过 `frame arg` 进入特定的 frame(用作输出本地变量) next (n) - 运行当前箭头指向行 continue (c) - 继续运行程序直到遇到断点。 示例 C 标准库中的 strlen 函数的作用是找到字符串 s 的长度,例子如下:\n#include \u0026lt;stdio.h\u0026gt; size_t strlen(const char *s) { const char *sc; for (sc = s; *sc != '\\0'; ++sc) /* nothing */; return sc - s; } int main() { // 创建 str 字符串 char str[] = \u0026quot;Hello World\u0026quot;; // 调用 strlen 函数,并把值赋予 length int length = strlen(str); // 在终端打印内容 printf(\u0026quot;The length of str is %d\\n\u0026quot;, length); return 0; } 如果你不熟悉 C/C++ 的话,可能不太理解 strlen 函数的实现方式,这时候就是 LLDB 大显身手的时候了,使用 LLDB 调试以下程序之前,有几个步骤:\n 把上面的例子保存为 test.c\n 在终端运行 gcc test.c -g -o test (这里的 -g 参数保证 LLDB 显示的是源代码而不是汇编代码)\n 终端运行 lldb test,这是告诉 LLDB 要调试哪个程序,没有问题的话,终端会输出:\n /path $ lldb test (lldb) target create \u0026quot;test\u0026quot; Current executable set to 'test' (x86_64). 运行程序 这时候 LLDB 已经在监听 test 程序了,test 的一举一动都逃不过 LLDB 的法眼。最基础的命令是 run,这条指令会开始运行 test 程序。终端会输出:\n1. Process 9782 launched: '/path/test' (x86_64) 2. The length of str is 11 3. Process 9782 exited with status = 0 (0x00000000) 这里,第一行标示了进程的 ID,第二行是 test 程序的输出,也就是 str 字符串的长度。最后的是程序的返回值,在这里 0 则为正常结束。当然,像这样仅仅有一个输出和返回值对我们调试没有什么帮助。因为程序运行得太快一下子就结束了,我们还没有来得及理解这个程序。LLDB 对于 printf 的优点在于可以逐步调试,我们可以选择一行行地运行程序,然后输出我们需要的堆栈信息以及变量值。让我们重新开始,我们先使用 Control + C 退出 LLDB 重新运行 lldb test ,然后运行 break main,这句指令代表我们在 main 函数的开头打上断点,(break 11 也能得到相同的结果,这里 11 是 main 的行号)代表让 test 程序在运行到 main 函数的时候暂停,这时候再次运行 run, 程序就会在 12 行停止。\n(lldb) break main Breakpoint 1: where = test`main + 33 at test.c:12:10, address = 0x0000000100000f01 (lldb) run * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100000f01 test`main at test.c:12:10 9 } 10 11 int main() { -\u0026gt; 12 char str[] = \u0026quot;Hello World\u0026quot;; 13 int length = strlen(str); 14 printf(\u0026quot;The length of str is %d\\n\u0026quot;, length); 15 return 0; Target 0: (test) stopped. 那么 break 指令是怎么实现的呢?为什么可以让程序在特定的地方暂停呢?简单来说:\n1. `break` 指令 会在参数所在地写入一个无效的地址值,在例子中,则是 main 函数。 2. 因为地址无效,所以 `test` 程序运行出错,抛出异常,系统会传送 SIGTRAP 信号给 LLDB。 3. LLDB 这时候可以查看需要的堆栈信息或者变量值。 4. LLDB 把正确的下一条指令重新写入到 test 程序中。 箭头指向的 12 行是下一条要执行的指令,这时候 str 还没进行定义,使用 print 指令来验证。\n(lldb) print *str (char) $0 = '\\0' 要运行 12行 的代码,我们试试 next 指令:\n(lldb) next * thread #1, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100000f15 test`main at test.c:13:18 10 11 int main() { 12 char str[] = \u0026quot;Hello World\u0026quot;; -\u0026gt; 13 int length = strlen(str); 14 printf(\u0026quot;The length of str is %d\\n\u0026quot;, length); 15 return 0; 16 } 再次查看 str 的值:\n(lldb) p *str (char) $1 = 'H' 这时候 str 已经被定义了,指向了 \u0026lsquo;H\u0026rsquo;,这也是我们预料之中。frame variable 用作列出当前所有的变量值。\n(lldb) frame variable (char [12]) str = \u0026quot;Hello World\u0026quot; (int) length = 0 如果要修改某个变量的值,可以使用 expr\n(lldb) expr *str = 'A' (char) $2 = 'A' // 再次查看 (lldb) frame variable (char [12]) str = \u0026quot;Aello World\u0026quot; (int) length = 0 使用 expr 之后,str 的值已经变成 \u0026ldquo;Aello World\u0026rdquo; 了。下一行要运行的代码是 13 行,这个表达式包含了一个函数调用,当运行 13 行的时候,strlen 函数会被压到 test 程序的栈顶,如下图。\n使用 step 进入函数内部(如果使用 next 的话我们会运行到 14 行,这时候 strlen 函数已经执行完毕了)。\n(lldb) step Process 14972 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = step in frame #0: 0x0000000100000e98 test`strlen(s=\u0026quot;Aello World\u0026quot;) at test.c:6:15 3 size_t strlen(const char *s) { 4 const char *sc; 5 -\u0026gt; 6 for (sc = s; *sc != '\\0'; ++sc) 7 /* nothing */; 8 return sc - s; 9 } Target 0: (test) stopped. 看到箭头指向的是 6 行,我们已经进入到 strlen 函数内部了。使用 backtrace 来显示当前的有效函数信息,可以看到我们当前 fram #0 也就是当前在 strlen。\n(lldb) backtrace * thread #1, queue = 'com.apple.main-thread', stop reason = step in * frame #0: 0x0000000100000e98 test`strlen(s=\u0026quot;Aello World\u0026quot;) at test.c:6:15 frame #1: 0x0000000100000f1a test`main at test.c:13:18 frame #2: 0x00007fff6d7e77fd libdyld.dylib`start + 1 从 6行的代码我们可以看到程序一直在 for 循环中运行,每次运行都会判断 sc 是否已经到达 s 字符串的结尾,如果是则停止 for 循环,strlen 函数最后返回 sc 和 s 的距离。strlen 函数结束后,返回值被赋予到 main 函数的 length 中。继续运行 next 命令,箭头指向 14 行,\n(lldb) n Process 15186 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100000f1f test`main at test.c:14:41 11 int main() { 12 char str[] = \u0026quot;Hello World\u0026quot;; 13 int length = strlen(str); -\u0026gt; 14 printf(\u0026quot;The length of str is %d\\n\u0026quot;, length); 15 return 0; 16 } 这时候 length 的值已经更新了,我们可以通过 frame variable 来验证:\n(char [12]) str = \u0026quot;Aello World\u0026quot; (int) length = 11 再次使用 next 命令,终端输出了 The length of str is 11,这也是我们想要的结果。\n总结 LLDB 的基本用法已经介绍结束了,虽然 LLDB 的命令非常多,不过关键的就是 break, next, step, frame, 这几个,更多的使用例子可以参考官方文档:https://lldb.llvm.org/use/map.html\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84/%E6%A0%88%E7%BB%93%E6%9E%84/%E6%A0%88%E7%BB%93%E6%9E%84%E4%B8%8A%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "栈结构(上)",
"tags": [],
"description": "",
"content": "概述 栈绝对是数据机构中最被低估的一类,许多对栈的理解仅仅停留在后入先出这几个字。却不知道这几个字真正蕴含的力量,首先介绍下栈结构,简单来说,当你手上有排序好的编号为1到n的书,当你从编号1开始把这些书放到一个箱子的话,那么这个箱子会变成。\n如果你把书再次拿出来,编号为n的书因为在顶部所以第一本被拿出来,如此类推,手上的书从编号1到n变成编号n到1。\n好吧,这有什么用,这不就是翻转一个队列吗?或者说,原本从头开始取,现在从最后开始取而已。有什么实际作用呢?这是一个非常好的问题。现实生活中我们一般只会将一维数组按顺序放入栈中,就如放书的例子,这样用途确实不大(除了单调栈之外,我们以后会介绍到)。但是当你将图或者树这类相对复杂的结构和栈结合起来的话,那么就能得到非常多的应用方式。而且当每个元素可以根据状态重复放入栈的时候,更加演变出非常复杂的使用方法。例如使用栈来实现 DFS 或者 递归。通过改变元素的状态以及运行顺序就能实现我们需要的算法。这属于比较高级的用法,不过只要你真正理解了栈结构的话,我觉得你也可以轻而易举地把递归改为栈来实现。\n树的遍历 常见的树的遍历类型有四种,前序,中序,后序以及层序,其中前三种非常类似,只是父节点以及子节点位置的相对不同。\n![img](father, child)\n实现方式也有多种,最简单的当然是递归实现,这里给出伪代码:\nfunction preorder(root): if not root: return print root preorder(root.left) preorder(root.right) 前序 [ root ] [ left ] [ right ] 中序 [ left ] [ root ] [ right ] 后序 [ left ] [ right ] [ root ] 为什么使用 stack,如何使用 stack\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E6%A0%88%E4%B8%8A%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "栈(上)",
"tags": [],
"description": "",
"content": "概述 栈绝对是数据机构中最被低估的一类,许多对栈的理解仅仅停留在后入先出这几个字。却不知道这几个字真正蕴含的力量,首先介绍下栈结构,简单来说,当你手上有排序好的编号为1到n的书,当你从编号1开始把这些书放到一个箱子的话,那么这个箱子会变成。\n如果你把书再次拿出来,编号为n的书因为在顶部所以第一本被拿出来,如此类推,手上的书从编号1到n变成编号n到1。\n好吧,这有什么用,这不就是翻转一个队列吗?或者说,原本从头开始取,现在从最后开始取而已。有什么实际作用呢?这是一个非常好的问题。现实生活中我们一般只会将一维数组按顺序放入栈中,就如放书的例子,这样用途确实不大(除了单调栈之外,我们以后会介绍到)。但是当你将图或者树这类相对复杂的结构和栈结合起来的话,那么就能得到非常多的应用方式。而且当每个元素可以根据状态重复放入栈的时候,更加演变出非常复杂的使用方法。例如使用栈来实现 DFS 或者 递归。通过改变元素的状态以及运行顺序就能实现我们需要的算法。这属于比较高级的用法,不过只要你真正理解了栈结构的话,我觉得你也可以轻而易举地把递归改为栈来实现。\n树的遍历 常见的树的遍历类型有四种,前序,中序,后序以及层序,其中前三种非常类似,只是父节点以及子节点位置的相对不同。\n![img](father, child)\n实现方式也有多种,最简单的当然是递归实现,这里给出伪代码:\nfunction preorder(root): if not root: return print root preorder(root.left) preorder(root.right) 前序 [ root ] [ left ] [ right ] 中序 [ left ] [ root ] [ right ] 后序 [ left ] [ right ] [ root ] 为什么使用 stack,如何使用 stack\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84/%E6%A0%88%E7%BB%93%E6%9E%84/%E6%A0%88%E4%B8%8A%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "栈(上)",
"tags": [],
"description": "",
"content": "概述 栈绝对是数据机构中最被低估的一类,许多对栈的理解仅仅停留在后入先出这几个字。却不知道这几个字真正蕴含的力量,首先介绍下栈结构,简单来说,当你手上有排序好的编号为1到n的书,当你从编号1开始把这些书放到一个箱子的话,那么这个箱子会变成。\n如果你把书再次拿出来,编号为n的书因为在顶部所以第一本被拿出来,如此类推,手上的书从编号1到n变成编号n到1。\n好吧,这有什么用,这不就是翻转一个队列吗?或者说,原本从头开始取,现在从最后开始取而已。有什么实际作用呢?这是一个非常好的问题。现实生活中我们一般只会将一维数组按顺序放入栈中,就如放书的例子,这样用途确实不大(除了单调栈之外,我们以后会介绍到)。但是当你将图或者树这类相对复杂的结构和栈结合起来的话,那么就能得到非常多的应用方式。而且当每个元素可以根据状态重复放入栈的时候,更加演变出非常复杂的使用方法。例如使用栈来实现 DFS 或者 递归。通过改变元素的状态以及运行顺序就能实现我们需要的算法。这属于比较高级的用法,不过只要你真正理解了栈结构的话,我觉得你也可以轻而易举地把递归改为栈来实现。\n树的遍历 常见的树的遍历类型有四种,前序,中序,后序以及层序,其中前三种非常类似,只是父节点以及子节点位置的相对不同。\n![img](father, child)\n实现方式也有多种,最简单的当然是递归实现,这里给出伪代码:\nfunction preorder(root): if not root: return print root preorder(root.left) preorder(root.right) 个人来说,我觉得将栈底想象成右边,元素从左边进行插入和获取会比较容易理解。\n前序 ( root ) ( left ) ( right ) ] 中序 [ left ] [ root ] [ right ] 后序 [ left ] [ right ] [ root ] 为什么使用栈,而不是其他数据结构来实现递归。 递归函数一般来说是需要作用于结构体里面的所有对象的,如果对树结构进行递归,那么递归应该对每一个节点都运行一次。(这里我们不讨论使用动态规划或者缓存来简化计算的情况)简单地来说,栈能够修改下一个执行的对象,例如当\n( root ) ] 当我们从栈中获取元素 ( root ) 后,正常的下一个执行的对象应该是空的,但是如果我们在每次执行前对当前元素进行一些操作,例如,将当前节点的右支点和左支点分别压入栈中,那么我们在执行之前就能得到这样的栈,下一个执行对象就变成 root.left 了\n( root.left) ( root.right ) ] 如何使用 stack "
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E6%A0%91%E7%BB%93%E6%9E%84/%E6%A0%91%E7%BB%93%E6%9E%84%E4%B8%8A/",
"title": "树结构(上)",
"tags": [],
"description": "",
"content": "概述 "
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84/%E6%A0%91%E7%BB%93%E6%9E%84/%E6%A0%91%E7%BB%93%E6%9E%84%E4%B8%8A/",
"title": "树结构(上)",
"tags": [],
"description": "",
"content": "概述 "
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/leetcode/",
"title": "Leetcode 算法",
"tags": [],
"description": "",
"content": "算法基础 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%BC%96%E7%A0%81/",
"title": "一文理解字符串编码",
"tags": [],
"description": "",
"content": "在打开网页或者文件的时候,你一定会遇过像这样的字符串乱码问题:\n É��OÇ��,常见的操作系统包括\u0026hellip;\n 同时或多或少也遇到Unicode, UTF-8, ASCII, Latin-1这些编码术语。编码问题可以说是新人必踩坑,虽然从最后的解决方案来看,可能两三句代码就能解决,但是实际大部分开发者,包括我以前,也没有真正地理解它。**原因并不是因为它复杂,而是它涉及了计算机科学中一个常见的问题,理论与工程实现的区别。理论上我们只需要按照A方案就可以解决问题,但是实际上,由于不同语言,不同系统的历史原因,实现的方案就变成多个,许多编程语言的编码实现都不同。**所以要真正地理解字符串编码,首先需要了解计算机的一些基础知识,包括字符串如何存储在计算机硬盘中。如果只是希望靠运气来解决或者避开它,反而会在一次次盲目的尝试中浪费更多的时间。如果你不熟悉Python代码的话,完全可以跳过这篇文章所有的代码段,它不会影响你对这篇文章的影响。\n 基础术语 计算机如何存储数据 ASCII编码 GBK编码 Unicode UTF-8编码 HTML实体编码 URL编码 常见问题 总结 基础术语 字符 字符串 键值表 字符串编码与解码 字符 A B C 天 气 エ ン コ 😁 上面的用空格分割的都是单个字符(Character),它代表对人类能看懂的有意义的语言文字。\n字符串 Hello 天气 Hola 字符串(Strings)就是多个字符组成的集合\n键值表 一一对应的表,函数\ny = x * 2 中每个x都对应着唯一的一个y值,x与y组成的集合就是键值表(Hash Table),例如:\n1 -\u0026gt; 2 2 -\u0026gt; 4 3 -\u0026gt; 6 这里的每一个x(1, 2, 3)都有对应的y(2, 4, 6)\n字符串编码与解码 编码指将字符串按照一定的模式(按照键值表的转换)转换成二进制数字,然后显示或者存储。如果按照上面的键值表,要把字符串\u0026quot;123\u0026quot;存储起来的话,先要转换成对应的\u0026quot;246\u0026rdquo;。因为计算机实际存储的是二进制数据,所以计算机实际存储的值是\n0010 0100 0110 2 4 6 与编码相反,字符串解码(decode)就是把二进制数据按照一定的模式转换成字符串显示。\n计算机如何存储数据 早期的计算机存储资源非常宝贵。所以计算机科学家们希望用最少的空间来存储字符。同时,计算机是使用二进制存储数据的,无论是文字,图片,数字还是其他数据,都是以数字\u0026quot;0\u0026quot;或者\u0026quot;1\u0026quot;存储起来的。举个例子,如果计算机要存储\u0026quot;BEE\u0026quot;这个字符串,它会先根据一个字母与数字的转换表把字母转换成二进制然后存储。这里我们使用一个简单的对应表叫做EngineGo表,EngineGo表用3位的二进制数字就能表示8种不同的字符。\nEngineGo表:\n 二进制 字符 二进制 字符 000 A 100 E 001 B 101 F 010 C 110 G 011 D 111 H 当我们打开文件编辑器,添加\u0026quot;BEE\u0026quot;这3个字母并保存的时候,计算机会根据EngineGo表存储数据\n001 100 100 B E E 当读取文件的时候,计算机会猜测应该使用哪个表来把二进制数据还原为字符串,如果它猜对了,使用EngineGo表还原的话,就会重新得到\u0026quot;BEE\u0026quot;这个字符串。不过如果计算机猜错了,使用其他键值表来打开这个文件的话,最终可能会报错,也可能是乱码,大部分的乱码都是因为这样而出现。理解了这个之后,其实就很容易理解字符串编码和解码了!\nASCII编码 计算机最初由西方国家设计以及发展,理所当然他们使用了英文作为常用的字符集,字符集包括大小写字母,数字加上一些标点符号和运算符号大概120个。3位二进制数字只能表达8个不同的字符,明显不够,最简单的解决方案就是使用更多的位来保存,7位能表示128个字符,加多1位用作错误检查。最后选择使用8位来存储字符,称为一个字节。8位数字对应的表就更长了(为了方便阅读把二进制转换成了十进制):\n 编号 字符 编号 字符 (省略)\u0026hellip; \u0026hellip; 64 @ 48 0 65 A 49 1 66(1000010) B 50 2 67 C 51 3 68 D 52 4 69(1000101) E 53 5 70 F 54 6 71 G 55 7 72 H 56 8 73 I 57 9 74 J 58 : 75 K 59 ; 76 L 60 \u0026lt; 77 M 61 = 78 N 62 \u0026gt; 79 O 63 ? (省略)\u0026hellip; \u0026hellip; 早期计算机科学家们统一用这张表作字符串编码,称为ASCII编码。存储和读取也是像使用EngineGo表一样简单。这时候存储\u0026quot;BEE\u0026quot;就会存储为:\n1000010 1000101 1000101 B E E 非常简单吧。\nGBK编码 ASCII编码只能表示128个字符。遇到中文,常用字就几千个肯定应付不来,其他亚洲语言也遇到这样的问题。所以一开始国内使用的并不是ASCII编码,而是GBK编码。本质其实也一样,一张更大的表,用更多的位来表示字符。GBK的编码方式比较有趣,是可变长度的编码,它为了兼容ASCII编码,使用了单字节编码和双字节编码。 如果遇到一个小于127的字符,那么编码方式就与ASCII表一样,遇到大于127的字符,就用两个字节表示一个汉字。当我们把\u0026quot;BEE\u0026quot;用GBK编码存储的时候,它和使用ASCII表存储的二进制数据是一样的。\n1000010 1000101 1000101 (无论使用ASCII编码还是GBK编码都得到这个结果) B E E # 以Python2为例,encode是Python中编码的方法 \u0026gt;\u0026gt;\u0026gt; foo = 'BEE'.encode('ascii') \u0026gt;\u0026gt;\u0026gt; bar = 'BEE'.encode('gb2312') \u0026gt;\u0026gt;\u0026gt; foo == bar True # 虽然bar使用gb2312编码保存,但是因为只包含ASCII中的字符,所以可以用ASCII来解码 \u0026gt;\u0026gt;\u0026gt; bar.decode('ascii') u'BEE' \u0026gt;\u0026gt;\u0026gt; u'BEE' == 'BEE' True 从上面的输出可以看到,如果只是存储ASCII表出现的字符,那么大部分编码表保存的结果都是一样。都能被ASCII解码,因为它们都需要兼容ASCII表\n不过如果我们要存储中文的时候,就不一样啦,例如存储\u0026quot;你好\u0026rdquo;,GBK编码会把这个字符串编码成\n11000100 11100011 10111010 11000011 \u0026ldquo;你\u0026quot;和\u0026quot;好\u0026quot;这两个字符是用两个字节保存的。 这时候使用ASCII编码保存的话就会报错,因为ASCII编码中没有\u0026quot;你\u0026quot;和\u0026quot;好\u0026quot;对应的值:\n\u0026gt;\u0026gt;\u0026gt; u'你好'.encode('ascii') Traceback (most recent call last): File \u0026quot;\u0026lt;stdin\u0026gt;\u0026quot;, line 1, in \u0026lt;module\u0026gt; UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128) 使用gbk编码就没有问题了。\n\u0026gt;\u0026gt;\u0026gt; u'你好'.encode('gbk') '\\xc4\\xe3\\xba\\xc3' **要注意这里的'你好'带有\u0026rsquo;u'前缀,代表这是一个unicode字符串。这与Python语言有关,如果有兴趣可以从常见问题中找到解答。**当你使用GBK编码保存文件,而文件里面包含了中文字符,那么别人使用ASCII表就无法解码。不过,如果GBK编码的文件只存储ASCII编码出现的字符,那么解码的时候也能正确解码,其实大多数编码问题都很好解决,只需要在文档的信息上面添加这是用什么编码的,打开的时候选择对应的解码就好,但是当文档中没有包含编码信息的时候,计算机就会猜测这是什么编码。很多国家都有自己的编码表,例如日本使用Shift JIS,韩国使用KS X 1001。即使是中文,还有繁体中文,简体中文不同的类别。即使你知道文档里面存的是中文,也不知道用哪个中文编码才能正确打开。\n\u0026gt;\u0026gt;\u0026gt; hello = '你好' \u0026gt;\u0026gt;\u0026gt; hello_gbk = hello.decode('utf-8').encode(\u0026quot;gb2312\u0026quot;) # 使用gb2312编码保存 '\\xc4\\xe3\\xba\\xc3' \u0026gt;\u0026gt;\u0026gt; hello_gbk.decode(\u0026quot;Shift-JIS\u0026quot;) # 使用Shift-JIS解码 u'\\uff84\\u7fb2\\uff83' -\u0026gt; 'ト羲テ' \u0026gt;\u0026gt;\u0026gt; hello_gbk.decode('ascii') Traceback (most recent call last): File \u0026quot;\u0026lt;stdin\u0026gt;\u0026quot;, line 1, in \u0026lt;module\u0026gt; UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128) \u0026ldquo;你好\u0026quot;经过GBK编码变成4个字节,从这4个字节,计算机无法知道它原本是什么编码的,它尝试用日本的Shift JIS编码来解码,这个情况下没有报错,因为Shift JIS中刚好有这4个字节对应的字符串。所以计算机就会猜这个文件是使用Shift JIS编码保存的。如果我们用ASCII表来解码,就会因为找不到对应的字符串而报错。\nUnicode编码 如果你足够聪明的话,最简单解决的方法就是,大家都用统一的表达方式,并且用足够多的二进制位来存储世界上所有字符串。这并不是痴人说梦,Unicode就是为此而生,它把每个国家的每个字符都编进去,最新的版本已经有 136,755个字符串了。例如你好,对应的是 U+4F60 U+597D。不同于之前的其他编码,Unicode不是一种存储方式,只是一个标准,它规定了表现形式,至于如何编码和解码则根据不同的方式,这是什么意思呢?\nUnicode中把每个字符串都定义了对应的表现形式:\n4f60 597d 你 好 Unicode特别的地方,它只指定了表示形式,而存储形式则可以根据需要而选择,如果根据我们之前的方法,每个字符都保存4个字节的二进制(把你好保存为00004f60,0000597d),那么这就称为UTF-32编码。很直观,既然能用4个字节表达所有字符串了,这没有任何技术上的问题。如果大家都使用UTF-32进行编码和解码的话其实就已经解决我们上面的问题了。\nUTF-8编码 新的问题其实是传输量的问题,原本传输\u0026quot;A\u0026quot;这个字符串,使用UTF-32编码的话需要用4个字节。而使用ASCII则只需要一个字节,如果日常生活只需要用英文,加上以前的存储空间非常贵,网络传输速度也慢。西方的国家存储或者传输中平白无故增加三倍的存储量当然不划算。如果把UTF-8编码和Unicode编码直接保存(UTF-32)作对比,UTF-8为了减少存储量,把常用的字符(例如英文字符)用一个字节来表示(其实就是ASCII编码),其他用两到四个字节表示。\n character encoding bits A ASCII 01000001 A Unicode 10011110 01000001(U+0041) A UTF-32 00000000 00000000 00000000 01000001 A UTF-8 01000001 你 ASCII 无法表示 你 Unicode 1001111 01100000(U+4f60) 你 UTF-32 00000000 00000000 10011110 01100000 你 UTF-8 11100100 10111101 10100000(e4bda0) 从上表可以看到,使用UTF-8编码之后,\u0026ldquo;A\u0026quot;还是只需要一个字节存储,而汉字\u0026quot;你\u0026quot;从4个字节减少为3个字节。把Unicode编码转换成UTF-8编码非常简单,我们首先需要一张转换表:\n Unicode范围 转换规则 0x00000000 - 0x0000007F 0xxxxxxx 0x00000080 - 0x000007FF 110xxxxx 10xxxxxx 0x00000800 - 0x0000FFFF 1110xxxx 10xxxxxx 10xxxxxx 0x00010000 - 0x001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 找出要转换的字符的Unicode范围,我们以\u0026quot;你\u0026quot;为例子,它的Unicode编码是U+4f60,对应的是第三列(07FF\u0026lt;4f60\u0026lt;FFFF),找到右边的转换规则。 把4f60的二进制1001111 01100000从右到左填入转换规则的x中,空的填0。1001111 01100000 -\u0026gt; 1110xxxx 10xxxxxx 10xxxxxx -\u0026gt; 11100100 10111101 10100000 最后得到的**11100100 10111101 10100000(e4bda0)**也就是\u0026quot;你\u0026quot;的UTF-8编码了。 很可惜,大部分国内的在线转码工具都把Unicode和UTF-8混淆了,如果你尝试汉字转换成UTF-8编码,工具返回的是它的HTML实体编码。\nHTML实体编码 编码在其他地方也有非常多的应用,日常接触到的还有HTML实体编码,什么时候需要用到HTML实体编码呢?你可以尝试新建一个后缀为.html的文件,内容为\n\u0026lt;!doctype html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;p\u0026gt;\u0026lt;/div\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/html\u0026gt; 保存,然后打开。奇怪,怎么没有把\u0026quot;\u0026lt;/div\u0026gt;\u0026quot;这个字符串显示出来呢?😱😱,因为浏览器把\u0026quot;\u0026lt;/div\u0026gt;\u0026quot;中的\u0026quot;\u0026lt;\u0026quot;和\u0026quot;\u0026gt;\u0026quot;当成标签的开始和结束解析。要解决这个问题呢?只需要把文件内容更改为:\n\u0026lt;!doctype html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;p\u0026gt;\u0026amp;#60;/div\u0026amp;#62;\u0026lt;/p\u0026gt; \u0026lt;/html\u0026gt; 一切正常, :D。浏览器解析HTML的时候会把特殊的字符串理解成非字面的含义,所以当需要显示这些特殊字符串的时候,需要经过下表的转换:\n 显示结果 描述 实体名称 实体编号 空格 \u0026amp;nbsp; \u0026amp;#160; \u0026lt; 小于号 \u0026amp;lt; \u0026amp;#60; \u0026gt; 大于号 \u0026amp;gt; \u0026amp;#62; \u0026amp; 和 \u0026amp;amp; \u0026amp;#38; \u0026quot; 双引号 \u0026amp;quot; \u0026amp;#34; ‘ 单引号 \u0026amp;apos; \u0026amp;#39; ′ 重音符 \u0026amp;acute; \u0026amp;#96; © 版权 \u0026amp;copy; \u0026amp;#169; ® 注册商标 \u0026amp;reg; \u0026amp;#174; ™ 商标 \u0026amp;trade; \u0026amp;#8482; 你 中文 \u0026amp;#x4F60; \u0026amp;#x4F60;或\u0026amp;#20320 好 中文 \u0026amp;#x597D; \u0026amp;#x597D;或\u0026amp;#22909 从上表可以看到,**特殊字符串是强制需要转换的,**而且实际上,所有字符都可以经过转换表示,只需要用\n \u0026amp;#x加上其16进制Unicode编码或者 **\u0026amp;#**加上其10进制Unicode编码 作为实体编号即可,浏览器既能解析字符串本身,也能解析其UTF-8编码。\nURL编码 URL编码其实也简单易懂,因为RFC3986中规定了URI中不能出现\n: / ? # [ ] @ ! $ \u0026amp; ' ( ) * + , ; = **当要表示这些字符的时候,使用%加上该字符的16进制UTF-8编码来表示,**出现非ASCII表的字符也一样,例如在浏览器输入\n https://www.example.com/你好\n 浏览器会自动把不合规定的字符转换成%加UTF-8编码再进行请求(但是在地址栏还是会显示原本的字符\u0026quot;你好\u0026rdquo;),当你粘贴在文本编辑器的时候就可以看到原本的URL变成:\n https://www.example.com/%E4%BD%A0%E5%A5%BD\n 这里可以看到\u0026quot;你好\u0026quot;使用了它的UTF-8编码表示\n常见问题 Python **python2在保存字符串的时候,是直接用终端的默认编码保存的。**一般Linux或者苹果系统的默认终端编码是UTF-8,Windows中文版的默认终端编码是cp936 (gbk)。当前终端的默认编码可以通过以下命令查看:\n\u0026gt;\u0026gt;\u0026gt; import sys \u0026gt;\u0026gt;\u0026gt; sys.stdin.encoding 'UTF-8' # 'cp936'在Windows系统下 我们可以测试一下:\n\u0026gt;\u0026gt;\u0026gt; \u0026quot;你好\u0026quot; '\\xe4\\xbd\\xa0\\xe5\\xa5\\xbd' # Linux/mac系统使用\u0026quot;你好\u0026quot;的utf-8编码保存 '\\xc4\\xe3\\xba\\xc3' # Windows系统使用\u0026quot;你好\u0026quot;的cp936编码保存 \u0026lsquo;u'前缀是Python语言的特色,加上\u0026rsquo;u'前缀的字符串会被当成unicode编码,进行字符串相加的时候要注意,非ASCII编码的字符,unicode编码和utf-8编码不能直接相加:\n\u0026gt;\u0026gt;\u0026gt; u'你好' + '费曼' Traceback (most recent call last): File \u0026quot;\u0026lt;stdin\u0026gt;\u0026quot;, line 1, in \u0026lt;module\u0026gt; UnicodeDecodeError: 'ascii' codec can't decode byte 0xe8 in position 0: ordinal not in range(128) \u0026gt;\u0026gt;\u0026gt; u'hello ' + 'world' u'hello world' 另外一个常见的问题是:\n\u0026gt;\u0026gt;\u0026gt; '你好'.encode('gb2312') Traceback (most recent call last): File \u0026quot;\u0026lt;stdin\u0026gt;\u0026quot;, line 1, in \u0026lt;module\u0026gt; UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128) 留意报错的信息,居然说的是ASCII编码无法解码字符串,为什么呢?其实在Python2中,当它要执行encode方法的时候,需要先把字符串转变成unicode编码,所以:\n\u0026gt;\u0026gt;\u0026gt; '你好'.encode('gb2312') # 实际python会这样处理 | \u0026gt;\u0026gt;\u0026gt;'你好'.decode('ascii').encode('gb2312') 解决这个问题也很容易,把'你好'改成u'你好'就可以了。\n总结 UTF-8编码是现在标准的解决方案,当遇到乱码或者编码出错的时候,先想想原本数据是用什么编码存储的,然后使用对应的方式解码就好。单纯从二进制数据是无法判断它是用什么编码存储的。\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/leetcode/dfs-%E8%A7%A3%E9%A2%98%E6%A8%A1%E5%BC%8F%E4%B8%8A/",
"title": "DFS 解题模式(上)",
"tags": [],
"description": "",
"content": "概述 这篇文章介绍 Leetcode 常见 DFS 问题的解题模式,希望你了解这些模式之后,对大部分 DFS 问题(hard 难度的需要一些变形)都能够迎刃而解。由于 Leetcode 上 DFS 问题中常见的都是无环图,所以我们这里也只讨论无环图的解题模式。阅读本文之前你需要对图的基础知识有一定的了解,包括什么是图?常见的图的类型有那些?(有向无环图,有向有环图),如何遍历图?(前序遍历以及后序遍历)。\n辨别问题 那么什么样的问题可以用 DFS 来解决呢?,DFS 问题常见的表达形式为:\n “给定一个图(树,字符串,矩阵),找到在遍历图的过程中,符合特定条件的数值或路径。”\n 上面的这个定义有点抽象,举两个例子:\n Leetcode 113 Path Sum II\n \u0026ldquo;Given a binary tree and a sum, find all root-to-leaf paths where each path\u0026rsquo;s sum equals the given sum.\u0026rdquo;\n “给定一个有向无环图(二叉树),找到在遍历图的过程中,符合特定条件的数值(路径和等于 sum )”\n Given the below binary tree and sum = 22, input: 5 / \\ 4 8 / / \\ 11 13 4 / \\ / \\ 7 2 5 1 output: [ [5,4,11,2], [5,8,4,5] ] Leetcode 200 Number of Islands\n Given a 2d grid map of \u0026lsquo;1\u0026rsquo;s (land) and \u0026lsquo;0\u0026rsquo;s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.\n “给定一个无向无环图(矩阵),找到在遍历图的过程中,符合特定条件的数值(岛的数量)”\n Input: 11010 11000 11000 00000 Output: 2 解题模式 虽然许多 DFS 问题都可以用 DP 来解决,通常效率也更高。不过 DP 的状态转移方程往往不容易想到,所以在面试的时候,先快速按照解题模式实现 DFS 的解法然后再优化成 DP 也是一个不错的方法。解决 DFS 问题最重要的是四点,1. 防止节点被重复遍历,2. 遍历前,检查节点是否合法,3. 检查遍历后的状态是否符合要求,4. 更新接下来 DFS遍历 的参数,解题模式主要包括三个部分:\n 主函数\n主函数要做的有两件事:第一,处理边界情况,例如图为空,第二,遍历整个图,如果题目要求返回的是布尔值(图中是否存在符合此条件的路径),那么遍历在找到符合条件的路径时就可以结束,**除了这种情况,都需要遍历图中所有可达节点。**若不能通过初始节点访问所有可达节点,那么在主函数就需要对每个节点进行 DFS 遍历。上面的例子 Leetcode 200 Number of Islands,遍历完初始节点后,因为其他节点都是未知状态,所以需要继续遍历。\n # 左侧为遍历初始节点后递归遍历过的点,右侧为仍需遍历的点 110 0 110 0 110 0 000 0 题目 要求 初始节点可以遍历到整个图 Leetcode 113 Path Sum II 返回所有符合条件的路径 是 Leetcode 200 Number of Islands 返回符合条件的路径的数量 否 DFS 递归函数\n这个函数是一个递归函数,里面调用了辅助函数,DFS 函数只需要对当前节点的子节点(如果是无向图则临近节点)进行遍历即可,其他功能通过辅助函数实现。\n 辅助函数\n里面包含了 is_valid 以及 match 两个子函数。分别用作判断子节点是否合法,以及当前状态是否符合条件。\n 具体实现 主函数伪代码\n 从初始节点可以访问所有可达节点(Leetcode 113 Path Sum II):\n function main_function(graph): # 边界情况,例如如果图是空的,或者初始节点本身就符合条件 if graph is empty return empty # 如果需要返回数值则创建变量(例如最大值,最小值),返回路径则创建数组: res -\u0026gt; a variable or array # 只需要遍历初始节点 element -\u0026gt; first element in the graph # 对初始节点进行 DFS 遍历 dfs(element, res) # 返回结果 return res 初始节点不能访问其他可达节点(Leetcode 200 Number of Islands):\n function main_function(graph): # 边界条件,例如如果图是空的,或者初始节点本身就符合条件 if graph is empty return empty res -\u0026gt; a variable or array # 对图里面每个元素进行 DFS 遍历 for every element in the matrix dfs(element, res) # 返回结果 return res 如果题目要求的返回值是布尔值的话,遍历图可以提前结束:\n function main_function(graph): res -\u0026gt; a variable or array for every element in the matrix if dfs(element, res) -\u0026gt; True return True return False DFS 递归函数\n 写代码前,先需要遍历节点的层级关系\n对于有向图来说,某一节点需要进行遍历它的子节点(如果是二叉树则是左右子节点,如果是字符则可能是临近字符)。无向图则遍历临近节点(如矩阵,可能遍历上下左右或者下右节点)。我建议大家在面试实现的时候可以绘制出遍历图,这样写代码的时候会比较有把握。以下是 Leetcode 200 Number of Islands 的遍历流程图:\n上图中,要防止无向图中(1,1)被重复遍历,这里可以使用一个小技巧,先把当前节点的值设为无效值(这样在递归遍历中不会原路返回),DFS 遍历结束再还原。\n 函数实现\n这里有四个重点,1. 防止点被重复遍历,2. 检查节点是否合法,3. 检查更新后的状态是否符合要求,4. 更新接下来 DFS 遍历的参数。以下实现了两种形式,形式一把 match 函数放在子节点的遍历中,这样速度相对比较快,不过主函数需要处理边界情况。形式二则把 match 函数放在 DFS 函数的开头,虽然速度较慢,但是容易实现,以下是伪代码:\n function dfs_first(element, res, current, target, path): # 输入参数中, # element 代表需要遍历的节点 # res 代表保存结果的最终容器 # current 代表当前状态 # target 代表目标状态 # path 代表遍历路径(可选) # 遍历每一个子节点 for each child in element: # 1. 大部分问题中,在同一节点的遍历中都不能重复使用同一节点,所以在无向图中,需要修改图的节点值为非法, graph-\u0026gt;val = unvalid value # 2. 检查子节点是否合法,包括是否已经遍历过,是否越界 if is_valid(child): # 3. 检查子节点与元素组成的新状态是否符合条件 if match(current, child, target): # 更新最终结果 res += new_res else: # 4. 遍历所有合法子节点,更新当前状态以及路径 dfs_first(child, res, current+child.val, target, path+child) # 恢复图的节点值 graph-\u0026gt;val = valid value function dfs_second(element, res, current, target, path): # 输入参数中, # element 代表需要遍历的节点 # res 代表保存结果的最终容器 # current 代表当前状态 # target 代表目标状态 # path 代表遍历路径(可选) # 1. 先验证当前状态是否符合条件 if match(current, element, target): # 更新最终结果 res += new_res return # 2. 遍历每一个子节点 for each child in element: graph-\u0026gt;val = unvalid value if is_valid(child): dfs_first(child, res, current+child.val, target, path+child) graph-\u0026gt;val = valid value function is_valid(child): # 如果 child 合法则返回真,否则返回假 # 例如 child 在矩阵范围中 if 0 \u0026lt;= child.i \u0026lt; length of matrix and 0 \u0026lt;= child.y \u0026lt; length of first row of matrix return True return False function match(current, child, target): # 如果当前 child 与 current 的组合满足题目与 target 的要求,则返回真 if current + child.val equal to target return True return False 原题分析 我们试试在例子中运用此解题模式,第一题:(以下为 Python 代码,形式二只需要把 match 函数移在 DFS 函数开头即可)\nclass Solution: # 形式一 def pathSum(self, root, sum): # 边界情况 if not root: return [] # 边界情况2,因为我们是在遍历中验证是否符合条件,所以要检查初始条件是否已经符合要求 if root.val == sum and not root.left and not root.right: return [[root.val]] # 因为初始节点可以访问所有可达节点,所以只需要遍历初始节点 return self.dfs(root, [], root.val, sum, [root.val]) def dfs_first(self, node, res, current, target, path): # 1. 遍历每一个子节点 for n in [node.left, node.right]: # 2. 检查子节点是否合法,是否已经访问过,是否越界 if self.is_valid(n): # 3. 检查子节点与元素组成的新状态是否符合条件 if self.match(current, n, target): # 4. 更新最终结果 res.append(path+[n.val]) else: # 5. 遍历所有合法子节点,更新状态 self.dfs_first(n, res, current+n.val, target, path+[n.val]) return res def is_valid(self, node): # 只要存在则为真 if node: return True return False def match(self, current, child, target): # 题目要求 child 必须为叶子节点,并且与之前值的和等于 target if not child.left and not child.right and current + child.val == target: return True return False 114 / 114 test cases passed. Status: Accepted Runtime: 60 ms Memory Usage: 19.1 MB 第二题也是类似的方法,形式一以及形式二都能实现,因为形式一以及形式二区别不大,所以这里我选择了另一种特殊的方式,把 match 函数提前。\nclass Solution: def numIslands(self, grid): # 边界情况 if not grid: return 0 count = 0 # 因为岛之间可能并不相连,所以需要遍历整个图 for i in range(len(grid)): for j in range(len(grid[0])): # match 函数 if grid[i][j] == '1': grid[i][j] = '#' self.dfs(grid, i, j) count += 1 return count def dfs(self, grid, i, j): # 遍历可能的子节点 for k, v in [(i+1, j), (i-1, j), (i, j+1), (i, j-1)]: if self.is_valid(k, v, grid): # 我把 match 函数抽离出来放在主函数中了, grid[k][v] = '#' self.dfs(grid, k, v) def is_valid(self, i, j, grid): # 如果 i,j 合法的话 if i \u0026lt; 0 or j \u0026lt; 0 or i \u0026gt;= len(grid) or j \u0026gt;= len(grid[0]) or grid[i][j] != '1': return False return True 47 / 47 test cases passed. Status: Accepted Runtime: 84 ms Memory Usage: 14.1 MB 总结 解决 DFS 问题最重要的是四点,1. 防止节点被重复遍历,2. 遍历前,检查节点是否合法,3. 检查遍历后的状态是否符合要求,4. 更新接下来 DFS遍历 的参数,只要按照这个思路,形式怎么写都没关系。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E7%BC%96%E7%A8%8B%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AD%A5/",
"title": "编程的第一步",
"tags": [],
"description": "",
"content": "编程的第一步 这一章节会讲述编程是什么,如何从零基础开始学习编程,我们不会教特定的编程语言,只会从最基础的计算机基础开始。 "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85python/",
"title": "安装 Python",
"tags": [],
"description": "",
"content": "本章节介绍如何安装 Python 语言 Python 是一门编程语言,在本教程我们会使用 Python 3.5 以上的版本,把它当成普通软件一样下载安装就可以,请根据自己的操作系统浏览以下章节进行安装(Linux用户,我相信你懂得如何安装的 :D):\n macOS安装Python Windows安装Python "
},
{
"uri": "https://www.enginego.org/%E5%BA%8F%E7%AB%A0/",
"title": "序章",
"tags": [],
"description": "",
"content": "计算机基础首页 作者:Windson Yang\n著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。\n在我们建立EngineGirls组织的时候,本意是希望组织定期的线下课程,向女性免费教授计算机编程的知识。她们对计算机很有热情,也希望学习到数据分析,网络爬虫,人工智能等方面的知识,从而运用在她们工作上。不过在教学的过程中, 我们发现课程往往花费了大量时间在解答计算机的基础知识。例如什么是终端(Terminal),路径(PATH),环境变量 (PATH)等术语。另外更重要的是,如果缺乏这些计算机的基础,即使最终学生们可以依葫芦画瓢写出一个应用,也并没有学到真正的知识,因为她们并没有真正的理解自己学到了什么。\n其实在刚接触编程的时候,我也常常被这些术语所困扰,而且在网上也没找到通俗易懂的解释。我希望各位可以把这本电子书当作计算机基础的入门课,令你不用因为专业术语而对编程却步。除此之外,我推荐可以上udacity的初级教程,里面有来自世界顶尖公司的工程师讲解,视频还穿插着精心选择的习题,包括我也在里面学习到非常多的知识,更重要的是,很多课程都是免费的。:D\n学习完这一部分的课程并不能使你成为一位程序员,你不需要成为一名程序员,你可以是会编程的老师,会编程的助理,这点可以使你在行业中更具有竞争力。而且世界上很多问题都没被解决。当你学会编程,用编程的眼光再去看自己平常的工作,或者就能找到一些属于你们行业的独特的问题,去解决它们。\n不要听别人说编程很难,或者你学不会这样的鬼话,相信自己的能力,去尝试,去努力一次。大部分程序员都不是从小就会计算机的,大部分程序员数学都很一般,大部分靠的只是刻苦。最后我引用鲁迅的一段话,共勉:\n 所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。 此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。\n 让我们开始吧 :D\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/",
"title": "常见问题",
"tags": [],
"description": "",
"content": "常见问题 这里包含了计算机初学者常见的问题 "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85%E7%BC%96%E8%BE%91%E5%99%A8/",
"title": "安装 Atom 编辑器",
"tags": [],
"description": "",
"content": "本章节介绍如何安装Atom编辑器 大家常用 Word 书写文档,因为有里面文字排版,添加目录等常用的功能。对应的,我们在编程的过程中也需要,代码高亮,缩进,编译等功能,所以需要有专门的编辑软件。本教程我们使用的是 Atom,当然你也可以选择自己喜欢的编辑器,例如 Pycharm, Vim 等。\n macOS 安装编辑器 Windows 安装编辑器 "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/",
"title": "环境搭建",
"tags": [],
"description": "",
"content": "配置基础开发环境 我们可以把编程当成是在电脑上进行写作,我们需要一些工具使写作效率提高。本教程用到的工具包括编程语言 Python 3.5 以上版本以及 Atom 编辑器,编辑器其实就类似我们平时使用的 Word 或者 Pages 文档工具一样,只不过把书写文字改为书写代码而已。在开始学习之前,请先安装 Python 和 Atom 编辑器:\n安装 Python Windows 安装 Python macOS 安装 Python 安装编辑器 Windows 安装 Atom macOS 安装 Atom "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E5%85%AC%E7%A7%81%E9%92%A5%E5%8A%A0%E5%AF%86/",
"title": "公私钥加密",
"tags": [],
"description": "",
"content": " 简介 对称密码 非对称密码 数字签名 SSH登录 Https传输 API调用 简介 我接触过不少工程师对于对称加密,非对称加密,公钥和私钥只停留在应用的层面,而并不了解背后的原理。所以在开发过程中犯了不少错误,而通常涉及加密传输或者加密存储的错误都比较严重,这篇文章着重介绍了密码学常用的工具以及常见场景。\n对称密码 我们日常接触最多的就是对称密码,它最重要的性质有两点:\n 对称密码中加密和解密使用的是同一个密码。 对称密码中加密后的密文只有该对称密码才能解密。 对称加密有很多名称,例如对称密码,私钥密码,它类似家里的保险柜,把密码设置成9527然后锁上,那么也需要使用9527才能打开。当你要把“芹菜,香菜”利用对称密码“000111”传输给朋友:\n# 原文 芹菜,香菜 # 约定一个密钥(不能被第三方知道) 000111 # 把信息和对称密钥异或运算,得到密文 101100 这时候你可以直接把密文“101100”告诉你的朋友。你的朋友使用约定的密钥“000111”对密文进行再一次异或就能得到原文。实际使用中,加解密不止异或一次那么简单,通常会使用分组密码多次迭代异或。常用的对称加密算法有DES与AES。\n DES AES 密钥长度 56位 128, 192, 256 位 加密方式 对称分组密码 对称分组密码 加密轮数 16轮 128位10轮,192位12轮,256位14轮 安全性 被攻破 安全 速度 较慢 较快 密钥配送 **对称密码最大的问题是密钥配送问题,也就是如何约定只有传输者和接受者都知道并且足够长的密钥“000111”,任何得到这个密钥的人都能够解密信息。**常用的解决方法有两种:\n 使用密钥分配中心\n Sunkist与Cherry想要加密传输信息。\n 密钥分配中心为每个人都生成一个密钥。\n Sunkist的密钥是000000。\n Cherry的密钥是111111。\n 当Sunkist与Cherry准备加密传输信息的时候,密钥分配中心使用伪随机生成器生成一个会话密钥。\n 会话密钥000111。\n 分别用Sunkist与Cherry的密钥来加密会话密钥,并且分别发送给Sunkist与Cherry。\n “Hello, Sunkist,会话密钥通过你的密钥加密后是000111。”\n “Hello, Cherry,会话密钥通过你的密钥加密后是111000。”\n Sunkist与Cherry使用自己的密钥对信息解密,得到会话密钥,然后使用会话密钥进行对称加密传输信息,传输完毕后销毁会话密钥。\n 不过这个方法的缺点也非常明显:\n 必须要有密钥分配中心才能加密传输信息,实际应用中非常不方便。 这个方法同样也会遇到密钥配送问题,要保证第二步中密钥分配中心生成为每个人生成密钥发送给他们的时候不被窃听。 密钥分配中心如果出现问题或者被攻击了。那么公司的所有传输信息都会被破解。 有什么更好地解决密钥配送方法吗?\n 非对称密码 **非对称密码(也称为公钥密码)与对称密码的不同点是非对称密码包含一个公钥和一个私钥。**它有几个性质:\n 公钥和私钥是严格符合数学关系一一对应且不可互换的。 公钥可以公开发布,私钥必须自己保管,不可以泄露。 使用公钥加密的内容只有对应的私钥才能解密,使用私钥签名的内容只有对应的公钥才能解密。 **要注意的第三点,虽然使用同样的分组迭代算法(AES,DES),但是公钥对明文运算称为“加密”,私钥对明文运算称为“签名”。**在RSA算法中,公钥和私钥的数学关系满足:\n公钥 * 私钥 mod L = 1 有些读者看到这个公式,可能会误以为公钥和私钥可以互换,都能用作加密,然后用另外一个解密。而且实际应用中,确实也能互相“解密”。不过,公钥在选取的时候还需要满足一些额外条件。(例如与L互质)所以使用公钥加密才符合严格的数学安全,而私钥“加密”不能。私钥只能用作签名,让持有公钥的人来验证这条信息确实是私钥持有者认证过的,实际用作加密的话安全性没有那么高。\n回到密钥配送的问题,**想象一个场景,Sunkist,Cherry,Wing三个约在一起见面。任意两个人沟通的内容,第三个人都可以听到。而且他们之间只能通过聊天沟通,不能通过肢体语言或者传纸条的方式交流。Sunkist与Cherry要如何传输信息而不被Wing发现?**这是一个非常有趣的问题,在阅读接下来的内容之前,读者们可以先自己尝试去解决这个问题。\n要解决这个问题需要两个步骤,**第一是商议密钥,要在Wing在旁边的情况下约定一个只有Sunkist与Cherry知道的密钥。(很神奇吧)第二是通过密钥把信息进行对称加密传输,这一步就和文章一开始提到的对称加密一样。**为了简单起见,我们假定这个场景中的三人都只会乘法,不会除法。简化版称为Sunkist-Cherry算法:\n 让Sunkist与Cherry都确定一个算法(通常是公开的算法)。\n Sunkist: “我会Diffie-Hellman算法和Sunkist-Cherry算法。”\n Cherry:“我只会Sunkist-Cherry算法,那我们就用这个算法吧。”\n Wing:“我知道你们要用Sunkist-Cherry算法了。”\n Sunkist与Cherry分别选择一个私人数字,这个私人数字必须保密。\n Sunkist: “我不会告诉你我选择了什么数字。”(Sunkist选择了数字5,其他两人都不知道。)\n Cherry: “我不会告诉你我选择了什么数字。”(Cherry选择了数字10,其他两人都不知道。)\n Wing:“我知道你们要用Sunkist-Cherry算法了。”\n 共同选择一个数字,作为公有数字。\n Sunkist: “我们就选择数字2作为公有数字吧。”\n Cherry:“好的!”\n Wing:“你们要用Sunkist-Cherry算法,而且公有数字是2。”\n Sunkist与Cherry分别把公有数字乘以自己的私人数字得到混合数字,然后告诉对方。\n Sunkist: “我的混合数字是10。”(5 * 2 = 10)\n Cherry: “我的混合数字是20。”(10 * 2 = 20)\n Wing:“你们要用Sunkist-Cherry算法,而且公有数字是2。Sunkist的混合数字是10,Cherry的混合数字是20。”\n 还记得我们一开始的假设吗?在这个世界没有人会做除法,如果Wing会除法的话那么它马上就能用混合数字除以公有数字,得到Sunkist与Cherry的私人数字了。幸好,在我们的假设下,它不知道如何计算出私人数字。\n Sunkist与Cherry分别把私人数字与对方的混合数字相乘,得到最终的密钥\n Sunkist把私人数字5以及Cherry的混合数字20相乘得到100。\n Cherry把私人数字10以及Sunkist的混合数字10相乘得到100。\n 整个过程中,公钥是2,最终商议出的密钥是100,Wing虽然知道Sunkist与Cherry聊天的全部内容,知道公有数字与它们的混合数字,但是却无法计算出密钥。\n Sunkist-Cherry算法利用了一个容易运算(乘法)但不可逆(除法)的数学技巧。在实际应用,我们最常使用Diffie–Hellman算法,它利用了基于有限域上的离散对数的性质。Sunkist-Cherry算法与Diffie-Hellman算法解决了当双方只能公开交换信息的时候商议密钥的问题。要注意,在这里的公私钥并不是用作互相加解密的。\n非对称密码除了解决商议密钥的问题,还常常用来解决另外一个问题,**例如使用RSA算法,生成一对一一对应的公私钥,经过公钥加密的内容只有私钥才能解密。通过私钥签名的内容通过公钥才能验证。**在实际应用中,使用公钥加密,私钥解密用在SSH登录以及Https传输中,而私钥签名,公钥解密则用在数字签名中。我们接下来详细说明下。\n数字签名 Sunkist要找Cherry借钱,大家商议立一个借据。不过如果在无法见面的情况下,如何立借据并且证明这个借据是Sunkist写的呢?这里需要用到RSA算法,Sunkist使用RSA算法生成一对公私钥,把公钥公布在自己的网站上,然后把借据通过私钥签名,**所有人都可以通过Sunkist的公钥尝试对签名后的内容解密,能解密成功则证明这个借据是Sunkist写的。**使用数字签名有几个好处\n 无需物理接触 容易验证,只需要用公钥去尝试解密就能验证。 防止Sunkist否认,因为公钥能解密的内容必定是对应的私钥加密的。 不过有一个问题,就是Cherry如何确定这个公钥是Sunkist的,当它要获取Sunkist的公钥的时候,有可能Sunkist的网站被黑了,被换作其他人的公钥了(中间人攻击)。要解决这个问题,我们可以使用数字证书来确保获取正确的公钥。\n数字证书 数字证书的作用就是获取某人的正确公钥,怎么做呢?首先我们需要一个中立可信的证书机构,它有点像注册中心,大家可以把自己的公钥放在它那里保管。当Cherry要获取Sunkist的公钥的时候:\n Sunkist先向证书机构注册自己的公钥。 Cherry向证书机构请求Sunkist的公钥。 证书机构用自己的私钥把Sunkist的公钥进行签名后发送给Cherry,一般来说我们浏览器默认会存储了证书机构的公钥,使用证书机构的公钥把签名后信息解密,然后得到Sunkist的公钥。 使用Sunkist的公钥把Sunkist发送的信息进行解密。 这里有个信任链,我们默认信任了浏览器中的证书机构的公钥,证书机构信任了Sunkist提交的公钥。我们使用证书机构的公钥解密就能得到Sunkist正确的公钥。\nSSH登录 SSH登录有两种方式:\n 使用密码登录 使用公钥登录 SSH在第一次登录主机的时候,会有类似的信息:\nThe authenticity of host 'host (12.18.429.21)' can't be established. RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d. Are you sure you want to continue connecting (yes/no)? 什么意思呢?主机会发送它的公钥指纹,你需要验证这个公钥是该主机的。**确认之后,它们一开始都需要通过Diffie–Hellman算法商议密钥进行加密传输。**然后再进入认证过程,之后的数据传输使用对称加密传输。其中密码登录方式的话直接使用对称加密验证密码。而使用公钥登录分为几个步骤\n 客户端使用非对称加密算法(RSA,椭圆曲线算法)生成一对公私钥对。 客户端把公钥放在服务端的anthority_key文件中。 客户端与服务端建立了加密通道之后,服务端会使用客户端的公钥随机加密一个信息然后发送给本机 客户端用自己的私钥解密,并且返回信息。 服务端认证返回的信息与随机生成的信息是否相同,相同则认证成功。 Https传输 Https和ssl握手的理解这篇文章有详细地解释Https传输的具体过程:\n 客户端的浏览器向服务器传送客户端 SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。 服务器向客户端传送 SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。 客户利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。 用户端随机产生一个用于后面通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。 \u0026hellip;. 我们可以看到,在第三步,客户端就使用数字证书技术来获取网站的公钥。接下来就商议密钥进行对称加密传输。可以说,总的来说,Https传输是先使用非对称加密验证以及商议密钥,再经过对称加密传输数据的。\nAPI调用 当我们使用第三方的API(例如七牛云,腾讯云的人工智能API),第三方为了验证调用是某个用户发起的,会提供一个公钥和一个私钥,要注意,这里的公钥和私钥并不是一一对应的,而是从两对公私钥中,分别取出一对的公钥与另外一对的私钥。\n剩下的公私钥由第三方保存。我们有两种方式使用这对公私钥:\n 客户端调用 我们把公钥放在客户端文件中(js文件,手机客户端),因为公钥是可以公开的,不用担心被窃取。调用的时候通过公钥把信息进行加密发到第三方,第三方用对应的私钥解密。\n 服务端调用 我们把私钥放在自己的服务端中。然后调用的时候通过私钥把信息进行签名发送给第三方,第三方用对应的公钥解密,如果能成功解密则代表验证成功。\n 总结 因为对称加密比非对称加密快得多。在实际应用例如Https传输,API调用,都是使用非对称加密验证以及商议密钥。然后通过密钥进行对称加密传输数据。 公钥和私钥是符合严格数学关系的一对一对应,一个公钥有且只有一个对应的私钥。 公钥加密的信息只有相应的私钥才能解密,私钥签名的内容只有相应的公钥才能解密。 为了保证系统的保密性,虽然公私钥能互相加解密,但是公钥和私钥并不能交换使用。(不能把私钥当成公钥用,把公钥当成私钥用。) 用公钥加密,私钥解密,通常用在数据传输协商对称密钥。或者进行少量信息传输与验证(SSH登录) 用私钥签名-公钥解密通常用在数字签名中。(证明文件是由私钥拥有者认证的。) 无论是对称加密还是非对称加密,密钥都需要保管好,只要其他人获得密钥,就能破解信息。要注意的是,在非对称加密中,公钥是可以公开的,只需要保管好私钥就好。\n参考 《图解密码技术》 《改变未来的九大算法》 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/",
"title": "基础知识",
"tags": [],
"description": "",
"content": "基础知识 这章解释了计算机基础中常见的术语以及常见问题。\n常见问题 一文理解字符串编码 公私钥加密 编程语言选择 如何准备技术面试 如何写一份更好的简历 术语 Ping命令 DNS查询 协议 域名 终端 路径 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87/",
"title": "如何准备技术面试",
"tags": [],
"description": "",
"content": "刚开始参加工作的时候,我对面试总是很恐惧,既担心简历无法通过初筛,也担心即使简历通过筛选,因为面试经验不多以及基础知识不扎实而导致发挥不好。我刚开始拿着普通的简历(专业不是计算机,作品也没多少)也得到了一线大厂的面试机会,但是因为根本没有准备,也不知道怎么准备。自然地,那次面试我表现得不好,也没有得到 Offer。\n恐惧也使我不想浪费时间去面试不同类型的公司。**我当初并不知道对比其他求职者,自己的优势和劣势在哪里,也不知道如何去准备面试,面试官看重的是哪些方面,更不知道自己到底值多少钱。**现在回过头看,我觉得当初只是在欺骗自己,我真正担心的是即使自己认真准备简历和面试也对结果毫无影响,更不敢去想如果放弃某个 Offer,找不到其他工作怎么办。\n工作了几年,当我有了越来越多的面试官的经验之后,我越来越发现认真准备简历和面试是非常重要的,**因为毫无准备就来面试的求职者真的太多了。**而且互联网公司招聘到合适的工程师实在非常难,有计算机基础知识,有项目经验,愿意学习而且愿意来这家公司,实在不好找。所以只要求职者能证明自己有一定的计算机水平并且愿意努力,市场上还是有非常多机会的。\n这篇文章我把这几年作为面试者和面试官身份的的经验給大家,希望大家可以从中学到一些面试的技巧,找到心仪的工作。大家也可以使用 Overseas Rabbit 进行简历 Review 和模拟面试,这样既能节省请假面试的时间,也能根据我们的反馈改善自己面试的表现。这样在真实面试的时候更有把握。\n1. 分析阶段 1.1 公司需要怎样的员工 我很喜欢 Google 前 CEO 施密特分享的一个故事,他刚到 Google 的时候,Google 还只是个小的创业公司。他一开始以为 Google 和其他公司没什么两样,直到有一个周五,拉里佩奇在用谷歌搜索一些关键字的时候,他发现出现了一些不相干的广告推荐(这个情况我们现在在国内最大的搜索引擎也经常看到)。施密特以为接下来就是开几个会议,然后分到具体的工程师手上解决。但是拉里佩奇没有这么做,他用纸条写下\u0026quot;These Ads Suck!\u0026quot;,附上相关的截图贴在布告栏上就回家了。接下来的 72 小时彻底改变了施密特的认知。在周一凌晨 5点,有几位并不是负责广告业务的工程师发来一份邮件,从头到尾阐述了这个问题产生的原因,他们的解决方案,以及这个计划对公司有什么影响。他们从公司的角度去思考,自愿自发地用周末的时间去解决并不属于自己范畴的问题。\n这样的员工我想就是每个公司都需要的:\n 不错的技术能力,工作认真负责,可以及时解决问题,能给公司带来实际效益 出色的团队合作精神,能与团队一起成长 愿意自我学习,投资自己 **所以求职者的简历与面试中必须能体现出这几点品质。**举个例子,要在简历或者面试中展现自己喜欢学习计算机知识,与其笼统地说:\n 热爱计算机,喜欢学习计算机系统的知识。\n 可以改为:\n 喜欢阅读计算机系统的书籍,完成《深入理解计算机系统》80% 以上的习题。并在博客(链接)分享学到的知识。\n 面试官在筛选简历看到的时候就会自然地打开博客,进一步地了解求职者(恭喜你,击败了其他 80% 的求职者)。另外,一些软技能,例如团队合作能力也是面试官非常注重的一点,面试过程中如果被问到有没有带领团队的经验,即使没有也不要简单地回答没有,可以这样回答:\n “我在以往的项目中与团队成员都能融洽相处,并且每个月都会做定期的技术分享互相学习,虽然没有带团队的机会,但是相信自己能够做到。”\n 当然这些回答不可能一下子能想到,面试方面的技巧必须多练。重要的是在职期间定期找几家公司面试练练手,一方面能知道市场的行情,找到其他更好工作机会,另一方面本身自己就有工作,等于手拿一个 Offer,面试的时候就能比较放松。未雨绸缪对于一位工程师尤为重要。等到离职再找工作就比较晚了。\n1.2 员工需要怎样的公司 找新工作之前,求职者需要先认真思考下几个问题,一份工作中你最看重的是哪些方面?\n 薪酬 公司名气与规模 公司福利/工作环境 / 地点 工作方向(假如你要从技术转向管理,这个岗位提供这样的可能吗?) 工程师文化 个人成长 有时候薪酬远不及公司名气与规模 / 工程师文化重要,有的公司能聚集一批优秀的工程师,那么只要认真待一两年,进步速度远比其他地方快,以后跳槽也会更加容易。有的厂则能提供大量隐性的福利(国内可以参考腾讯)。求职者需要真正地去思考自己想要去怎么样的公司。**工作和找男女朋友一样,找你喜欢的,而不是找你能找到的。**前几家公司的选择对你的职业规划会有很大的影响,大公司还是小公司,和你个人的性格或者职业规划有直接的关系:\n大公司 优势\n 入职薪酬较高,每年固定调薪,员工福利例如下午茶,文娱活动,年假都有保证。 通常都有大牛,而且工程师比较多,总能遇到一些志同道合的朋友。 跳槽到小公司比较容易,岗位也能得到提升。 劣势\n 刚进去的时候接手的可能都是比较枯燥的小项目。 可能需要维护几年前的没有文档没有测试的项目。(其实也能学习到很多) 比较容易安逸,缺乏学习的动力。 小公司 优势\n 相对来说,条条框框没那么多,偶尔迟到请假没什么关系。 什么都能学到,从开发到运维到测试。 项目可以加上自己的建议和想法,比较有成就感。 能直接向老板汇报,升职速度比较快。 万一上市了呢?(中国每天有一万家公司注册,上市的嘛\u0026hellip;) 劣势\n 入职薪酬比较低,员工福利嘛,不能保证。 有大牛的可能比较少,除非你事先知道(所以面试问问题非常重要)。 加班压力通常比较大,而且公司不一定会根据你的加班时间就涨薪。 跳槽到大公司比较难,除非你有非常好的简历与能力。 在国内来说,除了一些很 geek 的小公司,一般的小公司并没有那么自由,加班也可能很多。所有我觉得一开始选择大公司往往是不错的选择,之后跳槽的选择范围也更多。了解自己的想法之后,求职者可以根据自己的着重点,筛选公司,修改简历,复习常见面试题以及准备向面试官提问的问题。 最后这部分的提问非常重要,我常常期待求职者在面试结束后能问一些问题,但是很多求职者却没有,一方面怕面试官会觉得自己有很多要求,留下的印象不好,另一方面根本没有想好要问什么。这点我觉得非常不明智,公司和员工就是互相选择的,一定要多提问题,了解公司的文化以及岗位的职责。才不至于刚入职就因为不适应而要离开。这点我们在面试阶段会介绍。\n1.3 了解自己的优势 仔细分析自己的优势是什么,然后在简历以及面试过程中突出,优势可以从这几点入手,后面是面试官的理解:\n 大厂或者大型项目的经验 (能解决项目普遍出现的问题,技术水平靠谱) 作为主要参与者得过比赛名次(聪明,勇于尝试) 毕业于不错的学校(学习努力,认真) 维护优秀的开源项目 (懂得团队协作,喜欢学习,愿意了解项目原理) 发表过论文或者优秀的博客文章(研究能力强,分析能力强) 数据结构和算法基础好,Leetcode 中等难度都能 bug free(基础不错,即使项目经验少,培养起来也简单) 其中,必须根据自己的目标岗位强调自己的优势。例如,如果求职者要面试的是开发工程师,就应该突出项目经验以及对框架的熟悉程度,如果面试的是研究岗位,那么论文与文章的数量就比较重要。\n2. 准备阶段 2.1 随时都在准备 随时准备并不是鼓励频繁地跳槽,而是要有随时有跳出舒服圈的准备,也许求职者已经很满意现在的工作,薪酬,觉得习惯而且安逸。不过如果公司突然倒闭,或者部门被裁减,还能找到这样或者更好的工作吗?我建议各位,每两三个月可以去面试一两家公司,因为你已经有不错的工作了,所以可以带着轻松的心态去面试,同时也可以增加面试的经验。\n2.2 技术知识 基础知识\n基础知识主要包括:算法基础,编程语言基础,计算机网络,操作系统,数据库。\n 算法基础\n**基础的算法题,大厂都会考。**包括基本数据结构了解/实现,例如堆,栈,链表,队列,二叉树。**刷算法题的时候,要把每道题都当成面试题一样按步骤完成,完成一题之后总结经验。这样遇到变形题也迎刃而解。这里我推荐 Leetcode 以及 Hackerrank。**这里说一点题外话,可能有的同学有疑问,觉得这些平常工作都用不到,为什么还要花那么多时间在上面。其实不是的,第一,平常工作都能用到,无论从二分查找到复杂一点的前缀树。开发的过程中如果你知道这些算法/数据结构,就能根据自己的业务来选择最适合的算法/数据结构,减少整个项目的复杂度。 第二,数据结构和算法锻炼的是思维,刷算法题的时候,慢慢会学习到一些有趣的,巧妙的方法。它们能扩展你的编程时思考的范围。同时也要求你考虑到各种不同的边界情况。即使你不准备换工作,我也建议每天都刷一道算法题,日积月累,一年下来你的算法基础一定能比同龄人高出不少。而且当你真正理解算法题的知识之后,写程序 debug 和花在 Stackoverflow 的时间就会大大减少,往往知道哪里可能有问题并且能大幅地增加工作效率。\n 编程语言基础\n这点根据包括你最熟悉的编程语言的运行机制,实现原理。多线程/多进程基础实现,一些容易犯错的地方,网络上都有非常多资源,可以按需学习。\n 计算机网络\n主要考察 TCP/IP 与 HTTP 协议基础,如常见的状态码含义,常见的请求头,响应头,其中隐藏的安全问题,三次握手,四次挥手的原理。TCP 拥堵如何解决等常见问题。可以通过**《图解 HTTP》《图解 TCP/IP》**来快速入门。\n 操作系统\n包括操作系统的内存虚拟化,进程以及线程的基础知识(进程生命周期,进程调度),内核中断机制,线程同步机制,锁,互斥,信号量等。我推荐的是 Operating Systems: Three Easy Pieces(英文版),既学习到操作系统又能学习到英文写作,一举两得 :D.\n 数据库\n常见的事务隔离等级,Innodb的实现原理,索引类别以及优劣,为什么使用B+树结构,如何定位查询的瓶颈以及优化查询,一本**《高性能 MySQL》**基本就够了。 这样看起来要学习的实在太多,的确,这是大学几年下来的重要课程,**所以先通过面试找出自己的弱项然后再进行突击复习,效率会高得多**。\n 2.3 项目经验 公司的过往项目\n国内的技术公司,相对重视项目经验,所以在面试前,曾经参与过的项目需要认真回顾一遍,从技术选型,架构设计(即使是中途加入项目也应该对此有所了解),维护或者实现的功能细节,过程中遇到的技术难点,学到了什么知识,都可能被问到,必须好好准备。\n 开源项目\n开源项目可以让你和世界上顶级的工程师一起工作,学习软件设计以及语言的高级使用方法。同时能让你理解软件是如何运行 / 设计的。\n 参与较底层/偏向算法或研究的项目\n如果求职者未来想从开发转向研究的岗位,那么就可以阅读一些相关学术论文,写相关的文章分析与工具。\n 造轮子,实用工具\n从学习的角度来说,造轮子可以说是最好的方法,不过要给自己一个期限,不能无止境地把时间花费在程序的细节与优化中。知道原理,能够实现就足够了。尝试实现平时常用的 Web 服务器,Web 框架开始,有时间的话可以延展到操作系统或者编程语言(我遇到过这样的求职者)。自己写完再看看别人是如何实现的,学习他的优点。其实到最后,你会发现计算机是越学越容易的,如果你不了解同步异步,往往是因为你不知道 Web 服务器是如何实现,不知道系统调用是如何实现的。当你能自己去实现的时候,很多以前的问题也就迎刃而解了。\n 写论文,分享文章\n如何宣传你的开源项目或者业余项目?写一篇优秀的文章介绍它。同理,要证明你有喜欢计算机,有研究的能力,最好的方法也是写一些优秀的文章以及论文。\n 2.4 准备简历 简历准备可以参考我们的另外一篇文章如何写一份更好的简历,我筛选过超过千份简历,遇到太多太多千篇一律毫无重点的简历,凡描述都是熟练精通xxx框架,凡个人项目经验都是博客加爬虫。而且面试官都知道,越优秀的求职者,越重视自己的简历。求职者需要从面试官的角度来思考与筛选简历,几个要点是要注意的:\n1. 突出优势 简历并不是越长越好,最好的简历长度是一到一页半,列出你最优秀的项目经验以及奖项。至于语言或者框架,只是简单接触过的话就不用写上去了。面试官问你有没有学过其他的时候才说出来。(**假如你只是学过简单接触过 Go 却写在简历上,却被一些基础问题问倒了,这样反而会给面试官留下不好的印象,他会认为你对简历中的其他你真正熟练的语言也不太了解。)**常见的错误写法是:\n 精通 django 框架,熟悉 Python 语言\n 可以修改为:\n 精通 django 框架,是 django 的 Top100 代码贡献者。熟悉 Python 语言,理解 Python 垃圾回收,迭代器,装饰器等常用对象的实现原理\n 多花几分钟的时间,就能在求职者的简历里面脱颖而出了。\n2. 给出证明 前期负责前后端API设计,后期负责实时流消息处理应用系统构建和实现\n 面试官无法知道你做得怎么样,**建议根据“发生什么事”,“你做了什么”,“结果怎么样”三个点来修改。**同时这里必须出现数据作为参考,例如:\n 推动团队转用 Graphql 为新的 API 接口规范,从而减少 20% 的日均请求量,并节省了两台服务器资源。后期负责实现使用 RTSP 协议进行实时流消息处理,经过测试与优化,接口请求响应时间平均为 40ms,同时架构了能支持 50万 日活量的缓存服务器与后台服务器。\n 在项目中做的每一个选择必然是有原因的,而且必然会对项目产生影响。而在简历的项目经验中就是要把你产生最大的影响那部分写上去(删库就不用写了)。如果只是想面试官问到的时候再回答吧,面试的时候紧张,很容易忘记具体的数字以及细节。如果项目经验不多,可以把学校的专业排名(50/1000),员工考评(10/1000),优秀员工这些指标都加上去。不要觉得没有用,这绝对是大多数求职者忽视但是重要的点,这证明了你被学校 / 公司认可,起码比较靠谱。简历中如果既有一些较新的技术(例如 Rust,Go,当然你要真的了解),又有经典的必备的技能,那么就一定能够吸引到面试官的眼球。\n3. 其他能力 其他能力就是团队协作能力以及解决问题的能力,如果你已经在开源项目有不错的贡献,那么面试官就不用担心团队协作能力。至于解决问题的能力,你可以在项目经验中可以列出解决的比较复杂的问题,例如 \u0026ldquo;解决了服务端同时推送 10万 台设备的的并发与资源占用过多问题\u0026rdquo;。这样面试官就知道你既有团队协作能力又有解决难题的能力。面试题就不会出那么难了。 其实很多公司在面试的时候都会出一些非常难的题,并不要求面试者一定要解决,而是要看面试者在遇到难题的时候会怎么面对,是思考一下就放弃,是寻求面试官提示,还是从多个角度去解决问题。如果在简历中已经体现了这一点,那么面试的时候就能略微放松了。\n2.5 模拟面试 这个大家可能接触得比较少,如果你准备去面试一家非常喜欢的公司,面试之前,你应该先进行模拟面试,模拟面试的意思是让另外一名工程师充当面试官,对你进行面试,然后再把面试过程中的优点和缺点反馈给你。模拟面试既可以让你的朋友当面试官来面试你,也可以去找几家有类似岗位的公司。因为当你本来就没有一定要进该公司的想法,那么心态自然就能放轻松,带着轻松的心态去面试的话更能发挥好,**给自己信心,同时也可以问问面试官自己哪里不足,可以加强的。经过总结后,锻炼自己面试的技巧,包括技术的基础,以及如何问问题。**当你面试得多了,会发现问题其实都差不多,下次遇到也知道怎么回答了。平时也可以在一些免费在线网站上面模拟,例如以英文为沟通语言的 Pramp。\n3. 面试阶段 当你得到了面试的机会,开始进入重头戏了,无论你的履历如何出众,都不能对面试掉以轻心。我遇过不少简历不错但是面试一塌糊涂的求职者(很多公司都对伪造简历零容忍),结果当然没有录用他们。起码翻转二叉树要会写吧 :D,面试一般会有几轮:\n3.1 HR 电话确认 HR 会和你聊下天,确保你了解这个岗位的基本信息。也可能问几个关于你简历的问题,这轮只是考核下你的基础信息是否正确,看看你的谈吐是否正常(相信我,很多求职者如果不看自己的简历,连自我介绍都做不到)这轮放轻松,实话实话就好。\n3.2 远程面试(不一定) 这是技术面试的第一轮,可能会通过电话或者视频问一些技术问题,也可能是通过把算法题目发在在线文档,然后让你去解决。一般都是算法,数据结构的基础问题。如果遇到难的也不需要担心,提供解题的思路,即使最后不能 bug free,起码也能向面试官证明你的实力。\n3.3 家庭作业(不一定) 这轮并不常见,有的公司会让你实现一个小模块或者小工具。主要考核你实际情况下的开发能力。这点就要靠平时积累了,如何设计 API,使用什么设计模式,都有讲究。维护好的 commit messages 以及文档都很重要。平时多看看开源项目源码就好。Python 的话我推荐看 Requests 源码,常用而且简单易懂。\n3.4 现场面试 提问 测试用例 思考 阐述 伪代码 代码 检查 面试官会根据简历问一些项目上的问题,例如这个项目为什么要这么设计,开发过程中遇到最大的困难是什么。大厂的话,算法题是跑不掉的,面试官会出几道算法题写在白纸或者白板上。我明白很多求职者不喜欢白板面试,也觉得白板面试没什么意义。不过在我面试的求职者中,白板面试能力强的在实际工作中表现得也比较优异。白板面试确实难,不但对于你,对于其他求职者也是。要是你能做到,别人做不到,你就能在众多求职者中突围而出。 简述一下解算法题的几个步骤:\n出个经典题目 Two Sum:\n Given an array of integers, return indices of the two numbers such that they add up to a specific target. You may assume that each input would have exactly one solution, and you may not use the same element twice.\n 给出一个整数数组和一个目标数,返回两个索引值,它们对应的数组元素的和等于目标数,只有一个答案。\n 例子:\nGiven nums = [2, 7, 11, 15], target = 9, Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1]. 提问 这阶段的提问非常重要,因为你要 100% 地了解题目,才能解决题目。不要觉得提问得多显得愚蠢,提问得多代表你在思考,没有问题我反而会担心求职者是不是之前做过这题,或者根本没有思路。\n 这是一个有序数组吗? 不是(注意这里有个小陷阱,虽然在Example中给出的是一个有序数组,但是实际题目并没说这是一个有序数组,所以要考虑无序以及为空等边界条件) 数组可以包含负数吗? 不可以 如果数组为空或者只包含一个数字,是没有答案吗?对的 时间复杂度和空间复杂度有限制吗?没有 题目就转变成\n 一个只包含正整数的无序数组,要求返回两个不同的数组索引值,它们对应的数组元素的和正好等于目标数,如果数组为空或者只包含一个数字的话没有答案,其他情况有且只有一个答案。\n 这样就能排除一些边界情况了。然后写测试用例\n测试用例 target = 9 # 测试用例 [], [1], [2, 7], [2, 5, 7], [5, 4, 2], 空的,只有一个元素,正序,逆序,正常情况都写下来,面试官会对你考虑到那么多情况而加分。\n思考 先想想会用什么数据结构,链表,哈希表,堆,栈,二叉树,哪个结构能解决这个问题?如果真的没有思路的话,思考了之后,可以请面试官给点提示,这个其实也是团队合作的一种表现,请求提示不一定面试官就觉得你能力不行。\n阐述 边思考边向面试官说出你的思路,虽然你的思路可能比较乱。但是没关系。要大声肯定地说出自己的想法,同时可以向面试官提问,比看着题目 10分钟 不知所措要好得多。我面试过几位求职者,虽然他们没有顺利地完成算法题,但是能一直说出自己的思路,给我留下不错的印象。就像我之前说的,有些难题,面试官并不是期望你都能答对,只不过想知道你遇到难题是如何思考的,所以阐述自己的想法是非常重要的。\n伪代码 如果数组长度小于2,返回False 建立一个哈希表 遍历数组每一个元素: 如果目标值减去元素值在哈希表中 返回该索引与当前索引 否则把当前索引与值添加到哈希表中 时间复杂度为O(n),空间复杂度为O(n)\n代码 这题算简单:\nclass Solution(object): def twoSum(self, nums, target): # 如果数组长度少于2的话,无解 if len(nums) \u0026lt;= 1: return False tem_dict = {} for i in range(len(nums)): # 检测这个元素是否曾经出现过 if nums[i] in tem_dict: return [tem_dict[nums[i]], i] else: tem_dict[target - nums[i]] = i 检查 把测试用例带进去代码中检查,然后看看哪里可能会有问题,做出修改。\n3.5 非技术问题 接下来面试官可能会问一些非技术的问题:\n Q: 为什么选择这家公司?\n A: 面试之前对起码要浏览过公司的网站,了解公司有什么产品,这样既可以防止遇到皮包公司,或者小作坊欠薪拖薪。\n Q: 你曾经面临最大的专业挑战是什么?你是怎么战胜它的?\n A: 这个一定要准备好,不能说没什么挑战,没什么挑战代表你没有认真去思考,就算是最简单的增删改查或者前端的动效,背后的原理,网络协议的原理,你都应该去了解。对你在简历中的每一个项目,你都应该能说出里面最大的挑战,最有趣的部分是什么,这样面试官才能真正理解你在项目中做了什么,学习到什么。\n Q: 是什么为什么你选择离开你现任公司?你从你上一家公司学到最重要的是什么?\n A: 大多数求职者不喜欢这个问题,也不知道怎么回答。这个问题你能回答好的话就能拉出距离了。我觉得答案其实很简单,你们公司的项目有更好的发展前景/我想挑战自己在这一方面的能力等等。至于说旧公司薪酬太低,工时太长,没前途这些就免了。\n Q: 你的长期工作目标是什么?\n A: 这个看个人,转管理的话可能会加一轮问管理方面的问题,转资深工程师的话可以讲下自己打算钻研哪个方向,大数据,人工智能,区块链都可以。\n 3.6 求职者提问 这点非常重要,要预防你到了新公司之后,发现公司文化不适合你,再马上找新工作的话就不好了。\n Q: 你们新老员工的比例是多少?厉害的工程师有多少?研究生的比例有多少?\n A: 这个问题其实揭示了公司的文化,如果新员工非常多,公司也不算新的话,那么代表流动率很高,公司文化可能不是很好。第二个问题其实就是问有没有大牛,有多少。有厉害的工程师总比没有要好得多,进步的速度也更快。一个公司如果有比较多优秀的工程师的话,代表是不错的公司。\n Q: 如果我入职的话,会有入职培训吗?会被分到哪个项目组,项目组的成员构成是怎样?\n A: 这个可以了解公司的架构是不是清晰,个人职责划分是否明确。如果面试官回答不了这个问题,或者支支吾吾的话。即使你进去的话可能要兼顾几个项目,维护老项目。这些都要问清楚,你才知道自己大概的工作量有多少。维护旧项目虽然头疼,但是上线压力不大。如果新旧一起来,就要考虑自己是否适合这样的工作强度。\n Q: 我入职的前三个月,要完成什么工作来证明我的能力呢?\n A: 这个问题其实为下一个问题准备,如果我工作表现优秀的话,公司会不会有对应的奖励?\n Q: 多久进行一次调薪,工作绩效是如何计算的?是按项目收益,还是主管决定?\n A: 这个也是了解公司有没有实施奖励制度,通常回答准备中的都要留个心眼,可能一年都不会调薪。\n Q: 公司的五险一金是按什么比例缴的,是按最低标准还是可以自己缴纳更高比例?\n A: 一般这个会问 HR,如果小公司的话,也可以直接问工程师。五险一金看似没多少,但是每个月累积下来就很多了,这个需要和面试官确认。\n Q: 我今天面试的表现怎样,如果通过之后我还会经过多少轮,怎样的面试流程?\n A: 首先可以了解自己的不足,积累经验。也可以开始为下一轮复试做准备\n 4. 总结阶段 一次面试过来,可能筋疲力尽了。回想下自己哪里可以做得更好,简历哪里可以修改的。统计学告诉我们不要选择第一家面试的公司,多面试几家。不要欺骗自己,认真去思考每家的优点和缺点,和你的好朋友聊聊,寻求他们的建议。如果没有拿到 Offer 也没关系,重复上面的步骤,继续努力。两年前我连想都不敢想到美国的大公司工作,而现在的我就在为 Google 的面试做准备,就算我现在进不了 Google 又有什么关系呢?我还是在准备过程中学到很多知识。我很享受这段时间。相信自己,努力和汗水总会能得到回报的。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/",
"title": "术语",
"tags": [],
"description": "",
"content": "基础术语 一般我们从搜索引擎得到的名词解释都非常难懂,我希望用实际生活的例子来解析这些计算机的基础术语 "
},
{
"uri": "https://www.enginego.org/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/",
"title": "数据分析",
"tags": [],
"description": "",
"content": "数据分析 理解贝叶斯分类 理解贝叶斯分类 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E5%A6%82%E4%BD%95%E5%86%99%E4%B8%80%E4%BB%BD%E6%9B%B4%E5%A5%BD%E7%9A%84%E7%AE%80%E5%8E%86/",
"title": "如何写一份更好的简历",
"tags": [],
"description": "",
"content": "概述 从零开始写简历是一件痛苦的事,因为它既重要又耗时。写得太短的话觉得没诚意,写得太长的话又感觉无从下手。而且大多数求职者没有面试官的经验,没有阅读过其他求职者的简历,所以无法从优秀的简历中学习如何改进自己的简历。我遇过不少求职者写完简历后连自己都不忍心看,错别字连篇,排版混乱就投出去,这也不难怪没有获得面试的机会。加上程序员工资中位数较高,有大量人才从其他专业转来,竞争非常激烈。这篇文章分成两部分,简历中的常见错误以及如何写一份出色的简历,通过分享我们的经验以及技巧,帮助你从众多简历中脱颖而出。\n简历中的常见错误 1. 信息过多,缺乏重点\n信息过多的常见表现是十几行的技能列表, 我举一个血淋淋的例子:\n20 行的技能列表,这位求职者开始就把自己了解的所有工具都列出来,希望能够突显自己的经验和学习能力,但是却适得其反。因为大部分人包括 HR 遇到大段文字都会选择跳读,**不信你回头看看,第 3 行和 14 行其实是一样的。**我们首先要了解 HR 是如何筛选简历的,例如要招前端工程师,HR会先找简历是否有简介这类总结性的板块,看求职者是否符合基本的岗位要求。接下来会在技能列表中搜索 Vue,React,jQuery 等关键字。如果也符合要求,才会认真阅读整份简历的其他部分。平均来说,给每份简历的时间只有不到十秒钟。所以越简洁清晰的简历,HR 反而越有可能认真看。这里还有个小技巧,**投简历不要扎堆在周末投,而应该在平日投。**因为周一堆积了周五未处理完的简历以及周末的简历,是最多简历需要处理的时候,HR 花费在每份简历上的平均时间会相对较少。另外,HR 会倾向于把同一天的求职者当成竞争对手,从中挑选合适的,所以那么简历越多竞争也越激烈。\n大段的技能列表还有另外一个问题,当 HR 发现里面有几行是一些非常基础的技能时,反而会开始怀疑求职者的技术能力。想象下,当你上网搜索到一个 20部最佳喜剧电影榜单。却发现其中好几部都是之前看过的烂片,一点都不好笑。这个情况下,你就会开始怀疑这个喜剧电影排行榜有问题。技能列表也是一样的,基础的技能点越多,被怀疑的可能性也越高。 **那么技能列表应该这么写呢?因为 HR 既可能是工程师,也可能是非技术员工,所以技能列表也应该简短而排版清晰,让外行人也能快速定位技能。**我建议参考这种方式(熟练度从高到低进行排列,但不要强调熟练度):\n 后端框架:Django, Flask, Tornado 前端框架:Vue, React, jQuery 数据库:Redis, MySQL 工具:Docker, Jenkins, Git 其他:HTTP, TCP/IP, WebSocket 外语:大学英语四级,能流畅阅读英文文档 就是如此简短,让 HR 能快速定位到求职者的技能,做出是否看下去的判断。可能有读者会疑问了,这样好像太简单,无法突显我对工具的熟悉程度以及技术能力,这个问题,可以参考下文的简介部分来找到解答。\n2. 无意义描述\n第二个常见错误就是叙述项目经验的时候进行无意义的描述:\nXXX平台\n 根据项目任务要求完成规划工作和按时完成软件开发。 完成爬虫模块,展示模块。 开发后台管理系统,实现自定义分页,第三方登录。 完成数据整理与入库功能。 HR 无法从这样的描述中得到有效的信息,也无法判断求职者的技术能力。项目经验是最能够突显技术能力的地方,应该按照\n 使用什么工具: 使用 Scrapy 开发异步爬虫系统 实现什么功能: 构建 IP 代理池,优化爬虫策略和防屏蔽规则 结果怎么样: 提升 200% 网页抓取速度 三个点来修改,这里的 200% 量化数据是画龙点睛之处。就算没做太多统计和优化,也可以展示 CPU 或者内存负载数据。\nXXX平台\n 使用 Scrapy 开发异步爬虫系统,构建IP代理池,优化爬虫策略和防屏蔽规则,提升 200% 网页抓取速度。 优化项目结构,拆分出核心库、常量库、工具库等公共模块,使用 Vue 框架完成内部管理系统,实现自定义检索,第三方登录,自动化部署等 6个 核心功能。 负责数据的清洗与存储数据到 MySQL 数据库,通过日志分析定位慢查询,通过添加联合索引减少了 50% 数据库查询时间。 我们准备了一些简介/工作经历/项目经验的例句在 awesome-resume,大家在写简历的时候可以用作参考。例句范例:\n 有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读多个开源项目,学习改善代码的健壮性与扩展性。 有良好的代码编写习惯,具有良好的沟通、协作能力能力,有良好的职业道德和较强的工作责任感。 理解操作系统中进程、线程、死锁、虚拟化、文件系统等原理和简单实现。 有大型互联网分布式系统的架构设计和开发经验,拥抱新技术,有很强的学习能力。 有扎实的计算机理论基础,良好的算法与数据结构基础,了解计算机基本原理与常见机制。 如果你写完不知道写得怎样,也可以请我们帮忙简历分析 (https://osjobs.net/co/)。重要的是,从现在开始做数据统计,养成先测量再优化的开发习惯,尝试去发现和解决性能瓶颈。\n3. 排版杂乱,错别字多\n错别字绝对是零容忍,如果求职者连自己的简历都不重视,那么 HR 更不会重视。分享几个喜闻乐见的例子:\n 熟悉iOS发布上架流程,真机调戏。 熟悉 mysql 数据库,了解 MySQL 基本原理(术语前后不符) 还有的简历字体极小(因为内容太多,又要塞在两页中),行距小,难以阅读。中英文之间空格混用,模块之间没有明显的分隔,让 HR 找不到想要的信息。有些招聘网站并不能完全正确渲染 PDF 文件。(例如拉勾网,遇到 PDF 文件翻页就会有大片空白出现。),我找了两个真实的例子放在下面,你问为什么海投没有回应,HR 问你这样的简历怎么看。\n写完简历之后一定要自己仔细检查,再请一位朋友看一遍,请求中肯的建议。在排版上我建议大家用 HTML 写然后转成 PDF,HTML 比 Markdown 和 Word 格式更好进行样式与版本控制。读者可以使用我们的免费 HTML 模版和付费 HTML 模版(均通过热门招聘网站测试),确保自己的求职简历能更好呈现在 HR 面前。模版示例:\n如何写一份出色的简历 一份好的简历总体可以分为以下几个板块:\n 基本信息 个人简介 技能列表 工作经历 项目经验 教育背景 其他 这个顺序能让 HR 从浅到深快速了解求职者的优势与技能。(刚毕业的工程师或者有出色教育背景的工程师,可以把教育背景放在基本信息后面。)\n基本信息 姓名/电话 如实填写即可,要注意的是,投简历之前可以先在全国企业信用信息公示系统或者天眼查查询该公司是否真实存在,是否存在严重失信或者法律诉讼,有些猎头会随便起一个公司名字,专门接受简历然后再联系求职者,不小心投递了就出卖隐私了。另外如果公司涉及太多的劳动仲裁诉讼,那么也是一个值得考虑的地方。\n邮箱 写常用邮箱即可,论坛上,偶尔会讨论使用什么尾缀的邮箱看起来比较厉害,有的建议用 gmail,觉得能突显英语能力。有的建议使用 Outlook 或者 163,在国内能比较稳定地接受邮件。也有鄙视 qq 邮箱的,觉得太私人并且显得不专业。某些 HR 确实会因为邮箱尾缀而对求职者的初步印象产生影响。不过,只有在求职者的简历没有亮眼点,结构混乱的前提下,HR 才会因为这些小细节而筛掉他们。只要求职者在简历中展现出自己的优点,什么邮箱尾缀都不重要。\nGithub / 博客 放上 Github 或者博客链接的前提是它能突显求职者的编程能力,如果 Github 既没贡献过开源项目,一年就 commit 了几次的话就不要放进去了。如果没写过技术博客,或者很久没更新的话,我建议在准备面试的这段时间,每周根据复习的主题写一篇总结性的博客。这样一方面能够通过写文字强化理解复习的内容,为技术面试做好准备,另一方面也能作为简历的加分项。\n求职意向 这个细节很多求职者会忽略,一名 HR 可能会接受不同职位的简历,如果简历上没有注明应聘的职位,HR 也就不知道怎么去判断求职者合不合适了,最简单的方法就是直接看下一份简历。所以求职意向必须说清楚自己的目标岗位,例如前端工程师,Java 开发工程师。\n其他信息 其他信息包括政治面貌,性别,年龄,照片等。如果要投国企或者事业单位,党员的政治面貌可能有帮助,至于其他加不加没什么关系。合起来,基本信息部分可以像这样写:\n 张学礼 邮箱: [email protected] | 电话: 133-5555-6666 Github: http://github.com/abc | 求职意向: 爬虫工程师 简介 简介非常重要,也是HR开始了解求职者的第一步。在这里要体现出自己的优势。举个例子,在简历我常常看到这样的描述:\n 热爱计算机,喜欢学习计算机系统的知识。 如果你也这样写,那么完全体现不出自己的优点,因为这就像一句例句一样,没有任何自己的东西,所以需要根据自己的情况具体修改:\n 喜欢阅读计算机系统的书籍,完成《深入理解计算机系统》80% 以上的习题。并在博客(链接)分享学到的知识。 如果能够引导 HR 打开博客,进一步地了解求职者的话已经击败了大部分的求职者了。如果一开始不知道从何下笔的话,可以根据目标职位的岗位要求来参考,而且当看到目标职位要求熟悉多线程编程而自己不太理解的时候,也是一个非常好的复习机会了,面试绝对考啊。这里我们假设求职者喜欢的一家公司的岗位描述与要求如下:\n Python 爬虫工程师 岗位描述: 负责爬虫系统架构设计和开发; 参与设计系统技术方案,核心代码开发和系统调优; 参与各专项技术调研,新技术引入等; 岗位要求: 2年 或以上 Python 开发经验,本科及以上学历,计算机相关专业; 热爱计算机科学,精通 Python 语言,熟悉正则表达式,熟悉 MySQL 数据库; 熟悉 Python 网络编程,能够设计和维护基于 TCP/IP 协议的高性能事件驱动框架程序; 有强烈的求知欲,优秀的学习和沟通能力; 先分析下这个岗位的要求,1) 负责开发爬虫系统,2) 需要 Python 以及 MySQL 开发能力,3) 熟悉网络要求,对网络协议有了解。抓住这几点之后,我们就可以针对这个岗位写简介:\n 两年 Python 后端开发经验,熟悉 Scrapy 框架,熟悉 HTTP 协议、TCP/IP 协议,了解正则表达式,XPATH的用法,了解 Redis,MySQL 数据库与 Linux 系统的常见机制与原理。作为主力工程师参与设计与开发过多个项目,负责系统核心模块的开发,自动化测试与部署。有优秀的学习能力和团队沟通能力,过去一年中与团队十多次进行技术分享,主题包括 Python 协程原理分析,Python 性能优化技巧等。\n 简介分为三部分,第一部分展现自己的符合岗位要求的专业知识与技能列表,第二部分简单介绍之前主要工作职责(有爬虫开发,自动化测试与部署经验)。第三条列出软技能,以与团队进行技术分享为例子,突显出团队合作的能力。\n如果是非科班或者萌新工程师的话尽量从个人项目,学习能力以及软技能突显自己的能力。\n 两年独立开发 Web 项目经验,熟悉 HTTP 协议、TCP/IP 协议,了解 Redis,MySQL 数据库与 Linux 系统的常见机制与原理。了解项目开发流程及自动化部署,设计以及开发了 Todo-list,博客等项目,实现了浏览,评论,点赞等功能。热衷学习计算机技术,自学了计算机系统,数据结构等多个计算机课程。\n 非科班工程师要与科班工程师竞争,最好自己有做过岗位类似的项目。优质的个人项目也是加分项。博客,爬虫,Todo-list 这些实在太常见了,我不是说它缺少技术含量(可以看看 Python 作者 Guido van Rossum 写的爬虫),只是太多求职者都只完成非常简单的功能,没有深究原理。如果能完成一些与众不同的项目的话,那么就能吸引到HR的眼球了。我们整理了一些免费的项目实战项目 (https://github.com/resumejob/free-project-course)供各位选择。试想下,如果上面的简介改为:\n 两年 Python 后端开发经验,了解 Scrapy 爬虫框架,设计开发了简单的浏览器,实现语法解析,编译功能。开发了一个搜索引擎和社交网络,实现了搜索,关注功能。熟悉 HTTP 协议、TCP/IP 协议,正则表达式,XPATH 的用法,了解 MySQL 数据库 与 Linux 系统的常见机制与原理。热衷学习计算机技术,自学了计算机系统,数据结构等多个计算机课程。 是不是非常不一样了,HR也会愿意给机会这样特别的求职者。写简历的过程其实也是自我反省的过程,从中你能知道自己哪里不足,及时地弥补与学习,才能得到好的 offer。\n 有些工程师会有一些误区,他们觉得如果循规蹈矩地写简历,只会吸引到传统的,无聊的公司。我遇过不少在简介中写热爱自由,热爱生活,我想大多数人都喜欢自由,单纯写热爱自由并不能展现出真正的热爱,在计算机领域最好证明的方法就是使用自由软件与贡献开源项目。恭喜你,第一步的简介完成了。你抓住了 HR 的眼球,接下来的话就是要展现自己的能力。\n技能列表 像我在常见错误所指出,HR会直接在简历中搜索关键字,如果没有的话就会直接筛掉。所以技能列表可以按照类型把自己最擅长的工具列上去,熟悉度因为见仁见智,所以不用写,或者用进度条表示就好:\n 后端框架:Django, Flask, 前端框架:Vue, React, jQuery 数据库:Redis, MySQL 工具:Scrapy, Docker, Jenkins, Git 网络协议:TCP/IP, HTTP, Websocket 外语:大学英语六级,能流畅阅读英文文档 工作经历 如果你没有工作经历的话,这个模块可以跳过。一个要注意的点是工作经历的完整性,我有一次去 BAT 其中一家面试的时候(我常常去面试,参考如何准备技术面试),他们问我为什么没有把完整的工作经历写上去,因为他们要求从毕业到现在的时间不能出现空白期。我回答说一方面是保持简历的简短,另外一方面是最近的工作经历与这个岗位比较有关。不过这也是我仅有的一次被要求填写完整的工作经历,所以我的建议是简历上最好写上最近 2-3 间公司的工作经历,而且面试问到之前的工作的话要能正确地回答,并且连接所有时间点,不要让面试官觉得你在隐瞒什么。工作经历应按照最近的工作倒序列出,可以分为四点:\n公司名称 写上公司全称即可,如果产品比较出名但是公司不太出名的话,也可以把产品名加在后面:\n 独角科技有限公司(旗下产品 EngineGo) 岗位/在职时间 岗位 title 要注意,如果投的岗位是数据分析工程师,那么之前曾经担任爬虫工程师还是数据挖掘工程师对 HR 来说就不一样了,高级工程师的话也要加上去。在职时间要根据社保缴纳的时间写,有些厂会做背景调查的。\n 独角科技有限公司(旗下产品 EngineGo) 2014年6月-2016年6月 | 高级 Python 开发工程师 工作经历 工作经历突显的是在职的职责以及给公司带来什么效益,与接下来的项目经验不同,不需要详细写技术栈和项目细节,这里举一个我们例句中的一个例子:\n 作为组长负责设计和开发分布式网络爬虫系统,优化爬虫策略和防屏蔽规则,提升网页抓取的效率和质量。 根据行业需求分析设计方案可行性,对项目代码进行测试优化,协助持续集成与自动化部署,提高系统可用性。 负责 EngineGo 爬虫系统技术文档的编写以及维护,定期 review 团队的代码,定期组织团队技术分享。 项目经验 项目经验可以放在对应的工作经历里,每间公司选 1-2个 项目重点介绍即可。**需要详细描述主要开发或者维护的模块,使用了什么工具,以及达成的效果如何,这里以 EngineGo 爬虫系统为例,注意简洁和突出数据,**不要进行无意义描述,同时关键字应该加粗。\n 与产品经理保持沟通,使用 Scrapy 框架对爬虫模块进行重构,提高 200% 爬虫速度并减少服务器 20% CPU负载。改进爬虫策略,降低 40% 被屏蔽的请求数。 作为主要工程师设计以及开发物业模块,活动模块,实现报名,即时通知等 10个 功能。 使用Redis数据库实现分布式爬虫与数据缓存,减少 50% 数据查询时间。 与其他工程师合作,使用 Docker 对项目进行拆分重新架构, 减少业务模块之间的资源耦合, 实现持续集成与自动化部署。 大部分情况下,不建议简历中出现项目的图片,更好的做法是在不影响排版的前提下附上项目链接。\n教育情况 学校大家都会写,要注意的有几点,如果就读 211 / 985 等学校可以把学校放在前面,简介之后。另外,我碰到不少转专业的求职者直接不写原本的专业了,我觉得这毫无必要。HR 也不是傻的,看没写专业就知道是非科班的,还不如老老实实写下来。**高绩点 / 专业课分数高 / 奖学金 / **比赛获奖可以选重要的加上:\n XXX大学 | 计算机科学 2013年- 2017年 计算机系统(85分/专业排名18/100),数据结构(90分/专业排名10/100) 绩点:3.7 | 获得一次国家励志奖学金 2015-2016学年获得美国大学生数学建模竞赛一等奖 2013-2014学年获得广东省“砺剑杯”科技创新大赛二等奖\n 好吧,如果我的学校非常一般,专业也不对口怎么办。我们参考这篇文章的统计,HR看重求职者简历的哪些部分,来自好学校,好公司这不用说,完成 Udacity / Coursera 等课程也有加分。求职者可以像这样展示自己的教育情况。\n XXX大学 | 土木工程 2013年- 2017年\n Udacity | 机器学习工程师 Coursera | 计算机导论 | 操作系统 2016年- 2017年\n 我不敢说在国内的环境,这一定能加很多分,毕竟大多数公司看重的还是大学的学历。不过,一些比较开放的公司还是喜欢这样不断学习的员工。加上去,起码不会扣分。\n其他(可选) 可选项,也有可能成为加分项,国内比较少看重这点,不过我个人还是比较注重的:\n志愿者工作 协助组织翻译 Flask,Requests 第一版本文档,翻译十多篇技术文章(侧面突出了外语能力比较好)。 教导初中生从零开始学习 Python,并设计并编写自己的游戏。 其他项目 可以把自己的开源项目或者工作经历的次重要项目简单概括,例如:\n Cherry (https://github.com/Windsooon/cherry): 基于 sklearn 实现的一款具备高精确率,召回率的文本分类器,开箱即用,支持多种自定义算法以及可视化功能。 综合上面的几点,一份好的简历看起来应该是这样的,模版可以在 https://osjobs.net/co/ 找到:\n总结 这篇文章讲述的简历技巧面向的是国内的公司,如果投向外企的话,不是直接翻译那么简单,有兴趣了解的读者可以参考 Programmer Resume,里面提供了不少有用的资讯。总的来说,只要认真去修改简历,HR 是能感受到的,我希望各位看了这篇文章后能写出更好的简历,也能从众多求职者中脱颖而出,得到更多机会。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E7%88%AC%E8%99%AB/",
"title": "爬虫基础",
"tags": [],
"description": "",
"content": "爬虫基础 从零开始学习计算机爬虫 "
},
{
"uri": "https://www.enginego.org/",
"title": "编程超能力入门班",
"tags": [],
"description": "",
"content": "序章 作者:Windson Yang (Github, Blog)\n商业转载请联系作者获得授权,非商业转载请注明出处。有任何疑问欢迎联系我(wiwindson at outlook.com)\n在我们建立刚 EngineGirls 组织的时候,本意是希望组织定期的线下课程,向女性免费教授计算机编程的知识。她们对计算机很有热情,也希望学习数据分析,网络爬虫,人工智能等方面的知识运用在她们d的学业或者工作上。不过在教学的过程中, 我们发现课程往往花费了大量时间在解答计算机的基础知识。例如什么是终端(Terminal),路径(PATH),环境变量 (PATH)等术语。另外更重要的是,如果缺乏这些计算机的基础,即使最终学生们可以依葫芦画瓢写出一个应用,也并没有学到真正的知识,因为她们并没有真正的理解自己学到了什么。\n其实在刚接触编程的时候,我也常常被这些术语所困扰,而且在网上也没找到通俗易懂的解释。我希望各位可以把这本电子书当作计算机基础的入门课,令你不用因为专业术语而对编程却步。除此之外,我推荐学习 Udacity 的初级教程,里面有来自世界顶尖公司的工程师讲解,视频还穿插着精心选择的习题,包括我也在里面学习到非常多的知识,更重要的是,很多课程都是免费的。:D\n虽然学习完这一部分的课程并不能使你成为一位程序员,但是你不需要成为一名程序员,你可以是会编程的老师,会编程的 HR,这点可以使你在行业中更具有竞争力。而且世界上很多问题都没被解决。当你学会编程,用编程的眼光再去看自己平常的工作,或者就能找到一些属于你们行业的独特的问题,去解决它们。\n不要听别人说编程很难,或者你学不会这样的鬼话,相信自己的能力,去尝试,去努力一次。大部分程序员都不是从小就接触计算机的,大部分程序员数学都很一般,靠的只是刻苦。最后我引用鲁迅的一段话,共勉:\n 所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。 此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。\n 让我们开始吧 :D\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E9%9D%A2%E8%AF%95-101/",
"title": "系统设计面试 101",
"tags": [],
"description": "",
"content": "求职系列文章:\n 如何准备技术面试 如何写一份更好的简历 程序员国外求职指南 面试流程 这篇文章并不是教读者如何构建一个高可用的系统,而是着重于阐述系统面试的具体流程以及常见的错误,系统设计流程总体可以分成 5个 部分,总时长约一个小时\n 确定范围(5分钟) 粗略计算(5分钟) 整体架构(5分钟) 组件架构(20分钟) 架构优化(20分钟) 1. 确定范围 系统面试开始之时,面试官可能只会提出一个问题:\n “如何设计 Uber 这样的应用?”\n“如何设计 Whatsapp 这样的应用?”\n 如果你刚参加工作或者刚毕业,会对如何设计一个这样的系统束手无策,要设计高可用,易扩展的系统需要广阔的计算机知识以及大量的项目经验累积,也往往需要一个团队配合设计以及多个版本的迭代。所以,面试官并不期待你可以在一小时内把 Uber 或者 Whatsapp 的全部功能都设计出来。系统设计面试真正考核的是\n 把抽象问题转变成实际工程中能够解决的简单问题 利用自己现有的知识设计一个基本可用的系统 观察现有方案可能出现的瓶颈并提出解决方案 第一阶段,确定范围就显得特别重要了。我们需要先与面试官讨论该系统\n 需要实现的功能点 用户量,数据存储量,两者的增长量以及其他限制条件 1.1 需要实现的功能点 如果你之前没有使用过这些应用(Uber, Whatsapp),不要害怕通过提问来确认功能点。(这并不奇怪,2016年,美国有三分之一人没听过 Uber 或者 Lyft)。用来确认功能点的问题有:\n 用户可以使用这个应用做什么?\n这个应用有什么与众不同的功能?\n 设计架构本身花不了多少时间,我们值得投资更多时间在了解清楚问题中。切记,不要没有理解问题就动手设计。本文以设计 Uber 为例,Uber 包括多项功能,前端工程师关注显示地图,司机位置更新,日志传输。后端工程师关注车辆预订(包含定价,派单,司机信息),费用支付,用户评价,日志分析。我们先把想到的功能点都列在白板上,这里以后端工程师为例:\n第一步,我们从中选出几项主要功能,在 Uber 中,车辆预订功能最为重要,确定好之后与面试官进行确认:\n “我先从这些功能开始进行设计可以吗?”\n 我们必须经过面试官对设计的功能点认可才能进行接下来的系统设计。\n1.2 用户量,数据存储量,两者的增长量以及其他限制条件 接下来我们需要了解\n “每秒有多少用户预订车辆?有多少数据需要存储?用户每个月的增长量是多少?对响应时间以及服务器数量有没有限制?”\n 面试官可能会助攻一把:\n “车辆预订系统每秒有 1万次 请求,每天有 100G 数据需要存储,用户每月增长 10%,服务器不能超过 20台。”\n 我们在感谢面试官之后把这些重要数据记录在白板。不过面试官也可能简单地回答:\n “每天有 100万 人次预订车辆,数据库需要存储每次预订的所有信息,包括订单号,起点,终点,路径经纬度等。”\n 在不清楚所需数据范围的时候,需要进入粗略估算阶段\n2. 粗略估算 2.1 吞吐率 粗略估算是每名工程师都需要学习的技巧,它帮助我们把现有的数据转换成项目需要的数据。吞吐率是衡量服务器性能的一个常用指标,意思为服务器每秒钟能够处理的请求数量。Uber 每天有 100万 人次车辆预订,我们可以假定每次预订客户端发送两个 HTTP 请求,加上一些背景知识:\n 每天有 86400 秒 80% 的请求出现在 20% 的时间中 上下班时间请求为平时的三倍 我们能计算出 18000秒 需要处理 160万个 请求,平均 88 req/s。从而在高峰期,车辆预订功能每秒大约需要处理 300个 请求。**那么要达到 300 req/s 的吞吐率需要多少台服务器支撑呢?**答案根据业务的复杂度有非常大的区别,尤其在车辆预订功能里面涉及了定价,派单等多个业务逻辑以及数据库查询。这里为了给各位一个量化的标准,我引用了《构建高可用web站点》里的数据,注意,以下表格处理的是 简单的 web 请求,相应的吞吐率结果也较高。\n 服务器配置\n CPU:Intel(R) Xeon(R) CPU 1.60GHz\n 内存:4GB\n 硬盘转速:15k/min\n 软件 并发用户 总请求数 请求内容 缓存措施 吞吐率 Nginx/0.7 1000 50000 151字节静态文件 无 10556 req/s Apache/2.2 100 1000 151字节静态文件 无 6364 req/s Apache/2.2 100 1000 1.2m静态文件 sendfile() 590 req/s APC Cache 100 1000 PHP动态页面 PHP 动态缓存 473 req/s Apache/2.2 100 1000 PHP动态页面 无 51 req/s 我们可以看到,由于请求不同的内容,使用不同的软件以及设置不同的缓存策略都会给吞吐率带来极大的影响。Uber 业务逻辑比较多,我们估计单台服务器每秒能够处理 50次 请求车辆预订请求,预计用户月增长量为 10%,所以总共需要 8台 后端服务器来处理车辆预订功能。\n2.2 数据请求 假设每次车辆预约产生 3次 数据库查询以及 1次 数据库写入。每秒总共产生 900次 数据库查询以及 300次 数据库写入。那么这又需要多少台数据库支撑呢?你猜到了,这也是一个复杂的问题,涉及到查询热点的分布,数据库缓存的设置,索引设置等等。这里有两篇文章分析介绍了一些案例:\n High Performance MySQL Using mysql as nosql 我们保守估计单台数据库可以支撑每秒 2000次 查询以及 500次 写入。由于数据库对数据持久化以及可用性要求非常高,我们通常都需要添加主从热备,读写分离以及故障切换等功能,这些我们在之后的瓶颈分析再进行讨论。在这里,暂时我们先使用一台数据库。\n2.3 存储量 每天 100万 次预订,每次预订大概产生 10KB 左右的信息。每天产生 10G 的数据,加上其他功能大概每天 20G 左右。现在的硬盘都是以 TB 为单位了,所以存储量不需要担心。粗略估计结束了,把计算完的数据记录下来:\n3. 整体架构 我想大多数求职者都没有设计过 Uber,也没写过车辆预订的相应业务代码。但是没有关系,系统设计可以跳脱于应用本身,而从功能点出发,我们看看车辆预订功能实际完成了什么。\n 用户选择起点以及终点 \u0026mdash;\u0026gt; 服务器返回定价以及预估路线。 当用户点击车辆预订 \u0026mdash;\u0026gt; 服务器开始寻找司机,派单并显示该司机信息,位置。 如果你有 Web 开发经验,会发现这些功能点与常规的浏览器请求服务器的流程非常相似,虽然业务逻辑不同,但是都是客户端带参数请求服务端,服务端返回数据。我们可以把每个功能点理解成一个单独的 API 进行设计。最重要的是,在设计期间,你需要和面试官不断沟通,看他是否认可你的方案,根据他的提示来修改(面试官在面试前对题目的了解比我们要多,大多情况下跟着他不会错)。综合上面的内容,我们先画出一个粗略的整体架构。\n当用户请求 API 服务器之后,API 服务器会请求相应的业务服务器(业务服务器可能会互相调用,例如定价的时候需要先查询用户附近有多少司机)然后查询数据库,最后返回结果给用户。我们先从这个整体架构开始讨论每个功能的细节。\n4. 组件架构 这里我们阐述定价,派单,司机信息三个功能的架构设计。\n4.1 定价 定价背后的逻辑比较复杂,主要由于商业原因而不是技术原因,Uber 也一直在调整它的定价策略而达到利润最大化。不过,我们可以先设计一个粗略的版本,首先,分析影响价格的因素,\n 路程长度 需求量(预约人数与周围司机数量比值) 预订时间 地区消费水平 \u0026hellip; 这个表会包含非常多的因素,不过我们可以猜测里面最主要的三个因素以及它们的权重有以下的大小关系:\nW(路程长度) \u0026gt; W(需求量) \u0026gt; W(预约时间) 重复一次,这并不是真实的情况,只是我猜测的关系式,定价可能是通过一个公式计算出来:\nPrice = F(路程长度) * K(需求量) * G(预约时间) 其中,F,K,G函数分别对应一个哈希表,Uber 维护了每个因素与价格影响之间的哈希表,假定如下:\n 距离 价格影响 需求量 价格影响 预约时间 价格影响 1-2公里 起步价 10 元 1-2 1.2倍 早晚高峰 1.2倍 2公里以上 起步价格的 1.1倍 2-4 1.4倍 其他时间 1倍 一名用户在预约时,我们马上可以通过以上的公式以及表格计算出价格,例如用户在:\n 目的地离起始点1.5公里 附近2公里内有4名其他用户在预约,两名空闲司机(需求量为2) 早高峰时段 预约车辆,那么预估价格为\n10 * 1.4 * 1.2 = 16.8 元 这样看起来,一个粗略的定价功能就实现了,我们在白板中写下数据库结构以及 API 示例。这个功能用 NoSQL 实现会更容易,不过现阶段我们先使用 SQL 表:\n距离表 distance_range price 需求表 demand_range price 时间表 time_range price API 示例 API 示例主要描述输入以及输出即可,以下为 Python 代码,可以略过实现,不过注释要写清楚:\ndef calculate_price(latitude, longitude, current_time): ''' input: latitude (float), longtitude (float): current user location current_time (timestamp): user request timestamp output: price (float): estimated price ''' pass 4.2 派单 接收到预约请求之后,系统会派单给合适的司机,假设系统只会派单给 2公里 内在线以及空闲的司机。这里的三个要点 “2公里内”,“在线”,“空闲”,三个要点中,第一个和第三个比较容易,只需要在查询在线司机的时候附带筛选条件即可。假设数据库中存储了司机的信息,里面包含\n 基本信息,包括车型,车牌,评分 最后在线时的经纬度 最后在线时的时间 现在是否空闲 **那么就可以根据,经纬度以及是否空闲先筛选出附近可用司机的列表,然后再根据司机的车型,评分加权得到排名,选出最合适的司机分配给用户。**要点中的第二个,判断司机是否在线比较麻烦。这个功能也很常见,定价功能计算需求量的时候也要找到附近在线的司机,而像 Whatsapp 则需要检测用户是否在线,等到在线的时候把之前收到的信息推送给用户。判断在线这个功能有两种常见的方式实现,\n第一种解决方法最直观,司机客户端每 X分钟 发送一个请求给服务器,告诉服务器它现在在线。服务器在数据库中们维护着一个列表,列表中存储着最近 X分钟 有发送此请求的司机ID。每当我们要找在线司机的时候,我们在列表中直接筛选即可。这个方法容易实现,不过缺点也很明显,首先是反馈不及时,司机可能因为网络原因 X分钟 没请求服务器导致错过了派单,其次是司机下班了,但是系统在 X分钟 内依然派单过去。最后是服务器需要处理大量的司机客户端请求,对系统的性能有影响。这种解决方法适合在初期使用,因为实现简单,容易维护。\n第二种解决方法是使用双向连接,例如 Websocket,服务器可以直接查询客户端连接是否被关闭,司机是否在线。服务器同样在数据库维护着一个列表,每次用户连接服务器,服务器在列表中添加司机,一旦连接断了,则从列表中删除。我们在白板中写下具体架构以及数据库架构:\n司机表 driver car_number ranking last_online_time free \u0026hellip; API 示例 def find_driver(latitude, longitude, current_time): ''' input: latitude (float), longtitude (float): current user location current_time (timestamp): user request timestamp output: driver_id (list of int): use driver_id to request driver's info later ''' pass 4.3 司机信息 派单之后需要在用户的客户端显示司机的基本信息,实时位置以及车辆方向。此时 Websocket 双向链接的优势就显现出来了,服务端可以实时获取到司机的当前位置发送给客户端,然后客户端把司机的位置渲染在地图的道路上。这里面的难点非常多,像滴滴使用的是 FLP 融合定位,在GPS有问题的时候,使用 WIFI,道路匹配,车辆航位推算等方式来解决这个问题。具体细节如果你了解的话可以进行进行阐述,在这里我只是简单地当成客户端从服务端获取司机的经纬度然后在地图上渲染。\n4.4 数据存储 那么我们之前记录需要存储的数据该存储在哪里呢?服务器内存?文件?还是数据库?如果是数据库的话是 SQL 还是 NoSQL。几个基本原则是:\n 暂时不知道瓶颈在哪里,但是数据又需要持久化存储,我们优先使用 SQL 数据库。(Facebook 在有5亿日活量的时候,主要还是用 MySQL)。 关系性不强,需要频繁修改的地方(例如上面的司机在线列表)我们可以使用 NoSQL 存储在内存中。 大型并且很少需要查询的文件,例如日志,可以存储在文件系统内。 5. 架构优化 完成架构之后,我们需要进行架构优化,首先是业务逻辑上的优化,例如,要根据用户当前经纬度找到两公里内的司机,原始的方式把可以用户经纬度把所有在线司机的经纬度一一对比,然后找出距离少于两公里的司机。这个时间复杂度是 O(n),n 代表在线司机的数量。更好的方法是把地图分成两公里为单位的一个个格子,然后找与用户同一个格子的司机。时间复杂度马上降到 O(1) 了。其次是系统架构上的优化,需要我们观察系统瓶颈,消灭单点故障,保证高可用以及水平扩展。基本的优化思路如下,\n 服务器均使用服务器集群 服务器集群前加上负载均衡 计算耗时久的数据使用缓存存储, 关系型弱的数据使用 NoSQL 存储, 数据库使用主从热备,读写分离 在这个架构中,定价功能可以使用 NoSQL 缓存影响价格的因素对应的哈希表以及在线司机列表,派单功能可以使用 Websocket,再添加上服务器负载均衡以及数据库主从热备,最终我们可以得到这个架构图。\n得到这个架构图之后,可以就自己熟悉的地方与面试官进行深入交流,例如使用 LVS 还是 HTTP 的负载均衡,使用哪种主从热备方案,数据量大之后如何分区分表。我们在一开始计算出所需要的服务器以及存储量,之后面试官可能会提出限制,如何在减少服务器或者数据库数量的情况下保证高可用,这时候就需要利用我们平时积累的架构方案作出优化。就像算法题中,了解基础的数据结构能够帮助我们快速解决问题,系统设计也需要我们了解这些常用的软件架构以及使用场景,这里我列出了一些学习资源,你可以了解下。\n Inside NGINX: How We Designed for Performance \u0026amp; Scale Introduction to Redis Redis Sentinel Documentation(重点) How does Hadoop work and how to use it? Thorough Introduction to Apache Kafka 总结 系统设计资源非常多,理解常用的架构以及模式就能解决大部分的问题,同时,别忘记借助面试官的提示来解决问题。最后希望你能在系统面试中好好表现,:D\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%9B%BD%E5%A4%96%E6%B1%82%E8%81%8C%E6%8C%87%E5%8D%97/",
"title": "程序员国外求职指南",
"tags": [],
"description": "",
"content": "求职系列文章:\n 如何准备技术面试 如何写一份更好的简历 我去年第一次到欧洲中西部旅游,旅程中给我的感受是他们更懂得享受生活。印象最深刻的是一次在法国的普通小镇,南希,我和朋友到当地的酒吧 Les Berthom Nancy 喝酒,吧台里有一名典型的高帅法国服务生。过了一会服务生下班了,他换下了制服却没有离开,而是坐到我们旁边点了一杯啤酒自顾自喝起来,那一刻我突然觉得他过得比我要幸福得多。另外一次,之前在 TX 面试的时候,与面试官闲聊的时候我问他,你在这里工作开心吗?他看着我,想了一下然后回答,“我在这里工作了四年没有人问过我这个问题,我想的只是努力地去做好一款产品。” 我很欣赏他为公司付出的努力,同时也惊讶于四年来没有人问他工作得开不开心。国内企业普遍缺乏不同国家与背景的人才,身边的人与自己往往有类似的成长背景,加上等级森严的层级关系,工程师像是复制粘贴出来的。在那次面试后,每一次面试我都会问自己,我真的想在这里工作吗?\n或许就在这两个瞬间,到国外工作的想法就埋在我脑里了。几个月来,我一直在找国外的工作机会,经过过百份简历的投递和海量的面试(我面试了美国,加拿大,英国,欧洲,新加坡,香港这几个地方加起来超过 20 次),**最终决定到柏林的一家创业公司工作,他们像我一样热爱开源,还有令人激动的产品以及充满多样性的员工,来自不同国家与背景,这两点在国内都不常见。**这篇文章从我的亲身经历出发,希望能给国外求职的各位一些帮助。\n前期规划 自信 记得在电影《独立游戏大电影》中的主人公 Tommy 自我介绍说,我非常非常擅长编程。当时在屏幕前的我第一反应是被吓到了,虽然程序员之间存在鄙视链,但是我接触的大部分的程序员都谦逊而不擅于线下社交,从没听过自己非常擅长编程这样的直白。第二反应是有些不屑,我当时觉得要像 Linus 这样的程序员才能自称非常擅长编程吧,一般人配不上非常擅长这几个字。不过后来我慢慢明白,这或许是国内教育给我的烙印,缺乏自信而且不擅长表现自己。其实不需要成为 Linus 才能自称擅长编程,认真钻研于一个领域,有一定的技术能力,无论是做业务开发,运维管理还是人工智能,只要有人喜欢你的代码编织出来的产品,那么你就已经比世界上 90% 的程序员要优秀了。所以前期规划中,建立自信是最重要的,出国工作并不难,对学历和技术的要求也不高,只要有出国的意愿都应该去尝试。\n硬实力 自信心之外就是硬实力。主要包括语言能力与工作能力。有时候语言能力甚至比工作能力本身更重要,所以该专注于哪个方向,取决于你现在的程度。(小提示:如果职位描述是英语的话,代表求职者擅长英语即可,除非描述中额外注明)\n英语水平:\n 听\n面试官可能来自各个国家,口音有轻有重,其中印度与法国的口语比较难听懂,当两个英语都不好的人用英语互相面试,体验难以置信地差。记得有一次面试中我把 Symmetric and Asymmetric 听成了 Synchronize and Asynchronous,回答了一通之后,面试官给了我一个疑惑的表情然后说了句 \u0026ldquo;Forget about it\u0026rdquo;。所以如果你的英语听力也不好,建议上 Udacity 和 YouTube 编程频道练习听力,起码要熟悉一些常见的技术名词是怎么读的。不然面试的时候根本无法回答。\n 说\n面试前我找了一位英语老师(wx: bohe_yoyo)上了 10节 模拟面试的课程,她问我一些常见的面试问题,然后再根据我的回答给出反馈以及建议,所以在真实面试中,有不少问题我都曾经遇过,回答起来也就得心应手,在下面的章节我也会举一些常见的例子。另外,我还使用 Pramp 进行算法模拟面试,系统会自动匹配一名工程师与你交替做面试官和求职者,求职者需要使用半小时解决一道算法题并与面试官保持沟通。我匹配了几次,从一开始的战战兢兢到现在的心如止水,真正感受到 Pramp 的名字所暗示的 Practice makes Perfect,Pramp 既训练算法还训练表达能力,实在是两全其美,关键的是它还是免费的。\n 读\n阅读部分,最好是阅读自己感兴趣领域的文章,我从两年前开始每天阅读 Hackernews 的文章,里面发布着业内最新的资讯,有些文章艰深而且词汇量多,不像文档那么易读。令我坚持下来的是这里聚集着全球最优秀的工程师以及最新的科技资讯。两年后,我的阅读能力大有进步,现在能阅读原文的技术书籍,这部分没有捷径,只有每天积累。\n 写\n我使用 Grammarly 修正 GitHub,开源社区以及邮件中的语句的语法,同时坚持写英语博客。一部分锻炼自己的写作能力,另一方面也能让面试官更能了解我的技术水平。\n 工作能力:\n 工作经验及团队合作能力\n我刚大学毕业的时候,iOS 开发非常火,那时候无论是培训班还是科班出生的程序员都扎堆转向 iOS 开发,而且因为工资高,其他岗位都很羡慕,有点像现在的区块链。不过近几年 iOS 开发者越来越多,需求却不增反减。我认识的一些技术并不出众的 iOS 工程师,既担心被解雇,又没能力跳槽,**不懂得居安思危的工程师最容易遇到中年危机。**技术能力与工作经验并不是线性关系,技术能力需要从工作以及日常学习中有意识地累积。如果你无法从现在的岗位持续学习到新知识,这并不是一个好的兆头。\n从面试的角度来说,面试官可能从简历中挑选出一些项目,从中了解你在项目中担任的角色,遇到什么难点,如何解决困难。也可能会问一些 behavior 问题,**回答这些问题可以根据事先模拟面试中记下的要点来回答,这样会更容易记忆以及有条理。**举几个题目为例:\n What kind of colleagues do you like?\n Collaboration and easy going Creative and flexible Optimistic and keep on learning How do you keep up with current trends and advances in this field?\n Programmer must also be an eternal student. Networking with other programmers Be open to debate, discussion What does your best day of work look like?\n flexible time arrangement working at home highly effective teamwork and cooperation How did you get all of your work done with heavy pressure?\n make time arrangement in advance solve problems in order of priority set up rewards ask for supervise 开源项目经验以及博客\n这几个月的面试中,不止一次他们让我选出最自豪的博客 / GitHub 项目给他们看。一年多前我开始接触开源社区,慢慢开始阅读源码,贡献文档以及代码,现在主要贡献 CPython。我从社区中学到了优秀的编程的知识以及高效的团队沟通能力,我的博客内容也基本与开源社区或者 CPython 相关。总的来说优秀的 GitHub 与博客能让公司来主动找你,也能让他们看到求职者的实际技术能力以及团队合作能力。\n 我总结了以上的学习方法:\n 英语能力 工作能力 每天阅读 Hackernews 的文章 使用 Pramp 进行模拟面试 英语面试课程,矫正发音以及模拟面试 参与到开源社区,阅读优秀的开源项目代码 YouTube 学英文 养成写博客的习惯 使用 Grammarly 修正日常语法 简历以及 Cover Letter 当你到国外求职,意味着要和全球的优秀人才竞争,而且近几年越来越多其他专业的人才转到编程领域,竞争越来越激烈?我自己的 Startup 最近在招一名 Remote 的工程师,一个多月时间,我收到大约 100份 求职者的简历,里面不乏顶尖学校如伯克利,哥伦比亚,加州大学的应届生简历。也有几名十多年工作经验的工程师以及一名前谷歌的高级工程师的简历。\n同国内收到的简历一样,越优秀的人越重视自己的简历。简历的基本写法可以参考我们之前的文章如何写一份更好的简历,不过英文简历与中文简历要求不同,并不是直接拿中文简历翻译就可以,例如投美国的简历通常不允许出现照片,年龄,种族等个人资料。还有些公司会要求提供 Cover letter,也就是一段个人简介以及想加入该公司的原因(除了钱😂)。这部分代表求职者认真对待这次面试,并且事先有了解过这家公司。怎么写 Cover Letter 呢,可以分为两个部分,第一部分是个人简介,也就是精简版的简历,主要讲自己的相关工作经验以及特长,控制在四句话内:\n Dear XXX,\n Foure-year experience in computer programming has urged me to apply for this position. As you can see in the enclosed resume, I have a very strong academic background in operating systems combined with over five years research experience in search engine optimization. My recent job at CPython, allowed me to further develop and strengthen my technical skills.\n 第二部分介绍对公司的了解以及自己为什么适合这个工作,可以从岗位描述入手:\n I have studied your products carefully and found the following advantages\u0026hellip; I had experience in Natural Language Processing, web scraping, Go, and React during my last company, that is why I suitable for this role.\n 你也可以参考这里的 Cover Letter 例句。我自己的求职方向是后端工程师以及全栈工程师,所以准备了两份简历,以及几份 Cover Letter 根据不同情况投递,特别在乎的公司我会重新写一份 Cover Letter。对自己的简历没信心的你可以使用我们提供的简历 review 平台 。\n哪个国家 一开始我纠结着要到哪个国家,因为当你放眼全球的话,选择实在太多了。近的如新加坡,日本,远的如欧洲,美国。 **首先要清楚自己的目标,再决定走向哪个方向。**我很清楚自己的目标,我最终要到 Google,但是现在英语与技术能力不够好,所以先到英语系国家锻炼英语以及学习,永居暂时不是我的目标。如果你没去过当地旅游,我建议出国工作前最好先逛逛当地的留学生论坛或者华人论坛(美国的一亩三分地,德国的 abcdv)了解下华人眼中的当地情况。总体考虑的因素可以根据以下几点:\n 租金 房价 工资水平 消费水平 环境 空气质量 治安 教育 社会福利 移民难度 当然,这些国家不是说去就能去的,需要根据当地的要求申请工作签证。这里我整理了各个地区的工作签证要求,给大家做一个初步的了解。总体来说,新加坡以及香港相对容易,澳洲,欧洲以及美国较难。政策随时有变动,具体情况请参考链接\n 国家/地区 offer 要求 语言 证书及其他要求 永居要求 新加坡 Employment Pass 公司提供 offer,最少工资 $3,600/月(有经验的候选者需要更高工资) - 受认可的认证,如大学学位 工作两年后可以申请永居 香港输入内地人才计划 公司提供 offer,工资与市场水平相若 - 受认可的认证,如大学学位 连续居住7年可以申请永居 新西兰 Work visa 公司提供 offer - 受认可的认证,如大学学位,年龄不超过 55岁,需要体检,无传染病 工作两年后可以申请永居,高级人才可带上配偶以及儿女 澳洲短期 Visa 公司提供 offer 雅思:总分 5分,单项不低于 4.5分 托福 IBT:总分 35分,听读至少 3分,说写至少 12分等等 受认可的认证,如大学学位,最少两年相关工作经验(硕士与博士的研究时间也算),需要体检,无传染病,无犯罪记录 工作满三年可申请永居,可以申请携带家庭成员(配偶和未独立子女)一起前往澳洲工作或学习 澳洲中期 Visa 公司提供 offer 雅思:总分 5分,单项不低于 5分 托福 IBT:总分 35分,听读至少 4分,说写至少 14分等等 受认可的认证,如大学学位,最少两年相关工作经验(硕士与博士的研究时间也算),需要体检,无传染病,无犯罪记录 工作满三年可申请永居,可以申请携带家庭成员(配偶和未独立子女)一起前往澳洲工作或学习 德国工作签证 公司提供 offer - 受认可的认证,如大学学位,需要体检,无传染病,无犯罪记录 工作五年后可以申请永居 英国 公司提供 offer 英语测试中最少达到 CEFR level B1 一定的存款,过往 5年的旅游记录,无结核病证明,无犯罪记录 工作五年后可以申请永居 欧盟蓝卡 公司提供 offer,税前工资达50800欧元 - 受认可的认证,如大学学位,除了英国、爱尔兰和丹麦,其他欧盟成员国都签发欧盟蓝卡。 视乎国家而定 日本 公司提供 offer,相关的大学学位或者 10年 专业经验 - 受认可的认证,如大学学位 提供户口本,日本连续10年以上获得在留资格,并且其中有连续5年工作资格 加拿大 公司提供 offer - 受认可的认证,如大学学位 工作满一年可申请永居 美国 H1B 公司提供 offer - - 受认可的认证,如大学学位,运气超过 60% 申请者 求职平台 每个国家都有大量的求职平台,我一开始也常常担心会错过一些好公司。而且有些平台不是上传简历就好,还需要填写额外资料以及信息,极为耗时。所以最有效率的方式是找内推,搜索国外的国人内推会是比较靠谱的方式,其次才是使用求职平台。抱着多了解,多锻炼的想法,我海投了超过一百家公司。这里我总结了一些常用的平台,这些平台我几乎都投递过。从回复率来说,AngelList 以及 Whoishiring 比较高,我建议先从这两者入手投递:\n 通用 欧美 新加坡 AngelList Whoishiring 100offer Linkedin Stackoverflow Jobs Central Indeed EuroTechJobs JobsDB Glassdoor Technojobs JobStreet GitHub Jobs Jobs Central 另外一个方式是申请找工作签证,例如德国的 JSV 签证,这种签证给符合条件的人才几个月时间到当地找工作。\n面试 我面了大概 20家 公司,我很难说自己享受这个过程。面国内的公司我很少紧张,因为经验丰富,而且用的是母语。国外的面试看重的点不一样,只能从头开始练习,顺便提下,这个过程中没有一家公司甚至我现任的公司问过我的年龄,这也是我喜欢他们的一点。接下来我简单介绍面试流程以及各个流程要注意的地方:\n 约定时间\n如果公司觉得简历符合他们的要求,一般会请你使用 Calendly 约定面试时间,建议大家先把它的教程过一遍,我就因为不熟悉而错过了一次面试时间。选好面试时间后,使用手机/邮件设定面试前的 1小时 提醒自己,这时候抽时间了解下公司的背景以及产品,过一遍模拟面试中的常见问答。\n 面试准备\n一般视频面试使用的软件是 Google Hangout,如果没用过的话请先下载,然后学习如何开启摄像头,麦克风等基本操作。由于我所在的地区使用 Google Hangout 不太稳定,我一般使用 Skype 和 Zoom。这部分异常重要,想象下对方有口音而且网络不好断断续续,面试体验极差。建议大家测试下这几款软件,看哪款信号最好然后与公司沟通确定。\n 笔试题\n一般公司会使用 Hackerrank 让你解决一些算法题,这部分可以使用 Leetcode 多锻炼。有的公司会让你完成一个小项目,虽然耗时但是相对简单,可以阅读类似项目的文章,确保有简单的文档和清晰的 git commit messages 。\n 开始面试\n 自我介绍\n自我介绍大概 1分钟,可以从 3个 方面进行阐述:\n 项目经验以及技术能力 团队能力 为什么来这家公司 这个问题因为经常被问到,所以我建议你把自我介绍背下来。\n 专业问答\n相对于国内偏项目经验以及无意义的概念性问答。国外偏重编程基础,系统设计以及算法。编程基础根据你的方向因人而异,系统设计会问类似:\n How to design a newsfeed system like twitter, which part would be the bottleneck?\n 系统架构,工具选型 可能出现的瓶颈。 系统设计涉及到的技术细节比较多,需要多阅读一些系统设计的文章,这类问题可以参考这篇文章里提到的方法。\n 日常问答\n这部分同样可以根据模拟面试中记下来的要点回答,我们举一些常见的例子:\n Do you know anything about our company?\n I learned about it in advance, your company is engaged in …., with the idea of \u0026hellip;\u0026hellip; I think it fits my well.\n I have studied your products carefully and found the following advantages…… Several problems were also found \u0026hellip; I think we can improve in this way ……\n What do you think of your technical ability?\n After years of accumulation and growth, I am now confident in my own technology. I am a senior developer, and I feel capable of various development tasks. Q\u0026amp;A\n一般会给你提问的时间,这时候可以问问公司的对候选人的期望:\n I attach great importance to/ think highly of this interview, so I want to know more about the company\u0026rsquo;s expectations and goals for this job.\n 或者公司的基本工作情况:\n I \u0026lsquo;m used to getting to work on time, so I\u0026rsquo;d like to know your company\u0026rsquo;s work hours.\n The company is certain to often have temporary task, I can cooperate to work overtime, but do you have overtime pay?\n 我感觉国外面试的气氛都比较轻松,如果在面试过程中能发挥下幽默感,让面试官觉得这个人 nice 的话会比较好,另外一点就是放慢语速,因为对方是 native speaker,语速快很正常,如果英语水平不够却尝试跟上对方语速的话,第一会减少思考问题的时间,第二有时候口语不够好的话会卡在中间,断断续续使面试官非常难受。面试结束后,跟着邮件反馈进行下一步即可,国外一般处理时间是以周为单位的,定时发邮件问问进度就好,同时准备面试其他公司。\n 总结 同软件开发一样,找到好工作并没有银弹,经过这几个月的面试,我觉得自己的眼界也更加开阔了,了解到不同国家大企以及创业公司的文化。其实我们担心的并不是人到中年,而是人到中年却碌碌无为。希望大家能脱离 996,找到自己喜欢的工作。\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/",
"title": "算法",
"tags": [],
"description": "",
"content": "算法基础 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E5%8A%B3%E5%8A%A8%E6%B3%95%E8%B5%94%E5%81%BF101/",
"title": "劳动法赔偿 101",
"tags": [],
"description": "",
"content": "注:本文适用于中国大陆地区,不同地区的劳动仲裁情况可能有所差异\n “《劳动法》与《劳动合同法》是保护劳动者的。”\n 这是我经历劳动法维权时,律师朋友对我说的话,当初听起来只觉得是泛泛而谈,但当我为了维权而了解案例,研读法律法规的时候却慢慢感受到这句话的力量,也给了我争取合法权利的勇气。我的维权经历不算美好的回忆,当时的用人单位愿意按法律赔偿,但是其中涉及了离职证明内容,赔偿金额细节的商讨,所以结果不算愉快,但却是一个不错的职场经验。而且到最后你往往发现,即使你与上司的私交再好,离职的时候他也会站在用人单位的角度来处理事情。在此期间,我非常幸运有一位律师协助我处理,并最后得到我比较满意的结局。从我的经验来说,劳动纠纷时,先与用人单位进行协商会是比较好的方法,因为真正走劳动仲裁确实费时费力,准备资料,走法院,等待,最后拿到的钱还会打折。有些老板或者 HR 只是抓住劳动者不懂法的弱点以离职证明,调岗或其他手段侵害劳动者的权利。所以你不一定走到劳动仲裁这步,只需要向用人单位提出法律依据,让他们知道你懂法,而且他们很大概率会败诉的情况即可,毕竟他们都喜欢欺负软柿子。当然,在此之前首先你要知道自己有什么权利,才能知道如何谈判以及让步。**从我身边的案例来看,劳动者与用人单位发生劳动纠纷时,大部分情况下都是优势方。如果因为不懂法或者不想付出时间精力而放弃自己的权利,是逃避的行为,因为你无法担保下次会不会出现类似的情况。**所以,不需要害怕与他们据理力争,用人单位比我们更害怕劳动法。我希望这篇文章能帮到面临劳动法赔偿的各位,因为谁也不知道下一个被解雇的是不是自己。\n阅读本文之前,请大家花 5分钟粗略阅读劳动法,劳动合同法以及劳动合同法实施条例,它们比技术文档易读得多,不过相对细节也较少,没有对一些边界条件进行定义,当然也没有源码,需要参考实际案例的裁决,不过对基本条款有初步的了解是非常重要的。\n赔偿金额 常见的赔偿方式为标准赔偿以及双倍赔偿:\n 《劳动合同法》第四十七条:经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。\n劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。\n本条所称月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。应发工资是指劳动者提供正常劳动按照法律规定应当获得的全部工资,包括了基本工资、加班工资、奖金、津贴等。实发工资是劳动者每月实际拿到的工资,通常会被扣减一些费用,比如代扣代缴社会保险费、所得税,扣伙食费、房租费等,劳动者实际到手的金额通常会比应发工资少。经济补偿金的计算应当以劳动者的应发工资作为基数,而不是以基本工资、实发工资为基数。\n 标准赔偿:工作年限(包括试用期)超过六个月,赔偿一个月工资,超过一年少于一年半,赔偿一个半月工资,以此类推,这就是常说的赔偿 N 个月,N 在这里指的是工作年限。如果用人单位没有提前 30天 通知劳动者就解除劳动合同,需要额外支付一个月的工资,这就是 N+1 赔偿。\n双倍赔偿:标准赔偿乘以 2,只有在违法解雇的情况下才适用。\n要注意,这里的月工资是指劳动者提供正常劳动按照法律规定应当获得的全部工资,包括了基本工资、加班工资、奖金、津贴等。不扣除代缴社会保险费、所得税,扣伙食费、房租费等。(谢谢 Zhang_Siayng 的指正)\n加班费 很可惜,我没有找到维权成功拿到加班费的案例(欢迎大家提供),举证其实不难,最高人民法院在劳动争议相关司法解释中,对加班费举证责任问题作出了明确规定:“劳动者主张加班费的,应当就加班事实的存在承担举证责任。但劳动者有证据证明用人单位掌握加班事实存在的证据,用人单位不提供的,由用人单位承担不利后果。” 也就是说,与劳动仲裁争议事项有关的证据属于用人单位掌握管理的,用人单位应当依法提供,用人单位不提供的,应当承担不利后果。 **但是用人单位往往会在劳动合同中说明工资是包含加班费来逃避支付加班费。**而且当劳动者每月领取工资条,表明其已知悉工资(包含加班费)的具体数额及计算方式,但却未曾向公司对加班费的基数及计算方法提出任何异议的话,这样的情况,法院一般不支持加班费的赔偿。\n工作阶段 我把工作阶段分成四个阶段:\n 阶段 事件后 事件前 入职 接受了口头或者书面 offer 签署劳动合同 试用期 签署劳动合同 试用期结束 在职期 试用期结束 离职 离职期 自行离职 / 被解雇 / 协商解除合同 / 入职阶段 入职阶段不会涉及劳动赔偿法,一般发生经济赔偿的情况是用人单位撤回 offer。\n 用人单位向劳动者发了书面或者口头 offer 后撤回,令求职者辞去原本工作或放弃了其他 offer 所导致的经济损失。\n这种情况不适用于《劳动法》和《劳动合同法》,而是适用《合同法》:\n 《合同法》第十六条:要约到达受要约人时生效。\n 采用数据电文形式订立合同,收件人指定特定系统接收数据电文的,该数据电文进入该特定系统的时间,视为到达时间;未指定特定系统的,该数据电文进入收件人的任何系统的首次时间,视为到达时间。 《合同法》第十七条:要约可以撤回。撤回要约的通知应当在要约到达受要约人之前或者与要约同时到达受要约人。\n《合同法》第十八条:要约可以撤销。撤销要约的通知应当在受要约人发出承诺通知之前到达受要约人。\n《合同法》第十九条:有下列情形之一的,要约不得撤销:\n (一)要约人确定了承诺期限或者以其他形式明示要约不可撤销; (二)受要约人有理由认为要约是不可撤销的,并已经为履行合同作了准备工作。 《合同法》第三十条:承诺的内容应当与要约的内容一致。受要约人对要约的内容作出实质性变更的,为新要约。有关合同标的、数量、质量、价款或者报酬、履行期限、履行地点和方式、违约责任和解决争议方法等的变更,是对要约内容的实质性变更。\n《合同法》第三十一条:承诺对要约的内容作出非实质性变更的,除要约人及时表示反对或者要约表明承诺不得对要约的内容作出任何变更的以外,该承诺有效,合同的内容以承诺的内容为准。\n **简单来说,当你收到 offer 并且 offer 中承诺了入职期限,那么当你接受 offer 的时候这个合同已经生效,如果此时用人单位撤回 offer 的话可以要求对方赔偿。**另外,若实际劳动合同中的条款与 offer 不符合,等同于把旧合同作废,此时如果你不接受新合同的话,也可以要求公司赔偿,赔偿金额根据实际情况来确定,重要的是保存 offer 证据,入职前尽量要求纸质或者邮件 offer,如果是口头 offer 可以要求对方晚点再打来,然后做好录音的准备。要注意,这部分赔偿不适用劳动仲裁,是需要到法院提起诉讼的。\n John 手中有 A 公司通过公司邮箱发来的 offer,以及随后交流的微信和录音留存,完全可以去法院起诉讨回公道。用证据和 A 公司据理力争,A 公司自知理亏只好与 John 达成了和解,同时 John 也得到了相应的经济赔偿,并找到了另一个适合自己的工作。\n (引用自参考案例)。\n 试用期 试用期开始之后就要注意保留考勤记录,工资条等证据。以防需要劳动仲裁时缺乏证据,即使考勤记录无法导出(例如钉钉)也没关系,在劳动仲裁上依然可以申明,法庭会要求用人单位出示考勤记录的。不过最好有其他员工(在职员工,前同事或者维权员工也可以)的证词。以下是各种赔偿情景:\n 试用期没有购买社保\n 《社会保险法》第五十八条:用人单位应当自用工之日起三十日内为其职工向社会保险经办机构申请办理社会保险登记。未办理社会保险登记的,由社会保险经办机构核定其应当缴纳的社会保险费。\n《劳动合同法》第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:\n(三)未依法为劳动者缴纳社会保险费的;\n 如何赔偿:入职超过三十天未购买社保的话的话,劳动者可以自行解除劳动合同,赔偿金额根据标准赔偿。\n 没有签订劳动合同\n无论在试用期内还是试用期外,没有签订劳动合同都需要额外赔偿:\n 《劳动合同法》第十条:建立劳动关系,应当订立书面劳动合同。已建立劳动关系,未同时订立书面劳动合同的,**应当自用工之日起一个月内订立书面劳动合同。**用人单位与劳动者在用工前订立劳动合同的,劳动关系自用工之日起建立。\n 《劳动合同法》第十九条:试用期包含在劳动合同期限内。劳动合同仅约定试用期的,试用期不成立,该期限为劳动合同期限。\n 《劳动合同法》第八十二条:用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。\n 如何赔偿:你在某公司工作了 6个月,没有签订劳动合同,你可以解除劳动合同,然后要求额外赔偿 6个月的工资。\n 试用期过长\n因为试用期对劳动者的保障力度较少。按照劳动合同的期限,试用期也有相应的上限。(其实试用期是可以和公司商量的,入职时尽量和 HR 或者主管商量表现好的话一个月转正。)(参考案例)\n 《劳动合同法》第十九条:劳动合同期限三个月以上不满一年的,试用期不得超过一个月;劳动合同期限一年以上不满三年的,试用期不得超过二个月;三年以上固定期限和无固定期限的劳动合同,试用期不得超过六个月。\n 如何赔偿:你在某公司签订了两年的劳动合同,试用期却签订了 6个月(法律规定最高 2个月)。工作 6个月后你可以解除劳动合同并且要求公司赔偿 6 - 2 = 4个月的工资,\n 试用期被无理解雇\n你可能也听说过,有些公司把人招聘进来,然后试用期快到的时候就解雇他,重新招一批新人,公司依据的是《劳动合同法》第二十五条的第一项:\n 《劳动合同法》第二十五条:劳动者有下列情形之一的,用人单位可以解除劳动合同:\n(一)在试用期间被证明不符合录用条件的;\n(二)严重违反劳动纪律或者用人单位规章制度的;\n(三)严重失职,营私舞弊,对用人单位利益造成重大损害的;\n(四)被依法追究刑事责任的;\n 不过,要注意的是,这里的证明不是那么简单,不是领导或者主管一句态度不行或者能力不行就能解雇,通常来说在招聘信息以及劳动合同中需要列明职位要求:\n 在发布的招聘简章、招聘信息中明确录用条件和标准。用人单位在广告上发布招聘信息时,除了注明对职位的一些基本要求(如年龄、职业技术、学历等)外,还应对所聘职位的具体录用条件、岗位职责进行详细描述,并在与劳动者订立劳动合同时再次以书面形式明确告知。\n 建立试用期的绩效评估制度,明确考核标准、考核方式及考核方法。用人单位制定的考核内容、评分原则及决定劳动者是否最终被录用的客观依据应当事先告知劳动者,并让其签字认同。\n **简单来说,在试用期用人单位需要设立公开透明且双方同意的审核机制来确定试用期通过与否,例如绩效考核,考勤考核,否则等同于无理解雇,需要按照双倍赔偿。**另外,虽然法律没有规定在试用期期间用人单位需要提前多少天通知劳动者解除劳动合同,但是除非是违法或者严重失职,今天通知,明天走的这种情况是不合理的。\n 试用期辞职\n劳动者在试用期辞职的话,提前三日告知用人单位即可。\n 《劳动合同法》第三十七条:劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。\n **用人单位不能以任何理由强制劳动者继续工作,**即使签订的劳动合同规定了需要至少做满 1年 才能离职,劳动仲裁中也很大概率不会承认,同时不能以不出具离职证明等资料来威胁劳动者。\n 《劳动合同法》第五十条:用人单位应当在解除或者终止劳动合同时出具解除或者终止劳动合同的证明,并在十五日内为劳动者办理档案和社会保险关系转移手续。\n 《劳动合同法》第八十九条:用人单位违反本法规定未向劳动者出具解除或者终止劳动合同的书面证明,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。\n 在职期 调岗 / 降薪 / 强迫休假\n劳动合同中未注明的调岗需要经过劳动者同意,假如劳动合同中你的职位就是软件开发工程师,入职后用人单位想把你调到运维岗,因为你不同意而解雇属于无理解雇,按照双倍赔偿。(案例)\n 《劳动合同法》第三十五条 用人单位与劳动者协商一致,可以变更劳动合同约定的内容。变更劳动合同,应当采用书面形式。\n 在劳动合同期内降薪或者强迫休假等于修改劳动合同,需经过劳动者同意,未达成一致则需要按照标准赔偿。\n 强迫离职\n用人单位知道主动解雇需要赔偿,所以可能会用强迫你主动离职,例如安排你不想做的事情,或者以其他无赖的手段,他们目的只有一个,少赔偿。这时候你可以做出选择,一是纵容他们,不要赔偿金,自己离职,二是表明态度以及立场,收集证据和用人单位对抗,首先看看用人单位有没有不合法律法规的行为,例如拖欠工资,未缴社保,未签合同的行为。如果有的话,劳动者可以直接解除劳动合同,要求标准赔偿。\n 《劳动合同法》第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:\n(一)未按照劳动合同约定提供劳动保护或者劳动条件的;\n(二)未及时足额支付劳动报酬的;\n(三)未依法为劳动者缴纳社会保险费的;\n(四)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;\n 其次表明不会主动离职的态度,让他们自行决定。毕竟用人单位还是要照付薪水的,而且都会挑软柿子捏,表明态度,与其协商并获取部分赔偿也是一种解决方案。\n 无理克扣工资\n入职后会有员工手册,里面会规定公司制度,例如考勤以及请假情况。例如迟到 10分钟 扣 50块。**要注意,所有员工手册的修改都需要经过员工的口头或者书面同意才生效。**假如突然开会说迟到 10分钟 扣 200块,这时候你可以不同意,此时你可以与用人单位进行协商\n 《劳动合同法》第四条 用人单位应当依法建立和完善劳动规章制度,保障劳动者享有劳动权利、履行劳动义务。\n用人单位在制定、修改或者决定有关劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利、职工培训、劳动纪律以及劳动定额管理等直接涉及劳动者切身利益的规章制度或者重大事项时,应当经职工代表大会或者全体职工讨论,提出方案和意见,与工会或者职工代表平等协商确定。在规章制度和重大事项决定实施过程中,工会或者职工认为不适当的,有权向用人单位提出,通过协商予以修改完善。\n 如果协商无效的话,等同于不同意用人单位修改劳动合同的情况,可要求标准赔偿。\n 拖欠工资\n 《劳动法》第九十一条:用人单位有下列侵害劳动者合法权益情形之一的,由劳动行政部门责令支付劳动者的工资报酬、经济补偿,并可以责令支付赔偿金:\n(一)克扣或者无故拖欠劳动者工资的;\n(二)拒不支付劳动者延长工作时间工资报酬的;\n(三)低于当地最低工资标准支付劳动者工资的;\n(四)解除劳动合同后,未依照本法规定给予劳动者经济补偿的。\n 很简单,劳动者可以解除劳动合同,然后要求标准赔偿。\n 不续约劳动合同\n劳动合同期满的时候,如果用人单位不再与职工续签而导致劳动合同终止时,用人单位应当向劳动者支付经济补偿,按照标准赔偿。若用人单位如果提供了比原合同工资/福利低的合同要求劳动者续签,劳动者有权不续签,依然按照标准赔偿。\n 离职期 离职分成三种情况:\n 自行离职\n无论是试用期离职以及非试用期离职,用人单位不能以任何理由强制劳动者继续工作。\n 《劳动合同法》第三十七条:劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。\n 这种情况没有赔偿。\n 解雇 / 裁员\n只有经培训或调整工作岗位后仍不能胜任工作,才能解雇劳动者,不满足这个前提会被仲裁机构或法院认定为违法解除劳动合同,需要双倍赔偿,正常情况下的解雇按照标准赔偿。至于裁员:\n 《劳动法》第二十七条:用人单位濒临破产进行法定整顿期间或者生产经营状况发生严重困难,确需裁减人员的,应当提前三十日向工会或者全体职工说明情况,听取工会或者职工的意见,经向劳动行政部门报告后,可以裁减人员。\n 用人单位大幅度裁员时,需要提前三十天向劳动者说明情况,此时按照标准赔偿。\n 协商解除劳动合同\n双方都同意解除劳动合同,这时候的赔偿金额按照双方的约定进行计算。\n 离职证明威胁,HR 回访威胁\n无论在什么情况下,用人单位会以不开具离职证明,只开具开除证明或者 HR 回访差评来威胁劳动者都是违法行为:\n 《劳动合同法》第五十条:用人单位应当在解除或者终止劳动合同时出具解除或者终止劳动合同的证明,并在十五日内为劳动者办理档案和社会保险关系转移手续。\n《劳动合同法》第八十九条:用人单位违反本法规定未向劳动者出具解除或者终止劳动合同的书面证明,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。\n 如果用人单位在 15日 内没有出具证明,可以向劳动行政部门投诉,因此造成无法到新单位入职而产生损失时,可以要求原来的单位给予赔偿。同时要注意:\n 《劳动合同法实施条例》第二十四条 用人单位出具的解除、终止劳动合同的证明,应当写明劳动合同期限、解除或者终止劳动合同的日期、工作岗位、在本单位的工作年限。\n **离职证明里面是没有要求需要写明离职原因的,也不允许对劳动者的道德品行进行评价。**所以,从保护劳动者权益角度出发,用人单位出具的离职证明不应当记载对劳动者不利的事项,不得对劳动者的工作表现、工作能力以及道德品行等进行主观评价。如果需要记载,应当提前征得劳动者同意,否则,劳动者有权要求用人单位重新出具。\n 总结 总的来说,常见的劳动赔偿情景如上,当然还有很多例如工伤等情况我没有涉及,网络上有非常好的资源与案例,大家可以根据自己的实际情况参考学习。我讲的这些内容在专业律师眼中仅仅是常识,有时候我们眼中的常识却往往是别人的难点,例如重装系统。但是使用劳动法保护自己并不需要通过司法考试,就像重装系统不需要学会编程,只需要大家花点时间看案例,咨询律师即可。今天刚好是圣诞节,我希望大家可以过个开心的圣诞:D。\n本文章由 ResumeJob 贡献,ResumeJob 能帮助你审视简历,模拟面试,重新规划你的职业生涯。\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/leetcode/dfs-%E8%A7%A3%E9%A2%98%E6%A8%A1%E5%BC%8F%E4%B8%8B%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "DFS 解题模式(下)(未完成)",
"tags": [],
"description": "",
"content": "概述 这篇文章介绍 Leetcode 常见 DFS 问题的解题模式,希望你了解这些模式之后,对大部分 DFS 问题(hard 难度的需要一些变形)都能够迎刃而解。由于 Leetcode 上 DFS 问题中常见的都是无环图,所以我们这里也只讨论无环图的解题模式。阅读本文之前你需要对图的基础知识有一定的了解,包括什么是图?常见的图的类型有那些?(有向无环图,有向有环图),如何遍历图?(前序遍历以及后序遍历)。\n辨别问题 那么什么样的问题可以用 DFS 来解决呢?,DFS 问题常见的表达形式为:\n “给定一个图(树,字符串,矩阵),找到在遍历图的过程中,符合特定条件的数值或路径。”\n 上面的这个定义有点抽象,举两个例子:\n Leetcode 113 Path Sum II\n \u0026ldquo;Given a binary tree and a sum, find all root-to-leaf paths where each path\u0026rsquo;s sum equals the given sum.\u0026rdquo;\n “给定一个有向无环图(二叉树),找到在遍历图的过程中,符合特定条件的数值(路径和等于 sum )”\n Given the below binary tree and sum = 22, input: 5 / \\ 4 8 / / \\ 11 13 4 / \\ / \\ 7 2 5 1 output: [ [5,4,11,2], [5,8,4,5] ] Leetcode 200 Number of Islands\n Given a 2d grid map of \u0026lsquo;1\u0026rsquo;s (land) and \u0026lsquo;0\u0026rsquo;s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.\n “给定一个无向无环图(矩阵),找到在遍历图的过程中,符合特定条件的数值(岛的数量)”\n Input: 11010 11000 11000 00000 Output: 2 解题模式 虽然许多 DFS 问题都可以用 DP 来解决,通常效率也更高。不过 DP 的状态转移方程往往不容易想到,所以在面试的时候,先快速按照解题模式实现 DFS 的解法然后再优化成 DP 也是一个不错的方法。解决 DFS 问题最重要的是四点,1. 防止节点被重复遍历,2. 遍历前,检查节点是否合法,3. 检查遍历后的状态是否符合要求,4. 更新接下来 DFS遍历 的参数,解题模式主要包括三个部分:\n 主函数\n主函数要做的有两件事:第一,处理边界情况,例如图为空,第二,遍历整个图,如果题目要求返回的是布尔值(图中是否存在符合此条件的路径),那么遍历在找到符合条件的路径时就可以结束,**除了这种情况,都需要遍历图中所有可达节点。**若不能通过初始节点访问所有可达节点,那么在主函数就需要对每个节点进行 DFS 遍历。上面的例子 Leetcode 200 Number of Islands,遍历完初始节点后,因为其他节点都是未知状态,所以需要继续遍历。\n # 左侧为遍历初始节点后递归遍历过的点,右侧为仍需遍历的点 110 0 110 0 110 0 000 0 题目 要求 初始节点可以遍历到整个图 Leetcode 113 Path Sum II 返回所有符合条件的路径 是 Leetcode 200 Number of Islands 返回符合条件的路径的数量 否 DFS 递归函数\n这个函数是一个递归函数,里面调用了辅助函数,DFS 函数只需要对当前节点的子节点(如果是无向图则临近节点)进行遍历即可,其他功能通过辅助函数实现。\n 辅助函数\n里面包含了 is_valid 以及 match 两个子函数。分别用作判断子节点是否合法,以及当前状态是否符合条件。\n 具体实现 主函数伪代码\n 从初始节点可以访问所有可达节点(Leetcode 113 Path Sum II):\n function main_function(graph): # 边界情况,例如如果图是空的,或者初始节点本身就符合条件 if graph is empty return empty # 如果需要返回数值则创建变量(例如最大值,最小值),返回路径则创建数组: res -\u0026gt; a variable or array # 只需要遍历初始节点 element -\u0026gt; first element in the graph # 对初始节点进行 DFS 遍历 dfs(element, res) # 返回结果 return res 初始节点不能访问其他可达节点(Leetcode 200 Number of Islands):\n function main_function(graph): # 边界条件,例如如果图是空的,或者初始节点本身就符合条件 if graph is empty return empty res -\u0026gt; a variable or array # 对图里面每个元素进行 DFS 遍历 for every element in the matrix dfs(element, res) # 返回结果 return res 如果题目要求的返回值是布尔值的话,遍历图可以提前结束:\n function main_function(graph): res -\u0026gt; a variable or array for every element in the matrix if dfs(element, res) -\u0026gt; True return True return False DFS 递归函数\n 写代码前,先需要遍历节点的层级关系\n对于有向图来说,某一节点需要进行遍历它的子节点(如果是二叉树则是左右子节点,如果是字符则可能是临近字符)。无向图则遍历临近节点(如矩阵,可能遍历上下左右或者下右节点)。我建议大家在面试实现的时候可以绘制出遍历图,这样写代码的时候会比较有把握。以下是 Leetcode 200 Number of Islands 的遍历流程图:\n上图中,要防止无向图中(1,1)被重复遍历,这里可以使用一个小技巧,先把当前节点的值设为无效值(这样在递归遍历中不会原路返回),DFS 遍历结束再还原。\n 函数实现\n这里有四个重点,1. 防止点被重复遍历,2. 检查节点是否合法,3. 检查更新后的状态是否符合要求,4. 更新接下来 DFS 遍历的参数。以下实现了两种形式,形式一把 match 函数放在子节点的遍历中,这样速度相对比较快,不过主函数需要处理边界情况。形式二则把 match 函数放在 DFS 函数的开头,虽然速度较慢,但是容易实现,以下是伪代码:\n function dfs_first(element, res, current, target, path): # 输入参数中, # element 代表需要遍历的节点 # res 代表保存结果的最终容器 # current 代表当前状态 # target 代表目标状态 # path 代表遍历路径(可选) # 遍历每一个子节点 for each child in element: # 1. 大部分问题中,在同一节点的遍历中都不能重复使用同一节点,所以在无向图中,需要修改图的节点值为非法, graph-\u0026gt;val = unvalid value # 2. 检查子节点是否合法,包括是否已经遍历过,是否越界 if is_valid(child): # 3. 检查子节点与元素组成的新状态是否符合条件 if match(current, child, target): # 更新最终结果 res += new_res else: # 4. 遍历所有合法子节点,更新当前状态以及路径 dfs_first(child, res, current+child.val, target, path+child) # 恢复图的节点值 graph-\u0026gt;val = valid value function dfs_second(element, res, current, target, path): # 输入参数中, # element 代表需要遍历的节点 # res 代表保存结果的最终容器 # current 代表当前状态 # target 代表目标状态 # path 代表遍历路径(可选) # 1. 先验证当前状态是否符合条件 if match(current, element, target): # 更新最终结果 res += new_res return # 2. 遍历每一个子节点 for each child in element: graph-\u0026gt;val = unvalid value if is_valid(child): dfs_first(child, res, current+child.val, target, path+child) graph-\u0026gt;val = valid value function is_valid(child): # 如果 child 合法则返回真,否则返回假 # 例如 child 在矩阵范围中 if 0 \u0026lt;= child.i \u0026lt; length of matrix and 0 \u0026lt;= child.y \u0026lt; length of first row of matrix return True return False function match(current, child, target): # 如果当前 child 与 current 的组合满足题目与 target 的要求,则返回真 if current + child.val equal to target return True return False 原题分析 我们试试在例子中运用此解题模式,第一题:(以下为 Python 代码,形式二只需要把 match 函数移在 DFS 函数开头即可)\nclass Solution: # 形式一 def pathSum(self, root, sum): # 边界情况 if not root: return [] # 边界情况2,因为我们是在遍历中验证是否符合条件,所以要检查初始条件是否已经符合要求 if root.val == sum and not root.left and not root.right: return [[root.val]] # 因为初始节点可以访问所有可达节点,所以只需要遍历初始节点 return self.dfs(root, [], root.val, sum, [root.val]) def dfs_first(self, node, res, current, target, path): # 1. 遍历每一个子节点 for n in [node.left, node.right]: # 2. 检查子节点是否合法,是否已经访问过,是否越界 if self.is_valid(n): # 3. 检查子节点与元素组成的新状态是否符合条件 if self.match(current, n, target): # 4. 更新最终结果 res.append(path+[n.val]) else: # 5. 遍历所有合法子节点,更新状态 self.dfs_first(n, res, current+n.val, target, path+[n.val]) return res def is_valid(self, node): # 只要存在则为真 if node: return True return False def match(self, current, child, target): # 题目要求 child 必须为叶子节点,并且与之前值的和等于 target if not child.left and not child.right and current + child.val == target: return True return False 114 / 114 test cases passed. Status: Accepted Runtime: 60 ms Memory Usage: 19.1 MB 第二题也是类似的方法,形式一以及形式二都能实现,因为形式一以及形式二区别不大,所以这里我选择了另一种特殊的方式,把 match 函数提前。\nclass Solution: def numIslands(self, grid): # 边界情况 if not grid: return 0 count = 0 # 因为岛之间可能并不相连,所以需要遍历整个图 for i in range(len(grid)): for j in range(len(grid[0])): # match 函数 if grid[i][j] == '1': grid[i][j] = '#' self.dfs(grid, i, j) count += 1 return count def dfs(self, grid, i, j): # 遍历可能的子节点 for k, v in [(i+1, j), (i-1, j), (i, j+1), (i, j-1)]: if self.is_valid(k, v, grid): # 我把 match 函数抽离出来放在主函数中了, grid[k][v] = '#' self.dfs(grid, k, v) def is_valid(self, i, j, grid): # 如果 i,j 合法的话 if i \u0026lt; 0 or j \u0026lt; 0 or i \u0026gt;= len(grid) or j \u0026gt;= len(grid[0]) or grid[i][j] != '1': return False return True 47 / 47 test cases passed. Status: Accepted Runtime: 84 ms Memory Usage: 14.1 MB 总结 解决 DFS 问题最重要的是四点,1. 防止节点被重复遍历,2. 遍历前,检查节点是否合法,3. 检查遍历后的状态是否符合要求,4. 更新接下来 DFS遍历 的参数,只要按照这个思路,形式怎么写都没关系。\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/leetcode/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E8%A7%A3%E9%A2%98%E6%A8%A1%E5%BC%8F/",
"title": "二分查找解题模式",
"tags": [],
"description": "",
"content": "概述 二分查找是 Leetcode 常见的题型,难点有二,第一是编写正确的二分查找程序,第二是想到用二分查找来解这道题,后者明显更难。\n辨别问题 先分析二分查找算法的使用场景:\n 从一个有限的递增区间中,找到符合要求的极值。\n 我们需要从题目中找到有限递增区间以及目标,先从最基础的二分查找程序开始,\n Leetcode 704. Binary Search\n Given a sorted (in ascending order) integer array nums of n elements and a target value, write a function to search target in nums. If target exists, then return its index, otherwise return -1.\n 给定一个递增数组以及一个目标值,如果目标值在数组内,则返回其索引值,否则返回 -1。\n Input: nums = [-1,0,3,5,9,12], target = 9 Output: 4 Explanation: 9 exists in nums and its index is 4 有限递增区间是 [-1, 0, 3, 5, 9, 12],目标是找到值等于 9 的元素。\n如果你不太熟悉二分查找,可以从这张 Gif 中观察查找的过程: (引用自 https://brilliant.org/wiki/binary-search/)\n这题的有限递增区间与目标值都非常明显,其中递增数组,返回目标值的索引,这两个词暗示着使用二分查找解答。不过一些题往往不会直接给出这些关键信息,所以也不容易想到可以使用二分查找解答。这里有几个例子,你可以试试在其中找出有限递增区间以及目标:\n 69. Sqrt(x)\n Implement int sqrt(int x).\n Compute and return the square root of x, where x is guaranteed to be a non-negative integer.\n Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.\n 找到目标值的平方根,然后转成整数\n Input: 4 Output: 2 有限递增区间是 [0, 1, 2, 3, 4](0 到 目标值的值),目标是找到最大的数组中平方小于等于 4 的值。例子中,我们会先计算递增区间里 2 的平方与 4 的大小关系,然后更新搜索区间。\n 1044. Longest Duplicate Substring\n Given a string S, consider all duplicated substrings: (contiguous) substrings of S that occur 2 or more times. (The occurrences may overlap.)\n Return any duplicated substring that has the longest possible length. (If S does not have a duplicated substring, the answer is \u0026ldquo;\u0026quot;.)\n 给定一个字符串,返回最长的连续重复出现的字符串。\n Input: \u0026quot;banana\u0026quot; Output: \u0026quot;ana\u0026quot; 有限递增区间是 [0, 1, 2, 3, 4, 5](零到字符串的长度减一),目标是找到最长的有重复出现的字符串。例子中我们先计算字符串长度为 2 的所有字符串是否有重复出现的字符串,如果有的话,我们尝试找更长的长度为 3 的字符串,更新搜索区间,直到循环结束。\n 778. Swim in Rising Water\n On an N x N grid, each square grid[i][j] represents the elevation at that point (i,j).\n Now rain starts to fall. At time t, the depth of the water everywhere is t. You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most t. You can swim infinite distance in zero time. Of course, you must stay within the boundaries of the grid during your swim.\n You start at the top left square (0, 0). What is the least time until you can reach the bottom right square (N-1, N-1)?\n Input: [[0,2],[1,3]] Output: 3 Explanation: At time 0, you are in grid location (0, 0). You cannot go anywhere else because 4-directionally adjacent neighbors have a higher elevation than t = 0. You cannot reach point (1, 1) until time 3. When the depth of water is 3, we can swim anywhere inside the grid. 有限递增区间是 [0, 1, 2, 3](矩阵的节点总数),目标是找到数组中最小的且能够到达右下角的合法值。例子中先计算 time 为 1 的时候能否到达右下角,可以的话,就更新区间,找更短的时间。\n 像我一开始说的,二分查找的程序本身并不难,但是想到这题可以用二分查找来解答并不容易,另外常见的几个使用二分查找的提示语分别是:\n 时间复杂度与 O(log(n)) 有关 所求答案有明确的上下界限范围。 题目描述中涉及到数据结构的和(如一个全部为正整数的数组,此时可以先计算累积和(保证是递增数组),然后使用二分查找)。 解题模式 如果我们足够幸运找到了递增区间与目标,那么就可以开始编写二分查找的程序了,解题模式比较直观,先从递增区间的中间开始,验证 mid 是否符合条件,然后根据返回值更新查找范围,每次缩小一半范围直到跳出循环,以下是伪代码:\nfunction binary_search(array, target): left = 0 right = length of array while left smaller than right mid = (left + right) / 2 # 判断 mid 是否符合题目条件 if is_valid(mid) is True return mid else if other condition left = mid + 1 else: right = mid function is_valid(mid): # 返回 mid 是否符合条件的布尔值 以上是最基础的二分查找程序,只需要找到符合条件的值即退出循环,不过在上面的 Leetcode 例子中,大多需要找到极大值或者极小值,所以需要变形为:\nfunction binary_search(array, target): left = 0 right = length of array while left smaller than right mid = (left + right) / 2 # 使用 left 保存上次符合条件的值 if is_valid(mid) is True left = mid + 1 else: right = mid # 返回最后的合法值 return left function is_valid(mid): # 返回 mid 是否符合条件的布尔值 以上的程序实现起来并不难,不过初学者容易混淆一些边界条件导致死循环,我们这里通过分析两种正确的二分查找程序来说明如何编写正确的程序,给定数组:\n[1, 2, 3, 4, 5] 两种实现方式区别在于右边界的初始值:第一种实现方式定义右边界为递增区间长度,第二种实现方式定义右边界为递增区间长度减一:\n[1, 2, 3, 4, 5] | | left = 0 right = 5(递增区间长度,不可能是目标答案,不可达) [1, 2, 3, 4, 5] | | left = 0 right = 4(递增区间长度减一,可能是目标答案,可达) 我们从右边界的可达性,可以推理出二分查找函数中的其他要点:\n 跳出循环条件:当左边界等于右边界时,因为右边界不可达,所以此时的左边界也不可达,所以查找结束,跳出循环。 右边界更新值:右边界不可达是在整个函数都不变的,所以在更新右边界的时候,应该更新为验证过的 mid,而不是 mid - 1。(因为 mid - 1 是可达的) 以下是两种形式的伪代码:\nfunction binary_search(array, target): ''' 形式一:右边界不可达 ''' left = 0 # 右边界等于区间长度 right = length ofarray # 当左边界小于右边界时跳出循环 while left \u0026lt; right: mid = (left+right) / 2 if array-\u0026gt;mid equal to target: return mid else if array-\u0026gt;mid \u0026lt; target: left = mid + 1 else: # 右边界更新为 mid right = mid function binary_search_two(array, target): ''' 形式二:右边界可达 ''' left = 0 # 右边界等于区间长度减 1 right = length of array - 1 # 当左边界小于等于右边界时跳出循环 while left \u0026lt;= right: mid = (left+right) / 2 if array-\u0026gt;mid equal to target: return mid else if array-\u0026gt;mid \u0026lt; target: left = mid + 1 else: # 右边界更新为 mid - 1 right = mid - 1 实现方式 右边界初始值 右边界是否可达 跳出循环条件 更新右边界 一 递增区间长度 不能 while left \u0026lt; right 等于 mid 二 递增区间长度减一 可以 while left \u0026lt;= right 等于 mid - 1 你可以试试解决 Binary Search 来验证自己是否正确理解,其中两种实现方式也有共同需要注意的地方,在代码段:\nmid = (left + right) / 2 根据题目要求,可能需要改为 left += (right-left) / 2 的方式防止正整数溢出。 需要根据我们找的目标值是整数还是浮点数来决定是否使用整除,若目标值是浮点数,left 与 right 应该更新边界为 mid。(如果像之前一样更新为 mid - 1 或 mid + 1 的话,会错过 mid 到 mid - 1 / mid + 1 之间的可能答案) 原题分析 1044. Longest Duplicate Substring 如果我们知道这题是使用二分查找解答的话,那么只需要遵照上面的解题模式,找到递增区间,目标值以及 is_valid 函数即可。这道题的所求答案在 0 和 字符串长度减一之间,在例子 \u0026ldquo;banana\u0026rdquo; 中,答案一定是 0 到 5 之间,所以我们可以先猜测长度为 3 的所有字符串是否有符合条件的值,如果有的话,我们先保存下来,然后向右边找更长的字符串 4,在 \u0026ldquo;banana\u0026rdquo; 中,初始递增区间为 [0, 1, 2, 3, 4, 5] 我们先找出所有长度为 3 的字符串:\n ban ana nan ana 这里 \u0026ldquo;ana\u0026rdquo; 重复出现了,所以答案至少为 3。 我们减少搜索区间为 [4, 5],然后再继续找是否有字符串重复出现\n如何查找重复字符串 最简单的方法是使用 hash table 或者字典来保存出现过的字符串,然后比对是否重复出现,例如:\nhashmap = {} ban -\u0026gt; not in the hashmap -\u0026gt; hashmap = {ban} ana -\u0026gt; not in the hashmap -\u0026gt; hashmap = {ban, ana} nan -\u0026gt; not in the hashmap -\u0026gt; hashmap = {ban, ana, nan} ana -\u0026gt; in the hashmap -\u0026gt; return 'ana' 在 Leetcode 中,某些 testcase 会导致 Memory Limit Exceeded,所以需要我们自己实现简单的 hash table。不过本文主要说明二分查找,所以我直接使用 hash table。以下是 Python 代码:\nclass Solution: ''' 此答案会导致 Memory Limit Exceeded ''' def longestDupSubstring(self, S): self.ans = '' res, lo, hi = 0, 0, len(S) while lo \u0026lt; hi: mi = (lo + hi + 1) // 2 # 不同于简单的二分查找,我们要找的是符合条件的 # 最大值或者最小值,所以当 is_valid 返回 True 的时候 # 并不直接返回,得到答案,而是先保存当前符合条件的答案,然后持续更新 pos = self.is_valid(mi, S) if pos: lo = mi else: hi = mi - 1 return self.ans def is_valid(self, L, S): dic = {} for i in range(len(S)-L+1): if S[i:i+L] in dic: # 持续更新符合条件的答案 self.ans = S[i:i+L] return True else: dic[S[i:i+L]] = True return False 总结 观察题目能否用二分查找是解题的第一步,需要观察题目,找到有限递增区间以及目标,第二部则是正确实现二分查找算法(注意右边界的可达性)。\n"
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/leetcode/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E9%A2%98%E6%A8%A1%E5%BC%8F/",
"title": "单调队列解题模式(未完成)",
"tags": [],
"description": "",
"content": "概述 496. Next Greater Element I\n"
},
{
"uri": "https://www.enginego.org/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/app-store-%E8%AF%84%E4%BB%B7%E8%BF%87%E6%BB%A4%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "App Store 无用评价过滤",
"tags": [],
"description": "",
"content": "App Store 的垃圾评论不是一天两天了,垃圾短信\n苹果的不作为 我不清楚为什么苹果并没有用心在过滤垃圾评价这里,垃圾评价多得我几乎找不到正常的评价,而且我在美国区的 APP STORE 找不到这些情况,我不确定是因为美国区刷评价情况比较少(但是 刷亚马逊评价的多得一塌糊涂,即使现在比较少,将来刷评价的也会多)还是苹果放弃了这一块。苹果或者觉得减少垃圾评价对它的业绩毫无帮助,作为一名开发者,当看到别的应用因为刷评价排到搜索的前面而自己却在后面的时候,很难开心得起来。这篇文章让我们用一些简单常用的方法来识别垃圾评价,顺便学习下数据分析吧。\n目标 看起来我们的目标很一致,过滤垃圾评价,保留正常用户的真实评价。不过这里对于“真实”可能大家的理解不一致,每个用户的背景以及受教育程度同,评低分的原因也各不相同(有的可能因为这个应用要求评分就评低分,有的可能仅仅是发泄情绪,有的为了让评论能置顶所以评5星,但是里面的实际内容却是差评),所以这里我们的目标并不是筛选海军/机器人刷的评论,而是找出对用户最有用的评论,这里在 App Store 的默认评价也可以看到,虽然,往往出来的都没有用。\n结果 虽然最终我们会通过我实现的代码来找到这些有用的评价,但是代码以及算法本身却不是最重要的,如何理解数据并且懂得对数据进行不同维度的分析才是最应该学习的地方,这篇文章不会涉及太多代码以及技术细节,你们可以把它当成一个即开即用的工具就好,问题是如何使用这些工具,以什么顺序,什么方式来使用,如何测量最终的结果。\n有用的评价 直接想什么是有用的评价可能不是太容易,我们先判断什么是没用的\n 评价字数过短或者过长的 这点一开始大家不一定认同,不过我们对比下知乎以及Quora,你会发现知乎上有非常多抖机灵的回答,通常依旧是一两句话。但是 Quora 却规定回答必须超过一定的长度,硬性地确保回答的信息量达到一定标准。虽然这是产品选择的不同,不过从一定程度也能够杜绝一些纯粹发泄情绪或者无用的评价信息。同样,防止过长以及不相关的评价是防止用户在发现限制后乱填内容,我们也能通过其他方式来检测到。或者说,就苹果现在的情况来说,越少的评价反而更好。\n语法错误问题严重的,语句不通顺 这点我想大家没有异议,语法错误严重的,语句不通顺代表没有深思熟虑思考过就提交的。\n评价与应用主题不相关的 评论本身重复程度过高\n 评价之间重复程度过多\n 打广告的,让大家去哪里下载什么的 这个防止恶意刷评价(无论是刷自己还是刷竞争对手)\n当然,还有其他标准,这里只列举最常见的几种。中国区的评价数量都较多,我们可以列举最有用的50条评价,我想已经足够大家去了解这个应用的情况。让我们一条条来分析,\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E6%88%91%E4%BB%AC%E5%8F%AA%E8%BF%BD%E9%80%90%E8%B1%AA%E9%97%A8%E8%AF%91/",
"title": "我们只追逐豪门(译)",
"tags": [],
"description": "",
"content": "两年前,我在 HN 看到这篇文章,至今仍时常出现在我的脑中。文中谈到了问题在国内同样也在发生,我了解的许多公司招聘的时候只专注于“大厂”,“211/985” 这些标签,而不是求职者的真实能力。在某程度上,这也限制了阶级向上流动。这篇文章提出了一些有趣的解决方法,以下是原文翻译:\n我的好友 Mike 最近在找一份新工作,他刚刚以合同工的身份被微软解雇,我身边不少朋友也有类似的遭遇。和我一样,Mike 也有十一年的硬件行业工作经验,不同的是他并不认识那些身在豪门的工程师。所以我把他的简历内推给了一些急需招聘的工程师朋友。朋友们都认为 Mike 的简历不错,但大多数招聘人员在却在简历关就拒绝了他。\n被拒绝的原因通常是:\n “没有相关领域的工作经验。”\n “涉猎太广泛,支付,移动开发,数据分析和用户体验都有接触过。”\n “通常来说合约工技术水平不高。”\n 其中有一家公司 TrendCo,与其他数以千计的公司同样声称拥有世界一流的工程师,并且只会雇用最好的工程师。这几个回应是他们的招聘人员通过工程师传达给我的,工程师对这些回应也表示不满。但这确实代表了一大批公司对 Mike 的普遍反应。\n \u0026gt; “Mike 是一名 .NET 开发者,我们不喜欢有 Windows 开发经验的工程师” 我熟悉 TrendCo 的现状,不少员工都说已经火烧眉头了,他们核心系统的 QPS 低于 1k,高负载下偶尔会宕机。Mike 研发过可以承受更高数量级负载的系统,但他的经验显然是,无关紧要的。 我曾经面试过 TrendCo,他们的吸引点之一是它是一个创业公司,你能够接触各种各类的工作。TrendCo 需要聘请多面手,但 Mike 的对于他们来说显然是,涉猎过多。\n 和第一点一样,TrendCo 还抱怨 Mike 不是他们想要的类型,TrendCo 的大部分员工都是刚刚从一些最好的学校毕业 0-2年 的工程师。他们没有太多招聘的经验,大多数时候他们都选择从一些热门的大公司中进行招聘,而不是一些像微软这些无聊的公司。\n 无论你认为这种偏向某种类型而且拒绝其他类型的做法是否不妥,正如 Thomas Ptacek 所指出的,如果你目标人才的类型与其他人的类型相同,那么“你正在与市场上最富有的(或资金过剩的)科技公司竞争“。\n如果你看看萌新工程师的招聘数据,脸书给刚毕业的工程师超过 10万 美金一年,10万 的签约奖金,15万 的受限股票,加起来大概 16万 美金一年。而且第一年 24万 美金,谷歌也给了大概 10万 一年,1万 左右的签约奖金,18万7千 的受限股票,虽然比脸书低点,但是比很多那些宣称只招聘最优秀的工程师的公司要高得多,而且他们为了争夺工程师还可能会给更高,一些有经验的工程师得到的工资远比你想象的要高,如果你不是招聘人员,他们的薪酬水平很可能会高于您的预期。\nTrendCo 这种追逐豪门的做法令他只剩下两个选择,要不为员工付出更高的工资,要不提出一些没什么竞争力的薪酬方案。TrendCo 选择了后一个,这也解释了为什么他们拥有如此少的高级工程师 —— 随着工程师变得更资深,相应薪酬补偿会变得更高,所以你必须向他们给出非常有吸引力的薪酬方案,才能让他们接比其他公司低 15万 美元的工资。不过也因为经验老到,他们不太会相信这些股票期权的价值。\n先说清楚,我对拥有出色背景的工程师没有任何意见。我认识的这些人既拥有无可挑剔的面试技巧,并且在上次找工作时同时获得 5 到 10个 不错的 Offer。我也和那样的人一起工作过:他刚毕业薪酬就超过 20万 美元,但仍然物超所值。不过想一想。他有来自六家不同公司的 Offer,而他最多只能接受一家公司。包含午餐和电话面试的时间,这些公司平均用了 8个 小时来面试他。而且因为他们那么想要招到他,有些公司还花费了 5个 小时试图说服他接受他们的 Offer。为了六分之一 的招聘机会,这些公司加起来花费至少(8 + 5)* 6 = 78小时 的工程师时间在他身上。一般来说,出色背景的人当然非常优秀,但他们真的很难招到。招聘一些被低估的人相对会容易得多,特别是当你给不起市场价的时候。\n无论在招聘还是求职的时候,我都亲身经历了拥有出色背景下会发生的荒谬情况。先说招聘,在之前的创业公司,我尝试去招聘一名我见过最有创造力和最有趣的工程师,他因为大学的 GPA 太低而几年都没有找到工作,最终我们公司拒绝招聘因为觉得他的 GPA 太低代表不够聪明。直到几年后,谷歌愿意给他机会,从此之后他就一帆风顺了。实际上,他是那位说服我去谷歌的人,在谷歌,我尝试去招聘一名我认识的最有生产力的工程师,但是却被招聘人员因为技术不够好而拒绝。\n作为求职者方面,我经历了从门庭若市到无人问津的转变,我在威斯康星大学读完本科,那是一所自称全球前十的计算机科学/工程的 25所 学校之一,因此当我毕业时,一大批招聘人员主动联系我。但我觉得这很愚蠢,我在威斯康星大学读书不完全因为自己的能力,我只是刚好在威斯康星州长大。如果我在犹他州长大,我可能最终会去犹他大学读书。当把我和犹他州或者爱达荷州的人比较时,他们的教育水平其实基本上与我相同。威斯康星大学的声誉来自于拥有优秀的教授,但这些研究与实际教授本科生的水平并没有太大关系。尽管你可以在其他数百所学校接受相同的教育,但我凭着这个背景还是很容易得到面试的机会和找到好工作。\n我在 Centaur 公司度过了愉快的七年半,Centaur 在奥斯汀的硬件业界声名显赫,所以我在当地的硬件公司都很抢手,但我并不认识开发软件的工程师听说过 Centaur,这导致我在大部分软件公司都没有获得面试的机会,有几次我被积极内推,但是招聘人员依然不愿意和我聊天,我觉得这个情况太滑稽了,而朋友们都觉得非常沮丧。即使我获得面试机会,结果往往也很糟糕。一个典型的拒绝原因是 “我们每天处理数百万笔交易,我们真的需要具有更多相关经验,无需培训的人才”。最后谷歌愿意给我机会,在一个我加入之前还是 20% 时间的项目中,我是第二个认真研究深度学习性能的人。我们建立了世界上最快的深度学习系统。据我所知,他们现在已经开发了第 N代,不过多年后,即使我们建造的第一代产品也具有比我今天所知的任何其他生产系统更好的单点性能和性价比(当然,不包括这个项目的迭代版本)。当我在谷歌的时候,常常有招聘人员联系我,现在我在无聊的微软公司,基本没有招聘人员会找我。在找工作的时候我想知道自己现在有多受欢迎,相关领域的经验?有,多个领域的广泛涉猎?有,合约合同?没有,不过有三分之二的话还不错。\n这里我的观点不单单和自己有关,而是一个人在不同时期对雇主的吸引力差异很大,主要是由于一些表面因素而与实际生产力没有太大关系。这也是在谷歌常常发生的故事,如果你在谷歌工作之前雇佣他们,那会物超所值!但没有人(除了谷歌)愿意抓住这个机会。虽说给更多价格可以得到更好的质量,但 TrendCo 却一直只限定在招聘出色背景的人,导致他的招聘渠道越来越窄。\n我并不是故意批判像 TrendCo 这样的创业公司。无聊的老公司也有他们自己的准则。我一个朋友不能雇佣我内推的人因为他的团队不允许雇佣没有学位的人。另外一个朋友的团队不愿意招聘无业状态的工程师。这些决策不仅对公司而言不是最佳的,而且它们会在就业结果中产生影响,导致个人身上发生的好(或不好)的事件会跟随人们数十年。您可以在文献中看到各种领域中薪酬变化的类似研究。\nThomas Ptacek 有一个有趣的分享:“我们只要求面试者有 .NET 开发的业务经验,但是他们却向我们展示了一个需要 6个 小时来运作,利用椭圆曲线以及部分随机偏差的攻击脚本,里面还涉及傅里叶变换和BKZ点阵优化。” 如果你在一家不追逐豪门的公司,你会听到很多类似的故事。有些最优秀的人直到进入谷歌之前,都来自我从来没听过的学校和我从来没听过的公司,有些至今仍在那些你从没听过的公司。\n如果你阅读过 Zach Holman,你可能会想起当他说他被解雇的时候得到的一些回应:“如果一个雇主要解雇你,你不单单是在这份工作失败了,你连做人都失败了。” 许多人将就业状况和证书视为个人固有价值的衡量标准。但这些成功标志的一大部分,更不用说成功本身,靠的只是运气。\n解决方法? 我能理解为什么会这样。在个人层面,我们容易出现归因错误。在组织层面,快速发展的公司倾向于将大部分时间用于面试,而减少面试时间的显而易见的方法是仅面试具有“出色”背景的人。不幸的是,当你要招聘与竞争者同样的人才时,这会适得其反。以下是一些我的解决方案。欢迎大家给出更好的建议!\n点球成金 Billy Beane 和 Paul Depodesta 买下了 Oakland A\u0026rsquo;s 队,那是一支预算远低于顶级球队的棒球队,但他们通过寻找和雇佣那些在统计学上被低估了价格的球员创造了可以说是棒球历史上战绩最好的球队。令人意外的是,他们愿意公开他们是怎么做到的,Michael Lewis 写了一本名为《点球成金》的书来解释他们怎么做。尽管进行了前期宣传,但竞争对手需要也花费了数年时间才能跟上 Oakland A\u0026rsquo;s 的策略。你也可以在软件招聘中看到完全相同的东西。 Thomas Ptacek 一直在谈论五年来他们如何在 Matasano 雇佣极其优秀的人才。谷歌高层经常谈论他们拥有的有效以及被证明无效的招聘数据。他们谈到了为什么专注于顶尖学校是无效的,而且无法找到那些几年前就表现出色的员工,但这些都不能阻止 TrendCo 将招聘集中在顶尖学校上。\n培训/指导 你看到很多关于《点球成金》的讨论,但人们对\u0026hellip;培训和指导却没有那么重视,就是那种让“不是最优秀的人”变成最优秀的方法。\n让我们看看体育界的做法,篮球一个赛季中有很多不同的数据。如果我们看一下大学篮球,我们可以确定一些不起眼却能产生良好结果的方法。在一个竞争激烈的领域,每个球队都被期望指导和训练他们的球员。但是大多数科技公司根本没有尝试过,在中大型的公司,你会得到几天时间来“定位”自己,主要是一些法律和文书类的工作,以及偶尔的“培训”,这通常是一组视频和一些多选题,根本没有教到任何东西。即使你将被分配一名导师,他也很可能不会提供任何实际的指导,初创公司在这方面往往更糟糕!然而,做好这一点并不难。\n考虑到公司在招聘和保留“最佳人才”方面花费了多少钱,你会期望他们在培训上花费至少一些(非零)时间。同样奇怪的是,公司在招聘时不会更多地关注或培训和指导。我在团队中担任特定角色中学到的东西对我来说非常有价值,但那要么是一个愉快的意外,要么是我努力去做的事情,大多数公司都不关注这些东西。当然,招聘人员还是会告诉你“你在这里学到的东西比谷歌要多得多,这会让你更有价值”,暗示着这些值得你每年减薪 15万 美元,但如果你问他们如何创造出一个比谷歌更好的学习环境,他们从来回答不出来。\n流程/工具/文化 我曾在两家公司工作,这两家公司在工具选择上都拥有无限的资源。其中之一,我们称之为 ToolCo,对待选择与开发工具非常认真并且投入了大量精力。人们用“魔法”,“我见过的最好”,“我不敢相信这甚至是可能的”等短语描述他们所使用的工具。我明白箇中原因。例如,如果您想构建一个包含数百万行代码的项目,那么它们的构建时间可能会占用 5 到 20秒(假设你没有启用 LTO 或其他无法并行化运行的代码)。在平时你将使用多种看似神奇的工具,因为它们远远领先于外部世界。\n另一家公司,让我们称之为 ProdCo 为同样为了工具付出钱与精力,但并不真正重视它。人们会用诸如“世界级的烂软件”和“比其他任何地方都低效2倍”以及“我无法相信这甚至是可能的”的短语来形容他们的工具。 ProdCo 有一篇关于构建新系统的论文,他们声称的并行化/缓存,完成度和可靠性至少比 ToolCo 要低两个数量级。而且,根据我的经验,实际数字远比论文中的要更糟糕。在 ProdCo 工作一天的过程中,你使用的工具会比 ToolCo 的差非常多。这些细节加起来很容易产生比 “追逐豪门” 更大的影响。\n流程和文化也很重要。我曾经在一个没有使用版本控制或 bug 管理的团队工作过。在 Joel Test 中看似简单的东西,都会有很多团队会犯错。虽然我只加入过一个没有通过 Joel 测试的公司(他们得到了 12分 中的 1分),但我在的每一个团队都有明显的缺陷,其中技术缺陷是微不足道的(但如果发生在文化上则是一场灾难)。当我在谷歌时,我们在不同地方的团队之间存在非常糟糕的沟通问题。而我的解决方案很简单:我开始为所有本地会议和讨论打包会议记录,并从远程团队那里了解我们笔记中令他们感到惊讶的事情。这是任何人都可以做到的事情,但这对整个团队来说是一个巨大的生产力提升。我实际上从来没有遇到一个环境是无法用看似微不足道的东西来大规模地提高生产力,有时候人们不同意这个说法(例如,让非版本控制团队开始使用版本控制会花费几个月),但这是另一篇文章的主题。\n大多数公司都没有充分利用工程师。招聘“出色背景”的工程师然后放任他们的意义何在?你可以通过招聘普通人并培养他们成功来获得更好的结果,并且这样会便宜很多。\n结论 当我刚接触编程的时候,我听到很多关于程序员如何脚踏实地的故事,不像那些穿着西装制服带着领带的精英人士,你甚至可以穿着 T恤上班!但是如果你认为程序员不是一般的精英主义者,试试穿西装打领带去面试,这种情况下,你反而必须要努力证明自己符合他们的文化。我们认为自己与那些根据外表来判断人们的行业不同,但我们其实做着同样的事情,我们不会说人们不够聪明是因为他们血统有问题,只是当别人认为求职者不适合是因为他们不打领带,我们会说他们不适合是因为他们打领带,这其实是一码事。\n谢谢 Kelley Eskridge, Laura Lindzey, John Hergenroeder, Kamal Marhubi, Julia Evans, Steven McCarthy, Lindsey Kuper, Leah Hanson, Darius Bacon, Pierre-Yves Baccou, Kyle Littler, Jorge Montero, and Mark Dominus for discussion/comments/corrections.\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E7%BC%96%E7%A8%8B%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AD%A5/python%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%90%E8%A1%8C%E7%9A%84/",
"title": "Python 是如何运行的(未完成)",
"tags": [],
"description": "",
"content": "Python 是我以及许多人第一门深入学习的编程语言,在我初学 Python 的时候,并不需要了解它的运作原理,而随着我了解得越多,我想知道的也变得越多。我阅读了不少 Python 的源代码,也阅读了很多相关的文章,这篇文章会略过代码,从高层次的角度给各位一些指引。\n世界上最好的编程语言是什么,取决于你用来做什么,不过正确的答案往往是问你这个问题的人最喜欢的语言。如果你学习编程是为了乐趣的话,那么 Python 会给你很多乐趣。从服务器后端,机器学习,数据分析都有它重要的位置,而且可读性非常高。如果你是为了找份工作的话,学习什么语言就根据你所在地的招聘信息来定了,一般来说 Golang, Java, C++ 这几个都不错。Guido van Rossum 在 1989 年趁着家人去旅游写下的第一个版本是用 C 语言实现的(感谢那次旅游),至今最流行的实现也是这个版本,称为 CPython。\n不过无可否认的是,Python 正慢慢变成全球最受欢迎的语言,没有之一。\n那么计算机是如何理解并且运行 Python 的源代码的呢?分为编译与解释两个过程,首先,计算机按照顺序从上到下一条条执行指令的,计算机的优点是它能迅速地完成一些重复的工作,例如 1秒钟 进行百万次的加减,赋值。由于计算机不能像人类一样理解过于抽象的内容,例如函数,类(即使可以,也会非常耗时),所以需要把复杂,抽象的源代码简化为机器能够理解,能够快速运行的代码,这个过程就是 Python 的编译过程。\ntokenizer 过程 编译器首先要理解源代码,首先要分析每个字符串的含义,例如 def 和 class 这类关键字的含义与普通的字符串就不一样。\n举一个例子 hello.py\ndef say_hello(): print(\u0026quot;Hello, World!\u0026quot;) 下面我们使用 Python 自带的 tokenize 模块分析 hello.py,我们可以看到 tokenize 模块把 \u0026ldquo;Hello, World\u0026rdquo; 当成 STRING 类型,而 def, say_hello 等当成了 NAME 类型。\n$ python -m tokenize hello.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,3: NAME 'def' 1,4-1,13: NAME 'say_hello' 1,13-1,14: OP '(' 1,14-1,15: OP ')' 1,15-1,16: OP ':' 1,16-1,17: NEWLINE '\\n' 2,0-2,4: INDENT ' ' 2,4-2,9: NAME 'print' 2,9-2,10: OP '(' 2,10-2,25: STRING '\u0026quot;Hello, World!\u0026quot;' 2,25-2,26: OP ')' 2,26-2,27: NEWLINE '\\n' 3,0-3,1: NL '\\n' 4,0-4,0: DEDENT '' 4,0-4,9: NAME 'say_hello' 4,9-4,10: OP '(' 4,10-4,11: OP ')' 4,11-4,12: NEWLINE '\\n' 5,0-5,0: ENDMARKER '' parser 过程 理解了源代码包含哪些元素之后,就需要组合它的结构,组合成解释器能够理解的句子,就像把句子的主谓宾放好位置一样,这个过程称为 parser。\npyc 文件 经过 tokenizer 和 parser ,编译器把 .py 后缀的源代码转换成了二进制的 bytecode。也就是 __pycache__ 文件夹中的 .pyc 文件。\n在解释器眼中的 bytecode 就是一条条指令,我们可以通过 Python 自带的 dis 模块阅读 bytecode 包含的指令。 解释过程 我们通过 dis 模块获取了解释器所需要的反汇编码\n基于栈的虚拟机 栈是计算机中常用的数据结构,栈符合“后进先出”的规则,就像抽屉一样,按照顺序放 A,B,C。那么 C 会在抽屉顶,那么从栈中获取到的第一个元素是 C\nPython 使用的是基于栈的虚拟机,(另外还有基于寄存器的虚拟机),Python 会把源代码:\na = 'A' b = 'B' a + b 转换成类似\nSTORE_NAME(把 a 与 b 两个变量的值存在栈中) ... LOAD_NAME(获取栈顶的元素 a 和 b) ... BINARY_ADD(把 a 和 b 相加) 这样的字节码,Python 通过 STORE_NAME 先把 a 与 b 两个变量的值存在栈中,LOAD_NAME 获取栈顶的两个元素。\n运行过程 运行过程其实就是解释器(包含了基于栈的虚拟机)一句句地执行这些反汇编码,例如把变量压入栈,获取栈顶的变量。\n"
},
{
"uri": "https://www.enginego.org/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/%E5%BE%AE%E5%8D%9A%E6%8A%BD%E5%A5%96%E5%88%B0%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/",
"title": "微博抽奖到数据分析",
"tags": [],
"description": "",
"content": "感谢王思聪微博抽奖这个热点事件,我们获得了一个学习数据分析的机会😄😄。开始之前,我们先回顾一下这个事件:IG 战队在 LOL 比赛中夺冠,某王姓土豪发布了微博,从转发者中抽取 113名 用户送现金。\n我们可以看到大概有 2200万 人转发了该微博,开奖之后,出现了奇怪的事情,中奖者有 112名 女生,仅有 1名 男生。 这个悬殊的比例引起了一些争议:\n 有的人说王思聪在选后宫,只抽女生。 有的人从直觉出发说微博抽奖明显有猫腻,再怎么男生用户也不可能那么少。属于暗箱操作,虚伪抽奖,欺诈行为。 有的人从历史数据中发现微博抽奖女生中奖的概率要比男生大得多,而且得出了少女 \u0026gt; 小孩 \u0026gt; 男人的用户价值鄙视链。发表了微博抽奖扭曲了价值观之类的言论。 有的从新浪的角度出发,表明这只是新浪为了清除僵尸粉的措施,不小心把大部分男粉丝清除掉了。 有的人认为抽到的都不是 ig 的粉丝,这样就像把演唱会门票送给不是粉丝的人一样,并不公平。 作为一名旁观者,如果没有一定的统计与概率论背景,不太容易明白他们在说什么,更不用说判断对错了。如果你本身就不喜欢微博,会更容易认同诋毁微博方的观点。不过,这种根据自己的喜好所得出的结论是自欺欺人而不是真相。统计学是一个工具,而且是可以根据使用者的主观意识去操控的工具。我希望通过这篇文章通俗地解释整个事件背后的原理,让读者得出自己的结论,独立思考远比随波逐流重要。更好的消息是,这篇文章不需要任何数学背景知识。可以说,只要在今年双十一懂得如何凑单,都能看懂。(好吧,凑单其实好难,😂😂)\n小型微博 **要知道这个抽奖有没有猫腻,我们先要知道怎么的结果是有问题的,怎样的是正常的。**我们坐上时光机器,回到微博刚开始发展的时候,那时候微博一共只有 100名 用户,刚好 50名 男生 50名 女生,\n微博为了庆祝这个里程碑举办了一次抽奖活动,从所有用户中里面随机抽取 100名 中奖者。毫无疑问地,中奖者的男女比例一定是 1 比 1,因为把所有用户都抽到了,皆大欢喜。\n慢慢越来越多人加入微博,现在已经有 1000名 用户,男女用户比例不变,还是 1 比 1。微博举行第二次抽奖,这时候还是从所有用户中随机抽取 100名 中奖者,那么这个时候,中奖者的男女比例是多少呢?我们不知道,有可能还是 1 比 1,有可能 60名女生,40名男生。也有可能全是女生。不需要用到数学公式,我们知道,**男女用户数量相近的情况会比全是女生出现的概率要大。**为什么?想象你参加一个游戏。\n一个箱子里装了 50个 红色球代表女生 ,50个 蓝色球代表男生。充分混合后,要求从箱子里连续抽出 50个 红色小球。\n额,这太难了吧。你说。因为抽的时候必须**靠运气避开所有蓝色球。**好吧,那我修改下规则,同样的条件下允许你错 10次,抽 50次 里面有 40个红色球就好。这虽然还是很难,但是要比一开始要简单,**因为这种情况允许你犯错 10次,所以成功的机会更高。**回到刚刚微博抽奖的问题,抽 100名用户,要求全部是女生的话,必须避开另外的 500名 男生用户。所以这种情况出现的概率比允许犯错(90名 女生,10名 男生)要出现的概率更低,具体多低呢?下面这个图片显示了中奖者女生数量出现的概率大小,越高代表概率越大。\n我们可以看到中间 50 的高度最高,那么是不是抽到 50名 女生的概率最高呢?是,也不是,在考试中如果这样回答可能会得到满分,但这并不代表是事实。如果你手边有硬币的话,可以试试抛 10次,你会发现更有可能出现 6次 正面或者 6次 反面的情况,而不是刚刚好 5次 正面,5次 反面。奇怪,既然正反面出现的概率都是 50%,那为什么不是出现 5次 正面的概率最高呢?这和理论不符合。其实,这是因为这类图表没有列出所有的可能性,我重新绘制了一张新的图表,如下:\n**新的图表中,中间是抽到 50名 女生的概率,最右边是抽到 100名 女生的概率。而最左边 +2 出现的概率最高,+2 的意思是一方比另一方多 2名 的情况,也就是抽到 49名 女生和抽到 49名 男生这两种情况的概率相加,**你可能会问,这算一种情况吗?这就是现实问题有趣的地方,它跳脱于理论的束缚。这个抽奖如果抽到 100名 女生,我们会很惊讶。同时,如果抽到 100名 男生的话,我们也是同样地惊讶。**我们更关注的是一边比另外一边多的数量,也就是相对的关系。抽到 49名 女生和 49名 男生 这两种情况都是一方比另一方多 2名。所以都属于一种情况。**理解这点就能理解为什么硬币更容易出现 6次 正面或者 6次 反面,因为它们加起来出现的概率最高。\n从图表中我们看到,抽奖中最有可能出现的是一方比另一方多一些这种情况,其次是两边相等的情况,再其次是两边差距非常大的情况。这个时候,我们需要定义什么是可信的抽奖结果,也就是回答我们一开始的问题,怎样的结果才是正常的?从统计上来说,一般我们会选取最有可能出现的 95% 的区间作为可信范围,也就是上图的黄色区域,最右边的白色区域虽然是可能发生的,但是由于发生的概率太低,我们认为这种情况,是不可信的结果。也就是说,如果这次抽奖抽到 99名 女生,100名 女生的结果都代表这次抽奖不可信。\n现代微博 我们从时光机器中回到现代的微博,从微博 CEO 的微博中我们可以得知这次王思聪的抽奖转发参与的男女比例是 1 比 1.2\n参加男女比例相近,那么出现如此悬殊的比例,可信吗?不可信,有的人说这是**小概率事件,有可能发生。首先我们理解下什么是小概率事件。**生活中最容易接触的就是彩票,假设中彩票头奖的概率是一千万分之一,那么从个人来说,如果只买一张的话就中头奖,这就是一个小概率事件。不过如果我们从整体来说,假设一共有 2000万 人买彩票,每人买 1注,那么中彩票这件事就不是一个小概率事件了。可以说每开一次奖,都很有可能有人中头奖。\n那么抽到 112名 女生发生的概率是多少?我们要知道的是当微博抽奖有 2200万 人参加,男女比例 **1 比 1.2,**中奖者有 113名,其中 112名 都是男生或者都是女生的概率是多少。这是一个超几何分布的问题,没听过这个名词?没关系,因为我一开始也忘了。听起来复杂,最终只需要把上面的几个值代入一个公式就好,从结果来看,这个结果的概率低得难以置信,**数字上来说是把 0.01% 中间再加多 30个 0。**如果中彩票的概率是下图左边的圆,那么这个结果的概率就是右边那个点。\n什么,没找到点?对啊,因为小到我根本画不了。**形象地说,假设每秒都进行这样抽奖一亿次,对,一亿次,那么一亿年也不会抽到这样的结果。**这样的极少概率事件的发生非常不现实。在上面的图表中也属于白色区域中,到这里,这样看来,抽奖结果并不可信。我们是否就能确定有人控制抽奖结果呢?\n我们再看看这条微博,微博方解释,为了排除僵尸粉,微博赋予每名用户的权重都不一样,所以才会有那么悬殊的结果。权重是什么意思呢?我们可以这样理解,**微博根据情况给每位参加的用户发不同注的彩票,自然,彩票多的人中奖的概率就高。这里的彩票就是权重。**那么每位用户的权重怎么计算出来的呢?微博官方并没有给出公式,不过这里我们假设微博发现僵尸粉的特征是很少原创微博,微博很少被评论以及被转发。微博认为符合这类特征的就是僵尸粉。应该赋予低权重,反之则为活跃用户,应该赋予高权重,假设\n彩票数量 = 近30天发布原创微博数量 x 10 + 微博被评论量 x 5 + 微博被转发量 x 2 通俗地来说原创微博发得越多,被评论或者转发越多,那么权重越高,中奖的概率就越高。从下图来看,给予活跃用户更高的权重这种方式确实排除了大量的僵尸粉。 **但是这样的话,一些平时只浏览,很少发微博的人也会被降权。很多男生一般都是浏览微博,比较少发微博,微博无法分辨这是真人还是僵尸粉,只好抱着杀错一千也不放过的原则统一降权。**如下图:\n我们可以看到,活跃用户是女生的概率比男生要高得多,排除僵尸粉意味着排除更多的男生。如果提高男生中奖率的话那么也会提高僵尸粉的中奖率,允许僵尸粉中奖还是允许活跃用户中奖,微博选择了后者。我不知道微博是有意为之还是因为技术薄弱所以无法解决这个问题。不过如果这次抽奖抽到 10名 僵尸粉的话,对微博抽奖公信度的影响相信也会同样大。\n多少倍 那么许多读者想知道的,一名女生的中奖概率是男生的多少倍呢?或者换个问法,一名女生的中奖概率最少是男生的多少倍, 112 比 1 这个抽奖结果才在可信区域呢。我们可以推理得知,当男女中奖概率一样的情况下,最有可能(中间)抽到的是 56名 女生。下面的图表显示了中奖女生数量的概率,其中黄色区域为可信范围。我们刚刚已经讨论过,这种情况下抽到 112 比 1 的结果是不可信的。\n**也就是说,我们需要计算当女生的中奖概率是男生的多少倍下,黄色区域右边的边界刚好能碰到 112。**这不是一个简单的问题,因为需要计算累积概率。不过我们能用最容易想到的方法来解决。一个个地尝试,感谢 wanqi 老师的计算,通过几行简单的代码:\nall = 22000000 k = 112 m = 19000000:all cumulative = qhyper(0.95, m, all-m, k) result = m[which.max(cumulative)] # 21506336 weight = result / (all-result) # 36.8 我们得知当女生中奖概率是男生的 36.8倍 的情况下,出现 112 比 1 这个结果是可信的。\n**总的来说,虽然看起来男女参加比例是 1 比 1.2,实际上,女生的中奖概率最少是男生的 36.8倍。这并不是新浪的性别歧视,而是它的无能为力。**这个抽奖公平吗?看不同人的理解:\n 活跃用户会觉得公平,因为他们经常觉得自己给微博带来更多了的流量, 部分男生觉得不公平,因为他们的中奖率比微博眼中的活跃用户低得多。 部分 IG 的粉丝觉得不公平,中奖者起码要喜欢 IG 才行,那么怎么才算喜欢呢?发过微博算喜欢吗?去过现场算更喜欢吗?这没有定量的标准。 结束 我最近作为老师参加广外 Coding girls club 的活动,我们用一天的时间免费教授学生用 Python 做数据分析,这是一个非常有趣的体验,有趣得能放上 Airbnb 做付费体验了。很多学生来自翻译专业,工商管理,她们大部分以后并不会成为程序员,那为什么要去学习数据分析,艾倫伯格在他的书《数学教你不犯错》中给了一个很好的回答:\n“会数学就像戴上了X光眼镜,能从混乱无序的世界表象里,看透其后隐藏的结构。数学是一门不会把事情搞错的学问,它的技术与习惯经历过许多世纪的辛勤努力与论辩。一旦手中有数学当工具,你可以更深刻,更稳健,更有意义地了解世界。”\n阅读完这篇文章,希望你能独立思考得到自己的结论 🎉🎉\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/dns%E6%9F%A5%E8%AF%A2/",
"title": "DNS查询",
"tags": [],
"description": "",
"content": "当你在浏览器输入 www.apple.com。按下回车之后,浏览器跳到苹果的官网,把 iPhone 的介绍和图片显示出来。浏览器是如何通过这个域名找到 iPhone 的内容并且正确显示呢?第一步就要经过 DNS 查询。\nDNS 查询 DNS 查询其实很好理解,生活上比较贴近的例子就是通讯录,我们通常能够记住自己的电话号码,不过要找其他人的电话,一般都需要通过通讯录。我们输入朋友的名字,通讯录就会找到对应的电话号码。在这里,通讯录担任了把难记的电话号码转成好记的人名的工作。DNS查询其实也一样,负责把难记的 IP 地址转成容易记忆的域名。\n互联网刚开始发展的时候,联网的电脑加起来才几千台(没错~)。每台电脑都需要保存一个文件(称为 hosts 文件)用作记录域名对应哪个IP地址(就像通讯录记录人名对应哪个电话号码)。\nwww.enginego.org 104.24.120.11(每一行分别对应着域名和 IP 地址) www.apple.com 119.145.144.223 www.ieee.org 23.38.177.118 所以早期的时候,当我们在浏览器输入域名的时候,浏览器会先查询 hosts 文件,找到该域名对应的 IP 地址。然后再通过 IP 地址获取到服务器里面的内容并且显示在浏览器中。不过问题随着互联网的发展慢慢显现了:\n 每个小时都有新的域名被注册,新的主机加入互联网,现在已经超过 10亿 台设备接入互联网,如果每次查询的话都要从文件中找到对应的 IP 地址,那么会非常耗时,而且 hosts 文件也会变得非常大。 大部分的网站用户根本不会浏览,每上线一个小网站就要求全球的电脑都更新 hosts 文件这样显然小题大做了。 为了解决这几个问题,现代的 DNS 查询会按顺序经过 3个 步骤,一旦查询到结果,就会跳过剩下的步骤:\n 先查询浏览器有没有保留缓存\n如果你之前访问过这个网站,那么浏览器会保存网站对应的 IP 地址,这样下一次访问就能直接从缓存中取出地址,减少查询 IP 地址的时间。\n 查询本地的hosts文件\n如果hosts文件有对应域名包含的 IP 地址,例如\n www.apple.com 119.145.144.223 那么计算机就会直接使用这个 IP 地址,你可以查看自己电脑的 hosts 文件,不同系统分别存储在不同的位置\n Windows c:\\windows\\system32\\drivers\\etc\\hosts macOS /etc/hosts DNS 服务器查询\n当计算机在前两个方法都没有找到对应的IP地址,就会进行DNS服务器查询,它会询问最近的子域名服务器该域名对应的 IP 地址(默认是 ISP 运营商提供,在国内就是中国电信,中国联通,中国移动这几家),如果最近的子域名服务器也不知道的话会一级级向上查询,直到查询到根域名服务器。 (根域名服务器保存着所有域名与 IP 地址的对应关系,子域名服务器也会定时从上级服务器中获取最新的内容。)\n 经过这 3步,我们只需要记住某个网站的域名,就能获取到该网站的IP地址了。\n使用域名的优点 容易记忆\n计算机比较擅长处理数字,但是人类就差多了,访问苹果官网要背那么多数字显然不现实。使用域名就像我们使用手机的通讯录一样,我们会为常用的电话号码添加联系人姓名,打电话的时候直接输入名字就可以拨打,不需要背电话号码。\n 容易扩展\n早期的计算机和IP地址是一一对应的关系,但是一台计算机的性能并不能承受大量用户的请求。而一个域名可以背后可以对应多个 IP 地址。通过给不同地区的用户分配不同的 IP 地址,公司就能使用多台服务器来提供服务了。\n 层级关系\n从域名上我们可以判断 apple.com 与 edu.apple.com 都是属于 apple 公司的,这种子域名的层级关系也能有效提高用户的安全性。\n "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E7%BB%88%E7%AB%AF/",
"title": "终端",
"tags": [],
"description": "",
"content": "命令行界面 在计算机技术刚发展的时候,并没有像现在手机/计算机那么容易上手的操作界面。大家都需要通过一个黑乎乎的程序输入命令,然后等待计算机执行。\n上面我运行了 ls 这个命令,这个命令的作用是列出当前路径下的所有文件。可以看到里面包含了 10个 文件。\n└── 当前目录 ├── README.md ├── config.toml ├── data ├── layouts ├── static ├── archetypes ├── content ├── docs ├── public ├── themes 运行不同的程序需要使用不同的命令。在我们平时使用的操作系统中,如果需要打开一个叫做 normal.txt 的文本文件,双击文件就能打开文件看到内容了,不过在命令行界面中,需要使用 cat 命令:\ncat normal.txt 那么文本的内容会显示出来。其实终端就是一个能够接收输入(键盘,鼠标),能够通过特定指令运行计算机功能的软件。\n亲自试试吧 你可以尝试先打开自己计算机的终端:\n macOS: 同时按住\u0026quot;control\u0026quot;+\u0026quot;space\u0026quot;键,在搜索框输入termianl,\u0026ldquo;回车\u0026rdquo;\n Windows 7:打开\u0026quot;开始\u0026quot;菜单,在搜索框里输入cmd,\u0026ldquo;回车\u0026rdquo;\n Windows 8+:同时按住\u0026quot;win\u0026quot;+\u0026quot;Q\u0026quot;键,然后在弹出的搜索框里输入cmd,\u0026ldquo;回车\u0026rdquo;\n 成功打开之后,光标会停留在某一行。类似:\nWindsondeMacBook-Air:~ windson$ | 因为和计算机交互需要遵守一定的指令规则,终端默认会运行一个程序,常见的是 Bash(Bourne Again SHell)或者 sh,你可以尝试输入一个命令,然后让终端执行。\n macOS: \u0026ldquo;ls\u0026quot;+\u0026quot;回车\u0026rdquo;\n Windows: \u0026ldquo;dir\u0026quot;+\u0026quot;回车\u0026rdquo;\n 输入回车之后,你会发现终端显示出了一些文字,仔细观察的话你会发现其实是你电脑中某个目录下的文件。ls/dir 这个命令就是列出当前目录的文件。 当然这个命令还有很多参数,也就是你可以告诉计算机显示什么文件(隐藏/非隐藏),显示文件的详细内容(创建日期,大小),ls/dir 只会调用一个常用的默认配置,假如你想把隐藏文件也显示的话,在终端输入\n macOS: \u0026ldquo;ls -a\u0026quot;+\u0026quot;回车\u0026rdquo;\n Windows: \u0026ldquo;dir -a\u0026quot;+\u0026quot;回车\u0026rdquo;\n 终端还有很多其他命令,例如输出当前路径\n macOS: \u0026ldquo;pwd\u0026quot;+\u0026quot;回车\u0026rdquo;\n Windows: \u0026ldquo;cc\u0026quot;+\u0026quot;回车\u0026rdquo;\n 或者 ping 命令可以用作网络诊断。\nping www.apple.com 64 bytes from 27.148.139.136: icmp_seq=0 ttl=54 time=21.056 ms 64 bytes from 27.148.139.136: icmp_seq=1 ttl=54 time=21.575 ms 64 bytes from 27.148.139.136: icmp_seq=2 ttl=54 time=20.989 ms 为什么要用终端 以前的计算机根本没有图形界面,只能对着这样黑乎乎的界面一行行输入指令,然后祈祷它不会出错。既然现在已经有图形界面了,为什么我们还需要学习用终端呢?\n 多功能,一般一个软件只会专注于一个功能,例如 Word 进行文字编辑,PowerPoint 进行幻灯片编辑。你可以通过终端让计算机调用不同的程序完成多种多样的功能。\n 方便,对,一开始可能难以置信,但是使用终端确实比图形界面方便。你可能也会曾经遇过某一款软件更新,你找不到原本功能的按钮在哪里了。(例如某段时间Windows系统就把把“开始”菜单隐藏了。)使用命令(例如上面的 ls/dir )的话即使系统如何更新都能显示当前目录的文件。\n 速度快,效率高。当你要连接远程的服务器的时候,如果连接的是图形界面,那么由于网络原因以及机器性能的影响,通常都会卡得一塌糊涂。不过如果只连接命令行界面的话,那么对于网络以及机器的性能要求就非常低,能够极大地提高工作效率。\n 高效,跨平台,跨机器,假如今天你要清理电脑,**要删除一个大型文件夹中(可能包含子文件夹)超过 90天 之前创建的所有文件,**一般来说的话你需要一个个文件夹找,然后对每个文件夹里的文件进行时间排序,再手动删除超过 90天 的,如果里面包含了 100个 文件夹那么会费很多时间,同时也容易出错。使用终端的话可以直接运行\n find /path/to/base/dir/* -type f -ctime +90 -delete 而且只要跑同一个系统就不用担心命令会执行出错。\n 容易定制,使用指令的好处是很容易根据需求来修改,加入想删除的是所有超过 90天 之前创建的文件夹,只需要修改 \u0026lsquo;-type f\u0026rsquo; 为 \u0026lsquo;-type d\u0026rsquo; 即可,这里 type 代表的是指令针对什么类型操作,f 是 files,d 则是 directory。\n 当然,要学会基本的指令也要花时间,例如一个find指令就有超过50个参数(这个命令比较特殊,参数比较多,我也常不记得,想用的时候搜索一下就好)。\n如果你使用对的是Windows系统请注意,Windows有自己的一套特殊指令, 我不建议大家使用Windows来学习编程,相对来说macOX与Linux系统对开发者(也就是你)更加友好,使用他们会节约你很多时间。 不过这不代表你需要额外买一台电脑才能学习编程,只需要安装双系统或者虚拟系统就好,如果你没有接触过,没关系。我们会一步步介绍,你可以先在Windows中安装Docker,然后就可以在Windows系统中使用Linux了。\n"
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85python/macos-%E5%AE%89%E8%A3%85-python/",
"title": "macOS 安装 Python",
"tags": [],
"description": "",
"content": "安装 Python 访问 Python 下载地址,如下图:\n 点击上图蓝色框中的链接,像普通文件一样下载即可。下载完毕之后双击该安装包(一般情况下在下载目录中),出现下图,点击继续:\n 下图包括软件的基本介绍,点击继续。\n 下图包括软件的使用协议,点击继续,接着软件会弹出一个小窗口,需要你同意协议,点击同意按钮即可。\n 下图选择安装的地方,macOS 一般只有一个盘,点击继续即可。\n 出现下图后,点击安装,软件会弹出一个小窗口,要求输入管理密码,你输入管理密码之后,回车,然后软件会开始安装,稍等几分钟。 显示下图界面则代表安装成功了。如果没有安装成功的话请仔细核对以上的步骤重新安装一次。 "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85%E7%BC%96%E8%BE%91%E5%99%A8/macos-%E5%AE%89%E8%A3%85%E7%BC%96%E8%BE%91%E5%99%A8/",
"title": "macOS 安装编辑器",
"tags": [],
"description": "",
"content": "安装Atom编辑器 访问 Atom 下载地址,点击下图中的 Download 按钮\n 下载完之后安装,安装之后打开 Atom 程序,出现下图,点击蓝色框中的 Install a Package\n 在左边的搜索框中输入 script,然后点击右边的 Install,稍等5分钟\n 安装成功\n 安装成功之后可以尝试运行,在 Atom 中新建名为 first.py 的文件。粘贴这段代码进去:\n print('hello world') 如下图:\n然后使用快捷键\u0026quot;command\u0026rdquo; + \u0026ldquo;i\u0026quot;就能运行代码了。你可以看到上图的最下面显示了:\n hello, world [Finished in 0.276s] "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85python/windows-%E5%AE%89%E8%A3%85-python/",
"title": "Windows 安装 Python",
"tags": [],
"description": "",
"content": "安装 Python 访问 Python 下载地址,点击下图中蓝色框的链接,像普通文件一样下载即可。\n 下载完毕之后双击该安装包(一般情况下在下载目录中),出现下图,勾选 Add Python 3.7 to PATH 之后点击 Customize installation。\n 这是安装的选择,直接点击 Next\n 点击 Browse 选择安装路径,推荐安装在非系统盘,下面的例子安装在 E 盘,你也可以根据自己的情况选择路径,然后点击 Install。\n 这时候就会开始安装:\n 恭喜,安装成功!\n "
},
{
"uri": "https://www.enginego.org/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/%E5%AE%89%E8%A3%85%E7%BC%96%E8%BE%91%E5%99%A8/windows-%E5%AE%89%E8%A3%85%E7%BC%96%E8%BE%91%E5%99%A8/",
"title": "Windows 安装编辑器",
"tags": [],
"description": "",
"content": "安装 Atom 编辑器 访问 Atom 下载地址,点击下图中的 Download 按钮\n 下载后双击打开就可以了,软件的界面如下:\n 点击左上角 File 然后 new 来创建新的文件,接下来就可以在文档中写代码并且保存。\n "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E7%BC%96%E7%A8%8B%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AD%A5/%E4%BB%80%E4%B9%88%E6%98%AF%E7%BC%96%E7%A8%8B/",
"title": "什么是编程(未完成)",
"tags": [],
"description": "",
"content": "我常常去参加一些编程活动,意外的是,这些活动中我接触到很多想学习计算机的朋友。他们想学习编程,但是常常被计算机不同的范畴弄得头昏脑胀,**先学编程语言呢,还是数据结构和算法,先学建网站,写 APP,还是大数据,人工智能?**这里我推荐 Crash Course 计算机科学的视频,它从原理介绍了编程的起源以什么是编程,非常有趣和浅显易懂,适合完全零基础甚至初中生去看。\n 计算机科学速成课 (YouTube) 计算机科学速成课(中文字幕) 在电子计算机还没有被发明的时候。计算机指的并不是我们现在常识中的电脑硬件,而是使用计算器计算的人(大多是女性,我猜因为女性比较细心,容易管理)。她们会被分配一些数据和需要计算的步骤,接着按照步骤耐心地操作计算器,然后写出结果。如果你看过电影《隐藏人物》应该会对一大堆女生拿着计算器埋头计算的场景有印象。不过,你不会把用计算器按下数字得到答案当作在编程,只会把它当成是使用工具的一种方式,但是实际上它与编程并没有太大的区别。举个例子,我们现在需要把一个文本文件的数据转换为Excel表格保存。文本的内容是:\n Shall I compare thee to a summer’s day?\n Thou art more lovely and more temperate.\n Rough winds do shake the darling buds of May,\n 我们想把这些数据转换成Excel文件:\n A1 A2 A3 Shall I compare thee to a summer’s day? Thou art more lovely and more temperate. Rough winds do shake the darling buds of May, 与按计算器不一样的是,这里的数字变成了三行的文本,乘法法则变成了格式转换。得到的结果变成了Excel的文件。不过它们两者都遵循三个原则,接受数据,处理数据,输出数据,要解决这个问题就像按计算器一样简单:\n 读取文本文件中的数据。 把文字按照Excel的格式需要转换成新的格式。 把新的数据存储起来。 即使不会编程,我们也可以猜测到读取数据与存储数据应该非常简单(如果没猜对的话我偷偷告诉你),难点应该在转换这一部分。我们分析下这部分要怎么解决,要存储为Excel格式的文件,我们可以先参考Excel是如何存储数据的。我们假设我们要得到的Excel表格是这样存储的:\n A1: Shall I compare thee to a summer’s day? (black, bold, normal, 20)\n A2: Thou art more lovely and more temperate. (black, bold, normal, 20)\n A3: Rough winds do shake the darling buds of May, (black, bold, normal, 20)\n 看起来不像表格?没关系,Excel程序会自己处理的,你可以想象Excel程序是一条流水线,先把所有的数据读出来放在流水线上,当符合要求的产品经过,就进行加工,显示为不同的表格样式。上面的这种格式在编程中我们称这个为列表,它就像我们平时手机上的备忘录,或者购物前列出的购物清单,把要做的每一项按照顺序写下来。最后面的括号里面的是什么?我们试试把\u0026quot;black\u0026quot;改为\u0026quot;blue\u0026quot;再保存,发现字体的颜色都变成蓝色了,原来这些就是字体的设置,在编程中,我们称为参数。回到我们刚刚的任务,要把文本文件的数据转成Excel格式,只需要在原本文本文件的数据的每一行前面加上\u0026quot;A\u0026rdquo;,行号,冒号和用括号包含的一些参数就行了吧。Yoo!我们解决了一个重要的问题。实际上,Excel并不是这么存储数据的,不过我们可以把它当成这样,Excel提供了外部接口给我们使用,。等等,外部接口又是什么意思?回到计算器的例子,当我们输入528,按下等号的时候,我们不需要知道计算器是通过什么方式计算结果的,我们只知道只要我按照正确的规则输入,而不是输入5 28,我就能得到正确的答案。这里计算器提供的就是外部接口。你只需要按照规则把数据传进它提供的接口,就能得到结果。同样地,Excel也提供了类似的接口,只要你按照语法与顺序告诉它每一行的属性,它就能按照要求得到数据。\n不如先把数据转换成中间数据,再进行转换。其实当我们想到这一点,我们已经解决了这个问题了。什么?还没开始写代码?写代码是最简单的事情。初入编程的同学觉得编程难,往往他们纠结于编程的语法与编程语言的特性中,其实这些都不是最重要的。就像写作一样,你的文笔固然重要,但是更重要的是故事的构思与思想。这也是作家与语言学家最重要的区别。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E7%88%AC%E8%99%AB/%E7%88%AC%E8%99%AB%E5%9F%BA%E7%A1%80/",
"title": "Python爬虫基础(未完成)",
"tags": [],
"description": "",
"content": "这篇文章要求读者有一定的HTML基础知识,如果之前没有学习过HTML的话,可以先花一些时间在这里学习。当进行数据分析统计,或者数据收集之前,我们需要有数据。(这不是废话吗)数据的来源既可以是搜寻已有的数据集,也可以自己去收集数据。计算机中爬虫就是获取目标应用数据的方式,例如当你要做一个项目,你觉得知乎现在广告太多了,回答下面都是二维码,你打算统计答案中包含二维码占全部答案的比例。那么如何获取这些数据呢?第一个方法当然可以像平时一样一个个答案点开,这样的问题是效率太低。如果要统计1万个答案的话怎么办,再如果下次统计的是包含广告链接的话难道要重新一个个点开吗?这显然不现实。有没有一种高效率的方法解决这个重复性的问题。\n没有\n好啦,我只是开玩笑,怎么可能没有呢?:D,爬虫就是一个方法,如果你之前接触过Python爬虫,应该听过一堆requests, Scrapy, Asyncio, Beautifulsoup等库,也听过多线程,多进程,异步IO爬虫什么的。应该怎么选择呢?先不用急。如果你问这个问题,代表你对爬虫了解得不多,我们一步步从零开始介绍爬虫,之后也会涉及这些高级的爬虫。\n为了方便读者,我搭建了一个非常简单的网站,教程列表,它的结构非常简单,包括一个首页和三个子页面。\n/(首页) │ └── 子页面 ├── 1.html ├── 2.html ├── 3.html 点击首页的不同链接就跳转到对应的子页面。\n第一个小任务 我们的第一个小任务是获取首页中所有子页面的标题,也就是获取\nDNS查询 Ping命令 终端 好吧,这个任务看起来很简单,不写程序也能几秒钟完成,用鼠标复制粘贴就可以,不过先跟着我们一步步来。在首页空白处点击鼠标右键,然后点击查看网页源代码,可以看到一些HTML代码。\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;首页\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h2\u0026gt;教程列表\u0026lt;/h2\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;../1.html\u0026quot;\u0026gt;DNS查询\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;../2.html\u0026quot;\u0026gt;Ping命令\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026quot;../3.html\u0026quot;\u0026gt;终端\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 浏览器的作用就是根据这些代码渲染成我们平时看到的网站,可能包括背景,字体的大小,颜色。爬虫要做的其实也一样,不过要简单一点,爬虫是抽取HTML中出现的数据。当你观察上面的HTML代码的时候,发现我们要获取的标题都在\u0026lt;li\u0026gt;标签的\u0026lt;a\u0026gt;标签里面。**所以我们的任务就变成,\n 获取页面的HTML代码 从HTML代码中获取所有\u0026lt;a\u0026gt;标签中的文字 刚刚我们是通过浏览器获取源代码,如何用程序获取呢?第一个方法是使用Requests库,之后我们会介绍使用其他库获取的方式,一开始我们先从最简单开始。\n 打开终端\n 新建一个文件夹crawl,并进入文件夹\n 运行pip install requests(如果遇到错误信息的话,请把错误信息的最后一句复制到搜索引擎搜索解决方案,这也是锻炼编程与解决能力的一步)\n 遇到类似这样的信息代表安装成功啦,撒花。\nCollecting requests Downloading https://files.pythonhosted.org/packages/65/47/7e02164a2a3db50ed6d8a6ab1d6d60b69c4c3fdf57a284257925dfc12bda/requests-2.19.1-py2.py3-none-any.whl (91kB) 100% |████████████████████████████████| 92kB 208kB/s \u0026hellip; Installing collected packages: certifi, chardet, urllib3, idna, requests Successfully installed certifi-2018.4.16 chardet-3.0.4 idna-2.7 requests-2.19.1 urllib3-1.23\n 安装完成之后\n\u0026gt;\u0026gt;\u0026gt; python3 \u0026gt;\u0026gt;\u0026gt; import requests \u0026gt;\u0026gt;\u0026gt; response = request.get('https://privacycommons.github.io/') 获取源代码之后,如何抽取到\u0026lt;a\u0026gt;标签中的文字呢?第一种最直接的方法是做字符串匹配,找到\u0026lt;a\u0026gt;与\u0026lt;/a\u0026gt;之间的文字。\n start = '\u0026lt;a\u0026gt;' end = '\u0026lt;/a\u0026gt;' return response[len(start):-len(end)] 获取到一个页面的标题之后,如何获取其他页面呢?我们发现每个页面的URL有一些规则。结尾是数字,而且看起来每篇文章都有一个不同的数字作为它的编号。此时我们就可以遍历这些ID,获取每一个URL的链接之后再进行同样的抽取\u0026lt;title\u0026gt;中的内容。\nfor id in range(10): code block "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/ping%E5%91%BD%E4%BB%A4/",
"title": "Ping命令",
"tags": [],
"description": "",
"content": " 基础版 进阶版 基础版 ping命令 有时候当我们无法上网,会计算机的朋友会说,你ping一下网关,或者你ping一个网站看看。ping这个命令,其实是操作系统自带的命令之一,常用作在网络诊断中。你可以在终端中输入:\nping www.enginego.org 你可以把\u0026quot;www.enginego.org\u0026quot;替换成其他任意的域名,终端会显示:\nPING www.enginego.org (104.24.120.11): 56 data bytes 64 bytes from 104.24.121.11: icmp_seq=0 ttl=54 time=170.383 ms 64 bytes from 104.24.121.11: icmp_seq=1 ttl=54 time=170.053 ms 64 bytes from 104.24.121.11: icmp_seq=2 ttl=54 time=171.298 ms 64 bytes from 104.24.121.11: icmp_seq=3 ttl=54 time=170.352 ms 64 bytes from 104.24.121.11: icmp_seq=4 ttl=54 time=170.636 ms ... 这表示从我的计算机发送64个字节的数据到104.24.121.11 (这个是经过DNS查询后的www.enginego.org的ip地址),从发送到接受对方返回总共经过了170.xxx毫秒。代表www.enginego.org对应的那台服务器是开启并且响应ping指令的。\n如果返回:\nPING www.enginego.org (104.24.121.11): 56 data bytes Request timeout for icmp_seq 0 Request timeout for icmp_seq 1 Request timeout for icmp_seq 2 Request timeout for icmp_seq 3 这就代表连接超时,访问失败。原因有可能是本地计算机网络问题,也有可能是对应的服务器关闭或者不响应ping指令,或者你根本只是输错域名了。\n网络诊断 当我们要连接互联网的时候,无论使用手机还是计算机,一般都要经过网关 (路由器或者交换机),网关接受到数据包的时候经过NAT转换再连接到互联网。当无法上网的时候,可以通过以下两个方法排查故障。\n 检查计算机到路由器的连接是否正常\n ping 192.168.1.1 (路由器/交换机地址) 超时则代表: - 网线没有连接好或者 - IP地址冲突 然后检查路由器到互联网的连接是否正常\n ping www.enginego.org (网站地址) 超时则代表: - 路由器设置出错 - DNS解析出错 - 服务器关闭或者不响应ping指令 我们可以从这两步判断网络到底哪里出现了问题。在本地网络没有问题的前提下,使用ping命令可以判断目标主机是否开启及其响应速度。\n进阶版 ping指令实际是如何实现的 ping指令使用的是ICMP协议,就像我们每天在用的HTTP协议,客户端(我们的ping程序)根据约定好的协议发送二进制数据到服务器,然后服务器根据协议规则解析二进制数据中的设置与数据。 数据里面会指定使用第几个版本,发送请求的IP地址,接受请求的地址等等,如果大家对wireshark有了解,可以尝试打开wireshark之后再执行ping指令,然后查找ICMP协议对应的数据传输。\n当我使用计算机运行(这里出于方便显示把传输的数据转换成十六进制)\nping www.enginego.org 实际发送的数据是:\n0000 b0 7f b9 a3 68 36 98 e0 d9 9d a3 8f 08 00 45 00 0010 00 54 69 21 00 00 40 01 6f 64 c0 a8 01 58 68 18 0020 78 0b 08 00 47 00 62 90 00 03 5a a1 2a fa 00 06 0030 dd c7 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 0040 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 0050 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 0060 36 37 可以看到数据中包含了发送的类别,校验和,发送程序的进程编号(上图中的Identification),IP地址等等,例如**c0 a8 01 58对应着10进制的192 168 1 88,也就是我的计算机的IP地址。**注意ping指令是不需要指定端口的,它是根据协议头部信息纪录的进程编号来辨识返回的数据的。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E5%8D%8F%E8%AE%AE/",
"title": "协议",
"tags": [],
"description": "",
"content": " 什么是协议 协议的优点 协议与API 什么是协议 计算机使用了多种多样的协议,大家接触得比较多的是HTTP, TCP/IP, FTP, ICMP等,**计算机中的协议和我们现实生活中签的协议其实挺像,双方按照协议上的约定发送和解析数据。**举个例子,你和你的朋友通过短信约定明天吃饭的时间地点:\n 明天\n 中午12点\n 在公司等\n 即使改变文字的顺序\n 明天\n 在公司等吧,\n 中午12点\n 一般人也能理解。不过计算机不一样,你可以把计算机当成患强迫症的朋友,它会要求你发的信息一定要符合一个规则:\n 日期(两个字)\n 时间(四个字)\n 地点(四个字)\n 那么信息必须按照这个规定发送:\n 明天(日期)\n 上午9点(时间)\n 在公司等(地点)\n 只要你和朋友都愿意遵守这个约定发送和接受信息,那么你们就互相遵守了协议。\n协议的优点 **无论对计算机或者人类来说,信息都变得有序和容易处理。**当我们知道信息遵守协议A的时候,我们不需要阅读信息都知道前两个字是日期,接着是四个字的时间,最后是四个字的地点。\n举个常见的例子,当你使用浏览器访问www.apple.com,浏览器其实是按照HTTP协议的约定向苹果服务器发出信息,协议要求内容:\n第一行:请求方法和协议版本(8个字节) 第二行:请求的URL(30个字节) 第三行:缓存策略(30个字节) ... 实际发送的文字内容是:\nGET / HTTP/1.1\\r\\n(请求方法和协议版本) Host: www.apple.com\\r\\n(请求的URL) Connection: keep-alive\\r\\n(缓存策略) Pragma: no-cache\\r\\n Cache-Control: no-cache\\r\\n Upgrade-Insecure-Requests: 1\\r\\n User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36\\r\\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\\r\\n DNT: 1\\r\\n Accept-Encoding: gzip, deflate, br\\r\\n Accept-Language: en,zh;q=0.9,en-US;q=0.8,zh-CN;q=0.7\\r\\n 实际发送的二进制信息是:\n0000 b0 7f b9 a3 68 36 98 e0 d9 9d a3 8f 08 00 45 00 0010 02 0a 76 f9 40 00 40 06 1e d1 c0 a8 01 58 68 18 0020 79 0b fe 5b 00 50 55 fb ab bc e6 45 57 80 50 18 ... 我们可以看到每一行数据都遵守协议的规定传输,在HTTP协议里面这些信息统称为HTTP的请求头部(每行最后的\\r\\n是换行符,服务器读取到\\r\\n就知道接下来的内容是下一行)。它们大多有固定的选项,服务器拿到这条信息之后就可以直接对照协议的顺序来返回数据。想了解更多的朋友可以参考An overview of HTTP\n协议与API 有时候工程师也会称API为协议,当一组API非常有名,例如HTTP,FTP,大家都知道的情况下,我们可以称它为协议。另外API一般是给另外一方调用的,为了更好理解,假如你开发了一款照片上传的软件,可以让用户上传图片,保存自己与家人的回忆。开发完之后,有很多用户希望加上美颜的功能,你想了下,觉得自己开发这些功能需要花很多时间,看见EngineGo的美图软件有这个功能,那么可以使用它的美颜代码吗?EngineGo这套代码涉及到很多算法以及图形原理,即使把源代码给你,你一时半会也不知道怎么用,而且还考虑到商业秘密的因素,拿到源代码就更难了。这时候,如果EngineGo提供一个外部API(也可以称为接口),使用这个接口和遵守协议类似,首先要了解接口的格式\n这个API使用HTTP协议 第一行:图片 第二行:美颜级别 第三行:url是https://www.enginego.org/image/ 你不需要知道EnginGo如何实现滤镜和美颜的功能,他只需要按照协议把图片以及美颜级别发送到这个URL地址,服务器就会返回一个URL地址,地址包含一张美颜过的图片,你就可以把这张图片展示给用户了。简单来说,API就是接受输入(图片,美颜等级,URL地址)返回结果(美颜后的图片)的模块。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E8%B7%AF%E5%BE%84/",
"title": "路径",
"tags": [],
"description": "",
"content": " 计算机文件存储 当前路径 绝对路径和相对路径 切换路径 计算机文件存储 计算机文件的存储结构如下:\n/(根目录) │ └── usr ├── foo │ ├── first.md │ └── second.md ├── bar │ ├── sunkist.toml │ ├── cherry.toml 我们平时使用计算机,都会接触到文件夹以及目录,上图中,**计算机中只有一个usr文件夹,usr 文件夹中又包含了foo和bar两个文件夹,分别包含两个文件。**路径指的是文件/文件夹存放在计算机中的位置。要描述first.md这个文件的位置可以有两种方式,绝对路径和相对路径,其中绝对路径是\n/usr/foo/first.md 每个文件/文件夹都只有一个绝对路径,无数个相对路径,如果你不理解的话,没关系,我们从头一步步开始介绍。\n当前路径 我们平时用导航软件的时候,首先需要知道的是现在的位置,在计算机中也是一样,我们首先要知道自己在哪里,当你打开终端的时候:\n(masOS) WindsondeMacBook-Air:fun/ windson$ (Windsons) C:fun\\: 这里的\u0026quot;fun/\u0026ldquo;和\u0026quot;C:fun\u0026quot;指当前执行命令的地方,也就是你的当前路径。可以通过命令来查看当前路径的绝对路径\n(masOS) WindsondeMacBook-Air:foo/ windson$ pwd /usr/foo/ (Windsons) C:foo\\: cd \\usr\\foo\\ 当你在终端中输入命令,默认命令针对的是当前路径。例如输入\u0026quot;ls\u0026rdquo;,计算机会理解成\u0026quot;ls /usr/foo/\u0026quot;(Windows下输入\u0026quot;dir\u0026rdquo;,理解为\u0026quot;dir \\usr\\foo\u0026quot;),两个命令都会列出当前路径下的所有文件名称。\n绝对路径和相对路径 有一天,Cherry在路上遇到Sunkist,它问Sunkist的公司地址在哪里,Sunkist可能有两个答案:\n 以这里为起点,西南方向500米的石室大厦 中国广东省广州市思哲路石室大厦 第一个答案以当前的位置为起点所描述的称为相对路径。根据Sunkist当前的位置不一样,它回答的相对路径会不一样,第二个答案从国家到省份城市巨细无遗地描述称为绝对路径**,一个文件除非被移动,否则绝对路径是不会变化的。每个文件/文件夹在一台计算机中可能会有无数个相对路径,但是只有一个绝对路径。**回到文章最初的文件存储结构,first.md的绝对路径是\n/usr/foo/first.md (first.md的绝对路径) 这是从根目录\u0026rdquo;/\u0026ldquo;开始一层一层描述的位置。了解相对路径之前先有一点预备知识。在路径命令中\u0026rdquo;.\u0026ldquo;以及\u0026rdquo;..\u0026ldquo;这两个符号有特殊意义,**分别代表当前目录以及上一级目录。**如果当前路径是\u0026rdquo;/usr/foo/\u0026rdquo;\n/usr/foo/ (当前路径) ./ = /usr/foo/ (当前目录) ../ = /usr/ (上一级目录) 所以first.md相对foo目录的路径是\n./first.md (first.md相对foo目录的路径) 相对bar目录的相对路径是\n../foo/first.md (first.md相对bar目录的路径) 因为\n./first.md 表示的是当前目录下的first.md文件(first.md原本就在foo目录里面)\n../foo/first.md 表示的是先返回当前目录(/usr/bar/)的上一级目录(/usr/),然后再进入foo目录,找到first.md文件。所以同一个文件/文件夹对于不同的路径,有不同的相对路径。\n切换路径 那么当你从一个目录,如何跳转到其他目录呢?我们可以使用系统自带的\u0026quot;cd\u0026quot;命令。\ncd [PATH] . └── usr ├── foo │ ├── first.md │ └── second.md ├── bar │ ├── sunkist.toml │ ├── cherry.toml 这里的path可以是绝对路径或者相对路径,如果你要从\u0026rdquo;/usr/foo/\u0026ldquo;跳转到\u0026rdquo;/usr/bar/\u0026quot;,你可以使用:\ncd /usr/bar/ 或者 cd ../bar/ 总结 大家了解了路径的基础知识,之后学习环境变量就会很容易理解啦。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/hook/",
"title": "hook(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E5%86%85%E5%AD%98/",
"title": "内存(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E5%A0%86%E6%A0%88/",
"title": "堆栈(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E5%B5%8C%E5%A5%97%E5%AD%97/",
"title": "嵌套字(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E6%8A%93%E5%8C%85/",
"title": "抓包(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/",
"title": "操作系统(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/",
"title": "环境变量(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E8%BF%9B%E7%A8%8B/",
"title": "进程(未完成)",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/%E7%90%86%E8%A7%A3%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%88%86%E7%B1%BB/",
"title": "理解贝叶斯分类",
"tags": [],
"description": "",
"content": "作者:Windson Yang\n著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处(www.enginego.org)。\n贝叶斯模型在机器学习以及人工智能中都有出现,cherry分类器使用了朴素贝叶斯模型算法,**经过简单的优化,使用1000个训练数据就能得到97.5%的准确率。**虽然现在主流的框架都带有朴素贝叶斯模型算法,大多数开发者只需要直接调用api就能使用。但是在实际业务中,面对不同的数据集,必须了解算法的原理,实现以及懂得对结果进行分析,才能达到高准确率。\n cherry分类器 关键字过滤 贝叶斯模型 数学推导 贝叶斯模型实现 测试 统计分析 总结 cherry分类器 基础术语: cherry分类器默认支持中英文分类,用作例子的数据缓存中,中文训练数据包含正常,政治敏感,赌博,色情4个类别,英文训练数据包含正常邮件,垃圾邮件两个类别 (训练数据可以通过Google drive下载)。调用非常容易,使用pip安装后,输入句子:\n 警方召开了全省集中打击赌博违法犯罪活动专项行动电视电话会议。会议的重点是“查处”六合彩、赌球赌马等赌博活动。\n \u0026gt;\u0026gt;\u0026gt; import cherry \u0026gt;\u0026gt;\u0026gt; result = cherry.classify('警方召开了全省集中打击赌博违法犯罪活动专项行动电 电话会议。会议的重点是“查处”六合彩、赌球赌马等赌博活动。') Building prefix dict from the default dictionary ... Loading model from cache /var/folders/md/0251yy51045d6nknpkbn6dc80000gn/T/jieba.cache Loading model cost 0.894 seconds. Prefix dict has been built succesfully. 分类器判断输入句子有99.7%的概率是正常句子,0.2%是政治敏感,剩余0.1%是其他两个类别\n\u0026gt;\u0026gt;\u0026gt; result.percentage [('normal.dat', 0.997), ('politics.dat', 0.002), ('gamble.dat', 0.0), ('sex.dat', 0.0)] 其中对分类器判断影响最大的词语分别是赌博,活动,会议,违法犯罪,警方,打击\n\u0026gt;\u0026gt;\u0026gt; result.word_list [('赌博', 8.5881312727226), ('活动', 6.401543938544878), ('会议', 6.091963362021649), ('违法犯罪', 4.234845736802978), ('警方', 3.536827626008435), ('打击', 3.2491455535566542), ('行动', 2.8561029654470476), ('查处', 2.3860993362013083), ('重点', 2.126816738271229), ('召开', 1.8628511924367634), ('专项', 1.1697040118768172), ('电视电话会议', 1.1697040118768172), ('全省', 0.47655683131687354), ('集中', -0.6220554573512382), ('六合彩', -2.29603189092291)] 关键字过滤 要理解分类器的原理,可以先从最简单的分类关键词算法开始,输入句子:\n 奖金将在您完成首存后即可存入您的账户。真人荷官,六合彩,赌球欢迎来到全新番摊游戏!\n 使用关键字算法,我们可以将真人荷官,六合彩这两个词语加入赌博类别的黑名单,每个类别都维持对应的黑名单表。当之后需要分类的时候,先判断关键字有没有出现在输入句子中,如果有,则判断为对应的类别。这个方法实现简单,但是缺点也很明显,误判率非常高,例如遇到输入句子:\n 警方召开了全省集中打击赌博违法犯罪活动专项行动电视电话会议。会议的重点是“查处”六合彩、赌球赌马等赌博活动。\n 这是一个正常的句子,但是由于包含六合彩,赌球这两个黑名单词语,关键字算法会误判其为赌博类别,同时,如果一个句子同时包含多个不同类别的黑名单词语,例如赌博,色情的话,关键字算法也无法判断正确。\n贝叶斯模型 其实关键字算法已经接近贝叶斯模型的原理了,我们再仔细分析下关键字算法。关键字算法的问题在于只对输入句子中的部分词语进行分析,而没有对输入句子的整体进行分析。而贝叶斯模型会对输入句子的所有有效部分进行分析,通过训练数据计算出每个词语在不同类别下的概率,然后综合得出最有可能的结果。可以说,贝叶斯模型是关键字过滤加上统计学的升级版。\n当贝叶斯模型去判断输入句子:\n 警方召开了全省集中打击赌博违法犯罪活动专项行动电视电话会议。会议的重点是“查处”六合彩、赌球赌马等赌博活动。\n 它会综合分析句子中的每个词语:\n警方,召开,全省,集中打击,... 六合彩,赌球,赌马,... 输入句子虽然包含六合彩,赌球这些赌博常出现的词语,但是警方,召开,集中打击这几个词代表这个句子极有可能是正常的句子。\n数学推导 贝叶斯模型的数学推导非常简单,强烈建议大家静下心自己推导。\n这里为了简单起见,我们只考虑句子是正常或者赌博两种可能,我们先复习一下概率论的基础表达:\n P(A) -\u0026gt; A事件发生的概率,例如明天天晴的概率\n P(A|B) -\u0026gt; 条件概率,B事件发生的前提下A事件发生的概率,例如明天天晴而我又没带伞的概率\n P(输入句子) -\u0026gt; 这个句子在训练数据中出现的概率\n P(赌博) -\u0026gt; 赌博类别的句子在训练数据中出现的概率\n P(赌博|输入句子) -\u0026gt; 输入句子是赌博类别的概率(也是我们最终要求的值)\n P(赌博|输入句子) + P(正常|输入句子) = 100%\n 上图,中间重叠的部分是赌博和句子同时发生的概率P(赌博,输入句子),可以看出:\n P(赌博|输入句子) = P(赌博,输入句子) / P(输入句子) (1)\n 同理:\n P(输入句子|赌博) = P(赌博,输入句子) / P(赌博) (2)\n 把(2)代入(1)得到\n P(赌博|输入句子) = P(输入句子|赌博) * P(赌博) / P(输入句子) (3)\n 登登登灯,(3)就是贝叶斯模型定理。没看懂没关系,静下心再看一遍。要得到最终输入句子是赌博类别的概率P(赌博|输入句子),需要知道右边3个量的值:\n P(赌博)\n指训练数据中,赌博类别的句子占训练数据的百分比。\n P(输入句子)\n指这个输入句子出现在训练数据中的概率。我们最终目的是判断输入句子是哪个类别的概率比较高,也就是比较P(赌博|输入句子)与P(正常|输入句子),由贝叶斯定理:\n P(赌博|输入句子) = P(输入句子|赌博) * P(赌博) / P(输入句子) (4)\n P(正常|输入句子) = P(输入句子|正常) * P(正常) / P(输入句子) (5)\n 由于(4),(5)都要除于相同的P(输入句子),所以(4),(5)右边可以同时乘以P(句子),只比较等号右边前两个值的乘积的大小。\n P(赌博|输入句子) = P(输入句子|赌博) * P(赌博) P(正常|输入句子) = P(输入句子|正常) * P(正常)\n P(句子|赌博)\n最关键的就是求P(输入句子|赌博),直接求输入句子在赌博类别句子中出现的概率非常困难,因为训练数据不可能包含所有句子,很可能并没有输入句子。什么意思呢?因为同一个句子,把词语进行不同的排列组合都能成立,例如:\n 奖金将在您完成首存后即可存入您的账户。真人荷官,六合彩,赌球欢迎来到全新番摊游戏!\n 可以变成\n 奖金将在您完成首存后即可存入您的账户。六合彩,赌球,真人荷官欢迎来到全新番摊游戏!\n 或者\n 欢迎来到全新番摊游戏,奖金将在您完成首存后即可存入您的账户。六合彩,真人荷官,赌球!\n 稍微变换词语的位置就是一个新的句子了,训练数据不可能把所有排列组合的句子都加进去,因为实在太多了。所以当我们遇到一个输入句子,很可能它在训练数据中没有出现,那么P(输入句子|类别)对应的概率都为零,这显然不是真实的结果。也会导致我们的分类器出错,这个时候该怎么办呢?刚刚在贝叶斯模型中我们提到,它会将一个句子分成不同的词语来综合分析,那我们是不是也可以把句子当成词语的集合呢?\n 警方召开了全省集中打击赌博违法犯罪活动专项行动电视电话会议。会议的重点是“查处”六合彩、赌球赌马等赌博活动。\n 警方召开了全省\u0026hellip;赌马等赌博活动 = 警方 + 召开 + 全省\u0026hellip;+赌博活动\n 即:\n P(输入句子|赌博) = (P(词语1) * P(词语2|词语1) * P(词语3|词语2))|赌博) ≈ P(词语1)|P(赌博) * P(词语2)|P(赌博) * P(词语3)|P(赌博)\n P(警方召开了全省\u0026hellip;赌马等赌博活动。|赌博) = P(警方|赌博) * P(召开|赌博) * P(全省|赌博) \u0026hellip; * P(赌马|赌博) * P(赌博活动|赌博)\n 我们把P(输入句子|赌博)分解成所有P(词语|赌博)概率的乘积,然后通过训练数据,计算每个词语在不同类别出现的概率。最终获取的是输入句子有效词语在不同类别中的概率。\n 词语 正常 赌博 警方 0.8 0.2 召开 0.7 0.3 全省 0.7 0.3 赌马 0.4 0.6 赌球 0.3 0.7 赌博活动 0.4 0.6 \u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;- 综合概率 0.8 0.2 在上面的例子中,虽然赌马,赌球,赌博活动这几个词是赌博类别的概率很高,但是综合所有词语,分类器判断输入句子有80%的概率是正常句子。简单来说,要判断句子是某个类别的概率,只需要计算该句子有效部分的词语的在该类别概率的乘积。\n贝叶斯模型实现 要计算每个词语在不同类别下出现的概率,有以下几个步骤:\n 选择训练数据,标记类别 把所有训练数据进行分词,并且组成成一个包含所有词语的词袋集合 把每个训练数据转换成词袋集合长度的向量 利用每个类别的下训练数据,计算词袋集合中每个词语的概率 选择训练数据 训练数据的选择是非常关键的一步,我们可以从网络上搜索符合对应类别的句子,使每个类别的数据各占一半。不过当你理解了贝叶斯模型的原理之后,你会发现一个难题问题,就是如何保持数据的独立分布,例如你选择的训练数据如下:\n 赌博类别\n 根据您所选择的上述六合彩游戏,您必须在娱乐场完成总金额(存款+首存奖金)16倍或15倍流水之后,方可申请提款。\n 奖金将在您完成首存后即可存入您的账户。真人荷官 六合彩 欢迎来到全新番摊游戏!\n 正常类别\n Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。\n 理查德·菲利普斯·费曼,美国理论物理学家,量子电动力学创始人之一,纳米技术之父。\n 我们可以注意到六合彩,游戏这两个词语,只在赌博类别的训练数据出现。这两个词语对句子是否是赌博类别会有很大的影响性,六合彩对赌博类别确实是重要的判别词,但是游戏这个词语本身和赌博没有直接的关系,却被错误划分为赌博类别相关的词语,当之后分类器遇到\n 我们提供最新最全大型游戏下载,迷你游戏下载,并提供大量游戏攻略\n 会因为里面的游戏,将它判断为赌博类别,\n\u0026gt;\u0026gt;\u0026gt; result = cherry.classify('我们提供最新最全大型游戏下载,迷你游戏下载,并提供大量游戏攻略') \u0026gt;\u0026gt;\u0026gt; result.percentage [('gamble.dat', 0.793), ('normal.dat', 0.207)] \u0026gt;\u0026gt;\u0026gt; result.word_list [('游戏', 1.9388011143762069)] 所以,当我们要做一个赌博/正常的分类器,我们需要在正常类别的训练数据添加:\n 中国游戏第一门户站,全年365天保持不间断更新,您可以在这里获得专业的游戏新闻资讯,完善的游戏攻略专区\n 这样的正常而且带有游戏关键字的句子。同时当训练数据过少,输入句子包含了训练数据中并没有c出现过的词语,该词语也会被分类器所忽略。cherry分类器可以通过启用debug模式得到被错误划分的数据以及其权重最高的词语,你可以根据输出的词语来调整训练数据。我们之后可以通过Adaboost算法动态调整每个词语的权重,这个功能我们会在下一个版本推出。 另外一方面,现实生活中,正常的句子比赌博类别的句子出现的概率要多得多,这点我们也可以从训练数据的比例上面体现,适当增加正常类别句子的数量,也可以赋予正常类别句子高权重,不过要小心Accuracy_paradox的问题。我们在测试的时候,可以根据混淆矩阵以及ROC曲线来分析分类器的效果,再进行数据调整。\n词袋集合 为简单起见,本篇文章只选取4个句子作为训练数据:\n 赌博类别:\n 根据您所选择的上述礼遇,您必须在娱乐场完成总金额(存款+首存奖金)16倍或15倍流水之后,方可申请提款。\n 奖金将在您完成首存后即可存入您的账户。真人荷官 体育博彩 欢迎来到全新番摊游戏!\n 正常类别:\n 理查德·菲利普斯·费曼,美国理论物理学家,量子电动力学创始人之一,纳米技术之父。\n 在公安机关持续不断的打击下,六合彩、私彩赌博活动由最初的公开、半公开状态转入地下。\n 要计算每个词语在不同类别下的概率,首先需要一个词袋集合,集合包含了训练数据中所有非重复词语(_vocab_list),参考函数_get_vocab_list:\ndef _get_vocab_list(self): ''' Get a list contain all unique non stop words belongs to train_data Set up: self.vocab_list: [ 'What', 'lovely', 'day', 'like', 'gamble', 'love', 'dog', 'sunkist' ] ''' vocab_set = set() all_train_data = ''.join([v for _, v in self._train_data]) token = Token(text=all_train_data, lan=self.lan, split=self.split) vocab_set = vocab_set | set(token.tokenizer) self._vocab_list = list(vocab_set) 默认使用结巴分词进行中文分词(你可以定制分词函数),例如第一个数据:\n 根据您所选择的上述礼遇,您必须在娱乐场完成总金额(存款+首存奖金)16倍或15倍流水之后,方可申请提款。\n 分词后会得到:\n['根据', '您', '所', '选择', '的', '上述', '礼遇', ',', '您', '必须', '在', '娱乐场', '完成', '总金额', '(', '存款', '+', '首存', '奖金', ')', '16', '倍', '或', '15', '倍', '流水', '之后', ',', '方可', '申请', '提款', '。'] 我们去掉包含在stop_word.dat中的词语,stop_word.dat包含了汉语中的常见的转折词:\n 如果,但是,并且,不只\u0026hellip;\n 这些词语对于我们分类器没有用处,因为任何类别都会出现这些词语。接下来再去掉长度等于1的字,第一个训练数据剩下:\n['选择', '上述', '礼遇', '娱乐场', '总金额', '存款', '首存', '奖金', '16', '15', '流水', '申请', '提款'] 遍历4个句子最终得到长度为49的词袋集合(vocab_list):(这里使用的集合是无序的,所以你得到的结果顺序可能不同)\n['提款', '存入', '游戏', '最初', '六合彩', '娱乐场', '费曼', '奖金', '账户', '菲利普斯', '量子', '电动力学', '总金额', '上述', '活动', '状态', '物理学家', '公安机关', '荷官', '即可', '理论', '申请', '半公开', '选择', '15', '打击', '全新', '来到', '公开', '方可', '博彩', '完成', '理查德', '纳米技术', '不断', '存款', '之一', '创始人', '真人', '私彩', '持续', '根据', '必须', '16', '赌博', '欢迎', '体育', '转入地下', '首存', '流水', '美国', '礼遇'] 得到词袋之后,再次使用训练数据,并把每个训练数据都转变成一个长度为49的一维向量\ndef _get_vocab_matrix(self): ''' Convert strings to vector depends on vocal_list ''' array_list = [] for k, data in self._train_data: return_vec = np.zeros(len(self._vocab_list)) token = Token(text=data, lan=self.lan, split=self.split) for i in token.tokenizer: if i in self._vocab_list: return_vec[self._vocab_list.index(i)] += 1 array_list.append(return_vec) self._matrix_lst = array_list 根据您所选择的上述礼遇,您必须在娱乐场完成总金额(存款+首存奖金)16倍或15倍流水之后,方可申请提款。\n 对应转变成:\n# 长度为49的一维向量 [1, 0, 0, 0, 1, 0, ..., 1, 0, 1] 其中的1分别对应着数据分词后的词语在词袋中出现的次数。接下来将所有训练数据的一维向量组合成列表_matrix_list\n[ [1, 0, 0, 0, 1, 0, ..., 1, 0, 1] [0, 1, 1, 0, 0, 0, ..., 0, 0, 0] ... ] 要计算每个词语在不同类别下的概率,只需要把词语出现的次数除以该类别的所有词语的总数, cherry分类器出于效率的考虑使用了numpy的矩阵运算。\ndef _training(self): ''' Native bayes training ''' self._ps_vector = [] # 防止有词语在其他类别训练数据中没有出现过,最后的P(句子|类别)乘积就会为零,所以给每个词语一个初始的非常小的出现概率,设置vector默认值为1,cal对应为2 # vector: 默认值为1的一维数组 # cal: 默认的分母,计算该类别所有有效词语的总数 # num: 计算P(赌博), P(句子) vector_list = [{ 'vector': np.ones(len(self._matrix_lst[0])), 'cal': 2.0, 'num': 0.0} for i in range(len(self.CLASSIFY))] for k, v in enumerate(self.train_data): vector_list[v[0]]['num'] += 1 # vector加上对应句子的词向量,最后把整个向量除于cal,就得到每个词语在该类别的概率。 # [1, 0, 0, 0, 1, 0, ..., 1, 0, 1] (根据您所选择的...) # [0, 1, 1, 0, 0, 0, ..., 0, 0, 0] (奖金将在您完成...) # + # [1, 1, 1, 1, 1, 1, ..., 1, 1, 1] vector_list[v[0]]['vector'] += self._matrix_lst[k] vector_list[v[0]]['cal'] += sum(self._matrix_lst[k]) for i in range(len(self.CLASSIFY)): # 每个词语的概率为[2, 2, 2, 1, 2, 1, ..., 2, 1, 2]/cal self._ps_vector.append(( np.log(vector_list[i]['vector']/vector_list[i]['cal']), np.log(vector_list[i]['num']/len(self.train_data)))) 遍历完所有训练数据之后,会得到两个类别对应的每个词语的概率向量,(为了防止python的小数相乘溢出,这里的概率都是取np.log()对数之后得到的值):\n# 赌博 ([-2.80336038, -2.80336038, -2.80336038, -3.49650756, -3.49650756, -2.80336038, -3.49650756, -2.39789527, -2.80336038, -3.49650756, -3.49650756, -3.49650756, -2.80336038, -2.80336038, -3.49650756, -3.49650756, -3.49650756, -3.49650756, -2.80336038, -2.80336038, -3.49650756, -2.80336038, -3.49650756, -2.80336038, -2.80336038, -3.49650756, -2.80336038, -2.80336038, -3.49650756, -2.80336038, -2.80336038, -2.39789527, -3.49650756, -3.49650756, -3.49650756, -2.80336038, -3.49650756, -3.49650756, -2.80336038, -3.49650756, -3.49650756, -2.80336038, -2.80336038, -2.80336038, -3.49650756, -2.80336038, -2.80336038, -3.49650756, -2.39789527, -2.80336038, -3.49650756, -2.80336038]), 0.5) # 正常 ([-3.25809654, -3.25809654, -3.25809654, -2.56494936, -2.56494936, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -2.56494936, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -2.56494936, -2.56494936, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -3.25809654, -2.56494936, -2.56494936, -2.56494936, -3.25809654, -2.56494936, -2.56494936, -3.25809654, -2.56494936, -2.56494936, -3.25809654, -3.25809654, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -3.25809654, -3.25809654, -2.56494936, -3.25809654]), 0.5) # 词袋集合 ['提款', '存入', '游戏', '最初', '六合彩', '娱乐场', '费曼', '奖金', '账户', '菲利普斯', '量子', '电动力学', '总金额', '上述', '活动', '状态', '物理学家', '公安机关', '荷官', '即可', '理论', '申请', '半公开', '选择', '15', '打击', '全新', '来到', '公开', '方可', '博彩', '完成', '理查德', '纳米技术', '不断', '存款', '之一', '创始人', '真人', '私彩', '持续', '根据', '必须', '16', '赌博', '欢迎', '体育', '转入地下', '首存', '流水', '美国', '礼遇'] 结合向量和词袋集合来看,提款,存入,游戏这几个词是赌博的概率要大于正常的概率\n#赌博 提款,存入,游戏 [-2.80336038, -2.80336038, -2.80336038] #正常 提款,存入,游戏 [-3.25809654, -3.25809654, -3.25809654] 符合我们的常识,接下来就可以进行输入句子的分类了。\n判断类别 训练完数据,得到词语对应概率之后,判断类别就非常简单,**只需要把输入句子进行相同的分词,然后计算对应的词语对应的概率的乘积即可,**得到乘积最大的就是最有可能的类别。输入句子:\n欢迎参加澳门在线娱乐城,这里有体育,百家乐,六合彩各类精彩游戏。 先根据原先的词袋集合,先转变为一维向量\n# 词袋集合 ['提款', '存入', '游戏', '最初', '六合彩', '娱乐场', '费曼', '奖金', '账户', '菲利普斯', '量子', '电动力学', '总金额', '上述', '活动', '状态', '物理学家', '公安机关', '荷官', '即可', '理论', '申请', '半公开', '选择', '15', '打击', '全新', '来到', '公开', '方可', '博彩', '完成', '理查德', '纳米技术', '不断', '存款', '之一', '创始人', '真人', '私彩', '持续', '根据', '必须', '16', '赌博', '欢迎', '体育', '转入地下', '首存', '流水', '美国', '礼遇'] # 长度为49的一维向量 [0, 0, 1, 0, 1, ...] 然后与分别与两个概率向量相乘,求和,并加上对应的类别占比,对应的代码:\ndef _bayes_classify(self): ''' Calculate the probability of different category ''' possibility_vector = [] log_list = [] # self._ps_vector: ([-3.44, -3.56, -2.90], 0.4) for i in self._ps_vector: # 计算每个词语对应概率的乘积 final_vector = i[0] * self.word_vec # 获取对分类器影响度最大的词语 word_index = np.nonzero(final_vector) non_zero_word = np.array(self._vocab_list)[word_index] # non_zero_vector: [-7.3, -8] non_zero_vector = final_vector[word_index] possibility_vector.append(non_zero_vector) log_list.append(sum(final_vector) + i[1]) possibility_array = np.array(possibility_vector) max_val = max(log_list) for i, j in enumerate(log_list): # 输出最大概率的类别 if j == max_val: max_array = possibility_array[i, :] left_array = np.delete(possibility_array, i, 0) sub_array = np.zeros(max_array.shape) # 通过曼哈顿举例,计算影响度最大的词语 for k in left_array: sub_array += max_array - k return self._update_category(log_list), \\ sorted( list(zip(non_zero_word, sub_array)), key=lambda x: x[1], reverse=True) 通过计算:\n P(赌博|句子) = sum([0, 0, 1, 0, 1, \u0026hellip;] * [-2.80336038, -2.80336038, -2.80336038, \u0026hellip;]) + P(赌博) = 0.85\n P(正常|句子) = sum([0, 0, 1, 0, 1, \u0026hellip;] * [-3.25809654, -3.25809654, -3.25809654, \u0026hellip;])+ P(正常) = 0.15\n 最终得到P(赌博|句子) \u0026gt; P(正常|句子),所以分类器判断这个句子是赌博类别。\n\u0026gt;\u0026gt;\u0026gt; result = cherry.classify('欢迎参加澳门在线娱乐城,这里有体育,百家乐,六合彩各类精彩游戏。') \u0026gt;\u0026gt;\u0026gt; result.percentage [('gamble.dat', 0.85), ('normal.dat', 0.15)] \u0026gt;\u0026gt;\u0026gt; result.word_list [('六合彩', 0.96940055718810347), ('游戏', 0.96940055718810347), ('欢迎', 0.56393544907993931)] 测试 统计分析 算法分析 统计分析 测试方法有留出法(hold-out),k折交叉验证法(cross validation),自助法(bootstrapping),这里我们使用留出法,测试脚本默认每次从所有数据中选出60个句子当成测试数据,剩下的当成训练数据。重复进行测试10次。运行测试脚本\n\u0026gt;\u0026gt;\u0026gt; python runanalysis.py This may takes some time, Go get a coffee :D. Building prefix dict from the default dictionary ... Loading model from cache /var/folders/md/0251yy51045d6nknpkbn6dc80000gn/T/jieba.cache Loading model cost 0.914 seconds. Prefix dict has been built succesfully. +Cherry---------------+------------+------------+ | Confusion matrix | gamble.dat | normal.dat | +---------------------+------------+------------+ | (Real)gamble.dat | 249 | 0 | | (Real)normal.dat | 13 | 338 | | Error rate is 2.17% | | | +---------------------+------------+------------+ 输出分类测试数据的平均错误率为2.17%,同时我们可以通过混淆矩阵对分类器进行分析:\n 类别 含义 真阳性(TP) 输入句子为赌博类别,分类器判断为赌博类别 假阳性(FP) 输入句子为正常类别,分类器判断为赌博类别 真阴性(TN) 输入句子为正常类别,分类器判断为正常类别 假阴性(FN) 输入句子为赌博类别,分类器判断为正常类别 查全率(recall)(能找出赌博类别句子的概率)\n真阳性/(真阳性+假阴性) 249 / 249 = 100%\n 查准率(precision)(分类为赌博类别中的句子,确实是赌博类别的概率)\n真阳性/(真阳性+假阳性) 249 / (249 + 13) = 95%\n 如果业务的需求是尽可能找到潜在的阳性数据(例如癌症初检)那么就要求高查全率,不过对应的,高查全率会导致查准率降低。(可以这样理解,假如所有句子都判断成赌博类别,那么所有确实是赌博类别的句子确实都被检测到了,但是查准率变得很低。)影响查全率以及查准率的一点是训练数据数量的比例,日常的句子中,赌博类别的句子与正常类别的句子比例可能是1:50。也就是说随便给出一个句子,不用看内容,那么它有98%是正常的。不过在某些情况下,例如热门评论区打广告的用户就很多,那么这个比例就变成1:10或者1:20,这个比例是根据具体业务而调整的。训练数据也应该遵循这个比例,但是实现中,我们必须要找到大量独立分布的数据才能遵循这个比例,这就是机器学习数据常遇到的不均衡分类问题。要解决这个问题,可以引入Adaboost算法动态调整每个词语的权重。。我们可以通过-p参数输出ROC曲线:\nROC曲线横坐标代表的是假阳性(没有问题却被判断为有问题),纵坐标代表的是真阳性(有问题而且被判断出来),一个优秀的分类器尽可能维持高真阳性以及低假阳性。一般来说,如果一个分类器的ROC曲线包含了另外一个分类器的ROC曲线,代表此分类器在此数据集的分类效果更好。\n算法分析 上下文关联 分类器绕过 上下文关联 当我们计算P(输入句子|类别)的时候,我们把输入句子分成了词语的集合,**同时假定了输入句子中词语与词语之间没有上下文关系,**其实这是不完全正确的,例如:\n警方召开了全省集中打击赌博违法犯罪活动... 从常识句子的上下文判断,集中打击出现在赌博违法犯罪之前的概率,要比召开出现在赌博违法犯罪之前的概率高,不过当我们把输入句子分成词语的集合的时候,把它们看成每个词语都是独立分布的。这也是此算法称为朴素贝叶斯的原因,如果我们有大量的数据集,计算出每个词语对应词袋模型其他词语的出现概率值的话,可以提高检测的准确率。\n要注意的是,训练数据选择与最后进行分类的数据必须尽量关联,如果要检测的句子与训练数据有非常大的差别,例如检测的内容包含大量的英文单词,但是训练数据却没有,那么分类器就无法进行正确的分类。同时,输入句子过短的话,分类器也无法很好地进行分类。因为分类的结果会很容易被其中的一两个词语所影响。\n分类器绕过 分类器无法分辨重复内容或部分无意义文本,输入句子:\n 车厘子车厘子车厘子车厘子\n {{{{{{{{{{{}}}}}}}}}}}\n 加入博彩121加qq看头像,很为温暖文科楼课文你问你看我呢额可能我呃让你听客啊啊爱看就是是过分过分你问人人官方代购极为。\n 前两个是垃圾内容,但是即使我们添加垃圾内容的数据集,也很难判断正确。最后一个前一小段是赌博类别的句子,后面一长串是无意义或者正常类别的句子,分类器综合判断它是正确的句子。**解决这个问题我们可以用一个简单的方法,计算句子的熵,也就是无序程度。**每个句子都有合理的长度以及合理的无序程度,什么意思呢?句子的长度大约遵循正态分布,极长(不包含标点符号)或者极短的句子出现的概率比较低,同时,通常一个句子中的词语不会重复出现很多次,它的无序程度是在某个范围的。当我们看到前两个句子,因为它们词语的重复度非常高,所以句子的无序度非常低,如何计算句子的无序程度呢?\n 我们找两个输入句子作为例子,先把输入句子进行分词\n 车厘子是一只非常可爱的猫咪\n 车厘子车厘子车厘子车厘子\n [车厘子,非常,可爱,猫咪] [车厘子,车厘子,车厘子,车厘子] 计算每个词语出现的次数除于句子的词语数量:\nP(车厘子) = P(非常) = P(可爱) = P(猫咪) = 1/4 (句子1)\nP(车厘子) = 4/4 = 1 (句子2)\n通过计算熵的公式,带入每个概率值,最后除于句子的词语数量\n H = -sum(p(x)log2p(x)) H1 = ((1/4 * -2) - (1/4 * -2) - (1/4 * -2) - (1/4 * -2)) / 4= -2 / 4 = -1/2 H2 = 0 可以看到,在同样的句子长度下,第一个句子的熵为-2,第二个为0,可以设置一个熵的范围,如果低于该值,代表句子可能是垃圾数据。一般来说,先进行垃圾文本过滤,然后进行贝叶斯模型的分类,在工程中会有更好的效果。\n总结 **理解了贝叶斯分类的原理,你就能根据自己的业务需求,来判断使用什么分词函数,使用哪些stop_word,可以定制适合业务的数据集,同时根据输出的被错误分类的数据以及混淆矩阵,做出对应的调整。**如果有什么疑问,欢迎留言。\n"
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E9%80%89%E6%8B%A9%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "编程语言选择(未完成)",
"tags": [],
"description": "",
"content": "刚开始学习编程的时候,我也很困惑这个问题,有那么那么多的编程语言,常见的有\n python ruby php Go c java R \u0026hellip; 我们可以根据你的职业职业规划来选择要学习的语言:\n前端工程师 完成设计 后端交互 前端工程师主要的职责是把设计师的设计图实现,当设计师设计完界面之后\n完成设计 前端工程师需要用代码来实现设计图,例如界面的布局,按钮的颜色等等。这期间需要和设计师合作交流,讨论哪些方面在工程实现中可能会有困难,可能影响性能这类的因素。\n后端交互 完成设计之后,前端工程师需要根据不同的页面以及业务逻辑来与后端交互,这句话什么意思呢?假如你开发的是一个让用户写日记的应用。那么当用户写完日记之后,点击保存按钮,这个按钮要触发一个保存日记的事件,需要把日记根据协议传输到服务器,然后存储在数据库里面。听起来有点复杂,其实非常简单,因为都有通用的工具帮助你完成。\n后端工程师。 业务逻辑处理 前端交互 业务逻辑处理 前端交互 运维工程师 数据分析/机器学习工程师 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E5%9F%9F%E5%90%8D/",
"title": "域名",
"tags": [],
"description": "",
"content": "计算机知识往往互相关联,要真正了解域名是什么,必须先了解什么是URI。这里有一些常见的例子。\nhttps://www.apple.com http://www.w3c.org ftp://example.org/resource.txt file:///Users/example/hello.jpg 最后两个大家可能不太熟悉,以最后一个为例子,你们可以尝试把自己电脑中任意的文件拖放到浏览器中,浏览器会去猜这个文件类型,一些常见的文件类型例如图片,浏览器可以判断并渲染出来。同时浏览器的地址栏也会变成file开头。这个例子中的file就是这个URI所使用的协议。常见的协议还有\nHTTP 超文本传输协议 SMTP 邮件协议 telnet 终端传输协议 DNS 域名系统协议 DHCP 动态主机配置协议 好吧,我承认部分对于一般大众不是很常见,不过如果你想认真学习编程,起码看到这些名词的时候知道是什么。感兴趣的学生可以前往Lists of network protocols。那么协议又是什么呢?其实协议的本质非常简单。\n域名(Domain names)是互联网基础架构的关键部分。它们为互联网上任何可用的网页服务器提供了人类可读的地址。 任何连上互联网的电脑都可以通过一个公共IP地址访问到,对于IPv4来说,这个地址由32位组成(它们通常写成四个范围在0~255以内,由点分隔的数字组成,比如173.194.121.32),而对于IPv6来说,这个地址由128位组成,通常写成八组由冒号分隔的四进制数(e.g., 2027:0da8:8b73:0000:0000:8a2e:0370:1337). "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E6%9C%AF%E8%AF%AD/%E6%9C%8D%E5%8A%A1%E7%AB%AF/",
"title": "服务端",
"tags": [],
"description": "",
"content": "服务端 很简单,服务器就是一台**24小时运行**,运行着**专为服务器设计的系统**的**高性能计算机**。它也是由内存,硬盘,CPU组成。不过它会根据这台服务器的用途来配置不同的组件。有些服务器专门用来存储文件,图片,那么它就需要更大的硬盘,对CPU的要求就没那么高。有些专门用来进行高密度的计算,例如视频转格式,数据分析那么就要更快的CPU。 服务器会根据不同客户端的请求返回不同的内容,当你使用浏览器访问一个页面的时候,实际上浏览器就是寻找该页面对应的服务器获取相应的图片和文字内容。 客户端 常见的客户端包括浏览器,手机,它请求服务器需要的资源,然后客户端解析返回的内容并且显示给用户。 "
},
{
"uri": "https://www.enginego.org/%E5%9F%BA%E7%A1%80/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/%E5%A6%82%E4%BD%95%E8%BD%AC%E8%A1%8C%E4%B8%BA%E7%A8%8B%E5%BA%8F%E5%91%98/",
"title": "",
"tags": [],
"description": "",
"content": "如何转行为程序员\n我大学的专业并非计算机相关,从转行到得到第一份程序员的工作花了很多时间,经过了多年之后回过头看,走了不少弯路。如果你是非计算机专业却想转行为程序员的话,这是我个人的几点建议。\n理解热爱 转行之前你先问自己,为什么想要转行为程序员。你真的喜欢编程吗?转行为程序员的人非常多,你有足够的意志力打败他们吗?大多数程序员的工资并没有想象中那么高,而且工作时长也很长。需要经常熬夜加班。你能接受吗?我听过医学生和我说他想转行,因为觉得我们看起来很酷。我觉得这不是一个好原因。可能被电影渲染得多,大家只看到酷的程序员或者他们酷的一面,而没有看到这个行业辛苦的一面。搜索下程序员加班以及程序员中年危机会给你更好的认知。程序员我偶尔会看到这样的转行原因:“因为我编程实现了xxx,给我带来了成就感,所以我喜欢编程”。这本身并没有问题,但是错把学会一件新鲜事物,完成一件事带来的成就感理解为热爱编程那就大错特错了。其实无论什么行业,第一次设计出海报,,还是用毛笔写了几个自我感觉不错的字,或者仅仅是人生中第一次投中三分球都能带给你成就感。这与相应的行业并没有直接的关系。而且容易给你一种错觉,编程就是这样,按照教程一步步写项目,上线。然后下一个项目。那是因为新手项目故意忽略了那些枯燥的,带给你挫折感,但是又必须的东西,例如撰写文档,API 调试,版本控制,测试用例,代码重构等。编程并不需要超乎常人的智商,但是需要极其的耐心和锲而不舍的精神。我忘了多少次在电脑前因为不知道代码哪里出了问题而崩溃。不过,这也是磨练个人逻辑思维的方法,在这一次次的崩溃中我学会如何编程,如何调试。而我不知道有没有更好的方法。把热爱当成职业却不一样,例如你热爱篮球,你打球很快乐是因为你能够选择怎样去打球以及什么时候打。如果要求你每天7点起来,先做两小时“与篮球无关”的准备运动,例如力量训练,柔韧度训练,身体对抗。然后每天都要训练4个小时战术。那么你可能发现自己并没有那么热爱篮球。把他当成职业的话你需要忍受背后的辛苦。\n良师益友 不管是什么行业,这都是最重要的一点。我初学编程的时候只认识几个计算机专业的朋友,很可惜,后来他们去了实习之后太忙,也没有时间向他们学习。找到合适的老师是非常难的,因为新手的问题不单单是编程本身的问题,更多的是一些工具使用,环境配置的一些“无聊问题”,例如“如何运行 Java”,“如何安装 Python”,“Syntax error 这个报错什么意思?”,这些问题无论是多么有经验的工程师也无法全部解决,都需要依赖搜索引擎自己找答案。所以益友这时候就重要了,因为大家基础都差不多,遇到的问题也差不多。这么能找到这些朋友呢?最简单的方法,找对应的 QQ 群。搜索 Python Java 这些关键字进入里面的 QQ 群。这是我当初没有做的,虽然 QQ 群里面的成员参差不齐,但是起码帮助你缩小了搜索的范围。加十个群,每个群能认识一两个朋友那就够了。当然,他们的想法也不一定成熟,他们喜欢的教材和课程不一定适用你自己,你可以用作参考但需要判断的能力。不仅能互相学习交流,他们还能分享面试经验,简历经验,互联网企业永远都在招聘,有一些工程师会在群里招人,内推远比海投成功率更高。\n选对方向 请不要根据热点而选择方向,例如人工智能,区块链。99.9% 它不会让你找到一份工作,因为他的准入门槛极高,大多的在线课程只会教你如何使用一些开源库来实现功能,这固然重要,不过企业更多是需要能理解原理,推导以及理解公式的。那么应该做什么方向,前端还是后端,应该选择什么语言?我给不了一个很好的答案给你,通常来说,前端入门门槛较低,但是门槛较低代表竞争者越多,不一定是好处。我建议根据你想要工作城市的招聘网站的岗位数量来做选择,关键字就是 前端,后端,java,python,c++,php,.net 看看哪些岗位数量比较多。而且当你看了十个岗位描述之后,其实你就知道自己要学习什么了,例如 Java 基本都会要求会语言的基础,Spring。python要求的是django,php的是larvel。网络框架相对好学,主要靠经验积累和看文档的能力。\n学会学习 选对教材,市面上的教材,我在这篇文章列举了一些。至于要不要参加培训班,培训班水平层次不齐,大多数都只是照本宣科让你有学习的错觉,大部分还是得靠自己,好处是能找到线下的交流编程的人一起学习。\n学会问问题和找答案 互联网的问题在于资源太多而不是太少。新手在编程之前需要先学会如何使用工具找答案以及提问题。就像有一本我力荐的书“如何阅读一本书”一样,你需要在阅读其他书之前先阅读一本书。你需要做的事情其实很简单,第一,把搜索引擎改为谷歌,相信我,用谷歌搜索问题和答案能帮你节省非常多的时间。第二,学习英语,一开始英语不好的时候你可以选择把报错信息,\u0026ldquo;SyntaxError: invalid syntax 问题\u0026rdquo; 加上几个中文字来看到中文相关的搜索结果,当你愿意静下心看的时候,直接用英文搜索结果,你会开始熟悉并且爱上一个叫 Stackoverflow 的网站,他解答了基本所有程序员会遇到的问题。如果你的问题找不到答案,只有两个原因。这个问题太简单或者太难了。大多数情况都是第一种,你需要想清楚问题发生的原因,这也是锻炼逻辑思维的方法。\n求职准备 写好简历,做几个有技术含量的项目,即使是跟着教程做也要理解里面的内容。\n勇于尝试 去投简历吧。\n"
},
{
"uri": "https://www.enginego.org/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E4%B8%8D%E5%BA%94%E8%AF%A5%E8%B5%8C%E5%8D%9A%E6%9C%AA%E5%AE%8C%E6%88%90/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84/%E6%A0%88%E7%BB%93%E6%9E%84/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E7%9F%A5%E8%AF%86%E7%82%B9/%E6%95%B4%E6%95%B0%E6%B5%AE%E7%82%B9%E6%95%B0%E7%9A%84%E8%A1%A8%E7%A4%BA/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/%E7%AE%97%E6%B3%95/%E8%B0%B7%E6%AD%8C%E4%BB%8E%E9%9B%B6%E5%88%B0%E4%B8%80/%E7%9F%A5%E8%AF%86%E7%82%B9/%E8%BF%90%E8%A1%8C%E6%97%B6%E6%A0%88/",
"title": "",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/categories/",
"title": "Categories",
"tags": [],
"description": "",
"content": ""
},
{
"uri": "https://www.enginego.org/tags/",
"title": "Tags",
"tags": [],
"description": "",
"content": ""
}]