算法导论教学视频中的911

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

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

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

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

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

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

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;
	}

排序算法简单小结

最近开始准备算法。本来看的是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