一点一点前进...

0%

以下内容摘自《娱乐至死》:
乔治·奥威尔写了《1984》,阿道斯·赫胥黎写了《美丽新世界》
奥威尔所担心的,是书籍将被禁止流传;
赫胥黎所担心的,是书籍根本不用被禁止,因为人们将自发的不再读书;
奥威尔所担心的,有人将剥夺我们获取信息的权利;
赫胥黎所担心的,有人将给予我们太多信息,使我们只会被动接受、无法自拔;
奥威尔所担心的,真相将被隐瞒;
赫胥黎所担心的,真相将被无关的汪洋所淹没;
奥威尔所担心的,我们的文化将被禁锢,成为一片荒漠;
赫胥黎所担心的,我们的文化将因琐碎的杂草丛生,大众为微不足道的事情而痴迷。

《1984》中,政府用制造痛苦的方法来支配大众;
《美丽新世界》中,政府用制造娱乐的方法来支配大众。
在奥威尔看来,人类将毁于自己所憎恨的东西;
而赫胥黎认为,人类将毁于自己所迷恋的东西。

下面就随便写写看完书之后的感想,想法,观点。

这两个不同的世界,在今天看来也很有意思,有的国家像1984里面描绘的,有的国家像美丽新世界里面描绘的。在我看来,显然后者更可怕,大众在不知不觉之中失去了自我却丝毫不知。而我们目前所处的世界,更像是后者,或者说,在一步步的向后者逼近。

什么是自由?保持独立的思考。不管在什么样的世界,保持独立的思考都是第一位的。

美丽新世界否定了人自由的意志,一切思想都是被设定的。

站在1984和美丽新世界之外去读这两本书,人们可能会无法接受活在1984,但是,我觉得很多人可能会在不知不觉中接受美丽新世界,因为那个世界看起来没什么不好的。在那里,物质极大丰富,犹如描写中的共产主义社会。不过那个世界依然有特权,统治阶级能做任何想做的事情。人们从出生就被分成了三六九等,α是高等的,做着让β们觉得优越的事情,不过什么阶层,吃一颗唆麻就能体会幸福了。

吸食唆麻,就是在那个世界逃避一切的方式,还能体会到幸福。人们真心是娱乐至死,不去思考,不去想任何除了被设定以外的事情。野蛮人的妈妈天天做着培养相关的工作,但是不知道什么是化学试剂,读的书也就只有守则、操作指南这类玩意。只要做了分配的工作,就能得到任意想要的东西,还能有唆麻让人幸福,人们为什么要去思考呢。活着就是为了去娱乐,野蛮人的愤怒就是大家娱乐的点。这和我们的世界很像了,大多人刷微博、刷朋友圈,在消费着他人的故事;很多人每天除了工作来解决温饱问题之后,除了娱乐,其实也没有做什么有用的事情,毕竟,不管在现实世界,还是美丽新世界,像那个自愿到小岛上写作的α主动去寻找更高的追求的人,永远都只是一小撮人。

构建了美丽新世界,统治阶级让人们活在幸福之中,不主动思考来达到一个和谐、稳定的社会,不知道比1984高到哪里去。当然,两个作者都有着深邃的思想,写出不同的社会,不同的东西,完全是由当时所在的环境决定的,这里,我绝对没有贬低奥威尔的意思。

伯纳,一个可怜的α,个头比正常的α小,没有得到应有的尊敬。他对美丽新世界的抵抗,只是因为自己想得到关注罢了,和那个自愿去小岛上写作的α有着本质的差别,当他因为野蛮人得到应有的尊敬和礼遇之后,也开始维护这个美丽新世界了。现实生活里面,这种人也是很多很多的。

两本书有个共性,就是屏蔽了过去,屏蔽了历史,印证了那句话:掌握了现在的人,掌握了历史。历史,不仅仅是为了研究过去,更为了满足现在,谋划未来。古今中外的执政者,无一例外。

阶层固化问题。美丽新世界,阶级在出生就被设定了。在那里,人们完全没有进取的动机啊,不管日子过得怎么样,吸食一下唆麻,爽反了,谁还去管什么阶级不阶级呢?他们压根就没有这个意识,同时,媒介取代了信息,人们不需要去思考,给什么就接受什么,没有理解力,也就不会去思考去理解阶层问题。现实社会,虽然阶层固化也越来越严重,但是,人们还是有进取的想法,对阶层还是有自己的看法的,阶层流动还是存在的。不过基于媒介的干涉,娱乐的低层次,人们慢慢地也不关心这个问题了,这真是一个问题。

人们会反抗1984,因为那个世界里面的东西是我们讨厌的,但是人们不一定会反抗美丽新世界,因为那个世界里面的东西是我们喜欢的。这也就是为什么,有人写了些所谓的指南,让大家警惕起来,不要走向美丽新世界。

随便写写,到此为止。

题目链接

7有一个特殊的性质,以2为基底是111,以6为基底是11。也就是说,基底b > 1的情况下,整数n能够至少写成两个基底的循环整数。我们称这种整数位Strong Repunits,强循环整数。50以内,{1,7,13,15,21,31,40,43}这些都是满足该性质的。
题目为了检验你的算法,给出了1000以内强循环整数的和是15864。
求,10 ^ 12以内的强循环整数之和。

想知道一个整数是不是强循环整数比较难,反过来,我们构造强循环整数比较容易。
首先,任何一个数m,都可以循环整数的形式11,其基底是m-1。
那么我们从基底b = 2开始,1的个数从n = 3开始,开始构造循环整数,这样的循环整数一定是强循环整数,因为有一个基底是m-1的11。

首先贴出代码,再来解释一些小策略和我踩过的坑。

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
var repunits = new List<long>();
var MAX = Utils.Pow(10, 12);

int b = 2;
while (true)
{
int n = 3; // count of 1
while (true)
{
long sum = (long)((Utils.Pow(b, n) - 1) / (b - 1));
if (sum >= MAX)
{
break;
}

repunits.Add(sum);
n++;
}

if (n == 3)
{
break;
}

b++;
}

return repunits.Distinct().Sum() + 1;

第一个小问题,if n == 3那个判断,就是说,以b为基底的数111已经比10 ^ 12还大了,计算可以停止了。
第二个小问题,最后求和之前要调用Distinct()函数,因为会存在一些数,即能写成以b1为基底,n1个1,也可以写成以b2为基底,n2个1,那么这个数往List里面插入了两次,当然,repunits用Set就不存在这个问题了。
第三个小问题,求以b为基底,n个1的数的大小,不用每个1挨着求多少次方再加起来,使用等比数列的求和公式,直接算答案比较省事。

最后,说我踩得一个坑,注意,求b的n次方的那个函数,我使用了Utils.Pow(b, n),我使用二分法自己实现了一个简单的Pow函数。因为C#自带的参数是double,求值的时候会有精度损失,特别是b的n次方很大的时候,可能会少一点点或者多一点点,一旦强转成long的话,会多一或者少一,导致结果不正确。
开始的时候,计算了1000以内强循环整数的和,和题目给的一样,但是算10 ^ 12以内的值时,答案就不对了,想了半天,debug了一会,才发现是精度问题。于是乎,自己实现了一个,返回值是BigInteger,代码写得稍微啰嗦了点,但是很好的表现了二分法的内涵,哈哈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static BigInteger Pow(int b, int n)
{
if (n == 1)
{
return b;
}

var half = Pow(b, n / 2);
if (n % 2 == 0)
{
return half * half;
}
else
{
return half * half * b;
}
}

map操作本身就不多介绍了,直奔主题。

首先,我们实现一个一元的map,也就是说,传给map的函数只接受一个参数。

1
2
3
4
(define (unary-map fn seq)
(if (null? seq) '()
(cons (fn (car seq))
(unary-map fn (cdr seq)))))

很简单,就是一个简单的递归即可,就不详细的说了。我们举一个例子:

1
2
> (unary-map (lambda (x) (+ x 1)) '(1 2 3))
'(2 3 4)

在继续讲解之前,我们需要知道一些其他知识。Scheme内置的map可以处理任意多的参数,显然,我们需要知道如何定义任意参数的函数。
Scheme使用.来表示把后面的参数打包起来。
举个简单的例子说明(需要强调下,小点和字母d之间有一个空格):

1
2
3
4
5
6
7
(define (bar a b c . d)
(list a b c d))

> (bar 1 2 3 4 5 6)
'(1 2 3 (4 5 6))
> (bar 1 2 3)
'(1 2 3 ())

下面考虑实现自己的map函数了。
思路其实也很简单,把每一个list里面的第一个元素拿出来传入fn让其运算,然后呢,把每一个list里面的第一个元素删除,得到一系列的list,然后递归调用。
说起来容易,如何设计参数呢?
第一个参数是一个list,第二个参数是.,也就是可变参数,把后面的所有list打包传进来。
拿第一个list的第一个元素简单,如何把后面每个list的第一个元素取出呢?利用先开始写的一元map,(unary-map car other-list),就得到其余list的第一个元素,和第一个list的第一个元素连接起来,传给fn去做计算。
后续的移除第一个元素之后递归也是类似的。
最后把结果连接成一个list。

1
2
3
4
5
6
7
8
9
(define (my-map fn first-list . other-list)
(if (null? first-list) '()
(cons (apply fn
(cons (car first-list)
(unary-map car other-list)))
(apply my-map
(cons fn
(cons (cdr first-list)
(unary-map cdr other-list)))))))

简单的写两个case测试一下:

1
2
3
4
5
> (my-map + '(1 2 3) '(4 5 6) '(7 8 9))
'(12 15 18)

> (my-map (lambda (x) (+ x 1)) '(1 2 3))
'(2 3 4)

原题链接

在所有小于100的整数中,能被两个质数整除的最大整数是96,96 = 2^5 * 3。我们定义函数M(p,q,N):对于不同的两个质数p和q,M(p,q,N)是小于等于N中能被p和q整除的最大整数,如果不存在,M(p,q,N)=0
比如
M(2,3,100)=96
M(3,5,100)=75,而不是90,因为90能被2,3,5整除。
M(2,73,100)=0,因为2*73=146,小于100的数里面没有数能同时被2和73整除。

S(N)是对所有不同的M(p,q,N)求和。
题目最后要求是求S(10 000 000)。

解题思路很清晰,只是要尽可能早的排除不可能的质数组合。
首先获取0到5 000 000之间的质数,因为大于5 000 000的质数乘以最小的质数2也会大于10 000 000,M是0,不用考虑了。
然后就是组合每一个可能的指数对p和q,获得M(p,q,10 000 000)。
两层循环遍历p和q。
不妨设p比较小,用遍历所有的质数吗?显然不用,当p大于10 000 000的平方时,p和q的乘积就会大于10 000 000,M是0。
q也不用遍历所有大于p的质数,一旦p和q的乘积大于10 000 000,就可以停止了。
有了p,q,得到对应的M,然后放到一个List里面。
循环遍历完之后就可以把List里面的值求和。

给定p和q,如何得到M呢?
我用了暴力法,就是先通过Log(p, 10 000 000)和Log(q, 10 000 000)计算出可能的最大指数,然后两层循环,遍历所有可能的情况,得到小于等于M且能被2个质数整除的最大值。

在我的机器上,大约2s多一点就能得到结果了。
最后,贴一下我的代码:

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
const int MAX = 10000000;
public static long GetAnswer()
{
var Ms = new List<long>();

var primes = Utils.GenPrimes(MAX / 2);
for (int i = 0; primes[i] < Math.Sqrt(MAX); i++)
{
for (int j = i + 1; j < primes.Length; j++)
{
long p = primes[i];
long q = primes[j];
if (p * q < MAX)
{
Ms.Add(GetM(p, q));
}
else
{
break;
}
}
}

return Ms.Sum();
}

private static long GetM(long p, long q)
{
var max = 0L;
var e1Max = (int)(Math.Log(MAX) / Math.Log(p));
var e2Max = (int)(Math.Log(MAX) / Math.Log(q));
for (int e1 = 0; e1 < e1Max; e1++)
{
for (int e2 = 0; e2 < e2Max; e2++)
{
var product = Math.Pow(p, e1 + 1) * Math.Pow(q, e2 + 1);
if (product <= MAX && product > max)
{
max = (long)product;
}
}
}

return max;
}

我们都知道C++的vector的容量会随着添加的元素而自动增长,但是每次增长多少呢?原来的两倍?三倍?还是多少?下面,让我们来研究下增长因子是如何确定的。

首先,要阐述一个vector实现相关的事实:
vector使用allocator,而不是realloc,所以,不管你增长因子是多少,必然需要重新copy-cons(或move-cons)一遍。

废话不多讲了,下面进入正题。
假设,我们用了一个vector,其占用内存为M,内存布局如下图所示:

FreeMFree

我们一直使用这个vector,当元素太多导致内存不够的时候,要重新给这个vector分配内存,新分配的内存大小为f * M。
首先,要先分配f * M的空间,注意,这个时候M那块内存还没有释放掉:

FreeMf * MFree

然后把之前的M空间释放掉:

FreeFree(M)f * MFree

我们继续使用这个vector,内存又不够了,再次分配的内存大小为f * f * M。
和上面类似,分配内存时:

FreeFree(M)f * Mf * f * MFree
然后释放掉前一块地址:
FreeFree(M)Free(f * M)f * f * MFree

以此类推,第n次重新分配内存时,需要新分配f ^ n * M大小的内存,内存分布如下所示(带括号说明已经释放了):

Free(M)(f * M)(...)f ^ (n-1) * Mf ^ n * MFree
然后再把之前的f ^ (n-1) * M大小的内存回收:
Free(M)(f * M)(...)(f^(n-1) * M)f ^ n * MFree

我们思考这样一个问题:如果当第n次进行内存扩展时,前面n-2次操作释放的内存(包括第n-2次的内存和最开始占用的内存M)之和大于第n次所需要的内存,那么内存分配器就可以用之前留下来的内存而不用再往后去寻找Free的块。按照这个想法得到的内存分布如下:

Freef ^ n * M(GAP)(f ^ (n-1) * M)Free

这样做的好处有两个,一是可以提高内存分配器的效率,更重要的是,内存的使用更加紧凑,局部性更好,能够更好的利用缓存机制

上述想法包含一个约束条件,即下面的不等式(两边已同除了M):
f^n <= 1 + f + f^2 + … + f^(n-2)
当f=1.5时,n最小为5,也就是说,第五次进行内存扩展的时候,可以利用前面释放的内存。
当f=2时,2^n <= 2^(n-1) -1,不等式永假,也就是说,vector再也不能利用之前释放的内存。

从不等式可以得到,f越小,能满足不等式的n也会随之减小,但是如果f很小,会频繁的遇到内存不足进行扩展的情况,也就是说,会经常性的copy-cons或者move-cons vector里面的元素,得不偿失。而且5也足够小了,所以1.5是一个不错的选择。

读到这里,可能很多人会说,内存分配器是按照First-Fit来进行内存分配的吗?你怎么能假设“内存管理器就可以用之前留下来的内存而不用再往后去寻找Free的块”呢?而且,你这示例图都是连续的内存,实际情况未必如此啊。其实现代内存分配器很复杂,可能会把内存先分成若干块,每一块接受不同大小的内存分配,比如第一块都是内存需求量小于64B的,第二块都是内存需求量64B至128B,第三块都是需求量128B-1KB的等等;同时每一块又可能有不同的分配回收机制。所以实际情况远比我们想象的复杂,想要真实性能,做性能测试才是靠谱的途径。

排除这些外界因素,仅从本文分析的角度看来,增长因子1.5是个不错的选择,比2要好,因为使用2永远不会利用到之前释放的内存。

Facebook利用上述分析的原理(增长因子是1.5),配合jemalloc,开发了一套fbVector(已开源),性能比GCC的要好。
Microsoft的VC++ STL中的vector实现,增长因子也是1.5以获取更好的性能。
而GCC和CLang还停留在古老的增长因子为2的阶段。

下面这段小程序,能够帮你测试出当前实现的增长因子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <vector>
#include <iostream>

int main()
{
std::vector<int> arr;

for (int i = 1; i < 10; i++)
{
arr.push_back(i \* i);
std::cout <<
"Size: " << arr.size() << "; " <<
"Capacity: " << arr.capacity() << "\n";
}

return 0;
}

在Scheme定义一个函数,通常写法如下:

1
2
(define (sum x y)
(+ x y))

还有一种类似于定义变量的方式:

1
2
(define sum (lambda (x y)
(+ x y)))

利用lambda表达式,把sum和一个匿名函数联系在一起。
这种方式有点类似于JS:

1
var sum = function(x, y) { return x + y; }

利用lambda表达式,能够简化代码,下面举一个简单的例子。

1
2
3
4
(define (translate point delta)
(define (shift-by x)
(+ x delta))
(map shift-by point))

修改后:

1
2
3
4
(define (translate point delta)
(map (lambda (x)
(+ x delta))
point))

很显然,第二种更简洁直观。

Scheme还内置了let关键字,给人的感觉是创建了变量,其实不然,只是把某个表达式给了个名字罢了。
(let ((x exp1) (y exp2) (z exp3)) (fn x y z))
等价于
((lambda (x y z) (fn x y z)) exp1 exp2 exp3)
需要注意的是,let里面是个list,不保证求值顺序,所以exp3里面不能使用x和y。

好了,现在开始实现一个方法,返回集合的所有子集。

1
2
3
4
5
6
7
(define (ps set)
(if (null? set) '(())
(let ((ps-rest (ps (cdr set))))
(append ps-rest
(map (lambda (subset)
(cons (car set) subset))
ps-rest)))))

现在解释一下整个方法的思路:首先,如果set为空,返回一个由空列表组成的列表;下面运用递归的思想,定义ps-rest是set排除第一个元素的集合的所有子集,比如传入的set是’(1 2 3),排除第一个元素就是’(2 3),它的所有子集组成的列表是’(() (3) (2) (2 3)),列表里面共四个元素,这就是ps-rest。我们往ps-rest里面append包含第一个元素1的集合。具体append什么呢?我们mapps-rest,在每一项前面加上第一个元素1,ps-rest第一项是空集,连接完之后是(1),append到ps-rest里面,第二项是(3),连接完之后是(1 3),放到ps-rest里面,第三项第四项类似。最终,我们就得到了结果’(() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3))。

下面实现算法相对更复杂的全排列。

1
2
3
4
5
6
7
8
(define (permute items)
(if (null? items) '(())
(apply append
(map (lambda (ele)
(map (lambda (pers)
(cons ele pers))
(permute (remove ele items))))
items))))

解释下算法。从apply开始说吧。我们把一系列结果append起来,也就是把后面map里面的求值结果串起来。map的求值结果是什么呢?它遍历了输入的items,比如我们输入’(1 2 3),第一次遍历使用的是1,也就是向map的第一个参数lambda表达式传入了1,也就是ele是1。接着,对1做了什么呢?又是一个map,遍历了(permute (remove ele items)),remove是从items中移出ele,这个表达式求值结果是’((2 3) (3 2)),遍历它,把1加到每项的前面,得到的结果是’((1 2 3) (1 3 2))。至此,第一次遍历完成,回到外层的map,开始第二次遍历,传入ele为2,后续就不再赘述了。最终得到的结果是’((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1))。

假设有这样一个需求,我们需要对列表里面的每个数字都加倍,一种可能的操作是使用car cdr遍历递归,但是Scheme还提供了map这个内置函数,能够简洁快捷的实现。
先来看看这个函数:

1
2
> (map + '(1 2) '(3 4) '(5 6))
'(9 12)

map的作用就是遍历每一个列表,然后用第一个参数来对它们进行求值。比如上例,后续的参数是长度为2的列表,那么结果也是一个长度为2的列表,第一项就是把后面每个列表的第一项拿出来,使用+来就行求值,得到了9,类似的,第二项是12。

为了使数字加倍,我们需要定义一个double函数:

1
(define (double x) (* x 2))

使用map和double得到我们的目的:

1
2
> (map double '(1 2 3))
'(2 4 6)

再介绍两个内置函数apply和eval,关键字就表示出它们的作用:

1
2
3
4
5
6
7
> (apply + '(1 2 3))
6

> '(+ 1 2 3)
'(+ 1 2 3)
> (eval '(+ 1 2 3))
6

来个简单的示例看看它们的应用:

1
2
3
(define (average num-list)
(/ (apply + num-list)
(length num-list)))

从上面几个例子,我们可以看出,使用apply map使得(至少看起来是的)所有元素平等的共同参与运算,而且,比使用car cdr递归更加直观,简洁。
那什么时候用eval,按照js经验,可能会有不能用apply的时候,比如define or and这些时候,就只能用eval了。

最后,我们看一个稍微复杂的例子。实现内置函数flatten:
flatten作用使得列表里面的元素展开,使之平级,都是最外层列表的元素。比如((1 2) 3)flatten之后就是(1 2 3)。
思路是,如果seq(传入的列表)不是一个列表,就返回一个空列表;否则,我们使用map函数,对seq中的每一个子列表进行flatten操作,最后,使用append把这些结果连接起来。

1
2
3
4
5
6
7
(define (my-flatten seq)
(if (not (list? seq)) (list seq)
(apply append
(map my-flatten seq))))

> (my-flatten '((1 2) (3 ((4) 5)) 6))
'(1 2 3 4 5 6)

最后这个例子稍稍复杂了点,综合应用了apply map和递归的思想。

在写递归函数之前,先简单介绍几个后续会用到的内置函数:
car 返回列表的第一个元素,列表不能是空’()
(car ‘(1 2 3))
1
(car ‘((1 2) 3))
‘(1 2)

cdr 返回一个列表但不包含第一个元素,同样,列表不能为空’()
(cdr ‘(1 2 3))
‘(2 3)
(cdr ‘((1 2) (2 3) 4))
‘((2 3) 4)

car和cdr可以组合使用,比如cadr表示从不包含第一个元素的列表中返回第一个,即返回第二个元素
(cadr ‘(1 (2 3) (3) 4))
‘(2 3)
为什么可以组合用呢?Scheme的内存模型决定了这一点,以后再详解。

cons 把第一个参数(可以是列表)作为第二个参数的第一个元素,并返回
(cons ‘(1) ‘(2 3))
‘((1) 2 3)

append 把后续的参数加到第一个参数列表后面
(append ‘(1) ‘((2 3) 4))
‘(1 (2 3) 4)

有了以上铺垫开始写第一个简单的函数,求和:

1
2
3
4
(define (sum-of num-list)
(if (null? num-list) 0
(+ (car num-list)
(sum-of (cdr num-list)))))

如果传入的list是null,求值结果是0,否则,求值结果是list第一项加上其余项之和。
(sum-of ‘(1 2 3 4 5))
求值结果是15

再来个求list是否排序的通用函数

1
2
3
4
5
(define (sorted? list cmp)
(or (< (length list) 2)
(and (cmp (car list)
(cadr list))
(sorted? (cdr list) cmp))))

这个函数稍稍复杂一点。
如果说长度小于2,也就是1或者0,那么就是有序的,求值为真;
否则,比较一下第一个和第二个是否有序,如果有的话,递归地求剩余长度为n-1的list是否有序。
这里利用了逻辑运算的短路特性。

1
2
3
4
(sorted? '(1 2 46 7) <)
#f
(sorted? '("a" "b" "c" "d" "e") string<?)
#t

陆陆续续的,花了一个多月的时间,在网易公开课上看完了编程范式这门斯坦福的公开课,收益匪浅。公开课地址:http://open.163.com/special/opencourse/paradigms.html
虽说翻译多多少少有些问题,甚至包括英文字幕,但是瑕不掩瑜,课程本身是极好的。

花了半数的时间,讲了C语言底层的事情,包括内存布局,C-Style“泛型”方法和结构体,类型强转,函数调用,预处理器,编译器,链接器等等,让我对计算机的底层理解又上了一个层次;
花了几节课讲了多线程编程范式,由于课程是很久之前的了,只涉及了传统的多线程模型,讲了信号量,互斥,临界区,死锁,同步,生产者-消费者模型,还有哲学家问题等等。好几年过去了,我对计算机的理解俨然比考研时增进了很多,加之这个老师解释的到位,手把手的写代码,很容易就理解了当时考研时不懂的问题,对多线程这块的理解加深的不是一心半点,好赞;
几节课时间讲了Scheme这门函数式语言,函数式编程在当今又有再创辉煌的迹象,因为数据的不可变性,对于多线程来说是个天然优势,课程通过写常用的函数,或者实现一些内置的函数,深入的讲解了递归思想,阐述了如何用递归思想去解决问题,而且,让我了解了另外一个编程世界(之前只是听说过函数式编程而没有实践过)。
最后,还简单的介绍了Python和Haskell,前者是动态语言,后者是后起的函数式语言,介绍了它们和其他语言的异同,有对比,才有直观的感受,更容易让人理解。

除了学习到计算机知识本身,通过课程,也让我明白世界一流高校和我国高校的差距,没能力上清华北大,准确地说,是斯坦福和北邮的对比。
先说说老师,会多门编程语言,实现各种库函数得心应手,我不知道我校有多少老师能做到这个,应该还是有的,只是不多罢了;
更重要的是,全程几乎都是黑板粉笔板书,这才是最容易让人接受、跟着老师思路学习的方式,而不是一页页PPT的翻过去而不管学生到底能吸收多少,在我大学期间,见过三个老师全程板书,高数老师胡细宝老师,力学老师朱洪波老师,电动力学田贵花老师,本人脑子笨,学艺不精,但还是能懂得多数内容的,而那种直接放PPT的,实在是非我所能及啊。

再来说说学生。通常来说,好老师和好学生是相互映衬的,特别是好学生更重要,谁成就了清华北大,是那些各个省的拔尖人才啊。斯坦福大学的学生,毋庸置疑,北美(可能还有中印人才)最好的学生了,从上课可以看出,有些人提的问题是很有想法的,当然,也有些问题或者想法挺简单幼稚的,不过我都研究生毕业又工作2年多了,人家才大学二年级。
从公开课页面可以下载相关的讲义和作业,作业不能说很困难,但是每一次作业都很费时费力,要写代码,要求严格,特别是Scheme讲完居然要学生实现简单的解释器,逆天啊,老师仅仅讲了下内存分布之类的原理性内容,就要求这么高,实打实的培养工程能力。再反观国内,很多人大四毕业,也写不出个像样的小程序。

最后简单说说课程设置。他们一学期几门课,我们十几门课,我们还没有人家累。一个学期十来门课程,还要上些马哲毛概之类的没什么卵用的课程,能有多少时间精力去搞真正专业的东西呢?听课中,感觉他们除了上课,还有专门的讨论时间,老师或者助教也会去参与,我上课期间,基本就是老师来了讲课,我们听着,下课做作业,一般也就期末前能有一次或者两次答疑,更关键的是,我们教师学生比小的可怜,几百人就指望着一个老师,各种维度考量下来,我们能分到的资源少的可怜,没有讨论,没有和老师的切磋,自己瞎折腾,进步势必要慢一些。

国外发展了数百年到了发达国家,我们不到百年,相对底子弱些,只能自己加倍努力了。说到努力,比起国外顶尖的人,我这智商不足就算了,努力程度也不及啊,真替自己捉急。
一步一个脚印,慢慢进步,会通向彼岸的。

最后的最后,感谢Jerry Cain带来的精彩课程!

一本小说,科幻小说。为何买此书呢?一是听说过此书大名,二是京东满减凑单。花了三到四个下午就看完了此书。

摘抄几段译者序以做回顾,再说说自己的看法。

凡尔纳自幼喜欢旅游和航海,酷爱科学和幻想。他生活的年代,是资本主义上升的时代,也是科学技术飞速发展的时代。他的作品既是那个时代的产物,又是那个时代的一面镜子,表达了人们对摆脱手工式小生产、实现资本主义大生产的渴望,也反应出科学技术的发展在人们的思想领域产生的巨大影响。

虽然《地心游记》是一部充满传奇色彩的科幻小说,但它的诞生是和当时的历史、社会背景分不开的。一方面,欧洲殖民者出于建立各自殖民帝国的目的,掀起了一股探险狂热,在短短的时间里,他们相继征服了尼罗河的源头、撒哈拉沙漠、非洲大陆、南北两极,地球上人迹未至之处越来越少。另一方面,科学技术,特别是考古学和地质学得到了前所未有的发展。《地心游记》就是在这样的背景下应运而生的。

在小说中,凡尔纳不仅向人们讲述了一个科幻故事,而且以自己独特的方式,表明了他在一场贯穿整个十九世纪的重大科学争论中所持的立场。众所周知,当时的生物界在对于世界的看法上存在着两种截然不同的观点,一种是生物不变论,一种是生物进化论。尽管当时很多考古学的最新发现遭到了不变论鼓吹者的污蔑、打击和诽谤,但是凡尔纳仍然勇敢地把这些发现写进了他的《地心游记》里,为读者描绘了一副生物演化过程图,有力地传播了真理,支持了科学。

这本书对于我来说,有一个好处,出场人物少,主要刻画了我(主人公)、地质学家的叔叔和一个向导,主要篇幅都是说的这三个人的事。其他也就几个人物了,比如我喜欢的格劳本,家里的女仆。

我在探险的路上遇到很多很多问题,想回去,回到温暖的家,迷路,和叔叔仆人走散,而后昏厥,命悬一线,幸好有主角光环得以和叔叔相见而活下来。途中,时常想起家中的格劳本,当然,最后,他俩过上了幸福的生活。
半道上,我质疑了本书最大的疑点,当年写下纸条的人,又没有现在人的科技,没有压力计,凭什么知道自己是到了地心呢?

书中最引人入胜的一段描写是地心处有个海,岸上有很多很多史前生物物,海里有很多很多史生物物,还各种大战,电闪雷鸣啊,拍出能比前些天上映的《侏罗纪世界》更精彩。我和叔叔凭着主角光环,活下来了。不得不说,作者真是脑洞打开。
其实,作者描写这段主要是表明自己的立场:支持生物进化论。

在他们从地表往地心走的时候,记录了地球各个时期岩石、地层的变化,说明了作者那个时代,科学就挺发达的,而且,也表明作者绝对是喜欢科学的人啊。

在我看来,书中有个bug,就是到了地底下用的那个灯,感觉一直能在照明,一直在照明,我就想知道那是什么电池,或者能量设备,太tmd的牛逼了。

书中还表达了及其重要的一个观点:
科学,就是要有探索精神,敢于试错,从错误中吸取经验,不断的积累经验,修正当前的知识,最终接近真理。