约瑟夫问题的解法-良好接口的重要性

本文用一个简单的例子来说明接口设计的重要性。使用的是Linux kernel中list_head,顺便说一句,如果你想使用复合模式组织你的对象,那么Linux kernel中的kobject结构是个不错的选择,如果时间允许,我准备用一下,想象一下Linux是如何组织缤纷复杂的总线和外部以及内部设备的吧。
       从一个古老的又比较简单的问题说起,这个问题就是古罗马的约瑟夫问题:设有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; }

可见,这个代码很简单,除了我的函数以及变量命名不是很好之外,如果命名良好,只要看英语就可以知道整个解决思路了,没有任何让人看来是编程者专利的东西。



你可能感兴趣的:(编程,算法,struct,list,Go,linux内核)