修身仍旧还是第一位,而且必须要花一定的时间。
这个感觉完成得并不是很好,不过需要慢慢来, 多反省,多总结就好。
多看书,多看非专业书。
看的书还挺多的,待会再慢慢总结。
看完Graphlab和Spark的源码, 期间需要巩固C++和学习Scala。
Spark的源码看了大部分,了解代码的基本结构, 知道每一个功能的实现大概在什么位置。一直在跟进社区的Pull Request, 在研究Spark上面花了不少的时间,坚持下来有不少的收获。这一点明年继续保持。 Scala也在读Spark源码过程中不断熟悉,现在看Spark的源码不会有不理解的地方了。
至于Graphlab,完全没看。。
上完Convex Optimization、Probabilistic Graphical Models 和Neural Networks for Machine Learning这三门在线课程。
Convex Optimization比较圆满的完成了,看完了整本书, 同时顺便看了ADMM的那篇比较长的论文,Boyd老师真是大师, 讲课非常幽默,听完他的课之后对于优化这方面算是有了一个整体的框架, 了解了解决优化问题的思考方式。
另外两门课程没有参加,不过已经放在2015年的目标里了。
在github上为开源项目提交代码。
今年给Spark提交过几个patch,主要是和MLlib里面的决策树相关。
整体来说,2014年的计划完成了60%吧,由于完成了计划之外的一些事情。
今年选了一门外教上的高级编译技术的课程。 课程主要完成对LLVM进行扩展,让它能够支持使用并行机器指令完成长整数的运算。 在整个过程中,我看完了LLVM官网上的所有文档,整个代码也看了很多, 后端从LLVM IR到最终生成机器码的整个过程算是了解得比较清楚。 最终大致得完成了整个项目,还发现了LLVM里面的一些bug。 LLVM的代码写得还不错,是我看过的写得最好的C++代码之一。 唯一不足的是除了官方的文档,其他人写得关于LLVM的文章很少, 不管是关于实现还是使用的。学这门课最主要的还是锻炼了我的英语口语:D。
今年在Edx上学习了一门FP101的课程,比较基础地学习了Haskell, Haskell确实和我见过的程序设计语言不一样,它主张使用各种抽象来达到代码的复用。 其中,最难以理解的就是Monad这个概念,当时花了很长时间才明白Monad是什么, 而后通过Monad实现CPS让我不住惊叹于它的强大。现在,仍然不敢说已经完全理解Monad, 以后还需要多多使用来加深对它的理解。它能够给你一个全新的写程序的角度。另外, Haskell的类型系统真的很强大,当时在没有完全理解CPS的情况下可以依靠类型系统的提示写出正确的程序。
今年看了不少的论文(相对于以前来说),主要是和机器学习相关的, 看完这些论文除了让我对论文中描述的技术有更深入的理解之外, 更重要的是让我学会了一些看论文的方法,以及不再害怕阅读论文的能力。
虽然2014年最后一天的科目三没有通过,稍稍有点遗憾, 但是整个学车的过程还是让我成长了很多。 我克服了自己内心的恐惧,勇敢地走出自己的舒适区, 在一个自己非常不擅长的领域努力并取得了一定的成功。 学车整个的过程确实让我的内心变得更加强大,我也不断地弥补了一些自己性格上的弱势。 现在离最终拿到驾照还有很长的一段路要走,但是我相信我一定会成功的。
首先是和书名对应的这个观点——“人月神话”, 意思是说尽管一个项目的工作量可以用人月来衡量, 但是“人”和“月”这两个量度并不是可以互换的, 也就是说不能通过增加更多的人手来减少项目完成所需要的时间。 这主要是由于两方面的原因:
因此,作者提出了Brooks法则:
向进度落后的项目中增加人手,只会使进度更加落后。
这就是除去了神话色彩的人月。
概念完整性是指整个系统的设计具有一致性,系统只反应唯一的设计理念。 这要求系统的设计必须由一个人,或者非常少数互有默契的人员来实现。 在本书中,作者希望按照“外科手术队伍”的形式来组织团队, 团队的首席程序员类似于外科手术队伍的医生,他定义系统的功能, 设计程序,编制源代码,其他的人员负责给首席程序员提供必要的帮助。 或者,可以对设计方法和具体实现进行分工, 由架构师来完成设计,制定好技术说明,而实现人员负责实现。
概念完整性是如此重要,我所知的成功的软件项目都是反应了一两个天才程序员的理念, 由他们来决定整个项目的走向。
这是本书中一个颇具争议的观点:
在未来十年内,无论是在技术还是管理方法上, 都看不出有任何突破性的进步, 能够独立保证在十年内大幅度地提高软件的生产率、可靠性和简洁性。
首先,作者把软件项目的开发过程分为两部分:
作者认为,现在的技术或管理方法的革新,只能改进次要任务的生产率, 而根本任务的难度并没有发生改变。这样,除非次要任务能够占到所有工作的9/10以上, 否则总体的生产率不会有数量级的提升。
那为什么根本任务的生产率无法提高呢?这是由于开发软件系统需要面对这些无法规避的问题:
复杂度。一个软件系统有大量的状态,存在大量不同元素的相互叠加。 这使得软件系统的复杂性以指数的形式增长。而且,这些复杂度是软件系统的根本属性, 而不像数学和物理中那样可以建立简化的模型而忽略复杂的次要因素。
一致性。复杂度的问题不只是软件工程师才会面对,物理学家也会面临复杂度的问题。 但是他们相信能够建立一个通用的理论将这些复杂性统一起来,因为整个宇宙的规律是由上帝创造的, 而上帝不是反复无常的。而软件工程师需要控制的复杂度是随心所欲,毫无规律可言的, 需要遵循人为惯例和系统约束,需要和这些系统保持一致。 (可以这样理解,这些复杂度是由不同的PM带来的,我没有黑PM啊。。)
可变性。有两个因素导致软件需要经常发生变化, 一是软件系统改变的容易性使得有更多改变的需求(PM说,不就是改两行代码吗? 他绝不会轻易让人去改装一辆生产好的汽车);二是软件的寄主(操作系统,硬件)经常发生变化, 使得软件需要改变。
不可见性。这一点我不是很理解,可能随着图形界面的提出, 软件的可见性能够得到很大程度的改善吧。
似乎,作者提出的这些问题40年后仍然没有得到解决, 对抗软件复杂性和可变性一个比较好的方式是快速原型,然后不断迭代。
]]>2013对我来说不是平淡的一年。 这一年,我大学毕业,从一所大学来到另外一所大学, 分别原来的同学和朋友, 又在新的地方建立起了人际关系网; 这一年,我从大学生变成了研究生, 伴随着学历的增长,我的生活节奏也完全发生变化, 现在的我,开始沉下心来学习一些东西, 虽然现在仍然没有很大的成果。 这一年,我和她吵了不知道多少次架, 每次都是因我而起,我心里十分愧疚, 我现在仍然不够成熟, 心胸不够开阔。
多看书,多感受生活,开阔自己的胸襟
虽然这被放在了计划的第一条,但是完成的最不好。 这里的“看书”,当然是说非专业的书籍, 印象当中就看了《天龙八部》和《唐浩明评点曾国藩家书》(上下册), 家书下册还是这两天逼迫自己看完的, 看书之少,现在都替自己不好意思。
至于修身,也没有花多少功夫, 否则也不至于跟女朋友吵那么多架。 没有把这个放在比较重要的位置, 仍然还是我行我素, 只知道在专业方面努力, 其他方面都不在乎。 这一点,在来年必须要改。
学习一门新的编程语言,可能是go或者erlang
也完成的不是很好,大概的学习了一下go(完成了go tour), 没有用它写过实际的项目。
学完完有关数据挖掘的基础知识
这个完成得还不错, 掌握了很多的机器学习和数据挖掘方面的基础知识。 完成了毕业设计,使用scikit-learn开发过机器学习程序, 写了很多实现机器学习算法的matlab代码。 上了两门机器学习的课程,一门是浙大蔡登老师上的, 另一门是Coursera上的。
为github上的一个库提交代码
为TeamToy-Plugin这个项目写了两个插件, 一个是OpenId的,一个是创意墙(团队用户可以在上面分享创意,使用类似于Hacknews的排序, 这个插件没有提交)。
这个计划完成程度也不是很满意,投入的时间太少了。
2013年在专业方面的投入还是蛮多的, 看过的书不能算少,也不能算多:
《The C Programming Language》,这仅仅只有200多页的书, 除了让我知道如何写出好的C程序之外, 也让我明白了如何如何精简地表达自己的观点。
《The CPP Programming Language》,以前一直对这本书持保留看法, 觉得这么厚的一本书一定晦涩难懂, 直到我认认真真地把整本书都看了一遍, 才发现这是一本不可多得的好书。 整本书讲解清晰, 把C++这么复杂的一门语言的语言特性阐释得十分清楚, 而且,里面还有很多如何写出更好的程序的技巧, 很多时候作者的很多思想会引起我强烈的共鸣, 或者启发我深度的思考。
《Introduction to Data Mining》, 这本书总体感觉是介绍性质的,里面设计的理论不是很深, 数学讲解不是很多,不过能从中知道数据挖掘的各个方面。 可能Jiawei Han老师的那本数据挖掘更好一些。
《Pattern Classification》, 这本书是上机器学习课程的主要教材, 里面的理论讲得很深, 是一本比较好的参考书。
《The Practice of Programming》, 看这本书,完全是冲着Robert Pike的大名去的, 看完之后果然不失所望, 学到了很多的编程方法, 只是这本书的评注实在让人哭笑不得。
《Effective Java》, 这本书看的是中文版, 翻译得不是很好, 有少数几个地方有点难以理解。 由于以前写的Java程序太少, 看完这本书之后收益还是蛮大的, 至少现在知道怎么去写Java程序了, 同时对于面向对象的特性也有了更加深入的理解。
应该说,今年看书确实比以往有所长进, 因为看完书之后能够对它进行总结, 其中一些重要的观点和思想都记录下来, 而不是看完就忘记了。 我觉得这是一种很好的看书方法, 看书不应该一味的追求速度, 看完之后的总结很有必要。
2013年写过的代码不能算多,写的代码是PHP和Python, 但是有时候也看一些其它语言的书和文章, 然后拿着这些特性来进行一个比较, 想得比较多。同时, 有时需要写一些代码来测试一些有趣的语言特性, 这些代码可以作为以后的参考。
同时,今年看了几个开源项目的代码, 包括scrapy和redis, 看这些代码一方面能够开阔自己的视野, 另一方面对于如何写程序也能够有所体会。
2013年也看过一些论文, 包括google的三大论文, 还有graphlab的4篇论文, Spark的两篇论文。 对于后面的两种论文, 现在理解还不是很深入, 幸亏存在开源的代码能够加深理解。
在机器学习方面也看过一些论文, 其中推荐系统和聚类方面的论文看得比较多。
祝愿所有人新年快乐,新的一年实现自己的梦想。
]]>Bigtable是一个分布式的数据库, 它的出现主要是因为传统的关系型数据库在面对大量数据(PB级别)时不具有扩展性。 Bigtable在谷歌内部得到了广泛使用, Apache HBase是它的开源实现。
可以把一个Bigtable当成一个持久化的,分布式的,多维的map。
它的值通过(rowkey, columnkey, timestamp)
来索引。
其中rowkey
,columnkey
和值都可以是任意的字符串。
如下图所示。
在上图中,rowkey
是com.cnn.www
。
在Bigtable中,对row的操作是原子(atomic)的。
在Bigtable中,数据的保存顺序是通过rowkey
的字典序来维持的,
基于这个特点,可以通过挑选合适的rowkey
把相关的数据放在一起。
比如上图,通过使用倒序的主机名作为rowkey
,
可以把同一域名下的网页放在一起。
几个row组合起来形成一个tablet,
一个table由一个或多个tablet组成。
columnkey
通过column family
来进行划分,
如上图,anchor就是一个column family
,它有两个column key
:
anchor:cnnsi.com
和anchor:my.look.cn
。
contents也是一个column family
, 它只有一个column key
,
就是contents:
。column family
是访问控制的基本单位。
每一个column key
必须以column family:qualifier
的格式命名。
对于同一个rowkey
和columnkey
的组合,
Bigtable根据不同的timestamp
保存了不同的值。
通常,会保存最近的几个版本(具体的版本数用户可以指定),
过期的数据会被垃圾回收掉。
Bigtable的API非常简单,下面是两个使用API的例子:
1 2 3 4 5 6 7 8 |
|
上面的代码段对rowkey="com.cnn.www"
的行,
将columnkey="anchor:www.c-span.org"
的列的值设置为CNN
,
同时删除columnkey = "anchor:www.abc.com"
的列。其中Apply()
是原子操作。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
上面的代码段遍历rowkey="com.cnn.www"
的行中column family="anchor"
的所有列的所有版本。
Bigtable使用了几个部件构建而成:
Chubby,是Google发布的另外一个分布式系统,它具体的原理我还没有去看那篇论文, 现在只需要知道Bigtable使用Chubby来完成下面的事情:
Chubby对于Bigtable非常重要,如果它停止工作了, 那么整个Bigtable也停止工作。 Chubby的开源实现是ZooKeeper, 下次再来研究Chubby。
和GFS类似,Bigtable也由三个部分组成,分别是:
需要注意的是,与GFS不一样, 关于tablet的位置信息client不需要通过master就可以知道(接下来就会提到), 所以大部分情况下client都不需要和master交互, 这样master上的压力更小了。
Bigtable使用一个三层的结构来存放tablet的位置, 如下图所示。(论文上说这个和B+树比较像, 我倒觉得用inode来类比更加好理解)
首先,一个Chubby File保存了root tablet的位置。 root tablet是一个特殊的METADATA table, 它保存了所有其他METADATA tablet的位置。 每一个METADATA又保存了user tablet的位置。 所以,顺着这个结构走下来,就能找到任意的tablet的位置。
关于这个“位置”,我是这样理解的, 它应该是一个具体的tablet server的名字, client知道了哪个tablet server之后就向那个tablet server发出请求。 如果是这样的话,把tablet重新分配之后master需要去更新这些保存位置的tablet。
这里还有几个计算:
一个tablet一次只会被分配给一个tablet server, master保存下面的信息:
当一个tablet server启动时, 它会去获取Chubby的某个特定目录下的一个文件(一个tablet server唯一的对应这个目录下的一个文件)的互斥锁。 master通过检查这个目录查看哪些tablet server是alive的。 tablet server如果丢失了这个互斥锁,那么它会尝试重新获取, 如果这个文件不存在了,tablet server永远都拿不到这个锁了, 那么它会自动停止。 如果tablet server不工作了,它会释放这个锁, 这样master就知道它没有工作了,把它上面的tablet分配给其他的tablet server。
master会频繁地和那些正常工作的tablet server进行通信来获取它们的状态, 如果tablet server告诉master它失去了锁或者无法和这个tablet server进行通信, 那么master会尝试获取这个tablet server对应的文件锁, 如果能够拿到这个锁,说明Chubby能正常工作, 而这个tablet server要么死掉了要么不能和Chubby交互, 那么master就删除这个tablet server对应的文件, 这样这个tablet server就没用了, 然后master把这个tablet server上的tablet分配给其他的tablet server。
在master启动时,它会执行下面的步骤:
这里有一个问题是,如果METADATA的tablet没有被分配, 那么它就不能被读取,那么第4步就没法进行了。 这个问题可以这样解决,如果需要读取某个tablet时它还没有被分配, 那么先把它分配给某个tablet server,然后就可以继续接下来的步骤。
当下面的情况发生时,tablet的分配情况要进行调整:
前面两种情况都是在master进行的,所以master直接进行调整就行, 而第三种过程是在某个tablet server上进行的, master怎么知道的呢? 当tablet server对tablet进行切分时, 它首先在METADATA的tablet上记录下这个新的tablet, 然后通知master发生了改变。 如果这个通知丢失了, 那么当master去请求这个被切分的tablet时, tablet server会发现这个tablet的METADATA table只是请求的METADATA的一部分, 就知道发生了切分,然后告诉master。
下面来看下一个tablet具体是如何保存的。
根据这篇博客, 要保存一个tablet,有这么几个部分:
有了这几个部分,对tablet的操作也就变得容易了, 对于写操作,只需要记录把操作记录在commit log中, 同时写入memtable。对于读操作, 由于数据不仅仅保存在主SSTable上, 还需要结合memtable和次SSTable来进行。
Compaction主要是为了解决上面过程中出现的问题,它分为3种:
client可以把一些列组放在一起形成一个locality group, 在每一个tablet里面,会为每一个locality group生成一个单独的SSTable, 使用locality group的好处是:
为了提高读性能,tablet server采用了两种级别的cache:
读操作需要结合SSTable和memtable,因此, 可以通过bloom filter来制定某些locality group的数据不可能存在于某些SSTable, 这样就可以减少需要读取的SSTable的数量。 Bloom filter一般保存在tablet server的内存中。
在Bigtable中,每一个tablet server只保存一个commit log, 这个commit log保存了所有的tablet相关的log。这样做的好处是:
所有tablet的commit log组合成一个文件增加了恢复的复杂性, 因为这样不同的tablet可能被迁移到不同的tablet server, 这样所有相关的tablet server都需要读取这个commit log来获取tablet的信息, 这个commit log会被重复读多次。
解决这个问题的一个办法是在recovery时,
先把commit log使用<table, row name, log sequence number>
作为key进行排序,
这样一个tablet的commit log就是连续的,可以通过一次seek,
然后连续读就可以得到。
这个排序的过程也可以通过把这个commit log分成几块,然后并发地进行排序来加快速度。
为了避免GFS集群中由于网络原因或者load情况带来性能上的波动, 通常使用两个线程来完成写commit log的操作, 每一个线程有自己的commit log文件, 同一时课只有一个线程在写, 如果一个线程写出现了性能上的问题, 就切换到另外一个线程(因为两个线程使用了不同的文件, 可能分布到不同的chunkserver)。同时, 使用序列号来消除重复的commit log。
如果master把tablet从一个tablet server移到另一个, 源tablet server可以进行一次minor compaction, 这样uncompacted的commit log就减少了很多, 因为这个过程中可能会有其他的写操作, 所以在upload这个tablet时, 可以再进行一次非常快的minor compaction, 这样就不要进行recovery了。
利用SSTable的不可变性带来了以下的方便:
喜欢Octopress整体的风格,包括它的响应式设计,
还有特别好看的solarized
的语法高亮。
以前在jekyll
上写博客,在github
上面开了两个库,
原因在于Github Pages上不允许运行ruby脚本,
这样很多功能包括分页就都不能做了。为了完成分页的目的,
我在一个库里保存博客的源代码,使用jekyll
来生成静态页面,
然后另一个库也就是chouqin.github.io
就完全是静态页面,
把它发布到Github Pages上去。Octopress也是这样,
只不过它省去了我的麻烦,要发布到Github Pages,
只要一条rake deploy
就够了,非常方便。
来说一下具体的迁移过程吧。
其实完全是照着Octopress的官方文档一步步安装过来的, 官方博客已经写得很清楚了。Ruby管理使用的是rvm。
出现了一个问题是rake
安装的是10.1.0的版本,跟Gemfile对应的不一致,
直接把Gemfile里的那行改为gem 'rake', '~> 10.1.0'
。
首先是按照文档修改了一些_conf.yml
配置:
kramdown
pygments
来进行语法高亮disqus_short_name
。具体的配置可以查看我的github
由于使用的是kramdown
,它的语法包含数学,为了在页面上展示数学公式,
在source/_includes/head.html
上加入以下的内容:
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 |
|
原来的博客是基于jekyll
的,对于每一篇博客,修改这几个地方即可:
Included file 'JB/setup' not found in _includes directory
。comments: true
。因为生成的是静态页面,所以也可以发布到七牛来加速访问。 在部署之前,你需要先注册成为七牛用户, 然后获取AccessKey 和 SecretKey。
然后安装七牛的qrsync。
在octopress
目录下创建qiniu.conf
,写入以下内容:
1 2 3 4 5 6 7 8 |
|
最后执行qrsync qiniu.conf
,就能部署到七牛了。
对于计算机系的同学来说,MapReduce这个词应该并不陌生, 现在是所谓的“大数据时代”,“大数据”这个词被炒得非常热。 何为“大数据”?随着互联网的发展,现在的数据越来越多, 给原先的技术带来了两方面的挑战,一是存储, 如何存储这些PB级别的数据, 二是分析, 如何对这么大的数据进行分析, 从中提取出有用的信息。
MapReduce就是一个对大数据进行分析的框架。
使用MapReduce,用户只需要定义自己的map
函数和reduce
函数,
然后MapReduce就能把这些函数分配到不同的机器上去并行的执行,
MapReduce帮你解决好调度,容错,节点交互的问题。
这样,一个没有分布式系统编程经验的人也可以利用MapReduce把自己的程序放到几千台机器上去执行。
map
和reduce
都是来自于函数式编程的概念,map
函数接受一条条的纪录作为输入,
然后输出一个个<key, value>
对,reduce
函数接受<key, values>
对,
(其中values
是每一个key
对应的所有的value
),通过对这些values
进行一个“总结”,
得到一个<key, reduce_value>
。
比如拿经典的WordCount例子来说,对于文本中的每一个单词word
,
map
都会生成一个<word, 1>
对(注意如果一个文本中一个单词出现多次就生成多个这样的对),
reduce
函数就会收到<word, 1,...>
这样的输入,它的工作就是把所有的1
都加起来,
生成一个<word, sum>
。
MapReduce函数基于键值对进行处理,看起来很简单, 那很多的分析任务不仅仅只是简单的Wordcount而已,能够使用这两个函数来实现吗? 幸运的是,很多的大规模数据分析任务都能通过MapReduce来表达, 这也是为什么MapReduce能够作为一个框架被提出的原因, 在最后一个部分中会给出一些更加复杂的使用MapReduce的例子。
在大概了解了MapReduce之后,我们来看一下它到底是怎么实现的。 我们首先看一下一个MapReduce任务执行完需要经过那些流程, 然后再看一下在实现MapReduce的时候需要考虑的几个因素。
我们首先假定把一个任务分成M个map
Task和R个reduce
task,
如上图,
整个任务的执行过程如下:
首先根据map
的数量把原来的数据分成M个splits,每一个split对应一个map
task。
在集群的节点上启动master
,M个map
task和N个reduce
task,
master
把split分配给相应的map
task。需要注意的是,
一个集群的节点(又称作一个worker)上可能有多个map
task,
也有可能map
task和reduce
task在同一个worker。
每一个map
task读取自己的split,根据用户定义的map
函数生成<key value>
对,
把结果保存在本地文件中,
根据key
的不一样,结果被写入到R个不同的文件。
这些文件的位置会告知给master
,然后master
再告知给相应的reduce
task。
当一个reduce
task被告知这些文件的位置时,它通过远程调用读取这些文件的内容,
当和这个reduce
task相关的所有文件都被读到之后,它把这些内容按照key
进行一个排序,
然后就能保证同一个key
的所有values
同时被传给reduce
。
reduce
task使用用户定义的reduce
函数处理上述排序好的数据,
将最终的结果保存到一个Global File System(比如GFS),
这是为了保证数据的可靠性。
在上述的过程中,我们看到map
task的结果被保存在本地,
而把reduce
task的结果保存在具有可靠性保证的文件系统上。
这是因为map
task产生的是中间结果,当这些结果被reduce
之后,
就可以被扔掉,不需要备份,这样可以节约磁盘空间。
而reduce
task产生的是最终结果,需要一定的可靠性保证。
在对一个任务进行划分时,需要考虑split的粒度:
如果split太小,M就会很大,
master
需要纪录的数据就会很多,
就会消耗很多master
的内存。
如果split太大,一方面调度不具有灵活性,
因为调度是以split为单位的,一个较大的task无法被分割放到其他空闲的worker上去执行。
另一方面无法利用locality
进行调度,
因为map
task的输入文件一般保存在分布式文件系统上,
master
在调度时尽量把一个split分配到较近的节点上去执行,
如果split太大超过了一个文件block的大小,
这样可能两个block在不同的节点上,甚至跨了不同的机架,
这样无法利用locality
了。
所以,在实际应用中,split的大小为一个block。
由于MapReduce被分布到上千台机器上去执行, 错误是不可避免的。 MapReduc需要在节点发生故障时进行处理。
当一个节点发生故障,
在这个节点上的所有的map
task都需要重新执行,
因为map
task的结果是保存在节点本地的,
节点发生故障之后,这些结果就不可用了。
而成功执行的reduce
task就不需要重新执行了,
因为它的结果是保存在分布式文件系统上,
可靠性是可以保证的。
假设有一个的矩阵M, 它和一个n维列向量v的乘积是一个m维的列向量x,有
可以根据j,把M按列分成k块,把v也对应分成k块,
每个M块和对应的v块被分给一个map
task,
map
task生成的结果为对,
reduce
task把每一个i对应的所有加起来。
关系数据库表的Join,Selection,Projection, Union, Intersection, Difference,Group And Aggregation等操作都可以使用MapReduce来实现。
值得一提的是,对于Join运算,比如链接关系R(a, b)和关系S(b, c),
在生成以b为键的键值对时,需要指定来自于哪一个关系,
比如关系R生成的键值对的形式为<b, (R, a)>
,
这样reduce
时就可以根据这个信息进行组合。
假设有一个的矩阵M, 和一个的矩阵N, 它们的乘积是一个的矩阵P, 有:
矩阵M和矩阵N的乘法可以看成是关系M(I, J, V)和关系N(J, K, W)先进行一次Join, 再进行一次Group And Aggregation之后的结果, 因此可以直接通过两次MapReduce进行矩阵的乘法运算。
如果想要一次MapReduce就得到结果,可以在map
时以(i, k)为键生成键值对,
同样的,需要指明来自于矩阵M还是矩阵N,因此,相应的键值对的格式分别为
(对于矩阵M),(对于矩阵N)。
reduce
时进行相应的组合。
这几天一直在看The Google File System这篇论文, 看得很慢,一天才能看几页,有些地方还要反反复复看几遍才能“理解”, 但也确实学得了不少东西,包括一些分布式系统设计的基本思想, 以及如何根据应用的具体场景来做设计决策。 诚然,对于一个这么大的系统,要想弄明白它的全部细节是比较困难的, 能够把它的整个过程捋顺就可以了。本文中, 我把我对这篇论文印象比较深的内容用我自己的理解讲出来, 希望能够给对GFS感兴趣的同学一点帮助。
GFS作为一个分布式的文件系统,
除了要满足一般的文件系统的需求之外,
还根据一些特殊的应用场景(原文反复提到的application workloads and technological environment
),
来完成整个系统的设计。
一般的分布式文件系统需要满足以下四个要求:
(注:关于reliability和availability的区别, 请参考这篇)
基于对实际应用场景的研究,GFS对它的使用场景做出了如下假设:
GFS运行在成千上万台便宜的机器上,这意味着节点的故障会经常发生。 必须有一定的容错的机制来应对这些故障。
系统要存储的文件通常都比较大,每个文件大约100MB或者更大, GB级别的文件也很常见。必须能够有效地处理这样的大文件, 基于这样的大文件进行系统优化。
workloads的读操作主要有两种:
大规模的流式读取,通常一次读取数百KB的数据, 更常见的是一次读取1MB甚至更多的数据。 来自同一个client的连续操作通常是读取同一个文件中连续的一个区域。
小规模的随机读取,通常是在文件某个随机的位置读取 几个KB数据。 对于性能敏感的应用通常把一批随机读任务进行排序然后按照顺序批量读取, 这样能够避免在通过一个文件来回移动位置。(后面我们将看到, 这样能够减少获取metadata的次数,也就减少了和master的交互)
workloads的写操作主要由大规模的,顺序的append操作构成。 一个文件一旦写好之后,就很少进行改动。因此随机的写操作是很少的, 所以GFS主要针对于append进行优化。
系统必须有合理的机制来处理多个client并发写同一个文件的情况。 文件经常被用于生产者-消费者队列,需要高效地处理多个client的竞争。 正是基于这种特殊的应用场景,GFS实现了一个无锁并发append。
利用高带宽比低延迟更加重要。基于这个假设, 可以把读写的任务分布到各个节点, 尽量保证每个节点的负载均衡, 尽管这样会造成一些请求的延迟。
下面我们来具体看一下GFS的整个架构。
可以看到GFS由三个不同的部分组成,分别是master
,client
, chunkserver
。
master
负责管理整个系统(包括管理metadata,垃圾回收等),一个系统只有一个master
。
chunkserver
负责保存数据,一个系统有多个chunkserver
。
client
负责接受应用程序的请求,通过请求master
和chunkserver
来完成读写等操作。
由于系统只有一个master
,client
对master
请求只涉及metadata,
数据的交互直接与chunkserver
进行,这样减小了master
的压力。
一个文件由多个chunk组成,一个chunk会在多个chunkserver
上存在多个replica。
对于新建文件,目录等操作,只是更改了metadata,
只需要和master
交互就可以了。注意,与linux的文件系统不同,
目录不再以一个inode的形式保存,也就是它不会作为data被保存在chunkserver
。
如果要读写文件的文件的内容,就需要chunkserver
的参与,
client
根据需要操作文件的偏移量转化为相应的chunk index
,
向master
发出请求,master
根据文件名和chunk index
,得到一个全局的chunk handle
,
一个chunk由唯一的一个chunk handle
所标识,
master
返回这个chunk handle
以及拥有这个chunk的chunkserver
的位置。
(不止一个,一个chunk有多个replica,分布在不同的chunkserver
。
必要的时候,master
可能会新建chunk,
并在chunkserver
准备好了这个chunk的replica之后,才返回)
client
拿到chunk handle
和chunkserver
列表之后,
先把这个信息用文件名和chunk index
作为key缓存起来,
然后对相应的chunkserver
发出数据的读写请求。
这只是一个大概的流程,对于具体的操作过程,下面会做分析。
Chunk的大小是一个值得考虑的问题。在GFS中,chunk的大小是64MB。
这比普通文件系统的block大小要大很多。
在chunkserver
上,一个chunk的replica保存成一个文件,
这样,它只占用它所需要的空间,防止空间的浪费。
Chunk拥有较大的大小由如下几个好处:
client
和master
交互的次数。chunkserver
维护一个长TCP连接。master
的内存。大的chunk size也会带来一个问题,一个小文件可能就只占用一个chunk,
那么如果多个client
同时操作这个文件的话,就会变成操作同一个chunk,
保存这个chunk的chunkserver
就会称为一个hotspot。
这样的问题对于小的chunk并不存在,因为如果是小的chunk的话,
一个文件拥有多个chunk,操作同一个文件被分布到多个chunkserver
.
虽然在实践中,可以通过错开应用的启动的时间来减小同时操作一个文件的可能性。
GFS的master
保存三种metadata:
metadata保存在内存中,可以很快地获取。
前面两种metadata会通过operation log来持久化。
第3种信息不用持久化,因为在master
启动时,
它会问chunkserver
要chunk的位置信息。
而且chunk的位置也会不断的变化,比如新的chunkserver
加入。
这些新的位置信息会通过日常的HeartBeat
消息由chunkserver
传给master
。
将metadata保存在内存中能够保证在master
的日常处理中很快的获取metadata,
为了保证系统的正常运行,master
必须定时地做一些维护工作,比如清除被删除的chunk,
转移或备份chunk等,这些操作都需要获取metadata。
metadata保存在内存中有一个不好的地方就是能保存的metadata受限于master
的内存,
不过足够大的chunk size和使用前缀压缩,能够保证metadata占用很少的空间。
对metadata进行修改时,使用锁来控制并发。需要注意的是,对于目录, 获取锁的方式和linux的文件系统有点不太一样。在目录下新建文件, 只获取对这个目录的读锁,而对目录进行snapshot,却对这个目录获取一个写锁。 同时,如果涉及到某个文件,那么要获取所有它的所有上层目录的读锁。 这样的锁有一个好的地方是可以在通过一个目录下同时新建两个文件而不会冲突, 因为它们都是获得对这个目录的读锁。
Operation log用于持久化存储前两种metadata,这样master
启动时,
能够根据operation log恢复metadata。同时,可以通过operation log知道metadata修改的顺序,
对于重现并发操作非常有帮助。因此,必须可靠地存储operation log,
只有当operation log已经存储好之后才向client
返回。
而且,operation log不仅仅只保存在master
的本地,而且在远程的机器上有备份,
这样,即使master
出现故障,也可以使用其他的机器做为master
。
从operation log恢复状态是一个比较耗时的过程,因此,使用checkpoint来减小operation log的大小。 每次恢复时,从checkpoint开始恢复,只处理checkpoint只有的operation log。 在做checkpoint时,新开一个线程进行checkpoint,原来的线程继续处理metadata的修改请求, 此时把operation log保存在另外一个文件里。
关于一致性,先看几个定义,对于一个file region,存在以下几个状态:
在GFS中,不同的修改可能会出现不同的状态。对于文件的append操作(是GFS中的主要写操作), 通过放松一定的一致性,更好地支持并发,在下面的具体操作时再讲述具体的过程。
master
通过lease机制把控制权交给chunkserver
,当写一个chunk时,
master
指定一个包含这个chunk的replica的chunkserver
作为primary replica
,
由它来控制对这个chunk的写操作。一个lease的过期时间是60秒,如果写操作没有完成,
primary replica
可以延长这个lease。primary replica
通过一个序列号控制对这个chunk的写的顺序,
这样能够保证所有的replica都是按同样的顺序执行同样的操作,也就保证了一致性。
对于每一个chunk的修改,chunk都会赋予一个新的版本号。
这样,如果有的replica没有被正常的修改(比如修改的时候当前的chunkserver
挂了),
那么这个replica就被stale replica
,当client
请求一个chuck时,stale replica
会被master
忽略,
在master
的定时管理过程中,会把stale replica
删除。
为了尽量保证所有chunkserver
都承受差不多的负载,
master
通过以下机制来完成:
注意,master
不仅考虑了chunkserver
的负载均衡,也考虑了机架的负载均衡。
Read操作其实已经在上面的Figure 1中描述得很明白了,有如下几个过程:
client
根据chunk size的大小,把(filename,byte offset)
转化为(filename,chunk index)
,
发送(filename,chunk index)
给master
master
返回(chunk handle,所有正常replica的位置)
,
client
以(filename,chunk index)
作为key缓存这个信息
client
发(chunk handle,byte range)
给其中一个chunkserver
,通常是最近的一个。
chunkserver
返回chunk data
直接假设client
已经知道了要写的chunk,如Figure 2,具体过程如下:
client
向master
询问拥有这个chunk的lease的primary replica
,如果当前没有primary replica
,
master
把lease给其中的replicamaster
把primary replica
的位置和其他的拥有这个chunk的replica的chunkserver
(secondary replica
)的位置返回,
client
缓存这个信息。client
把数据以流水线的方式发送到所有的replica,流水线是一种最高效利用的带宽的方法,
每一个replica把数据用LRU buffer保存起来,并向client
发送接受到的信息。client
向primary replica
发送write请求,primary replica
根据请求的顺序赋予一个序列号primary replica
根据序列号修改replica和请求其他的secondary replica
修改replica,
这个统一的序列号保证了所有的replica都是按照统一的顺序来执行修改操作。secondary replica
修改完成之后,返回修改完成的信号给primary replica
primary replica
向client
返回修改完成的信号,如果有任何的secondary replica
修改失败,
信息也会被发给client
,client
然后重新尝试修改,重新执行步骤3-7。如果一个修改很大或者到了chuck的边界,那么client会把它分成两个写操作, 这样就有可能发生在两个写操作之间有其他的写操作,所以这时会出现undefined的情况。
Record Append的过程相对于Overwrite的不同在于它的错误处理不同,
当写操作没有成功时,client
会尝试再次操作,由于它不知道offset,
所以只能再次append,这就会导致在一些replica有重复的记录,
而且不同的replica拥有不同的数据。
为了应对这种情况的发生,应用程序必须通过一定的校验手段来确保数据的正确性, 如果对于生产者-消费者队列,消费者可以通过唯一的id过滤掉重复的记录。
Snapshot是对文件或者一个目录的“快照”操作,快速地复制一个文件或者目录。
GFS使用Copy-on-Write实现snapshot,首先master
revoke所有相关chunk的lease,
这样所有的修改文件的操作都需要和master
联系,
然后复制相关的metadata,复制的文件跟原来的文件指向同样的chunck,
但是chuck的reference count大于1。
当有client
需要写某个相关的chunck C时,master
会发现它的reference count大于1,
master
推迟回复给client
,先新建一个chunk handle
C’,
然后让所有拥有C的replica的chunkserver
在本地新建一个同样的C‘的replica,
然后赋予C’的一个replica一个lease,把C’返回给client
用于修改。
当client
请求删除文件时,GFS并不立即回收这个文件的空间。
也就是说,文件相关的metadata还在,
文件相关的chunk也没有从chunkserver
上删除。
GFS只是简单的把文件删除的operation log记下,
然后把文件重新命名为一个hidden name, 里面包含了它的删除时间。
在master
的日常维护工作时,
它会把删除时间删除时间超过3天的文件从metadata中删除,
同时删除相应chunk的metadata,
这样这些chunk就变成了orphan chunk,
它们会在chunkserver
和master
进行Heartbeat
交互时从chunkserver
删除。
这样推迟删除(原文叫垃圾回收)的好处有:
master
的日常处理中,可以使用批处理这些操作,
平摊下来的开销就小了这样推迟删除的不好在于浪费空间,如果空间吃紧的话,client
可以强制删除,
或者指定某些目录下面的文件直接删除。
GFS,MapReduce,BigTable并称为Google的“三架马车”, 既然看了GFS,怎么能不看另外两篇? 欲知Mapreduce,BigTable到底是怎么一回事, 请静候我接下来的博文。
]]>Markdown和 reStructuredText(下面简称rst) 是现在比较流行的轻量级标注语言, 这些语言拥有比较强大的表现力,可以通过简单的书写代码就可以写出包含代码,图片,数学公式等各种格式的文档。 这样,我们不用把心思花在各种格式的调节,而只需要专注于文档的内容就行了。 我们通过标记语言写成的文本文件能够被转化为html, tex, pdf,epub, word等各种格式, 我们只需要利用标记语言提供的语法把文本文件写好,相应的转换器会为你转换为特定的文档格式。 我的博客其实都是使用markdown一个特定版本kramdown写的, 然后转换为html来发布。
学习这些标记语言其实很简单,远远比学习latex简单,把它提供的一些语法都写一遍,比如说怎么写标题, 怎么写列表,怎么插入代码,怎么插入数学符号等等。知道怎么写了之后, 就多练习,写得多了,熟悉之后就会发现,确实能够省去你控制格式的很多烦恼。
Markdown有很多超集,比如上面提到的kramdown,这些超集在markdown的基础之上有提供了一些功能, 比如说原生的markdown是不支持数学公式的,但kramdown支持, 它可以把你写在两个在源文件中的这样一段markdown代码:
$$
O(g(n)) = \{f(n): \exists c,n, \forall n \geq n_0, 0 \leq f(n) \leq cg(n)\}
$$
转化为这样一段html:
<script type="math/tex; mode=display">
O(g(n)) = \{f(n): \exists c,n, \forall n \geq n_0, 0 \leq f(n) \leq cg(n)\}
</script>
包含这个的html如果里面包含MathJax这个js库的话, MathJax就会把上述的一段html转换为如下的数学公式呈现给你。
如果你使用了kramdown去处理一个包含数学公式的markdown,却发现没有转化为数学公式, 那么极有可能是没有把MathJax的库包含到这个markdown中,需要通过下面这一行代码引用:
<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
下面我介绍一个转换模板语言的神器——Pandoc, 它能够在各种格式的文本之间进行转换,而且支持的格式非常多。 我觉得使用pandoc比较适用的场景是你要写一个几页的文档,这个文档里包含了代码,数学公式等。 在这种情况下,你先写成一个markdown的文本文件,然后利用pandoc转换成需要的格式(pdf, html等)。 而如果写比较大型的文档,或者是写书,我觉得待会我要介绍的Sphinx可能更加合适。 当然,也有很多人使用markdown来写书。
使用这些标记语言,包括我下面提到的rst, 转换为pdf, 最大的不好就是如果里面包含了中文,就需要一些比较麻烦的处理。 因为这些标记语言在转换为pdf时,都是先转换为latex,而latex对中文的支持又不是那么的好。 对于pandoc,如果需要转换为中文的话,需要另外提供一个模板, 这个模板引入了一些中文需要的包,还声明了一些中文的字体。 需要注意两点:
fc-list :lang=zh
查看已经安装的中文字体。xelatex
作为latex引擎。我这里有一个我自己写的一个用于转化简单pdf的模板。 记得修改成你已经安装好的字体。这样就能通过下面的命令生成pdf:
pandoc -o solution.pdf solution.md --latex-engine=xelatex\
--template=mytemplate.tex
Sphinx是一个很强大的用于制作文档的工具, 几乎所有的Python文档都是用Sphinx这个工具生成的, 在Python代码里面的注释能够很方便的转换为相应的API文档。 但是它并不仅仅局限于Python,它现在也能够支持C/C++, 而且在朝着更多的语言发展。除去和语言相关的成分, 它本身就十分适合写文档。
Sphinx使用rst作为它的标记语言,而且扩充了一些rst的模块,
用于更方便的书写文档,比如说toctree
。
而且它还有很多的html主题可以使用,能够让你生成的html比较美观。
很多人拿Sphinx用来写书,同样的,如果要生成pdf,还是会面临中文问题, 好在还是有方法可以解决一些比较简单的问题, 在这个文档中, 作者比较详细地介绍了一些它在写这本书时遇到的一些问题, 虽然还有些许问题没有解决,但已经能解决大部分的问题了, 从作者生成书籍的质量可以看出。
我其实就是打算用Sphinx来记录一些学习的笔记, 暂时还没有出书的打算,因此针对于我的需求, 我使用这两个步骤达到生成pdf的目的:
latex_elements.preamble
:'preamble' : r"""
\usepackage{float}
\usepackage{xeCJK}
\usepackage{indentfirst}
\setlength{\parindent}{2em}
\textwidth 6.5in
\oddsidemargin -0.2in
\evensidemargin -0.2in
\usepackage{ccaption}
\usepackage{fontspec,xunicode,xltxtra}
\setmonofont[Mapping={}]{Source Code Pro}
\setCJKmainfont[ItalicFont={Adobe Kaiti Std R}, BoldFont={Adobe Heiti Std}]{Adobe Song Std}
\setCJKmonofont[BoldFont={WenQuanYi Zen Hei Mono}]{Adobe Fangsong Std}
\setCJKsansfont[ItalicFont={Adobe Kaiti Std R},BoldFont={Adobe Heiti Std}]{Adobe Song Std}
\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt
\renewcommand{\baselinestretch}{1.3}
\captiontitlefont{\small\sffamily}
\captiondelim{ - }
\renewcommand\today{\number\year年\number\month月\number\day日}
\makeatletter
\renewcommand*\l@subsection{\@dottedtocline{2}{2.0em}{4.0em}}
\renewcommand*\l@subsubsection{\@dottedtocline{3}{3em}{5em}}
\makeatother
\titleformat{\chapter}[display]
{\bfseries\Huge}
{\filleft \Huge 第 \hspace{2 mm} \thechapter \hspace{4 mm} 章}
{4ex}
{\titlerule
\vspace{2ex}%
\filright}
[\vspace{2ex}%
\titlerule]
%\definecolor{VerbatimBorderColor}{rgb}{0.2,0.2,0.2}
\definecolor{VerbatimColor}{rgb}{0.95,0.95,0.95}
""".decode("utf-8")
.. raw:: latex
\renewcommand\partname{部分}
\renewcommand{\chaptermark}[1]{\markboth{第 \thechapter\ 章 \hspace{4mm} #1}{}}
\fancyhead[LE,RO]{学习笔记}
\renewcommand{\figurename}{\textsc{图}}
\renewcommand\contentsname{目 录}
这样就能生成看上去还比较不错的pdf了。
注意不能直接通过make latexpdf
去生成pdf,
而是需要先通过make latex
生成tex文件,
再使用xelatex
去编译tex文件生成pdf.
我在这边博客中主要总结了一些编译中文pdf的经验, 至于这些标记语言的语法,应该不难, 多写写,就会熟练的,我就不在这赘述了。
]]>好了,言归正传,这一个月,都在忙着做毕业设计, 包括看论文,写代码,找数据,做实验。一个月的时间, 就完成了这么一件事,效率确实不算高, 主要原因在于对于搞科研还是新手, 很多情况下都是在尝试了各种可能之后才找到方法, 有时一个人的蛮干还不如直接通过其他方式获得出路。 哎,科研就是要耐得住寂寞,不停地调参数,不停地跑数据, 把各种可能性都尝试一遍,之后才有好的结果。
先简单介绍一下我这个论文的要求,论文的题目叫
信息缺失情况下的社区挖掘
,其实就是一个聚类算法,
根据节点的某些属性和节点之间的边的关系,把图中的比较接近的节点聚到一起,
形成一个社区。而信息缺失,是指节点中的某些边的关系并不知道,
在这样的情况下进行聚类。采用的办法是首先利用节点之间的属性和边的关系,
通过机器学习方法获取所有节点之间的距离,然后再根据这个距离对节点进行聚类。
下面我来说一下整个的过程。
首先是准备数据。一开始准备爬微博的数据, 所以就去网上搜各种微博的爬虫, 然后终于找到了这个一个简单的分布式新浪微博爬虫, 在这个基础上进行了一点小修改,弄了一个简单的单节点爬虫。 这个爬虫可以抓取用户的微博,用户的个人资料以及用户之间的关注关系。 然后,又使用NLPIR对微博进行分词, 提取出关键字作为用户的属性,同时以用户之间的关注关系作为边,这样就开始实验。 可是,不知道是实验室的网络不稳定还是新浪微博的限制,这个爬虫很难稳定地抓取微博的数据, 最后实在没办法,学长建议我去网上直接找别人爬取到的数据集。 于是,我先后使用了Google+, Facebook, Twitter(这三个数据集都是在Stanford Large Network Dataset Collection上找到的), 和Flixter数据集做实验。其中使用Flixter数据集做出来的结果也还可以,可是师兄说一定要有Ground Truth用于验证, 所以我又只好去找其他的数据集, 最后终于在这找到了可以用于实验的数据集。
然后是实现代码去做实验。这个代码其实两部分,第一部分要利用机器学习去学习一个距离, 第二部分是基于距离进行聚类。为了学习距离,需要求解一个最优化函数, 而这里面涉及的数学的公式好复杂,实现起来也非常困难, 找到了一份和这个论文比较相似的论文的源代码,却发现里面错误好多,有些地方也不知道从哪里开始改。 大概修改了一下就把这个距离拿到第二步去聚类,发现结果非常差, 然后也不知道是第一步的问题还是第二步的问题,但是我就只会改第二步的代码:P。 终于在第二步实在找不出什么错误然后第一步的结果又不好之后,我去问老师, 老师让我使用谱聚类算法来验证距离是不是正确的, 因为谱聚类算法也是基于距离的聚类算法,如果谱聚类算法得不到正确的结果,那就是距离的问题了。 多好的主意啊,我当时怎么就没想到呢,因为我当时还没听说过谱聚类算法。 于是我就拿谱聚类算法去验证,果然是距离没学对,而除了代码没写对之外, 有一个重要的步骤没有做,对Feature做Normalize, 这个步骤对于使用节点的属性来说非常重要,它保证了所有属性的作用是均等的。 而后,我又在这里找到了一些其他的学习距离的算法, 我使用了其中的DCA算法来作为我的第一步,相当靠谱。 通过谱聚类算法验证之后,我发现我的第二步算法就几乎没有什么问题了, 跑出的结果也比较让人满意。
大概就是这么一个比较纠结的过程,总结出一些经验吧:
>>>import this
写代码还是要多检查,一个bug除了导致几个小时的结果无效,还要浪费更多的时间去找到它。
在一个步骤保证正确之前,不要急于开展下一步,这样只会增加更多的复杂性。
沉下心来研究论文,尽量采用和别人一样的方法,得到的结果总是好些,虽然不知道为什么好。
不要畏惧实验和失败,说不定下一次就是成功的那一次。
真爱生命,远离科研。
在最短路径问题中, 希望在一个图中找出从一个节点到另外一个节点的最短路径, 可能要找出从某一个特定节点到其他节点的最短路径, 也可能是找出所有节点对之间的最短路径, 图中边的权重可能为负值,甚至包含负的回路, 因此,在不同的情况下,有不同的算法适合求解问题, 关于求解所有节点之间的最短路径问题已经在动态规划时详细地解释过Floyd算法, 现在就谈谈两个处理单源最短路径的算法, Dijkstra算法和Bellman-Ford算法。
Dijkstra算法用于求解所有边的权重为非负值时的单源最短路径问题, 它与Prim算法很相似,利用一个集合S保存已经求出最短路径的节点集合, 开始是S只包含源节点s, 每次从V-S中挑选出一个距离S中节点最近的节点u放入S,同时正确地设置u的邻居节点的路径长度, 直到S为V。
对于Dijkstra算法的正确性,只需要递归地证明下面的性质成立:
当节点u被加入到S时,u到s的最短路径已经被正确的设置, 而且u到s的最短路径上的前趋节点w已经被添加进S。
Bellman-Ford算法用于求解权重可以为负值时的单源最短路径问题, 而且它还可以用于判断图中是否存在s可达的负的回路。 Bellman-Ford的执行过程就是运行|V|-1次更新操作, 每次更新操作遍历每一条边(u, v)更新dist[v],伪代码如下:
procedure update((u, v)):
dist[v] = min(dist[v], dist[u]+w(u, v))
for k := 1 to |V|-1
for (u, v) in E:
update((u, v))
要证明这个算法的正确性,先说明它的两个性质:
有了这个性质,对于任意节点v, 通过对v到s的最短路径的长度做一个简单的归纳, 就能够说明当算法结束时,dist[v]能够被正确的设置。
在我看来,Bellman-Ford算法的价值并不在于这个算法本身, 它给出求解一些问题的普遍思路。对于有着相互依赖的k个元素, 如果它们之间的关系很复杂,比如如果和之间有关系,那么和之间可能就会有关系, 而且这种关系可能构成一个循环,如果每次找到了两个元素的关系就去更新其他元素的关系就会陷入一个死循环, 但如果利用Bellman-Ford算法的思想,就能明白对于任意两个元素和,它们之间的关系最多隔着k-2个中间元素, 第1次更新的时候可以把没有隔中间元素的两个元素的关系确立好,第2次更新的时候可以把只隔1个元素的两个元素确定好, 依次类推,当k-1次更新的时候,所有元素的关系都能确定好。就不需要一确定两个元素的关系就去考虑要不要更新相关的元素, 思路显得清晰。
最大流问题是图论中一类很重要的问题,因为它和线性规划也有着很强的关联, 所以它的应用也十分广泛。在最大流问题中,对于图G,有两个特殊的节点s,t, 它的任何一条边都有一个容量c,对每条边的一个特定赋值称为一个流f, 流必须满足两个性质:
最大流是想找出一个f,使得最大。
求解最大流的算法非常直观:
要证明这个算法的正确性,需要了解图的(s,t)-割, 以及割的容量,一个图的(s,t)-割把图分成互不交叉的两个组L和R, 使得s在L中,t在R中。该割的容量就是横跨L和R两个集合的所有边的容量之和, 有如下性质成立:
对于任意流f,任意(s,t)-割(L, R),
说明割的容量是任何流的大小的上限。
下面说明最大流算法的正确性。当程序终止时,残留网络中不存在由s到t的路径, 那么令L为中s可达的所有节点,R为其他的节点,那么此时size(f) = capacity(L, R)。 这是因为对于任何从L到R的边e, 有
因为这其中任何一个违反都会导致e的终止节点在中从s可达。所以此时size(f) = capacity(L, R)。 所以对于任意流f’, 都有,意味这f是一个最大流。
关于最大流还有两个很重要的东西:
2012年对我来说绝对是不一样的一年,求职,面试,外推,实习, 以前对我来说很遥远的事情在这一年真真切切的发生。 有成功,也有失败,有欢笑,也有泪水,有面临抉择时的犹豫不决, 也有选择之后的淡定从容。这些经历,大多是艰辛的, 却没有将我击垮,能让自己觉得人生的充实。世界末日都照样过了, 还有什么挺不过呢?
在学习方面,2012年学习了很多新技术,新的语言, 对于算法,也有了更深入的理解。总的来说,还是广度有余,深度不足。 多多接触一些新的东西当然是有必要的,无论是对于开阔视野还是为了选择一个自己感兴趣的方向, 可是一个人必须要有所专长,在一个方面成为专家,这样才能体现个人的价值, 我在这方面仍然有所欠缺,希望在未来几年能够再某一个方面深入研究下去。 为什么是几年,因为我觉得有所建树,必须经过几年的积累。
在其他方面,相对来说就做得太少了一点,生活显得略微单调了一些。 空闲时间除了打打球,打打dota,似乎就没有干过别的什么事。更让我觉得愧疚的事, 很少看专业方面的其他书,思考的方式也越来越朝着计算机那个非0即1的方向去转了。 现在的情商好像是在下降,在2013年中一定要强迫自己多读书,多读与专业无关的书籍。
今年最好用的工具还是微博,在这上面可以很好地打发时间, 同时可以获取很多很有意思的信息。虽然说能学到的东西很少, 但它确实能很大程度地开阔视野,你能了解到很多你在现实生活中很难碰到的一些人和事。 喜欢微博,喜欢它带给我的快节奏信息。
今年最深的几点感触:
2013 To-Do-List:
祝愿我的亲人和朋友身体健康,考研的同学考上好的学校, 工作的同学工作顺心。
]]>对于广度优先遍历,在每次遍历时,都试图把同一个级别的所有节点先遍历, 再遍历下一个级别的节点。它的实现方式是通过一个队列保存需要去遍历的节点, 在每次遍历到一个节点时,都把它的邻接节点放入队列(如果这个节点没有被遍历过的话)等待着被遍历, 这样能保证更深层次的节点会遍历在它的任何邻接节点的后面,因为它的邻接节点更先进入队列。
广度优先遍历可以用来求解无权图中的单源最短路径问题, 对于节点s,在它上面执行一次广度优先遍历就能求出各个节点和s相距的节点数, 这也就是它们到达节点s所需的距离。对于广度优先遍历, 算法首先会发现和s距离为k的所有节点,然后再去发现和s距离为k+1的节点。 这其实和Dijkstra算法是采用同样的思想,先找出和s距离更近的节点, 在这基础上再找出更远的节点,因为这样的话当确定好了更远节点的路径时, 就不需要反过来再去修改更近节点的路径,因为通过更远节点的路径肯定没有先前的那条路径好。
深度优先遍历与广度优先遍历不一样,在遍历到每一个节点时,尽量往更深节点去遍历, 遍历完之后再考虑同一个级别的节点。它的实现方式不需要队列, 直接通过递归函数就可以完成这个步骤。每次遍历到一个节点时,先把当前的节点设为已经遍历过, 然后在它的邻接节点上执行递归遍历的函数(没有遍历过的邻接节点)。如果对整个图进行深度优先遍历, 首先会选定一个节点,然后在它上面执行深度优先遍历,如果遍历到了所有节点就停止, 否则选择另一个没有遍历到的节点重复这个过程直至所有的节点都遍历完。在深度优先搜索的过程中, 会形成一棵棵以挑选出的节点为根的深度优先树组成的深度优先搜索森林。
在深度优先遍历遍历到每一个节点时,可以利用一个计数器记录遍历节点开始时的时间和结束时的时间, 依据这个时间来确定节点遍历的相对顺序。设节点v遍历的开始时间为d[v],结束时间为f[v], 那么时间区间[d[v], f[v]]就代表了这个节点处在遍历过程中的时间,更具体的说, 是节点处在递归函数的栈中的时间。因此对于任何节点u, v, [d[v], f[v]]和[d[u], f[u]]只能是不相交或者是一个包含另一个。 (通过栈可能更好理解一点)
利用这样的计数器,可以完成对图的很多操作,比如说拓扑排序和有向图的连通性检查。
拓扑排序是对于有向无环图的节点进行一次排序,使得对于任意的节点u, v,如果u有一条边到v, 那么u一定出现在v的前面。
利用深度优先遍历得到的f值,可以很容易地对节点进行拓扑排序:按f值的大小进行排序, 具有较大f值的节点排在前面。
可以利用f值进行排序是通过如下性质保证的:
如果f[u] > f[v],那么一定没有从v到u的边。
要证明这个性质,可以考虑f[u] > f[v]的两种情况:
[d[u], f[u]]与[d[v], f[v]]不相交且在其之后,那么此时遍历完v之后, u还没开始遍历,此时不可能有v到u的边,因为如果有的话那么遍历v完成之前就会遍历u,矛盾。
[d[u], f[u]]包含[d[v], f[v]],那么说明在遍历u结束之前遍历到了v,这样就存在一条从u到v的路径, 如果有v到u的边,就会形成一个环,与无环图矛盾。
对于处于有向图的同一个强连通分量的任意节点u, v,存在一条路径从u到v, 同时也存在一条路径从v到u。显然任意两个连通分量不能相交,目的是找出有向图中的所有强连通分量, 注意单个节点可以作为一个强连通分量。
利用深度优先遍历,可以确定有向图的所有强连通分量,方法如下:
这个算法的正确性由下面的性质保证:
对于任意节点u,v,u,v处于同一强连通分量当且仅当它们在第2步中处于同一棵深度优先搜索树。
证明这个性质,不妨设f[u] > f[v]:
右边到左边,如果u, v在同一棵深度优先搜索树中,说明在反图中有一条u到v的路径, 也就是原图有一条从v到u的路径,只需要证明原图有一条从u到v的路径即可。 和证明拓扑排序一样,f[u] > f[v]只有两种情况:
[d[u], f[u]]与[d[v], f[v]]不相交且在其之后,那么此时遍历完v之后, u还没开始遍历,由于存在从v到u的路径,那么遍历v完成之前就会遍历u,所以这种情况不可能。
[d[u], f[u]]包含[d[v], f[v]],这种情况是唯一可能的情况,此时存在一条从u到v的路径
因此证明结束。
在最小生成树问题中,给定一个无向连通图G = (V, E),从E中挑出权值和最小的|V| - 1条边, 使得这些节点依旧连通。
有两种算法求解最小生成树问题,而且两种算法都是贪心算法,都满足贪心算法的贪心选择性质。
在Kruskal算法中,每次都试图挑选权值最小的一条边, 但要保证添加这条边不会导致存在回路,如果会导致回路,则不考虑这条边。
可以很简单的通过修改最优解的方式证明这个算法的贪心选择性质,从而证明算法的正确性。
在Prim算法中,利用一个不断增长的集合S,这个集合原来只包含一个元素, 然后每次往集合S中添加一个节点,这个节点是V-S中距离S中任意节点最近的节点, 在把这个节点添加到S的同时,把这个节点和S中与这个节点最近的节点组成的边添加为最小生成树的边。 显然,Prim算法能够保证每次添加一条边时,都不会导致回路。
同样可以很简单的通过修改最优解的方式证明这个算法的贪心选择性质,从而证明算法的正确性。
]]>动态规划是一种解决问题的通用方法, 在求解最优化问题的时候, 动态规划能够综合考虑到各种情况然后做出最优的选择。 可是,对于一些特殊的问题, 在从子问题中做出选择时,可以不用考虑所有的子问题, 而只需要考虑一个子问题, 因为选择这个子问题一定可以达到一个最优解, 在这种情况下,就可以采用贪心算法来解决。 贪心算法的好处在于,不需要考虑所有的子问题, 只需要考虑一种情况,这时求解的子问题的数目就减少了, 同时,可以采用一种自顶向下的方法解决问题, 逐步地把问题转化为更小的子问题,直到可以轻易解决。
书上给出了一个这样的例子来说明从动态规划算法转化到贪心算法。
有n个活动组成集合,它们需要占用同一个资源, 这个资源在同一个时间只能被一个活动占用,每一个活动使用资源的起始时间和结束时间分别为 ,如果区间与区间不重叠, 就称活动与活动是兼容的,求S的一个由互相兼容的活动组成的最大子集, 假设活动已经按结束时间排好序。
对于这个问题,可以定义如下子问题, 表示从S的子集中得到的最大兼容子集, 其中, 也就是说,是由所有在结束之后开始, 在开始之前结束的活动组成的集合。 通过定义这样一个子问题, 就可以很容易地得出如下递推公式,
从而利用动态规划解决这个问题。
然而,动态规划并不是一个最优的解法, 因为要求解的子问题的个数为个, 而且求解每个子问题时选择的个数也有个, 这就导致了通过动态规划求解问题所需的复杂度为。
如果采用贪心的算法,每次通过选取中具有最早结束时间的活动作为划分元素的话, 那么就可以通过
得到一个最优的解,而不用考虑其它元素作为划分元素时的子问题。
定义函数递归函数RECURSIVE-ACTIVITY-SELECTOR(S, f, i, n)求取子问题的解, 伪代码如下:
def RECURSIVE-ACTIVITY-SELECTOR(s, f, i, n)
m := i + 1
while m <= n and s[m] < f[i]
do m := m + 1
if m <= n
then return {a[m]} union RECURSIVE-ACTIVITY-SELECTOR(s, f, m, n)
else return EMPTY_SET
求解原问题就可以通过调用RECURSIVE-ACTIVITY-SELECTOR(s, f, 0, n)来完成。 从代码中可以看出,每个活动都只被检查过一次,如果函数调用的时间可以忽略不计的话( 这个递归地算法可以通过迭代来实现,所以算法本身可以不用考虑这个问题), 那么整个算法的时间复杂度为。相比于动态规划,效率要提高很多。
关于为什么可以将选作分界元素需要说明两点:
一定有某一个最大兼容子集包含元素,也就是说, 如果要挑选出一些元素组成最大兼容子集, 那么先把选中放入这个集合中肯定能导致一个最优解。 关于这种命题的证明,普遍的方式是假设有一个最优解没有包含这个元素, 那么把这个元素和某个元素替换后会导致一个不会比原来差的解。 书上也是采取这样的证明方式。以后在证明最小生成树的贪心算法时也是采取这样的方式。
把放入最大兼容子集后,这个最大兼容子集的其余元素一定是子问题的一个解。 这个比较好证明,只需要证明对于任意和兼容的元素, 一定有 即可。
有了这两点,就能得出:
也就是将选作分界元素, 同时可以不需要求解。
需要注意的是, 这个等式只有在时成立, 但是从代码中我们可以看到, 在调用RECURSIVE-ACTIVITY-SELECTOR(S, f, i, n)时, 此时可能被添加进最大兼容子集的元素集合是, 而不是,然而这并不会导致什么问题,因为在选取时,已经过滤掉了开始时间小于的元素, 随着迭代次数的加深,那些开始时间小于的元素也会被过滤掉,因为开始时间小于也一定会小于, 在更深的迭代时同样会被过滤。
采取这样的方式实现代码能够降低时间和空间的复杂度,因为如果每次求解子问题时都需要求出这个集合的话, 需要的时间来求出,同时也需要一个数组来保存这个子集,显然不如书上的这个实现简单。
这个问题如果用动态规划的话可以采用另外一种思路, 令c[i]表示集合中,包含元素的最大兼容子集的元素个数, 那么有如下递归公式成立
利用这个递归公式求解的话,时间复杂度可以降到。
总的来说,贪心算法比动态规划效率要高,实现起来也相对简单一些, 同时也是一般人比较容易想到的算法。但是这并不意味着它比较简单, 因为并不是所有的问题都能够使用贪心法得到解决, 它比动态规划有更多的限制条件, 它必须满足两个基本条件:
贪心选择性质:一个全局最优解可以通过局部最有解得到。 这是贪心算法与动态规划的不同之处。 证明贪心选择性质一般用上面提到的修改最优解, 使其包含局部最优解, 然后得到一个不差于最优解的解,就能证明。
最优子结构,这和动态规划一样,可以通过”剪切,粘贴”的方法证明。
贪心算法可以用于求解很多问题, 在图算法中的最小生成树算法, 单源最短路径算法等都是利用贪心法解决, 关于这些算法,我在图算法时再详细阐述, 现在说另外一个比较典型的贪心算法的例子。
哈夫曼编码,这个基本是任何算法书都需要提及的使用贪心算法的例子。 有n个不同字符,知道它们的出现的出现次数, 如何设计一个前缀编码使得整个文件的编码最短。利用一个优先级队列, 每次把队列的频率最小的两个元素出队,组成一个新的元素然后入队,直到队列只剩一个元素为止。 这个算法贪心选择性质的证明也比较简单。
前面说到的贪心算法都是用于求解最优化问题, 在应用贪心算法的时候,看问题是否具备贪心选择的性质, 如果具备,就通过贪心法求解。 有另外一种问题也可以归入贪心算法的范畴,这样的问题不是求最优解, 而只是需要正确地的解决某一个问题,在这种问题中,没有最优解可言, 只是求出问题的正确的解就可以。在解决问题的过程中,可能会有很多选择, 类似于要不要选择某个元素,要不要把某个变量设成True, 这许多的选择组合起来就会导致解有很多种可能性, 目的就在于从这许多的解中找出满足指定条件的一个解,如果存在的话。 这就类似于走迷宫,有许多的分叉路口,就很多种走法, 从这些走法中找出一个能走到出口的就可以。
对于这些问题,暴力搜索每一种可能性显然不是一种好方法, 动态规划也可以用于解决这样的问题, 在解决一个问题时利用一个子问题是否满足条件来决定当前的问题是否满足条件。 而贪心算法在解决这种问题时采取了一种更加直观的想法: 必须要做某一个选择, 否则就是不满足条件的解。如果问题具有这样的性质,就可以很容易地选出一个解, 因此每次的选择都是唯一的,然后再看这个选择是否满足条件即可。如果满足条件, 那么就找到了一个解,如果不满足,那么就没有解,因为其他的解肯定不满足条件。 下面通过Horn公式来更详细的说明。
在Horn公式中,我们指定了一些布尔变量(比如x,y, z)必须满足的性质,通过两种公式给出:
问题是给定一个Horn公式,确定是否可以给公式中出现的变量赋予合适的布尔值,使所有的这些公式都能得到满足。 很显然,要满足蕴含式,需要要把一些变量设为True, 而要满足析取范式,需要把一些变量设为False。 对于每一个变量,赋值的方式都有两种,遍历所有的可能性显然是效率不高的。
贪心算法通过这样的思路来解决:
说明两点:
为什么这个算法是正确的。如果找到了一个解,显然是正确的。 如果没找到解,为什么就无解呢?因为前面的把变量设为True是“必须”的, 也就是说,一个变量被设为True,那么在任何正确的解中,它必须被设为True, 所有在这些必须的步骤都完成之后,如果不满足条件,也就没有解可以满足条件了。
重复检查所有蕴含式是否满足的次数的不超过n+1次,其中n为变量的个数。 因为除了第一次检查,其余每一次检查都是因为前一次有变量被设为True, (否则如果没有变量被设为True, 那么所有的蕴含式都满足,就不会再去检查了), 而变量被设为True的次数不超过n次,所以检查次数不超过n+1次, 这就保证了检查所有蕴含式是否满足并不是一个复杂度非常高的过程。
在这种贪心算法中,解决思路是找出那些必须要执行的步骤,一步一步,将问题简化, 最终解决。
B树也是一种平衡地二叉查找树,类似于红黑树, 它也能够支持动态地插入,删除和查找数据。 B树主要用于保存数据到磁盘中, 比如很多数据库的索引就是通过B树进行保存。 由于磁盘的读取速度相对于内存要慢得多, 所以尽量地减少IO的次数显得非常重要, B树也正是实现了这样的思想,B树的深度不会太深, 这样查找数据所需要遍历的节点数就会很少, 相应的IO次数也会减少。
在B树中,一个非根的节点至少包含t-1个关键字, 这样每一个非根的节点就至少有t个子女,这样一棵包含n个关键字的B树的高度h至多为 。但是一个节点包含的关键字又不能太多, 一方面是因为一次IO只能读取指定数量的字节, 另一方面太多的关键字会导致定位一个关键字比较耗时,所以B树的每个节点包含至多2t-1个关键字。
这些性质能够保证B树查找的性能,但是在插入和删除的时候就需要维护这些性质。 具体表现在:
当插入一个关键字到一个已经满了节点(包含2t-1个关键字),就需要把这个节点分裂成两个, 同时把这个节点中间关键字插入到它的父节点,这样可能会导致父节点的关键字超过2t-1, 所以需要一直向上到根来进行维护。
当删除一个关键字时,会导致只有t-1个关键字的节点不满足性质,这时可以通过向它的兄弟“借”一个关键字, 如果兄弟包含至少t个关键字的话,否则,就和兄弟合并,然后从父节点删除一个关键字, 同样的,此时需要一直向上到根来进行维护。
书上在插入和删除节点的时候为了避免向上过滤来进行维护, 在遍历到某一个节点的时候,就把性质维护好,这样当一个节点遍历到的时候, 就能保证它的父节点不需要再去维护了。这样的方法,对于插入还好, 对于删除操作就显得有点复杂,考虑了好多种情况, 我觉得还是向上过滤进行维护比较好,并没有增加复杂度。
]]>由于红黑树这种结构很好的平衡性(树的高度不会很高), 对于动态变化的集合插入,查找,删除等操作的复杂度都比较低, 通过给它的节点增加一些其他的属性, 能够得到一些在特定情况下很有用的数据结构。
通过给红黑树的每一个节点x附加属性size[x], 表示以x为根的子树的节点个数, 可以通过递归确定一棵红黑树的第i小关键字的元素。 由于包含n个元素的红黑树的高度为,每次递归高度都会下降一层, 所以查找第i小关键字的时间复杂度为。
在第9章中,给出了获取n个元素的数组中第i个元素的的算法, 而一个包含n个元素的数组如果是有序的话,那么获取第i个元素的复杂度是。 但是这种扩充的数据结构的好处在于它的动态性,它的插入和删除的时间复杂度也是, 而一个有序数组的插入和删除的复杂度是。
在区间树中,每一个节点x包含了一段区间int[x],同时包含了一个max[x], 表示以x为根的子树中所有区间的右端点的最大值,同时每一个节点的key[x] = low[int[x]], 也就是说是把每一个节点所包含区间的左端点作为key。
区间树使得查找整个红黑树中与某一个区间i重合的区间变得十分容易, 可以如下递归实现:
情况1,2很自然,情况4也比较好理解, 因为如果i的左端点大于左儿子的max,它就大于那么对于左子树中所有节点的右端点, 左子树中一定不存在和区间i重合区间。情况3需要一定的思考, 因为如果i的左端点小于当前节点左儿子的max,并不能保证它一定不会与右子树中的区间重合, 所以如果只是递归查找左子树,如果左子树中有区间和i重合,那么能够返回正确结果 (因为只需要找到一个和i重合的区间就可以),如果左子树中没有区间与i重合, 那右子树中可能会有区间与i重合,导致没有返回正确的结果。情况是这样的吗?
不是这样的,下面可以证明在情况3时如果左子树中没有找到与i重合的区间, 那么在右子树中也一定不存在和i重合的区间。
假设左儿子的max来自于high[j](也就是说,是左子树中区间j的右端点), 那么一定有
否则区间i和j重合,对于右子树中的任意区间k,一定有
所以k与i不重合。
动态规划主要用于求解最优化问题, 它的基本思想是通过把子问题的结果保存起来, 这样当遇到一个更大的问题时,如果它需要解决子问题, 那么可以直接使用保存好的子问题的结果,而不用再去重复解决子问题。
使用动态规划必须要满足两个基本的条件:
在确定了一个问题适合采用动态规划进行解决之后, 仍然需要考虑几个问题。最主要的问题就是如何将问题利用更小的子问题来进行解决, 此时的思路是通过把原来的问题通过转化变成更小的子问题, 利用合适的转化可以把一个问题缩小成一个更小的子问题,比如最长递增子序列问题, 求以元素n结尾的最长递增子序列的长度, 可以转化为求以比n小的排在n前面的某个元素结尾的最长递增子序列的长度。 通过转化之后问题就缩小了。
将问题转换成子问题之后,必须要知道如何通过子问题的解得到原问题的解, 也就是所谓的递推公式, 比如上述问题,知道n之前的某个小于n元素的最长子序列的长度之后, n的最长子序列的长度就是这个长度加1。
一个问题可能转化成多个子问题,比如说上面的问题, 比n小的排在n之前的元素可能有多个,这时,就需要从多个子问题之间做出选择。 这个选择通常是比较容易的,直接从所有根据子问题递推得到的解中选出一个最优解即可。 一般情况下,还需要记录此时的选择,用于构造出一个最优解。
把上述的问题都考虑好之后,就可以按照问题的大小, 从小到大依次将各个问题解决,直到达到所需要的问题的大小, 就得到了所需问题的解。
求解方法: 定义c[i, j]为序列和的LCS的长度, 有如下递推公式成立
因为c[i, j-1]相对于c[i-1, j-1]就多了一个元素c[i], 最多能够为最长公共子序列多增加长度1,同理对于c[i-1, j]也是如此。
问题求解:
Floyd算法用于求解图中所有节点中的最短路径。 基本思想是通过n(n为节点个数)次循环更新所有节点对之间的最短路径, 伪代码如下:
for k := 1 to n
for i := 1 to n
for j := 1 to n
if path[i][k] + path[k][j] < path[i][j] then
path[i][j] := path[i][k]+path[k][j];
在每次迭代时,对于任意节点对(i, j),如果(i, k)的路径长度加(k, j)的路径长度小于 原来的(i, j)的路径长度, 那么就将(i, j)的路径长度更新为(i, k)的路径长度加(k, j)的路径长度(也就是说, 让(i, j)的最短路径经过k)。 算法的时间复杂度为。
动态规划对于这个算法的理解是,在迭代k结束之后, 此时(i, j)的最短路径为仅使用{1, 2, …, k}作为中间节点的最短路径, 当循环结束时,k=n, 此时(i, j)的最短路径为使用任意节点作为中间节点的最短路径, 也就是(i, j)之间的最短路径。
凭直接感觉,比如说(i, j)之间的最短路径为:
其中, 如果最外层的循环遍历到时,因为此时不能使用作为中间节点, 所以不会把(i, j)之间的最短路径经过, 当允许使用作为中间节点时, 有不会再去遍历让(i, j)之间的最短路径经过, 所以,(i, j)之间的最短路径没有被找到,这个算法好像是错误的。
我开始老是纠结在这样的感觉中, 认为Floyd算法可能并没有找到一条最短的路径, 可是又总是找不到一个反例, 后面经过仔细地思考,总结出两种方法来说明这个算法的正确性, 解除了我的疑虑。
通过证明以下循环不变式证明算法的正确性:
在第k轮迭代开始前,对于任意节点对(i, j), 此时的最短路径长度为使用节点{1, 2, .., k-1}作为中间节点的最短路径长度。
对于这个循环不变式,定义表示(i, j)之间仅使用节点1,…,k作为中间节点的最短路径的长度, 利用这个递推公式:
可能要更好理解一些。
可以通过这样一种思路来证明:假设k是(i, j)最短路径中最大的中间节点, 也就是说对于(i, j)最短路径中任意的中间节点m, 有,这样, k是(i, j)最短路径中最后被迭代的节点,如果在迭代到k时, 能够将(i, j)之间最短路径长度正确的设置,那么这个算法就是正确的。 在迭代k时,如果(i, k)和(k, j)之间的最短路径已经被正确设置时, 那么(i, j)在迭代k结束之后能够被正确设置。
下面通过归纳(i, j)路径中中间节点的个数n来证明在迭代k(k是(i, j)最短路径中最后被迭代的节点)时, (i, k)和(k, j)最短路径已经被正确的设置:
最后还是吐槽一下动态规划这一章的翻译真的很烂, 感觉和前面的章节不是同一个人翻译的, 很多语句很不通顺,比如说有个反问句就绕了我好久, 试着推测原文才明白什么意思。 人和人之间还是有差距的啊,不管做什么都是, 希望后面的几章能够翻译好一点,bless!
]]>由于散列表的元素个数小于关键字的取值集合U, 因此会有两个不同的关键字映射到散列表的同一个槽上, 这时就发生了碰撞。发生了碰撞时, 书上给出了两种方法来解决, 而且保证此时的散列表平均情况下的查找复杂度是O(1)。
在链接法中,关键字映射到同一个槽上的元素通过一个链表来保存, 此时散列表T[0..m-1]的任意元素T[j]是一个链表, 当插入一个元素时,将元素放在它所对应的槽所指向链表的头部。 下面对链接法的性能进行分析。
定义散列表T的装载因子, 其中n是元素个数,m是散列表槽数, 我们假设元素满足简单一致散列的条件: 任何元素散列到m个槽中的每一个的可能性是相同的。
用表示链表T[j]的长度,有 $$ E[n_j] = \alpha = n/m $$
有如下性质成立:
链接方式散列表在简单一致假设下,查找一个不存在元素所需时间的期望为
假设查找的元素是k, 它所对应的槽为h(k),链表T[h(k)]的长度, 所以平均情况下需要遍历一个长度为的链表,外加常数的散列函数时间和寻址T[h(k)]的时间, 总共为
链接方式散列表在简单一致假设下,平均查找一个已存在的元素所需的时间为
对于任意元素x,检查的元素个数等于x所在链表中,出现在x之前的元素个数加1。 设是第i个插入的元素,i=1,2,..,n, 定义:
由简单一致性假设,,所以检查元素个数的期望为:
所以平均查找时间为:
在开放寻址法中,对于每一个关键字k,定义探查序列 <h(k, 0), h(k, 1),…, h(k, m-1)> 是<0, 1, …, m-1>的一个排列。在插入某一个元素x时,如果它的关键字是k, 按照它所对应的探查序列从h(k, 0)到h(k, m-1)依次检查散列表,如果h(k,i)是空槽, 那么将x插入到这个槽,否则检查h(k, i+1)。在查找时, 也是沿着探查序列开始寻找。
探查序列的计算方法有很多,比如说线性探查法,二次探查法,双重散列法。 但是这些技术都不能保证一致散列的假设: 对于每一个关键字k, <h(k, 0), h(k, 1),…, h(k, m-1)>是<0, 1, …, m-1>的任何一种排列的可能性是相同的。
在一致散列的假设下,有如下性质成立:
向一个装载因子为的开放寻址散列表中插入一个元素,平均情况下最多进行次探查。
因为要插入一个元素x,只需要做一次查找x就能找到一个空槽,所以探查次数与查找一个不存在元素的查找相同。
一个装载因子为的开放散列表中查找一个存在的元素的期望探查次数至多为
对于每一个元素x,查找它所需要的探查次数与插入它所需要的探查次数相同, 对于第i个插入的元素x, 所需的探查次数最多为, 所以平均的探查次数最多为:
对比开放寻址法与链接法,链接法能够支持装载因子的情况, 而开放寻址法不能支持。
二叉查找树和红黑数都是用来存储动态集合的数据结构, 红黑树对二叉查找树进行了扩展,通过一些额外的性质, 保证了二叉查找树的平衡性, 这样就能够保证树的高度为O(lgn), 其中n是节点的个数。 有了这些额外的性质时, 在插入节点或者删除节点的时候就需要一些额外的操作来保持这些性质。
二叉查找树所支持的基本操作有:
插入。插入也可以通过简单的递归来实现:
相对于二叉查找树来说,赋予了每一个节点红色或者黑色, 同时整个红黑树需要保持下面的性质:
其中,性质3可以不用考虑,因为在红黑树中, 所有的叶节点都是NIL, 它永远都是黑色。
有了这几条性质之后, 能保证一棵有n个节点(不包括NIL叶节点)的红黑树高度至多为2lg(n+1)。 这时,查找操作能够在O(lgn)的时间内完成。
红黑树的性质可能在插入或者删除节点的时候被破坏, 此时需要一些操作来维护红黑数的性质。
插入一个节点时,始终把新插入的节点的设成红色, 这时,会有两种原因造成红黑树性质的破坏:
函数RB-INSERT_FIX_UP()用于在插入z时红黑树T性质的保持:
def RB-INSERT-FIXUP (T, z):
while color[p[z]] = RED
do if p[z] = left[p[p[z]]]
then y ← right[p[p[z]]]
if color[y] = RED
then color[p[z]] ← BLACK ###case1
color[y] ← BLACK ###case1
color[p[p[z]]] ← RED ###case1
z ← p[p[z]] ###case1
else if z = right[p[z]]
then z ← p[z] ###case2
LEFT-ROTATE (T, z) ###case2
color[p[z]] ← BLACK ###case3
color[p[ p[z]]] ← RED ###case3
RIGHT-ROTATE (T, p[p[z]]) ###case3
else (same as then clause
with “right” and “left” exchanged)
color[root[T]] ← BLACK
这个函数能达到目的因为:
如果是情况1,p[z]和z的叔叔都是红色,可以把p[z]和y(z的叔叔)都设为黑色, 然后把p[p[z]]设为红色,这样就把这种红红的不一致向上传递了两层, 这种不一致在向上传递的过程中会有三种情况:
无论是哪一种情况,要么会被解决,要么传递到根由根来解决。
在删除一个节点时,如果被删除的节点是红色, 那么不会有问题,因为它的儿子和父亲都是黑色, 不会违背性质4, 同时任何路径上的黑色节点的个数也不会发生变化。 但如果删除的是一个黑色的节点y,会有以下原因导致性质违背:
函数RB-DELETE-FIXUP()用于在删除节点x的父亲时性质维护:
def RB-DELETE-FIXUP(T, x):
while x != root[T] and color[x] = BLACK
do if x = left[p[x]]
then w ← right[p[x]]
if color[w] = RED
then color[w] ← BLACK ###case1
color[p[x]] ← RED ###case1
LEFT-ROTATE (T, p[x]) ###case1
w ← right[p[x]] ###case1
if color[left[w]] = BLACK and color[right[w]] = BLACK
then color[w] ← RED ###case2
x ← p[x] ###case2
else if color[right[w]] = BLACK
then color[left[w]] ← BLACK ###case3
color[w] ← RED ###case3
RIGHT-ROTATE (T, w) ###case3
w ← right[p[x]] ###case3
color[w] ← color[p[x]] ###case4
color[p[x]] ← BLACK ###case4
color[right[w]] ← BLACK ###case4
LEFT-ROTATE (T, p[x]) ###case4
x ← root[T] ###case4
else (same as then clause with “right” and “left” exchanged)
color[x] ← BLACK
从代码中可以看出,原因1或者原因2都没有进入循环,直接通过把x设为黑色就能解决问题。 解决原因3的基本思路是给x赋予一层多余的黑色(充当一个黑色节点的计数),试着把这个多余的黑色往根传递, 在向上传递的过程中,可能会遇到3种情况:
为什么再调整过程中旋转的次数不超过3次?简单看来,有如下转换关系:
情况1会有旋转,如果进入了情况3或情况4,将导致循环终止,此时旋转次数不超过3次, 但如果进入进入情况2,那么将会进入新的循环,此时有可能碰到情况1然后再次旋转, 然后进入再进入情况2…这样一直向上到根,旋转的次数可能会超过3次, 是这样吗?
上面的情况的是不可能发生的,因为情况1会把p[x]设为红色,如果此时进入情况2, 在新的循环开始时,新的x就是p[x],它的颜色是红色,直接会退出循环,把x设为黑色, 调整结束,不会再继续向上传递。所以旋转的次数不会超过3次。
]]>合并排序和堆排序在最坏情况下能够在O(nlgn)时间内排序n个数, 而快速排序则能够在平均情况下达到这个上界。 这些算法在确定元素的次序时, 都是基于元素间的比较。 这类排序算法称为__比较排序__。
比较排序的时间下界是O(nlgn), 这意味着所有的基于比较的排序算法,在最坏情况下都要用 次比较来完成排序。
这是因为比较排序可以被抽象为__决策树__, 决策树是一棵满二叉数, 它的每一条从根节点到叶节点的路径都对应于比较排序的一次执行过程, 达到叶节点时,叶节点确定了这次排序的结果。 所以比较排序算法的最坏情况的比较次数等于决策树的高度。 n个数的排列总数有n!,每一种排列都必须在决策树的叶节点中出现, 高度为h的决策树的叶节点个数最多为,故有:
所以比较排序的时间下界是O(nlgn)。
如果已经知道n个元素都是来自于0到k的整数, 其中, 那么可以通过统计0到k中的每一个数在n个元素中出现的次数来达到排序的目的。 计数排序的运行时间为 。
书上在实现计数排序时,
其实只需要让C[i]记录i在数组A中出现的次数,然后遍历一遍数组C就可以输出排序的结果。 具体遍历方法如下:
j <- 1
for i <- 1 to k
while C[i] > 0
do C[i] <- C[i] - 1
A[j] = i
j <- j + 1
就能把排序好的结果保存到A中,不需要另外的数组B。 但是书上的这种方法有一个好处,它能保证排序是__稳定__的。 也就是说,具有相同值的元素在输出数组中的相对次序与输入数组中的相对次序一样。 因为是采取对A的逆向遍历,两个相同的元素中,位置靠前的元素后被遍历,此时C[i]已经变小了, 所以也会被放在更靠前的位置。稳定排序的好处在于它能够保证基数排序的正确性。
基数排序主要解决的问题是对于多位整数的排序问题。 比如有n个d位数,每一位可以取k个不同的值,要对它进行排序, 一般的想法是先对最高位进行排序, 然后对于高位相同的子数组按次高位进行排序,依次类推。 这种想法的好处在于如果两个数中高位较大的数一定较大, 所以很容易把各个子数组的排序结果进行合并。比如排序10进制的三位数, 3XX一定都大于2XX,所以把2XX的子数组放在3XX的子数组前面就能保证合并结果的有序性。 但是它的不好的地方在于需要维护大量的子数组(随着递归的深度加深,子数组个数增多), 这对于原始的基于纸带的排序的是不可行的。
那有没有一种排序方法,既能使后面的排序利用到前面排序的结果, 而且能够不需要维护大量的子数组呢?基数排序就是这样一种排序方法, 它先把数组按最低位进行排序,然后再对结果按次低位进行排序,依次类推。 每一次的排序都必须是__稳定排序__,这样能保证在按某位进行排序之后, 整个数组在从该位到最低位的子序列上都是有序的,可以通过一个简单的归纳加以证明。
同时,在对n个b位数进行排序时,每次可以按r位进行排序, 而不仅仅是1位,这样能够在的时间内完成对数组的排序。 可以选取适当的r达到最好的时间性能。
从一个数组中找出最大值或者最小值需要n-1次比较, 比如寻找最大值,首先将最大值设为第一个元素的值, 然后让n-1个元素和最大值进行比较,如果大于最大值, 就将最大值设为它,一共需要n-1次比较。
而如果是同时找出最大值和最小值呢, 当然可以分别按上面的方法找出最大值和最小值, 一共需要的比较次数是2n-2。
书上给出了另外一种方法: 成对的处理元素,将较小者与最小值相比,较大者与最大值相比, 这样能将比较次数降为。
书上给出了两种方法:
第一种方法利用随机化快速排序算法中的RANDOMIZED-PARTITION函数对数组进行划分,然后根据 i是在哪一个部分中去相应部分中进行查找,这种方法能保证运行时间的期望是线性,代码如下:
def RANDOMIZED-SELECT(A, p, r, i)
q <- RANDOMIZED-PARTITION(A, p, r)
k <- q - p + 1
if i = k
then return A[q]
elseif i < k
then return RANDOMIZED-SELECT(A, p, q-1, i)
else return RANDOMIZED-SELECT(A, q+1, r, i-k)
简单说明一下在划分的两个子数组中,如果有一个长度为0, 为什么不会它调用RANDOMIZED-SELECT:
第二种方法通过保证每次的划分是一个好的划分保证算法的线性时间。 具体的划分方式是:
这个划分是一个好的划分, 因为能保证划分出来的两个子数组中任意一个的长度都不会超过某一个特定值。 在个组中, 假设所有的中位数组成数组B[1..m],其中 , 假设x = B[k],k = , 在所有中位数在A[k..m]的组中,除去最后一组和x所在的组之外,其他的组至少有3个元素大于x, 所以大于x的元素个数至少为:
类似的小于x的元素个数至少有个, 所以至多有个元素被递归的调用SELECT, 有了这个结论之后就能保证SELECT函数可以在线性的时间内完成。
]]>而我,也是在粗粗了解了各种算法和实现的基础上学习这本书的, 一开始扫了一下书的第一章,第二章和第六章, 发现和其他的算法书还差不多嘛, 直到看到第七章快速排序, 看到作者在大概描述完快速排序算法之后 (这个快速排序的划分函数还和我以前见过的都不一样,更加容易理解和实现), 转而开始分析快速排序的性能和随机化版本,我才明白, 我不能再这么浮躁地只是抱着了解了解算法的目的来学习这本书了。 于是我又回过去仔仔细细地从头看到了第7章, 虽然说这本书里的定理和数学公式很多,但是并不难理解, 因为作者总是把每一个步骤解释地十分细致和透彻, 每一步的证明没有很大的跨越, 每一个结论的得出都会指明依据的定理或者是前面的结论。 所以说好好看下去其实并没有很大难度, 关键是要能够静得下心。
下面是1~7章中我的几点体会:
循环不变式用于证明算法的正确性, 它能够保证一个算法能够终止, 而且当它终止时,得到的结果是正确的结果。 循环不变式的证明由三个部分组成:
循环不变式与数学归纳法十分类似,采用的也是同样的思想。 在应用循环不变式对算法的正确性进行证明时, 难点不在于上述的三个步骤,而在于循环不变式的构造, 要构造一个循环不变式能够在循环过程中始终保持, 而且能体现算法正确性,这确实需要一定的技巧。 这就类似于在应用数学归纳法时选取归纳条件。
比如书中思考题2-2:(证明冒泡排序的正确性)
for i <- 1 to length[A]
do for j <- length[A] downto i+1
do if A[j] < A[j-1]
then exchange A[j] <-> A[j-1]
b)对于2-4行(内层循环)给出一个循环不变式,并证明这个循环不变式是成立的。
内层循环的作用是把A[i]到A[n](n是length[A])中最小元素放到A[i], 可以采用如下的循环不变式来表示:
在每一轮迭代的开始,子数组A[j..n]中最小的元素位于A[j]。
下面对这个循环不变式进行证明:
渐进符号用户描述一个算法的复杂度,以前只知道 渐进符号用于描述算法的量级,比如 说明 的量级是 。
书上给出了准确的定义:
同时有:
其他的符号如, 都是类似的定义。 当然,我们只需要知道用来确定一个函数的上界, 用来确定一个函数的下界就可以了。
分治法是一种很常见的算法设计方法, 分治法的时间复杂度一般由如下的递归式给出:
其中,f(n)一般用渐进函数表示。
对于这样的递归式,主定理给出了计算T(n)的方法:
运用主定理,能够很快地求出分治算法的复杂度。
指示器随机变量的定义如下:
指示器随机变量是随机变量的一种,可以求出它的期望如下: $$ E[X_H] = Pr_H $$
指示器随机变量有一个很好的性质,它只能取0或者1, 可以把它这些变量加起来求总的发生次数, 因此当它应用到重复随机试验中时, 统计重复试验某一事件发生次数的期望, 比如在随机化的快速排序中统计交换次数的期望, 可以通过如下公式得到:
要使等式的第二步成立,不一定要保证之间是相互独立的。
这周的读书笔记就先写到这里,下周开始写第8章开始的内容。
]]>在一般的数据库教材中,讲到设计库表结构的设计, 都是将数据库设计范式作为设计的准则,因为这样设计的数据库表重复很少, 这样就会减少存储空间, 同时因为重复的内容少,维护起来也很方便, 因为如果很多的重复的话一个信息的更改可能会需要在多处进行更改才能保证数据的一致性。 然而,数据库范式并不是万能的,这样设计出来的数据库会造成查找时间的加长, 因为要查找一些数据往往需要将多个表join起来,而join是比较费时间的数据库操作, 同时,一些有关系的列被分散到各个表中,不好组合在一起建成一个索引, 这样也会减低查找的速度。 所以在设计数据库的表的结构时,要结合应用的实际情况,平衡考虑。
使用“空间换取时间”的理念,在数据统计的过程中,为了方便同时, 我们会建立一些缓存表, 这些表的目的是把一个表中的多行信息或者多个表的信息综合起来, 比如有一个表保存了今天的用户访问记录, 就可以建成一个daycount表统计每天有多少用户访问, 这样如果需要查询某一天有多少用户访问的时候直接去这个表中去查了。 缓存表引入的一个问题是缓存表的维护, 一般是通过一些周期性跑的脚本去更新这些缓存表, FlexView这个工具能够自动的帮我们维护这些缓存表, 有兴趣的可以尝试一下。
尽量选择小的,简单的数据类型,能用tinyint就不要用int,能有enum就不要用varchar。 同时尽量不要用NULL,因为NULL会引入许多的问题,首先是它不容易被索引, 同时保存它占用了更多的空间。
有时,建立索引一个字段的索引往往就够用了,因为通过这一个字段,就能过滤掉大部分的行, 剩下来的行数比较少,这样再进行过滤或者是sort, group等操作都不会有很大的压力。 然而,如果表中的数据非常多,一个字段过滤之后可能数据还是非常多, 这时就需要建立一个多字段的索引,又称为组合索引。
组合索引的顺序选择非常重要,因为mysql只能拿一个索引的前缀进行索引。比如有一个索引 (col1, col2),那么如果你的where语句中col1 = a and col2 = b或者是col1 = a, 这时mysql可以使用这个索引。但如果你的where语句是col2 = b这时就不能使用这个索引。 另外一个关乎索引顺序的地方是如果你的过滤条件是一个范围(range condition)的时候,再拿上面的索引为例, 如果你where语句有col1 = 1 and col2 < 2时,两个过滤的条件都会在使用索引过滤的时候起到作用, 而如果where语句是col1 > 1 and col2 = 2,这时只有第一个过滤条件在过滤索引时起到作用,也就是说, 在这时存储引擎会把col1 > 1的所有行都返回到mysql server, 而后服务器再通过col2 = 2过滤掉一些行,这个过滤的效果显然不好, 特别是当数据量较大的时候。关于这个的详细解释, 可以参考这篇The MySQL range access method explained。
既然索引的顺序这么重要,那如何去设计列的顺序呢?我总结了一些我的看法:
要知道一个索引设计得好不好,它有没有在sql语句中被合理地使用, 通过explain sql来查看sql一些执行细节是很有必要的。 explain sql的输出结果中,除了包含索引选择等信息, 还会告诉你诸如mysql如何对结果进行排序等信息。
MySQL Explain – Reference 这篇博客详细地讲述了explain语句输出的各个字段表示的意义, 特别是将type(access type)这个部分讲得特别清楚。
设计好表的结构和索引之后,仍然需要采取适当的方式来进行查询才能达到更好的执行效率。 《High Performance MySQL》给出了几个优化的技巧:
前两天@老赵发起了一个资助大学生读书的计划, 我有幸得到了他资助的《算法导论》一本。 这样一本书并不是随便就能得来的,正如老赵所说:
因此,千万不要把这个计划当做是免费的图书来源,选书要谨慎,拿到书就要好好阅读。 在我看来,参与这个计划其实更多的是压力。 当然,有适当的压力对于学习也是很有好处的,不是么?
而我,也正是需要这样的一种压力,能够督促我更好地去学习这样一本经典教材。 因此,接下来我会经常地在这里公布我的读书笔记,更多的是自己学习算法的心得体会, 希望能够多多有所感悟,学到更多的东西。:)
特别感谢老赵的热心,能够让我们阅读这些经典的书籍,也希望他能够帮助更多的人。
]]>废话不多说,先上干货,中文编码杂谈, 这篇文章是淘宝搜索技术团队写的,深入浅出,基本上将中文编码的各个方面讲得十分细致,而且十分通俗易懂。 我很难讲得比这篇文章更好了,我主要从几个侧面来阐述一下我对于中文编码的理解。
中文编码其实就是将中文转化为二进制比特串的过程,而不同的编码方式会把同一个中文字符转化为不同的二进制表示, 比如“中”这个字,通过utf-8编码会转化为二进制E4B8AD,而在计算机中,所有的数据都是通过二进制保存,这样我们就可以 通过二进制E4B8AD来保存“中”字,然后我们如果需要读取保存的这个字,我们首先需要知道编码方式是utf-8,然后就能将 E4B8AD转化为“中”。
python提供了对unicode很好的支持,同时也能将unicode转化为其他的各种编码。
下面通过代码来对解释一下pytho中的编码问题。
>>>a = "我是123"
>>>a
'\xe6\x88\x91\xe6\x98\xaf123'
>>>type(a)
str
>>>len(a)
9
从上面的代码可以看到,python把”我是123”这个字符串当成是str类型,其实只是把这个字符串的编码 二进制当成中文来处理。当我们通过输入法输入”我是123”时,输入法会根据我们系统的LACALE值将”我是123” 编码成相应的二进制,而python遇到二进制值时的处理也是要根据系统的编码方式,如果是一个python的脚本, 我们可以通过再脚本的头部通过
`# -*- coding:utf-8 -*- `
设置python处理编码的方式。
同时可以看到”我是123”这个字符串的长度是9,这是因为字符串a在utf8编码时的二进制表示为'\xe6\x88\x91\xe6\x98\xaf\x21\x22\x23'
,
一共占了9个字节,python的len()函数对于字符串就是计算它占了多少个byte,所以:len(a) = 9。
下面看如何与unicode进行相互转化:(此时采用的编码是utf8,其他的编码也是一样的处理):
>>>ua = a.decode("utf-8")
>>>ua
u'\u6211\u662f123'
>>>print ua
我是123
>>>type(ua)
unicode
>>>len(ua)
5
>>>b = ua.encode("utf-8")
>>>b
'\xe6\x88\x91\xe6\x98\xaf123'
将字符串转换为unicode通过decode()函数,反过来通过encode()函数。同时也可以通过:
ua = unicode("我是123", "utf-8")
或者是
ua = u"我是123"
得到同样的unicode。可以看到ua的长度为5,因为在unicode中不管是汉字还是字母或者是数字, 都当作同样的字符来进行处理,这样一个unicode的长度就是所有字符的个数,而不管这些字符是中文 汉字还是英文字符。这样的好处就在于能够很好地定位到一个具体的字符,字符串的截取以及正则表达式 匹配等操作都十分方便。所以推荐在处理包含中文的字符串时,先把这个字符串转化为unicode,然后再 进行操作,操作完以后再encode成字符串。
关于中文编码就很浅显地谈到这里,希望能给大家带来帮助,有什么问题可以在留言中和我讨论。
]]>这段时间真的很忙,忙得都没时间好好看书,好好写博客。
每天都要上班,还要忙着保研的各种事情,有时还在保研和直接工作中纠结, 导致一有一点空闲时间,就什么事情也没想干了。
还好,现在这些事情也终于告一段落,也不用再去纠结什么了,终于可以静下心来 去做自己喜欢做的事,踏踏实实地学习某些东西了。
但是总结一番还是有点必要的,不然这些日子的纠结不是白费了吗。
其实这个星期的前两天还在纠结当中,可是就好像砸中牛顿的那个苹果一样, 某种东西突然在我的头脑中闪现,让我顿悟,这个东西就是:梦想。
梦想,别扯淡了,这年头谁还看中这个。
我看中,我认为一个人一旦失去了梦想,活着也就没有什么意思了。 能支持一个人不断向前,不断超越自己的动力,也只有梦想。
我的梦想又是什么呢?很简单,在自己所在的领域有所建树,然后把 这个领域的技术能够向前推动一点,哪怕只是一点。而这个简单的梦想, 仍需要很多的努力才能实现,我的希望是能够一直在这个努力的过程中, 不要因为其他的东西的干扰而偏离了梦想。
而我差点因为微软的offer而偏离,极高的工资,舒适的工作环境,做的却 不是自己喜欢做的事。我明白,相对于其他在微软实习或者工作的人来说, 我还是相差太远。他们都是计算机竞赛的佼佼者,身上顶着各种光环,在计算机方面的积累比我 多了好几年,单就他们做过的题,我可能一辈子都来不及做完。我现在的学的东西也不是很扎实, 什么都知道一点,可都不精,如果再在微软待下去,或许就会沉浸在这种安逸的生活中,无法再 往一个更高的方向发展了。对于正值奋斗年华的我来说,过分的安乐真的不是什么好事。 而且在微软做的不是我喜欢做的事,要想突破感觉还是太难了。
那为什么不把工作当成白天的事,然后空闲时间去做自己喜欢做的事呢?我也考虑过这个问题, 最后发现这很难成功。因为这样每天最多能花两个小时在自己喜欢做的事情上面,而同时可能会 有各种事情打乱你的计划,想对于正式工作的每天八小时,这其中的差距可想而知。在互联网这样一个 高速发展的行业,低速成长是很容易被淘汰的,这样的过程,最多持续半年,我想就会终止,然后渐渐地 开始沦落为毫无激情的上班族。能够通过用这种方式坚持的,至少在中国我还没有听说过。
所以,我还需要很多的积累,扎实地磨练技术,同时更加开阔自己的眼界,而这些,通过研究生的三年,能够做到。
总的来说,就是接触了很多的东西,但没有什么谈得上精通。
语言确实用过很多,上过两个学期的C++课程,用C++写了数据结构的作业和USACO上的一些题,对于C++,应该算是最 熟悉的语言之一了吧,它的语法,还有很多相关的概念,包括虚函数,多态等等,都已经掌握了。可是不能算精通,对于它 的掌握仅仅局限于课堂,没用过STL,看过的书也就只有《C++ Primer》一本(教材除外),也没用它开发过大型的项目,对于 C++这样一门庞大的语言来说,这些还远远不够。
关于C,也就用它写过操作系统的大作业,对于Unix环境下的C编程有一定的了解,现在如果要用C写一个大型的项目的话, 应该也没有什么问题,但如果要称得上是精通,还需要大量的练习才行。
关于php,python,现在在上班的时候用得比较多,也正在处于一个水平稳步上升的阶段,我想经过一年的积累,这两门语言应该 是能够相当熟练,甚至是精通。
关于javascript,CSS(如果它也算语言的话),基本的语法也都知道,可是积累还远远不够,特别是javascript,这门上手容易,精通确 很难的语言,因为工作主要偏后端,所以熟练程度还是不够。
关于C#,用它做过几个项目,用了WPF,ASP.NET,感觉C#这门语言还是比较容易上手的,写起代码来也很方便,配合上VS这个强大的IDE, 开发还是挺快的。我对它的了解还比较基础,至于它的反射,Delegation,Event等等东西,只是清楚概念,没有实际使用过。
另外,我上过程序设计语言这门课程,在这门课程中,我接触了大量的语言,也用这些语言写过程序。 使用过Java,Perl,Scheme,Haskell,Prolog这些语言。值得一提的是,scheme和prolog我都写过好几个程序, 虽然这两门语言都比较奇怪,但是写程序时确实能够开阔思路,还是两门比较有趣的语言,我比较喜欢。
对于“语言之争”,我没有特别的看法,我也没有特别的感觉说只用某一门语言或者只喜欢某一种语言。我觉得在不同的领域, 不同的场景,可能有些语言比另外一些语言要适合一些,比如说开发系统级别的应用程序,对执行效率要求比较高,这样, C或者C++可能要适合一些,又比如一些做一些自动事情的脚本,可能python或者php用起来更加方便一些。每个语言都有它的 优点,对于语言我还是没有什么挑剔。
对于专业知识,大部分都只限于课程,都是通过教材来了解,虽然这些教材都是国外的经典教材, 可是我觉得我们利用的还是太少了,大家都平时没怎么认真学习,然后到了考试,老师会给出几个 重点,然后大家根据这几个重点复习,应付考试而已。这些经典的教材,又岂是短短的这几天能够 掌握的?而我也不过是把这几天的时间分配到了整个学期而已,也就是说,我会跟着老师的上课进度, 把这些教材看一遍,看得有多深入,浅尝辄止罢了,粗粗地过完内容,然后总结一下了事。掌握得不是很扎实, 导致成绩也不是很好。专业基础课中,掌握得比较扎实的应该算操作系统和编译原理了吧,因为这两门课都做过 课程设计,多多少少需要了解一些东西。另外数据库也算把教材看得比较透彻的一门课,虽然考试成绩不咋的。
只是知道常见几种算法:贪心法,分治法,动态规划。数据结构了解了:数,堆,哈希表,图,以及图的遍历, 最小生成树,最短路径。对于算法来说,和那些比较厉害的人的差距就在于:练习不够。别人能够做到各种算法 烂熟于胸,看到某类问题立刻想到相关的算法,因为什么,就是练习了那么多,一个问题做上10几遍,怎么能不熟。 所以在算法方面,我还得勤加练习,不然只能维持在现在的,看上去都会的水平。说现在找工作只看算法有点绝对,但 只要算法好想找任何工作是绝对没有问题的,各种经历告诉我这个观点的正确性。另外,在此提醒自己,《算法导论》 一定要坚持看完了,给自己下定一个决心吧。
首先是表达能力有待增强,把自己清楚的东西讲到让别人也清楚这确实是一种艺术。写博客是一种很好的锻炼自己表达 能力的手段,所以我会经常把自己对技术的感悟通过写博客的方式表达出来,同时也分享了东西给大家。
其次是提高英语的口语能力,多说,多听。
在研究生的三年中,我需要达到下面的目标:
目标并不多,也不是很远大,但仍然需要认真地去执行。
另外,希望在这将近一年的实习生涯中达到下面的目标:
前方路已经越发明朗了,接下来就是坚定地走下去。
]]>