October 21, 2006

献给新访客的一则笑话——古老,但很神奇

  • 诶,我的Blog已经很久没更新了——得菌痢住院5天啦,今天终于出来啦!
  • Lava-Lava平台上“科技自主创新网”部落的同仁们可能已经看到了我的Blog荣幸进入你们的“酋长推荐”标签页,但似乎没有人给我的文章留下评论。我估计这是大家都没有接触过Scheme语言的缘故。所以我决定写一篇这样的文章,让新来的访客们看一看Scheme时间的精彩——当然,我没有要贬低其它语言(除了Java)的意思,事实上我最喜欢的语言之一就是C。好了,不罗嗦了,让我们一起来推开Scheme那道门吧。

人物:XX大学计算机系的社友Go4——8呆,F1,老农和小四。
时间:“晚汇报”时间…
(老农在计算机系混的时间不短了,可惜技术一直没长进——连打电脑游戏都“不上档次”(小四语)。这不,昨天打CS又被F1欺负了,现在正郁闷着呢。)

老农(上网无聊中):这年头,电脑好的人就是吃香啊~~(旁白)俺编程也差,游戏也差,废啦~~有了,上CSDN.net,找点文章进修一下。
F1:老农,怎么样,CS技术不行啊!好好练啊!
小四(推推眼镜):老农伯伯!算了吧,我看你还是把编程学学好吧,哈哈,你那本《数据结构》好像还是新的吧?
(老农翻着最新的Blog文章,忽然眼睛一亮。)
小四:嘿,发现什么了?
老农(连忙把Firefox最小化):没什么,又不是黄网激动什么?!
老农(旁白):不错,就用这篇文章K.O.他们。
老农(满脸堆笑):嘿,你们仨过来,我在网上发现一道数据结构方面的面试题目,想不想试试?
(正在上铺捧着SICP发呆的8呆忽然从发呆状态切换到亢奋状态,人啊~~)
8呆:废话,快说!
老农(奸笑中):设计一个函数visit_tail,要求通过一次遍历找到链表中倒数第n个节点,然后从它开始用函数func例遍后面的所有链表元素,链表可能相当大,可使用辅助空间,但是辅助空间的数目必须固定,不能和n有关。还有,不需要给出链表的其它操作函数。
8呆(再次切换回发呆状态):无聊…这也能叫题目…
老农(怒):那你做啊!
(8呆在他的SICP上写了点什么,然后很潇洒地离开了寝室)
老农(专向另两个人):既然他不参加,那我们三个比吧。
小四、F1(信心实足):那现在开始计时吧。
……
(老农把刚才背下来的代码改了改,花了3分钟抄在终端里)
老农(觉得时机移到):我好了,你们呢?
小四(大喊):解决解决!
F1:恩,我也好了,只是还没做单元测试。
(老农、小四:寒~~)
(3个人凑在老农的电脑上开始比拼)
老农:你们看,我是用C写的,已经测试过的,思想是,用两根指针,第一根先出发,相距n步后第二根出发。然后同时步进,直到第一根指针达到末尾,然后用func()函数对第二个指针开始的子链表进行例遍。
(小四和F1仔细一看,大笑不止。老农的代码如下(注释是笔者所加,其实是F1和小四看到各句时的反应):)

typedef struct{ //哟,终于不用struct Node了,
int data;
Node * next;
} Node;
Node * visit_tail(iNode * head,int n,void (* func)(int cur)){ //K&R时代的函数指针,见到老佛爷啦!!

Node *pfirst; //吓,终于用到匈牙利命名法了(老农:寒~)
Node *psecond;

pfirst=head;

for(int i=0;inext;
}
psecond=head;
while(pfirst!=NULL) {
pfirst=pfirst->next;
psecond=psecond->next;
} //什么破编程风格,真实版初学者啊
for(int i=0;idata)
psecond=psecond->next;
}
} //靠,一个函数解决所有问题,高耦合啊!

老农(再怒):小四少罗嗦!你的呢!
小四:在这儿,好好学学吧!
(大家一看,小四也用了C,但…好漂亮啊。代码如下(注释是小四的解释):)

typedef int int
//没有C++一样“泛型”!
typedef struct {
T data;
Node * next;
} Node;

typedef struct {
Node * pre;
Node * curr; //当前结点与List绑定,不但可以分步例遍,还能保证正确初始化
} List;

void init_curr (List * l) {
l->curr = l->pre; //什么叫“编程风格”,知道不?!
}

void visit (List *l, void (* func)(T data)) {
while (l-curr) { //少用!=NULL
(* func)(l-curr->data);
l-curr = l->curr->next;
}
} //使用内联结点的公用访问函数,降低耦合

void index (List * l, int n) {
if (n) return NULL; //结点下标从1开始
init_curr(l);
if (n > 0) {
for (int i = 0; i <>curr = l->curr->next;
} //小四(自我陶醉):漂亮啊
if (n < n =" -n;" tmp =" l-">curr;
for (int i = 0; i < tmp =" tmp-">next;
}
while (tmp) {
l->curr = l->curr->next;
tmp = tmp->next;
}
}
return l->curr; //方便用户,增强鲁棒性能
}

void visit_tail (List * l, int n, void (* func)(T data)) {
index(l, -n);
visit(l, func);
} //多清爽

老农(倒):我的挽回面子计划就这么,完了…
小四(偷笑):你还是面对现实吧…
F1:小四你先别得意,我的你们还没拜读过呢!
(大家跑到F1电脑前一看,先是被长度吓了一跳,然后,无语。)

package datastruct.fifi.com.baidu.hi; //加入我的数据结构Java包

//当然只有包内成员才能创建Node对象
protected class Node {
private Object data; //多态性,让你们的typedef去死吧!
private Node next;

//构造函数
Node (data) {
this.data = data;
}

//获取下一个元素
public Node next () {
return this.next;
}

//变换下一个元素,用户不能调用
protected void setNext (o) {
this.next = o;
}

//取数据
public Object getData () {
return this.data;
}

//换数据
public void setData (data) {
this.data = data;
}

//根据Effective Java的最高指导,要重写toString()方法
public String toString () {
return this.data.toString();
}

//自定义异常类,数据结构下标越界
public class IndexOutOfRangeException extend IndexOutOfBoundsException {
public IndexOutOfRangeException (int lower, int upper, int index) {
super("Lower: " + lower +", Upper: " + upper + ", index: " + index);
}
}

//用于被继承的访问类
public class Visitor {
public operation (Object o) { //访问操作
System.out.println(o);
}

public class List {
private Node preFix;
private Node current;
private int size;

List () {
current = preFix = new Node();
size = 0;
}

//自定义异常类,数据结构下标越界
public class IndexOutOfRangeException extend IndexOutOfBoundsException {
public IndexOutOfRangeException (int lower, int upper, int index) {
super("Lower: " + lower +", Upper: " + upper + ", index: " + index);
}
}

//用于被继承的访问类
public class Visitor {
public operation (Object o) { //访问操作
System.out.println(o);
}
}

public class List {
private Node preFix;
private Node current;
private int size;

List () {
current = preFix = new Node();
size = 0;
}

//定位函数
private void index (int i) throws IndexOutOfRangeException {
if (i > size || i < -size-1) { thows new IndexOutOfRangeException(0, size+1, i); } this.init(); if (i >= 0) {
for (int j=0; j < current =" current.next();" i =" -" j="0;" current =" current.next();" current =" current.next();">_< 。。。 F1:we得意。 (3人正在争执着,忽然门“吱”的一声(什么破门)开了,8呆走了进来。) 8呆:吵什么呢,比完了没?我是第一吧? F1、小四、老农:什么啊,你不是自顾自走了啦?! 8呆(诧异):我走之前已经写好啦。 (8呆拿来他的SICP,只见上面写了一行Scheme代码:) (define (list-visit-tail l n func) (for-each (list-tail l n) func)) (编辑公曰:上面这段Scheme代码滴意思斯酱紫滴:先定义一个名为list-visit-tail的过程,然后用内置宏list-tail取list的后n位组成list返回,再用操作for-each进行例遍。) (完) 结语: 这个题目其实对于任何函数式编程语言来说都是一两句话。Scheme、Haskell它们都来自于世界上第二古老的语言Lisp,但它们的思想博大精深——基于lambda演算理论的函数式编程,古老,但很神奇。 参考资料:

October 20, 2006

上帝是个程序员——从《黑客帝国》看软件设计

上个星期我把《黑客帝国(The Matrix)》三步曲完完整整看了一遍,联想到《UNIX程序设计的艺术》一书中所说的17条UNIX哲学,深感古老的UNIX确实是软件设计思想的集中体现。Robert Metaclf说过:“如果将来有什么技术取代了以太网,那么它仍会叫以太网。”也许,The Matrix的内部开发代号中就有"UNIX"这个单词吧!
  1. 模块性原则:写简单的,通过干净的接口可被连接的部件。相比UNIX,The Matrix绝对是一个超巨大的软件,它的设计中也充分体现了模块化思想:整个系统有后台数据库(这可是墨菲斯说的)用来存储任何物件;前台有和人脑后插座相连的输入/输出接口,当然程序还可以使用电话工作;前后台之间有由知情者们组成的控制程序,标准的MVC模式。
  2. 清楚原则:清楚要比小聪明好。The Matrix的源代码我们是没法儿研究啦。不过,整个人类世界居然能被显示在3个小小的液晶屏里,而且接线员告诉我们…他能…知道这儿有个金发美女,从这一点你应该能想像出The Matrix有多么的清晰。
  3. 合并原则:设计能被其它程序连接的程序。这个还用的着多罗嗦吗?天哪,与人类的物理接口,用手机可以和真实世界交流,通过固定电话进行接入和切断,还有那个设计近乎完美的、和外界传输意识的火车站,真是一应俱全
  4. 分离原则:从机制分离从策略,从实现分离出接口。The Matrix的实现相当复杂,但设计了很好的接口;看看墨菲斯他们使用的训练场吧,它其实是早期The Matrix版本的一个变型,程序员能很轻松地控制它(最拉风的是第一部中凭空制造的那两排枪,无敌了)这说明了The Matrix的接口设计非常简洁而有序。
  5. 简单原则:设计要简单;只有当你需要的时候,增加复杂性。The Matrix的简单恐怕初看电影的人体会不深:看这那些标志性的绿色字符就烦。但你仔细想想,这可是一个时间啊!从接线员的一些操作细节也能感受到The Matrix的简单——火线教学只需要搜索内容然后按下LOAD按钮,我晕。
  6. 节俭原则:只有当被证实是清晰,其它什么也不做的时候,才写大的程序。The Matrix的的在节俭上下的功夫我们很难研究,毕竟这只存在于源代码中。个人感觉,至少从电影上看是非常华丽的,不过那是为了票房而下的功夫。
  7. 透明原则:为使检查和调试明显更容易而设计。The Matrix的调试只提供了输出部分(安全的需要吗,设计师本人是不能搬演上帝的),在每艘飞船上都有一个GUI程序来检索它的调试信息,黑客们用它作了GIS,哈哈。
  8. 健壮性原则:健壮性是透明和简单的追随者。没有一个软件没有Bugs,但有的软件可以让Bug在关键时刻失去用武之地,比如那个火车站,即便是尼奥这样的超人也无能为力啊,只能从这个出口跑会另一个出口啊。
  9. 表现原则:把知识整理成资料,于是程序逻辑能变得易理解和精力充沛的。不知道设计师是怎样开发The Matrix的,不过从它那5个前世可以看出它的进步是卓有成效的。犯错误不要紧,把经验记下来是最重要的,不是吗?
  10. 最小意外原则:在接口设计中,总是做最小意外事情。当然The Matrix也有让人超意外的时候,比如莫罗恩手下那些打不死的程序们。但别忘了他们来自于黑客帝国的早期版本。后来的莫罗恩自己和开锁人不都是普通人吗?
  11. 沉默原则:当一个程序令人吃惊什么也不说的时候,他应该就是什么也不说。《UNIX程序设计的艺术》的作者对这个原则的解释有问题(或者是翻译问题)。沉默原则应当是这样:当一个程序按照用户已有的知识完成了工作是,他应该什么也不说。UNIX命令行的程序通通是这样,尤其是gcc。The Matrix是不是为一群人按照自己的意志控制另一群人而设计的,也不存在除维护人员以外的用户,当然不会“说”什么。
  12. 修补补救:当你必须失败的时候,尽可能快的吵闹地失败。这也就是The Matrix中偶尔出现的幻象、上帝、狼人的出现了就很快消失的原因。
  13. 经济原则:程序员的时间是宝贵的;优先机器时间节约它。无语了,The Matrix工作的硬件系统都是算法设计复杂度度量是使用的“理想计算机”,感觉起来一切都是原子操作,只有与人传输数据时有忽略不计的延迟。
  14. 产生原则:避免手工堆砌;当你可能的时候,编写可以写程序的程序。这个原则的前提是,系统本身必须是可以被自描述的,就像Lisp语言。The Matrix也是被高度自描述的,那些维护程序不都是用The Matrix本身创建的吗即便是莫罗恩或史密斯这样的大拿?利用系统本身工作可是黑客帝国的一大特色啊。
  15. 优化原则:在雕琢之前先有原型;在你优化它之前,先让他可以运行。这就是The Matrix有6个版本的原因——事实上人们讨论的最多的第一个版本就是一个原型,设计师自己也说,仅仅用作测试,后来“庄稼全死了”。
  16. 差异原则:怀疑所有声称的“唯一真理“。最后这个The Matrix版本有它的“唯一真理”,可惜设计师在努力之后仍然留下了尼奥这个“余数bug”。设计师是绝对的马克思主义者,也只有在他的眼中,世界是物质的。
  17. 可扩展原则:为将来做设计,因为它可能比你认为来的要快。The Matrix的可扩展能力具体有多强我们不得而知,不过在电影中它确实被扩展了:希安的程序员们为他们的训练场程序编写了原本不存在的应急保护模块,能让可能损坏人体的元素失去真实(什么是真实?)质地,否则尼奥第一次“跳楼”就要被摔死了。:)
《黑客帝国》绝对是一部不看人生不完整的电影,不但充满了哲理,而且对于UNIX/Linux的爱好者们而言很有感召力,不是吗?:)

October 19, 2006

Scheme数据结构——向量也疯狂

学过数据结构的人都知道,一棵完全二叉树(除去最低层元素从左到右排列,其它的层都为满的)可以保存在一个数组中 这个完全二叉树存储在vector中就像这样:#(4 6 2 8 7 3) 可以看出一个结点下标 i 与它的左右孩子结点下标 j1,j2具有如下关系: j2 = 2*i+1,j2 = 2*i +2 然后,我们通过调整数组上结点的排列,把它变成一个“二叉堆”。这里显示的是一个最大堆,即:对于(vector-length a) => n,当(< (+ (* 2 i) 1) n) => #t时,有(> (vector-ref a (+ (* 2 i) 1)) (vector-ref a i) => #t;当(< (+ (* 2 i) 2) n) => #t时,有(> (vector-ref a (+ (* 2 i) 2)) (vector-ref a i) => #t。
这么复杂的S-exp,说白了,就是每个结点的左右孩子结点(如果有的话)都小于这个结点。 知道了这些,我们就可以来考虑一下怎么把完全二叉树转成最大堆了。方法是,先根据公式求出第一个非叶结点下标(define n (/ (- n 1) 2)),然后比较左孩子结点(vector-ref a (+ (* 2 i)与右孩子结点(vector-ref a (+ (* 2 i)的大小,将较大者与(vector-ref a i)比较,如果更大就互换。然后对于(- i 1),(- i 2)完成以上步骤,于是,最大堆就构造完成了。
  • 调整结点8:

  • 调整结点3:
  • 调整结点8:最后,最大堆有什么用呢?废话,当然是堆排序了;我最喜欢的排序算法就是它,因为它充分体现了“数形结合”的思想。 因为排序过程中整个堆都在大规模地变化(当然反应到数组上不是这么回事),所有就不再进行图解了;大致将一下即可:先将整个vector调整为最大堆,然后将堆顶的那个最大的元素与堆中最后一个元素互换,接着调整前 (- n 1) 个元素为最大堆,再将堆顶元素与堆中最后一个元素互换。。。如此反复(其实就是逐个排出最大元素),时间复杂度为O(n*log2 n)。
大致代码(缺少heep的实现):
(define (heep-sort a)
(let* ((n (vector-length a))(tmp 0) (i (- n 1)))

(begin
(make-heep a) ;调整a为最大堆
(when (> i 0)
(set! tmp (vector-ref a 0))
(vector-set! a 0 (vector-ref a 1))

(vector-set! a 1 tmp)
(heep a i 0) ;在向量a上从下标0开始调整长度为i的一段为最大堆
(set! i (- i 1)))))) ;最好用尾递归代替循环

October 18, 2006

Scheme数据结构——list数组

(By the way,我终于知道如何删除list的头节点了。必须要使符号指向一个头节点,再由它确定当前节点,否则无非实现元素“脱链”)

list数组,顾名思义,由list构成的数组(或矩阵),学名不明。它不是一种具体的数据结构,但常被用作表示其它数据结构。它的一般形式如下:
当然,这里的vector也可以是一个矩阵。用Scheme实现时,注意list不需要随机插入元素的功能,但能随机脱链,且只能从尾部插入(技巧:(set-cdr! (list-tail l (- (length l) 1)) obj))——list中的数据往往是无序的。可以把它设计得更强,但并不实用。
下面讲它的两种典型应用。

一. 图的邻接表存储结构
  • 用矩阵的第一列存储结点,第二列存储结点的号码,第三列保存此行所对应结点链接的边的结束结点号码(当然还可以再用一列保存权值)。这样说有点绕人,我们看个例子好了:
  • 这是一个有向图。让我们看看它是怎样用邻接表存储的:
  • 复杂吗?吓,一点也不。它可以用来对付边较稀疏的有向图。
二. 链表法解决哈希冲突
  • 讲解哈希表关键字冲突的一般思路是建立哈希函数组,逐个调用。但这个方法有个明显缺点:如果把哈希表作为一种服务提供给操作对象的话用户就麻烦大了。用向量上链接的list来保存hash code相同的元素是个不错的办法。
  • 例子:这个哈希表的元素是 a={16, 54, 66, 43, 29, 55...},m=13,哈希函数为 h(K)=K mod m(省略了其它7个元素):
  • 用矩阵的第一列存储基本元素,多于的“同位素”保存在临时开辟的list中。这样的设计重点考虑了效率,比较使用,但会给元素的删除带精神分裂般的麻烦,插入元素也够戗;如果你追求程序的“漂亮”,可以把所有元都平起平坐地存储在list里,不使用矩阵,元素的删除操作全是脱链。
从上面两个例子可以看出,list数组主要用于解决稀疏的“同位素”的存储问题。确定“同位素”的同位存储性质(比如同hash code元素的平等地位)和必要性(比如有向图结点拥有边与其起始结点的偏序关系),是使用它的重要前提。

October 17, 2006

let, let* 和 letrec(转)

使用 let, let*, letrec 都可以在当前环境中构造局部变量。这种 变量的生命会延续到这个环境消失为止。

这就像 C 语言里的

{  int x = 10;
int y = 20;

foo(x,y);
}

但是有一点不同就是,Scheme 的 let 生成的环境是分配在堆里而不 是像 C 那样分配在栈里的。所以 let 的局部变量有可能在 let 的 block 执行完毕以后还继续存在,只要有某些东西引用到它们。

这样我们可以制造一些返回函数的函数,这些函数拥有自己的状态记 忆,而这些记忆并不是全局变量,它们有点像 C 函数的 static 变 量。

下面是几个例子:

(define (function-gen n)
(let ((local-var 0))
(lambda ()
(display "The local-var is ")
(display local-var)
(newline)
(set! local-var (+ 1 local-var)))))

(define f1 (function-gen 0))
(define f2 (function-gen 100))

(f1)
(f2)

函数 function-gen 接受一个参数 n,并且把它保存到自己的局部变 量 local-var. 它返回一个新的函数,这个函数被调用就会打印 local-var 的值,并且把 local-var 的值加 1.

我们用 0 和 100 作为参数传递给 function-gen,生成了两个函数 f1 和 f2. 这是两个起点不同的计数器。f1 从 0 开始,而 f2 从 100 开始。每次被调用两个函数都打印自己的数字,并且加 1.

可见,f1 和 f2 所见到的 local-var 是两个不同的空间。也就是说, 每次调用 function-gen,都会由 let 生成一个新的变量 local-var, 这个变量将一直伴随新生成的函数。

注意 let 里的 binding 是这样产生的。首先,进入 let 时,我们 只看到外层的绑定,然后每个 let 绑定的右边被 eval,然后这些值 被放到临时的一些空间,所有的右边都求值完毕后,这些值被一一赋 给左边的名字。

如果我们的代码不是那么简单,我们在 let 里生成了一个函数。比 如这样:

(define (func-gen)
(let ((x 10) (y 20))
(lambda (a b)
(+ x y a b))))

(define bar (func-gen))

func-gen 函数中被调用时,它在 let 空间中生成了一个函数,并且 作为 func-gen 的返回值送到外层,它被绑定到最外层的环境中的 bar 变量。那么这个函数引用了这个环境,这个 let frame 不会被 回收。

bar 如果在外层层环境被调用,那么它的名字绑定环境仍然是 let 里面的环境。也就是说,它仍然可以使用局部变量 x 和 y!

如果我们调用

(f 1 2)

就得到结果 33.

这相当于同时赋值。

所以在下面这种情况里,内层的 let 绑定 b 时,实际上使用的是外 层的 x 在计算。

(let ((x 10)    ; bindings of x
(a 20))) ; and a
+----------------------------------------------------------+
| (foo x) scope of outer x |
| (let ((x (bar)) and a |
| (b (baz x x))) |
| +------------------------------------------------+ |
| | (quux x a) scope of inner x | |
| | (quux y b) and b | ) |
| +------------------------------------------------+ |
| (baz x a) |
| (baz x b) |
+----------------------------------------------------------+

October 15, 2006

Scheme数据结构——简单二叉树


终于把用Scheme写的二叉树发出来了,前两天网卡没钱拉。

层序例遍算法写出来了,可惜用不了——没法儿用Scheme实现队列!我折腾了半天,写出这么一个怪物:
(define queue list)
(define (queue-push! q o)
(set-cdr! (list-tail q (- (length q) 1)) (cons o null)))

可pop还是实现不了——不知道怎么删除pair,于是放弃。
把简单二叉树的实现贴在这儿吧,虽然没有一点实用价值。

(define null '()) ;用null代替空list

(define (child data left right) ;用它来申明一个“孩子”
(cons data (cons left right)))
(define (child? o)
(and (pair? o) (pair? (cdr o))))
(define (child.data o)
(if (child? o)
(car o)))
(define (set-child.data! o data)
(if (child? o)
(set-car! o data)))
(define (child.left o)
(if (child? o)
(cadr o)))
(define (set-child.left! o p)
(if (child? o)
(set-car! (cdr o)) p))
(define (child.right o)
(if (child? o)
(cddr o)))
(define (set-child.right! o p)
(if (child? o)
(set-cdr! (cdr o) p)))

;先序例遍递归版,用一句(if (not (null? o)...)省了不少判断
(define (child-tree-pre o func)
(if (not (null? o))
(begin
(func (child.data o))
(child-tree-pre (child.left o) func)
(child-tree-pre (child.right o) func))))
;后序例遍
(define (child-tree-post o func)
(if (not (null? o))
(begin
(child-tree-post (child.left o) func)
(child-tree-post (child.right o) func)
(func (child.data o)))))
;。。。中序~~可怜没有层序和分步例遍~~
(define (child-tree-mid o func)
(if (not (null? o))
(begin
(child-tree-mid (child.left o) func)
(func (child.data o))
(child-tree-mid (child.right o) func))))

October 11, 2006

比Boyer-Moore更快的字符串查找算法(转)

字符串查找算法中,最著名的两个是KMP算法(Knuth-Morris-Pratt)和BM算法(Boyer-Moore)。两个算法在最坏情况下均具有线性的查找时间。但是在实用上,KMP算法并不比最简单的c库函数strstr()快多少,而BM算法则往往比KMP算法快上3-5倍。但是BM算法还不是最快的算法,这里介绍一种比BM算法更快一些的查找算法。

例如我们要在"substring searching algorithm"查找"search",刚开始时,把子串与文本左边对齐:

substring searching algorithm
search
^


结 果在第二个字符处发现不匹配,于是要把子串往后移动。但是该移动多少呢?这就是各种算法各显神通的地方了,最简单的做法是移动一个字符位置;KMP是利用 已经匹配部分的信息来移动;BM算法是做反向比较,并根据已经匹配的部分来确定移动量。这里要介绍的方法是看紧跟在当前子串之后的那个字符(上图中的 'i')。

显然,不管移动多少,这个字符是肯定要参加下一步的比较的,也就是说,如果下一步匹配到了,这个字符必须在子串内。所以,可以 移动子串,使子串中的最右边的这个字符与它对齐。现在子串'search'中并不存在'i',则说明可以直接跳过一大片,从'i'之后的那个字符开始作下 一步的比较,如下图:

substring searching algorithm
    search
    ^

比较的结果,第一个字符就不匹配,再看子串后面的那个字符,是'r',它在子串中出现在倒数第三位,于是把子串向前移动三位,使两个'r'对齐,如下:

substring searching algorithm
      search
       ^

哈!这次匹配成功了!回顾整个过程,我们只移动了两次子串就找到了匹配位置,是不是很神啊?!可以证明,用这个算法,每一步的移动量都比BM算法要大,所以肯定比BM算法更快。

下面是这个算法的c代码。注意我假设了每个字符的值都介于0-127之间(即纯ascii码)。

char *qsearch(const char *text, int n, const char *patt, int m)
{
// get the length of the text and the pattern, if necessary
if (n < 0)
n = strlen(text);
if (m < 0)
m = strlen(patt);
if (m == 0)
return (char*)text;

// construct delta shift table
int td[128];
for (int c = 0; c < 128; c++)
td[c] = m + 1;
const char* p;
for (p=patt; *p; p++)
td[*p] = m - (p - patt);

// start searching...
const char *t, *tx = text;

// the main searching loop
while (tx + m <= text + n) {
for (p = patt, t = tx; *p; ++p, ++t) {
if (*p != *t) // found a mismatch
break;
}
if (*p == 0) // Yes! we found it!
return (char*)tx;
tx += td[tx[m]]; // move the pattern by a distance
}
return NULL;
}

注:这个查找算法称为Sunday算法,它是BM算法的一种改进型。

October 9, 2006

Scheme数据结构——神奇的pair

Scheme中提供了两种可以存储泛型数据的数据结构——list和vector。list其实是一个链表,它是由pair实现的。学了Scheme很久才发现,原来pair其实是其它语言中我们超常用的Node(节点)!这下我总算理解了Scheme是怎样架起数据结构的了。

一. 链表(内部已实现)
  • 每个pair的左值存储数据,右值存放指针(Scheme中cons值即引用):
  • 然后用指针指向(其实是赋值)下一个pair,链中最后一个为null(空值):
  • 这个语法树转换成Scheme代码就是(value . (value . (value . null)));当然只是“意思”一下,实际不可能有同名符号。
二. 二叉树(正在编写中…)
  • 又一个用节点连起来的基本数据结构。每个二叉树child(孩子)可以这样设计,用第一个pair的左值保存数据,右值为另一个pair,它的左右值表示孩子的指针:
  • 代码为(value . (left . right))
  • 然后,n个这样的child组合为一棵二叉树。同样,没有孩子时指针为null:
  • 上面这棵树的代码大致是这样:(value . ((value . ((value . (null . null)) . (value . (null . null)))) . (value . (null . null))))。
  • 我的二叉树实现正在写着,面前至少可以用(child)代替(cons(cons))申明孩子节点,下一步编写tree的整个数据结构支持,最好能支持一次例遍。
有了链表和二叉树,队列,栈,线索二叉树之类就都能直接实现了。其它的数据结构如集合,树,图,HashTable等等多是由向量+pair构成的,不典型,暂且不提。

October 7, 2006

谈谈数组元素左旋问题(下)

上篇中 ,我给出了一些解决数组元素左移问题的不符合要求或很慢的解法。在下篇中,我将讲解3个很有“来头”(至少出现在《编程珠玑》里了:))的O(n)级算法。
第一个算法思想上和我曾经想到的一个直接插入元素的算法基本相同,也是通过递归地逐个放置数组元素来左旋数组的。只不过我使用的方法是逐个临时测试是否可以按照公式放置,而这个算法经过周密计算,直接给出了符合各种情况的通用下标计算方法:
对于任意的元素下标 i,它的新下标 i` = ((k - m) + i ) % k。
代码如下:

//先求最大公约数,其实有很多方法,这个叫“辗转相除法”
int facter (int i, int j) {
while (i != j) {
if (i > j) {
i -=j;
} else {
j -= i;
}
}
return i;
}
//元素互换函数,无聊。。。
void swap (int *n1, int *n2) {
int tmp;
tmp = *n1;
*n1 = *n2;
*n2 = *tmp;
}
void turn_left (int *a, int m, int k) {
int i, tmp, f;
k = k % m;
if (k == 0) {
return;
}
f = facter(m, k);
if (f == 1) {
i = 0;
do {
i = (i+k)%m;
swap(a, a+1);
} while (i != 0);
} else {
for (i = 0; i < tmp =" i;" tmp =" (tmp+k)%m;" href="http://lichray.blogspot.com/2006/09/blog-post_25.html">那篇文章中用图示详细分析了数组元素的位移过程。如果你想找到一点新的灵感,可以把一些代表元素的卡片(凯库勒?!)用线穿起来反复地排,看看开始时和结束时,数组产生了哪些变化:
似乎看不出什么端倪。。。但如果把每种颜色的线都反序一下,
呵,很漂亮啊。再分析一下——原来,要让数组元素左旋m位,只需先将数组全部反序,然后将下标0~m-1和m~k-1的两部分分别反序就行了!最快的算法居然就这么简单!
用Python表示一下(这个代码是可运行的):

def turn_left (a, m):
a.reverse()
a[:m].reverse()
a[m:].reverse()
return

C代码就不用写了吧,用上面那个swap()函数+循环将数组反序而已啊。
最后一个方法,说真的,我没看懂。。。据说利用了数论知识,也很快,原理和本文第一个其实区别不大,只是计算新下标的算法更先进了:

/*照抄编程珠玑,伪代码(汗~),懒得翻译。n是数组长度,r是左旋位数,gcd()函数计算最大公约数,for后面的式子表示i的取值范围。*/
for i = [0, gcd(r, n))
t = x[i]
j = i
loop
k = j + r
if k >= i
k -= n
if k == i
break
x[j] = x[k]
j = k
x[j] = t

还得好好学数学啊!理解这个会有时!
真希望所有的程序员(代码书写er就算了)人手一本《编程珠玑》(Programming Pearls),书中自有黄金物,早日摆脱索然无趣的代码!

October 1, 2006

Scheme学习笔记(五)

(这应当是最后一篇了,明天来继续数组左旋...

八. 宏
  • MIT-Scheme的宏定义:
    (define-syntax 宏名
    (syntax-rules()
    ((模板) 操作))
    . . . ))
  • 在操作时,先用let绑定参数,然后用lambda定义过程:
    (let ((本地操作 (lambda 参数 宏主体 ...)))
    (lambda (e r)
    (apply 本地操作 (cdr e))))
  • (define-syntax start
    (syntax-rules ()
    ((start exp1)
    exp1)
    ((start exp1 exp2 ...)
    (let ((temp exp1)) (start exp2 ...))) ))
  • 实现的短小的定义:
    (define-macro MACRO-NAME
    (lambda MACRO-ARGS
    MACRO-BODY ...))
九. 结构
  • 结构模板定义:(defstruct 结构名 属性(不构成list))
(defstruct tree height girth age leaf-shape leaf-color)
  • 新建结构:(make-结构名 属性符号 值 符号 值)
(define coconut
(make-tree ’height 30

’leaf-shape ’frond
’age 5))
  • 使用结构:(结构名.属性名 结构对象);返回属性值,(set!结构名.属性名 结构对象 新属性值);更改属性值
(tree.leaf-shape coconut) => frond
(set!tree.height coconut 40)
(tree.height coconut) => 40
  • defstruct本身未提供,需要宏来定义,具体不再写出。
十. 面向对象
  • Scheme标准默认未提供!我看也用不着。
祝所有Sceme的爱好者们早日摆脱无聊和世俗的coder世界!