约瑟夫问题的解法-良好接口的重要性
2012-01-01 13:05
246 查看
本文用一个简单的例子来说明接口设计的重要性。使用的是Linux kernel中list_head,顺便说一句,如果你想使用复合模式组织你的对象,那么Linux kernel中的kobject结构是个不错的选择,如果时间允许,我准备用一下,想象一下Linux是如何组织缤纷复杂的总线和外部以及内部设备的吧。
从一个古老的又比较简单的问题说起,这个问题就是古罗马的约瑟夫问题:设有n个人围坐在圆桌周围,现从某个位置 i 上的人开始报数,数到 m 的人就站出来。下一个人,即原来的第m+1个位置上的人,又从1开始报数,再是数到m的人站出来。依次重复下去,直到全部的人都站出来,按出列的先后又可得到一个新的序列。为了节省篇幅,将i定为1。
对于解决这类问题,最后留下的最有价值的东西就是问题本身的解决思路,而不是一堆看起来很棒的代码,这些代码用尽了所有的语法特性,为了寻求更多的荣誉,动用了大量的编程语言…这些都是垃圾!古罗马没有编程语言,但是人家照样完美的解决了这个问题,编程语言只是一个工具,所以不要动不动就代码啊代码,实现啊实现,貌似啊貌似,作为从事精准工作的程序员,如果你觉得你的想法有道理,那就直说,绝对不要使用诸如“貌似…吧”之类的短语,而这种短语在编程相关的论坛上十分常见,实际上发言人所要做的仅仅是呈出一个意见,十有八九是在挑刺,挑出楼上一个人代码的一个毛病。还是那句老话,编程并不一定比烧锅炉更有技术含量,它在某种意义上称不上一种真正的技术,就像锄头一样,工具而已,真想搞有技术含量的,那就去搞火箭,卫星,生物医学,基因工程,超弦之类的…言归正传,不管任何算法题目,最有价值的东西就是算法本身,也就是解决方案的思路,而不是什么代码。
以上的单链表实现,我们发现了两个问题,第一个问题就是在go_to函数中维护了三个指针,分别是当前指针,当前指针的前一个,当前指针的下一个。为了维护这三个指针中的后两个,一定要在链表初始化时定位最后一个节点以及保存链表头。第二个问题就是在main函数中用递推方法组织了整个算法,实际上我们发现,这是个明显可以递归解决的问题。关于第一个问题,交给了list_head实现,关于第二个问题,以下给出递归实现。
以上的代码看来,明显简洁了不少,没有用main函数组织逻辑,算法逻辑全部交给了go_to,需要注意的是go_to的返回值,由于只是介绍思路,因此没有将“出列者”保存于任何容器,只是简单的printf打印,而实际上,应该另外单独维护一个容器,比如栈或者队列,然后在printf的地方将出列者放进队列,然后在main函数中统一打印之。
在递归实现中,go_to函数中的三个指针依旧,能否去除它们呢?那就是使用双向链表,可是我们怎么实现双向链表呢?
接下来就不贴代码了,和单向链表相区别的是初始化函数以及go_to函数,不必再维护三个指针了,也不用维护全局变量了,只需要简单取出prev和next即可。现在,唯一的问题就是代码还是体现不出解决问题的思路,还是堆砌了很多编程技巧相关的代码,比如链表操作之类的,很显然的做法就是将这些封装成接口,然而这种封装只能让这个约瑟夫问题看起来好些,对于其它的问题是无法重用的,因为entry结构体是不可重用的。
Linux内核中list_head解决了这个问题。
有了上述接口,连同kernel自带的,我给出使用list_head实现的约瑟夫问题的代码:
可见,这个代码很简单,除了我的函数以及变量命名不是很好之外,如果命名良好,只要看英语就可以知道整个解决思路了,没有任何让人看来是编程者专利的东西。
本文出自 “我来,我看,我征服” 博客,请务必保留此出处http://dog250.blog.51cto.com/2466061/1270877
从一个古老的又比较简单的问题说起,这个问题就是古罗马的约瑟夫问题:设有n个人围坐在圆桌周围,现从某个位置 i 上的人开始报数,数到 m 的人就站出来。下一个人,即原来的第m+1个位置上的人,又从1开始报数,再是数到m的人站出来。依次重复下去,直到全部的人都站出来,按出列的先后又可得到一个新的序列。为了节省篇幅,将i定为1。
对于解决这类问题,最后留下的最有价值的东西就是问题本身的解决思路,而不是一堆看起来很棒的代码,这些代码用尽了所有的语法特性,为了寻求更多的荣誉,动用了大量的编程语言…这些都是垃圾!古罗马没有编程语言,但是人家照样完美的解决了这个问题,编程语言只是一个工具,所以不要动不动就代码啊代码,实现啊实现,貌似啊貌似,作为从事精准工作的程序员,如果你觉得你的想法有道理,那就直说,绝对不要使用诸如“貌似…吧”之类的短语,而这种短语在编程相关的论坛上十分常见,实际上发言人所要做的仅仅是呈出一个意见,十有八九是在挑刺,挑出楼上一个人代码的一个毛病。还是那句老话,编程并不一定比烧锅炉更有技术含量,它在某种意义上称不上一种真正的技术,就像锄头一样,工具而已,真想搞有技术含量的,那就去搞火箭,卫星,生物医学,基因工程,超弦之类的…言归正传,不管任何算法题目,最有价值的东西就是算法本身,也就是解决方案的思路,而不是什么代码。
垃圾实现:
用一个main函数实现的那种代码绝对是写给会C语言的人看的,不懂编程的人根本看不懂,竟然还上了wiki,这种实现绝对是垃圾!在这里也就不贴代码了,google一下或者摆渡一下,全都是这种代码。我也不是说我实现的就一定好,其实我写的代码也很垃圾,但是绝对比把所有逻辑都交给main要好。以下是我的实现,一步一步引导,最终说明接口的重要性。普通单链表实现:
以下的代码使用了最常见的普通单向链表,一般都是这种设计的,如果你不想花点时间设计通用接口的话。虽然这种实现体现了封装,但是还是没有办法直接体现算法的思路,没法让人一眼看出你是怎么想的。下面是代码:#include <stdio.h> struct entry { int value; struct entry *next; }; struct entry *g_list = NULL; struct entry *g_last = NULL; void init_list(int *vs, int len) { int i = 0; struct entry *head = (struct entry*)calloc(1, sizeof(struct entry)); struct entry *first = g_list = head; head->value = vs[0]; for (i = 1; i < len; i++) { struct entry *next = (struct entry*)calloc(1, sizeof(struct entry)); next->value = vs[i]; head->next = next; head = next; } head->next = first; g_last = head; } int go_to(struct entry *list, int T) { struct entry *pre = g_last; struct entry *curr = list; struct entry *next = list->next; int i = 0; for (i = 0; i < T-1; i ++) { pre = pre->next; curr = curr->next; next = next->next; } pre->next = next; g_list = next; g_last = pre; return curr->value; } int main (int argc, const char * argv[]) { int va[] = {1,2,3,4,5,6,7,8}; init_list(va, sizeof(va)/sizeof(int)); struct entry *thread = g_list; do { printf("out man:%d\n", go_to(g_list, 4)); }while (g_last != g_list); printf("last man:%d\n", go_to(g_list, 4)); return 0; }
以上的单链表实现,我们发现了两个问题,第一个问题就是在go_to函数中维护了三个指针,分别是当前指针,当前指针的前一个,当前指针的下一个。为了维护这三个指针中的后两个,一定要在链表初始化时定位最后一个节点以及保存链表头。第二个问题就是在main函数中用递推方法组织了整个算法,实际上我们发现,这是个明显可以递归解决的问题。关于第一个问题,交给了list_head实现,关于第二个问题,以下给出递归实现。
递归实现
基本思路没有什么变化,只是更加清晰了,使用了递归#include <stdio.h> #include <stdlib.h> struct entry { int value; struct entry *next; }; struct entry* init_list(int *vs, int len) { int i = 0; struct entry *head = (struct entry*)calloc(1, sizeof(struct entry)); struct entry *first = head; head->value = vs[0]; for (i = 1; i < len; i++) { struct entry *next = (struct entry*)calloc(1, sizeof(struct entry)); next->value = vs[i]; head->next = next; head = next; } head->next = first; return head; } void go_to(struct entry *list, struct entry *pre_l, int T) { struct entry *pre = pre_l; struct entry *curr = list; struct entry *next = list->next; int i = 0; if (list->next == list) { printf("last:%d\n", list->value); return; } for (i = 0; i < T-1; i ++) { pre = pre->next; curr = curr->next; next = next->next; } pre->next = next; printf("ddddd:%d\n", curr->value); go_to(next, pre, T); //return curr->value; } int main (int argc, const char * argv[]) { int va[] = {1,2,3,4,5,6,7,8}; struct entry *ll = init_list(va, sizeof(va)/sizeof(int)); go_to(ll->next, ll, 4); return 0; }
以上的代码看来,明显简洁了不少,没有用main函数组织逻辑,算法逻辑全部交给了go_to,需要注意的是go_to的返回值,由于只是介绍思路,因此没有将“出列者”保存于任何容器,只是简单的printf打印,而实际上,应该另外单独维护一个容器,比如栈或者队列,然后在printf的地方将出列者放进队列,然后在main函数中统一打印之。
在递归实现中,go_to函数中的三个指针依旧,能否去除它们呢?那就是使用双向链表,可是我们怎么实现双向链表呢?
简单双向链表实现
一步一步的,现在使用双向链表实现,所谓双向链表,那无非就是在单向链表中增加了一个指针,那就是:struct entry { int value; struct entry *next, *prev; };
接下来就不贴代码了,和单向链表相区别的是初始化函数以及go_to函数,不必再维护三个指针了,也不用维护全局变量了,只需要简单取出prev和next即可。现在,唯一的问题就是代码还是体现不出解决问题的思路,还是堆砌了很多编程技巧相关的代码,比如链表操作之类的,很显然的做法就是将这些封装成接口,然而这种封装只能让这个约瑟夫问题看起来好些,对于其它的问题是无法重用的,因为entry结构体是不可重用的。
Linux内核中list_head解决了这个问题。
list_head实现
Linux内核中的list_head是一个体现OO思想的良好设计的“侵入式”双向链表。所谓侵入式,那就是它不和任何特定的数据结构相耦合,谁想将自己组织成链表,只需简单包含list_head字段即可,然后可以通过该字段在对应结构体中的偏移来根据list_head字段的地址取出相应结构体的地址,十分方便好用,内核的list.h头文件定义了几乎所有的操作,在约瑟夫实现中,还需要一个接口,那就是list_step,该接口实现向前(向后-还没有实现)推进N的功能:struct list_head *list_step(struct list_head* list, int step) { int i = 0; while(i++ < step) list = list->next; return list; }
有了上述接口,连同kernel自带的,我给出使用list_head实现的约瑟夫问题的代码:
#include <stdio.h> #include <stdlib.h> #include "list.h" struct entry { struct list_head list; int value; }; struct list_head *init_list(int *vs, int len) { int i = 0; struct list_head *head = NULL; for (i = 0; i < len; i++) { struct entry *next = (struct entry*)calloc(1, sizeof(struct entry)); INIT_LIST_HEAD(&next->list); next->value = vs[i]; if (i==0) head = &(next->list); else list_add_tail(&next->list, head); } return head; } void go_to(struct list_head *list, int T) { int i = 0; struct list_head *curr = NULL, *next = NULL; struct entry *e = NULL, *e2; if (list_empty(list)) { e = list_entry(list, struct entry, list); printf("last:%d\n", e->value); return; } curr = list_step(list, T-1); next = list_step(curr, 1); list_del(curr); e = list_entry(curr, struct entry, list); printf("curr:%d \n", e->value); go_to(next, T); } int main (int argc, const char * argv[]) { int va[] = {1,2,3,4,5,6,7,8}; struct list_head *head = init_list(va, sizeof(va)/sizeof(int)); go_to(head, 4); return 0; }
可见,这个代码很简单,除了我的函数以及变量命名不是很好之外,如果命名良好,只要看英语就可以知道整个解决思路了,没有任何让人看来是编程者专利的东西。
本文出自 “我来,我看,我征服” 博客,请务必保留此出处http://dog250.blog.51cto.com/2466061/1270877