不出国不知道书贵

今天学校图书馆搞国外原版新书展销,以为和上次的二手书一样能够淘到一些打折好书,遂一早同小翼等舍友们奔赴图书馆一楼。

到了才发现原来展销的主要目的是给学校图书馆荐购外文书,因为这些书不打折。国外书籍之贵之前也有所耳闻,今天总算见识了。某MM怯怯的问一本中等大小(虽然不该以尺寸论英雄)的hard cover的价钱,答曰150,还是刀,差点把MM吓晕。另有一大叔的书叫价30镑(50刀),却已是中等偏便宜的书了。

这么贵的书,想必即使生活在美元结算的国度也吃不消吧,怪不得上次展销的全是二手书,还看起来那么新。一本算法导论,新书85刀,Amazon上卖59刀,二手的卖30刀左右,用上一两年后估计还能以15-20刀转手,大致也能接受。而如果Kindle上有的话,价格在10刀以内。

想起侯捷和刘江偶尔对国内技术图书定价的抱怨。第一是作者版税太低,第二是像卖白菜似的论斤卖书;而另一方面很多人感叹技术书籍太贵买不起。技术图书这块,算是图书界里受盗版影响比较小的一个小众分类。销量低,印刷成本高,盗版商自然也没有利润空间。技术书作者大多有正式的工作,这部分人工资也不低,所以写书的机会成本也高。两个因素一综合,书价自然也上去了。

而对于技术图书的消费者而言,理论上说,如果一本书能给他带来5000元的收入,扣去了读书所耗费的机会成本3000元,那么这本书即使售价1500,买下来看还能获得500元的收益。图书市场是个垄断竞争的市场,其中有许多卖者。我们也可以选择其他的书,只能给你带来4500元的收入(书不是完全适合你的需求),除去读书机会成本3500(书写得比较前一本糟糕),但只要定价不超过500元,获得的收益就大于购买第一本书。或者消费者可以不买书,为阅读和寻找资料付出更多的成本(比如上网搜资料,看枯燥无味的文档)。

对于技术图书的生产者,作者和出版社而言,他们追求的是利润最大化。图书的成本由固定成本(稿酬、编辑、制版)和变动成本(印刷、运输、分销)组成。销量大摊薄了固定成本,自然定价低。不幸的是技术图书市场销量低,但需求富有弹性(消费者对价格较为敏感,需求却可有可无),因此生产者也没赚到多少钱。这背后同样伤害了消费者,因为没有多少人再愿意花大量时间来写好书。

算法导论教学视频中的911

最近紧跟算法男小翼的步伐,研究点算法相关的问题。不为投身科研,也没太多个人兴趣相关,只是打打基本功,对付找工作的笔试和面试问题。

这基本功,主要就是数据结构+算法,而这一行的最热门书籍莫过于算法导论(Introduction to Algorithm),Knuth大神的TAOCP非我等现有功力所能研究。虽说此书为经典中的经典,但毛病也是有的,就是罗嗦,另外也不够生动。厚厚的1200页的大部头,兼具斗殴砸人睡觉垫头的功能。

正当我读此书昏昏欲睡之时,突然想起来两年多前从Jetty那里拷来的算法导论视频。其英文不难,戴上耳机听基本可以完全听懂,而上课的内容也不深,正好做看书的辅助。

看到第三课Divide and Conquer A的开始时,图中的Leiserson上课前说了一大堆话,我以为是有啥通知没注意,突然听到一个give blood觉得不对,重新拉回开头看发现讲的正是911恐怖袭击。而上课的当天正是2001年9月12日。MIT离纽约和宾州不远,心理冲击是显而易见的。而似乎Leiserson有个同事或者以前博士生碰巧在美联航11号航班上。

放大看Annoucement。第二个就是献血的通知。

我这个应该是旧的视频,在网上看到的都是新版的视频,第三课换成Erik上了。

端午节草泥马围观记(多图)

端午假期第二天,和LP及小翼夫妇前往红山森林动物园慕名前去围观草泥马。以下图片均为小翼摄制。

三只熊猫,一如既往的很懒+很脏:

LP可爱的兄弟,算是今天看到最活跃的动物了:

游乐场里的摩天轮。游乐场4个人只玩了碰碰车,继上次之后我又把膝盖给撞伤了。小小爆料一下,小翼居然不敢坐旋转飞机,哈哈。

狮虎兽。搞这东西人类造孽了

小袋鼠。才知道原来袋鼠不会走,除了跳以外,剩下的移动方式就是用前肢和尾巴做支撑,把后腿腾空后往前挪,看起来很笨。

今天终于看到了企鹅,了却了LP的心愿。

运气不错,碰上了企鹅吃饭。

这些家伙还很挑剔,不小心掉到地上的鱼就不吃了。

只认钱不认人的鹦鹉正在被一坨硬币调戏中,不知能否识别假币

最后是今天的主角:草泥马两只。那只黑白的比较像百度上的草泥马,可惜一直在后面吃东西

这只长得不像,倒是凑了上来

中间还看了小猪跳水,可怜的LP被溅起的脏水喷了一身。。在此cmft。。

KMP算法简介及简单Java实现

继续复习算法+数据结构。今天简单写写KMP算法,不是那个KMPLAYER。。。同时这个话题也有很优秀的博文介绍,如Matrix67,我这里只是做些简单的小结。
KMP算法用于字符串的模式匹配,也就是查找某个字符串里是否存在某个子串,并返回位置,也就是Java/C++里的String.indexOf()方法。  字符串匹配最朴素的算法就是一个一个去比较,出错了就回溯到下一个起点继续比较。这样的复杂度有O(mn),即原字符串×匹配串。而KMP算法能把时间复杂度降到O(m+n)。KMP这个缩写来自于Knuth,Morris和Pratt,瞬间想到了AVL树。
前面提到,朴素的匹配算法在出错后需要回溯到原来起点。但KMP不需要回溯到起点,直接从出错的位置继续进行比较,从而达到了O(n)。之所以不需要回溯,KMP的秘诀在于其用匹配串(Pattern)计算出来的一个数组F。每当进行字符串的比较卡壳在匹配串的第j个位置时(即Target[i] != Pattern[j]),查阅这个数组F,就可以知道下一次比较时j的值。举个例子:

  • Source串(Target):            a b a b a b a b a c b
  • 匹配串(Pattern):                a b a b a c b
  • 根据Pattern得出的数组F: 0 0 1 2 3 0 0 (P怎么计算的后面会讲)

一一比较到第五个时前面的字符串都相等,到第六个时,由于Target[5] != Pattern[5],所以需要知道下一次比较时,新的j值。j值越大,省略的比较越多。对于朴素的匹配算法,一旦卡壳在位置j,则下一次直接把j置0,即从第一个开始比较,并且回到这次比较的起点。而KMP则是得到新的j值(新的j值显然小于当前j值),并且不回到比较的起点,而是从这个地方继续比较下去,不吃回头草。
回到刚才的例子:

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a b a c b
Pattern: a b a b a c b
F:       0 0 1 2 3 0 0
j:       0 1 2 3 4 5 6

在j==5的地方,比较卡壳了,因为这时Target[5] != Pattern[5],于是我们查找这时的F[j-1]==3,把j退回到3,继续比较。

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a b a c b
Pattern:     a b a b a c b
F:           0 0 1 2 3 0 0
j:           0 1 2 3 4 5 6

这个时候发现Target[i] == Patern[j],于是继续前进

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a b a c b
Pattern:     a b a b a c b
F:           0 0 1 2 3 0 0
j:           0 1 2 3 4 5 6

到i=7, j=5时,比较又卡壳了。故伎重演,设j=F[j-1](如果依然Target[i] != Pattern[j],则j=F[j-1]直到j=0。 如果这时候仍然 Target[i] !=Pattern[j],就需要跳过Target[i]了)

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a b a c b
Pattern:         a b a b a c b
F:               0 0 1 2 3 0 0
j:               0 1 2 3 4 5 6

剩下就一路比较到结束。
如果把目标字符串(Target)改成abababadcb:

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a d a c b
Pattern:         a b a b a c b
F:               0 0 1 2 3 0 0
j:               0 1 2 3 4 5 6

Target[i] != Pattern[j], 则j = F[j-1] = 1

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a d a c b
Pattern:             a b a b a c b
F:                   0 0 1 2 3 0 0
j:                   0 1 2 3 4 5 6

仍然Target[i] != Pattern[j],则j = F[j-1] = 0

i:       0 1 2 3 4 5 6 7 8 9 .
Target:  a b a b a b a d a c b
Pattern:               a b a b a c b
F:                     0 0 1 2 3 0 0
j:                     0 1 2 3 4 5 6

仍然Target[i] != Pattern[j],且j==0,则跳过Target[i],从Target的下一个字符开始比较。


以上介绍了KMP算法是如何使用神奇的P数组,在O(n)内完成字符串的搜索。下面的内容自然就是如何计算这个P数组,而且也还是在O(n)时间内。不过可能有些绕弯和复杂。
事实上,计算P数组本身只和模式字符串(Pattern)相关,而与目标(Target)字符串无关。因此我们只关注Pattern:ababacb。P数组的正式名称为失效函数(Failure Function),f(j)的定义为,当模式字符串中第j+1个字符(Pattern[j])与目标字符串失配(mismatch)时,模式Pattern中应该由哪个字符(新的j)与刚才失配的字符对齐继续进行比较。对于我们刚才的使用的P数组而言,f(j) = F[j-1]。
f(j)实际上就是要寻找最大k(k<j)使得:

 p0 p1 ... pk-1 pk = pj-k pj-k+1 ... pj-1 pj

似乎看到了模式匹配的影子?的确,这个查找最大k的过程,实质上仍然是一个模式匹配的过程,只是目标和模式现在是同一个串的一部分。
我们使用递推的方式计算数组F(类似DP,但并不满足最优子问题)。设f(j)=F[j-1] = k,则

 p0 p1 ... pk-1 pk = pj-k pj-k+1 ... pj-1 pj

如果pk+1 == pj+1,则f(j+1) = f(j) + 1
如果pk+1 != pj+1,则需要在 p0 p1 … pk-1 pk寻找子串 p0 p1 … ph-1 ph使得

 p0 p1 ... ph-1 ph = pk-h pk-h+1 ... pk-1 pk = pj-h pj-h+1 ... pj-1 pj

如果ph+1 == pj+1,则可知f(j+1) = f(k)+1 = f(f(j)) +1;
如果ph+1 == pj+1,则需要在p0 p1 … ph-1 ph中寻找更小的h,把pj+1给匹配进来。如果死活找不到这个h,则f(j+1)=0,即把j归0位。
继续Pattern=ababacb的例子:

a b a b a c b
0 0 1 ? ? ? ?
0 1 2 3 4 5 6

对于j=3,有f(3) = f(2) + 1 = 2

Pattern: a b a b a c b
F:       0 0 1 2 3 ? ?
j:       0 1 2 3 4 5 6

当j=5时,F[4] == 3,Pattern[5] != Pattern[3],

Pattern: a b a b a c b
F:       0 0 1 2 3 ? ?
j:       0 1 2 3 4 5 6
             a b a b

那就试试f(2)=F[2-1]=1,发现Pattern[5] != Pattern[1]

Pattern: a b a b a c b
F:       0 0 1 2 3 ? ?
j:       0 1 2 3 4 5 6
                 a b a b

因此f(6) = F[5] = 0。
由于P只需要根据Pattern生成一次,所以KMP特别适用于多个Target字符串,单个Pattern串的情况。


下面给出算法的Java实现。我自己写的,可能还有些错误。遗憾的是Java里String.indexOf()方法的实现仍然是用朴素的字符串匹配方法。

	public static int indexOf(String target, String pattern){
		if(pattern.length() == 0){
			return 0;
		}
		int[] fail = getFail(pattern);
		int i=0,j=0;
		while(i < target.length()){
			if(target.charAt(i) == pattern.charAt(j)){
				i++;j++;
			}else if(j == 0){
				i++;
			}else{
				//往回查找j的位置
				j = fail[j-1];
			}
		}
		if(j < pattern.length()){
			return -1;
		}else{
			return i - pattern.length();
		}
	}
	//生成失效函数
	private static int[] getFail(String pattern){
		int[] fail = new int[pattern.length()];
		for(int j=1;j  0){
				k = fail[k];
			}
			if(pattern.charAt(k) == pattern.charAt(j)){
				fail[j] = k + 1;
			}else{
				fail[j] = 0;
			}
		}
		return fail;
	}

foxmail不错

没有公司邮箱的时候盼公司邮箱,有了公司邮箱的时候纠结用什么软件收邮件。总之,我这个人很爱操心,很爱纠结。。大家bs我好了……
一开始用的gmail,考虑到之前一直是用自己gmail的私人邮箱处理公事,用gmail收信不会耽误事情。但是,实践和时间证明,还是会耽误事情,因为该死的gmail延迟20分钟以上,有时候比较着急的邮件刷半天也刷不出来。实在不适合我这种急性子。
第二个选择是office的outlook。一开始装office的时候顺道装上了outlook,可我怎么都觉得outlook难看,又觉得2007版的很占资源,虐待我512的内存,所以,用了半天就抛弃了。
final选择——foxmail。一开始我楞没想到这个软件,今天早上脑袋里突然浮现出了foxmail的红色图标,就上网搜了一个装一下。界面很友好,最最最最关键的是:可以直接把由excel转化成的cvs通讯录导进去!!!超级简单,之前我导入gmail的时候就失败了,以为是excel变来的cvs不能用呢,原来不是啊~~
总结:经过一番折腾,foxmail胜出。

魔都果然是个金钱都市

这么感慨只是因为今天中午去公司对面交通银行开户的时候意识到魔都什么都要收费——在银行办业务,银行要复印你的身份证,居然还要问客户收5毛钱。。真无聊。。。
我现在算是明白为什么很多人在魔都都是月光族,我曾经一度坚信自己是可以存一点钱的,但是现在……我努力的目标是:不月光

排序算法简单小结

最近开始准备算法。本来看的是Algorithms(算法概论),后来瞄了瞄算法导论,决定还是从基本的数据结构看起。Eric Raymond曾经说过:Show me your code and conceal your data structure, and I shall continue to be mystified. Show me your data structure, and I won’t need your code; it’ll be obvious.
虽然看的是数据结构的书(殷人昆的那本C++),但第一眼还是算法–排序。主要是昨天的IBM笔试,把排序的一些基本知识都给忘光了。下面进入正题。
排序首先分为内排序和外排序。其中内排序是基础,外排序则是在内存不足的情况下,对内排序进行了一些相应的改进。
简单的内排序主要有以下几种:
插入排序

简单插入排序。假定前i个元素有序,从后面不断取出元素插入前面有序数组中。复杂度为O(n2),最好情况为O(n)(数组原本有序)。稳定排序。
折半插入排序。简单插入排序的改进。在插入过程中,使用折半搜索找到插入的位置,其他方法相同。虽然比较的次数下降到了O(nlogn),但是移动次数仍然为O(n2)。稳定排序。
链表插入排序。改进简单插入排序,减少了移动次数。但比较次数仍然为O(n2)。稳定排序。
希尔排序(Shell Sort)。每隔几个元素为一组,进行简单插入排序;不断缩小间隔直至对整个数组进行简单插入排序。由于前面阶段每次排序元素数目少,所以开销不大;后面阶段中,每次参与排序的元素或多或少都有点排序的基础了,所以效率也挺高。没有确定的时间复杂度分析结果。Knuth做了大量的实验,结论大概为O(n^1.25-1.6n^1.25)不稳定排序(听说有稳定排序的实现,但先权当不稳定)。

交换排序

冒泡排序。不断交换,每一次Pass都把最小的换到最前面或者把最大的换到最后面。O(n2)。稳定。这是我接触的第一个算法,不过当时死活没明白,因为只有小学3、4年级。老爸倒是明白了,当时无限崇拜。
快速排序。Divide & Conquer思想。面试里很喜欢考的排序算法。把小于数组里某个数(这个数的挑选有学问)的数放到这个数的左边,大于的数放到这个数的右边。然后递归排序其左右两边的子数组。O(nlogn)。不稳定。

选择排序

直接选择排序。每次从后面的数组里挑最小的一个放到最左边。O(n^2)。不稳定排序。
竞标赛排序(胜者树)。改进了选择算法,使用两两比较的算法选择最小数。O(nlogn)。稳定排序。
堆排序。所有的parent < child。O(n)的建堆时间,很适合求一个数组内top n个数。O(nlogn)排序时间。不稳定排序

归并排序(Merge Sort)

2路归并排序。迭代或递归的从小到大不断合并。O(nlogn)。稳定排序。

基数排序

扩展了桶排序(Bin Sort)的思想,把桶分级了。可能需要消耗较多的空间。应用受到局限。O(d(n+radix)),d为级别数,radix为桶数(假定每一级的桶数相等)。稳定排序。

外排序主要在归并排序上做文章。如k路平衡归并,扩大初始归并段,还有借鉴了霍夫曼树的最佳归并树优化,均是为了减少IO次数。值得一提的是,k路平衡归并里使用了败者树。和胜者树不同的是,败者树每次选择了败者进入上级节点,而把胜者信息单独记录。从而简化了每次重构(UpdateTree)的过程,比如不需要判定对手是在左边还是右边,直接和parent做比较(因为上次的败者被选择进了parent)。
另外排序算法还有其他的衡量指标,比如空间复杂度。部分算法是就地排序,使用O(1)的空间,比如插入排序(链表除外),直接选择排序,堆排序,交换排序。
只是简单的整理。更详细的资料可参阅维基百科词条:排序算法Sorting Algorithm

5.22 城北骑行自驾游

这几天闲来无事又缺乏锻炼,于是继续和小翼同学骑车自驾游。今天的路线在城北,终点燕子矶。全程27公里。

522路线图
路线图

先发路线图。从汉口路出发->丹凤街->仁和街->百子亭->玄武湖环湖路->黄家圩->和燕路到燕子矶。回程绕幕燕沿江观光带(下燕路)->中央北路->中央路->傅厚岗->仁和街->丹凤街去金润发买东西。
燕子矶门票十元,没进去看。在外面随便拍了点照片。
远看的燕子矶,能看到点石体。
燕子矶外围
燕子矶外围

江边,隐约看到二桥。画面正前方那个背书包背对镜头的小孩子一分钟后将在爬墙时光荣的掉入江水里。
20090522181
江水,远处江面有几艘船。对面是八卦洲。从地图上看,这个地方的纬度位置大约相当于泰山新村。
20090522184
八卦洲

回程,从下燕路(下关-燕子矶)走。本来以为是一条沿江的破路,没想到最近新修得很漂亮,适合新手练车以及七十码。
路边的不知名寺庙
20090522188
路的一侧是幕府山,一侧是长江,很不错的景致
20090522189
幕府山北侧的景点:头台洞、二台洞和三台洞。收没票,没高兴进去。拼音打出来的是投胎洞、二胎洞,囧。。
20090522194
江上是中石化的加油站,不过是给船加油,稀罕
20090522195
最后是在靠近南京港的地方,南京大屠杀下关草鞋峡遇难同胞纪念碑。在这里日军杀害了手无寸铁的五万七千名放下武器的士兵和平民,是南京大屠杀中遇难同胞最多的地方。
20090522196
草鞋峡纪念碑

消费券盛况

5.20是南大校庆(虽然这日子和建校不搭介,哪天城头变幻大王旗不知北大会不会改改校庆玩玩),学校给每人发了10块钱餐票,仅当日有效。由于院里同一年级的同学大多数都不在学校(实习或者在家里),所以我们工学的研究生一人拿了20块钱,加上舍友辉哥天津出差,于是我们宿舍3人占有了80块钱的餐票,前一天摩拳擦掌准备大吃特吃(对面宿舍更绝,彭同学估计一人掌控了100块钱)。
我们的习惯是11点半开饭,为了抢吃炒菜,特地11点就到食堂,无奈发现炒菜窗口早已人头攒动,估计要排20分钟的队。旁边大娘水饺无人问津,显然不收券,嗯,不和谐。三个人华丽地合伙打了25块钱的中饭,一边抱怨着一食堂昂贵的价格。
吃饱了决定去水果超市碰碰运气。没想到居然是人山人海。小小的水超,不到10平方米的房间,除了四周的水果架子以外,里面至少挤了20个人,和高峰期的公交车有得一拼。不断有人在为凑齐10块钱的水果而不超标太多而绞尽脑汁。我的预算比较松,扛了一个最贵的西瓜,3.2一斤,在闷热极度拥挤的房间里排了10多分钟的队才交上票。我的预算是25元,抱了个24.4的瓜。
由于下午去了趟浦口,晚上回来得晚了。拿了10块钱的券本来打算吃开心哈利的炸鸡,无奈虽然排队付账的人不多,但排队等吃的人没有一个排也有2个班。炒菜早已售罄收摊,水超依然人满为患,连售卖饮料的窗口也排起了长队。最后拿券换了一篮的杨梅,掏钱吃饭走人。
事实上南大这次消费券的投入并不大,按鼓楼在校学生大约1.8万人估计,投入消费券面值18万,实际使用金额还要再打一个折扣。却也造成如此盛况(如果正常吃食堂的话,中餐+晚餐10元其实还是有点扛不住的),消费刺激作用可见一般。寒假回家时适逢台湾发放消费券,每人3600新台币,折合人民币900元。杭州也在年初发放了类似的市民消费券,人手50元。4万亿的经济刺激计划,摊到14亿中国人头上,就是每人RMB2850,但是这个计划几乎都是投资部分,消费只是其中很小的一块。出口短时间是没指望了。一边在投资上下猛药,一边忽视不温不火内需,国民收入总不能一直这么花吧?