Eye Free的诞生是为了解决一个实际问题。在TMM英语培训中有这样一种题型:给出一个由英文字母组成的12*12的矩阵,听录音,然后把听到的单词从这个矩阵中找出来,本来是要练习听力,却本末倒置,听懂单词并不困难,但找起来相当费劲(容易导致眼疲劳,所以起名叫Eye Free)。每个单词在矩阵中都呈一条直线排列,可以把整个矩阵看作一个坐标系,单词就相当于坐标系中的一个矢量,从坐标系中某一点开始,向8个方向(东,南,西,北,东南,东北,西南,西北)中的某个方向走,并终结于这个坐标系内。
我们不难把它抽象成一个编程问题:
输入:一个字符串
输出:矩阵中出现过这个字符串的所有位置
大部分人一开始都会想到的一种方法是,每查一个单词,就从单词的第一个字母开始匹配,如果矩阵中某个字母和所查单词的首字母一样,就从它相邻的字母中寻找单词的第二个字母,依此类推。也就是说,查每个单词查找都要遍历整个矩阵,暂且把选中一个字母之后向8个方向匹配的操作复杂度看成1,于是这种算法的复杂度就是遍历矩阵的复杂度即n*n。
稍加考虑后,我想出另外一个更巧妙的办法。把矩阵切成一行一行的,共N行,则所有横向排列的单词都能在这N行中找到。按此方法,从所有的8个方向去切这个矩阵,就得到一个字典,所谓的字典就是所有可能出现的单词都必定是其中某个或某些条目的子串。在字典的每个条目中使用查找子字符串的函数strstr便可快速找出所查单词。此算法的复杂度即为字典中条目的个数,它是一个常数与N的乘积,是线性增长的。用大O法来表示,之前的遍历矩阵的算法复杂度是O(n*n),字典法则是O(n)。
本程序的代码实现中,有三点值得借鉴:
(1) 避免重复计算,哪怕多占用一些空间。见函数make_invert,为了避免重复计算,还在结构体node_t中加了一个字段,char *end。
(2) if和switch尽量写在循环外,哪怕为此多定义一两个变量。见函数mask。
(3) 为了减少编码时的麻烦,可以加一些冗余但方便理解和操作的数据结构。例如记录矩阵四条边的char * edge[4][N]。
以上三条可以归结为一点,那就是牺牲空间来减少时间,包括程序运行时间和编写程序的时间。现在的趋势就是主存越来越大,CPU的速度却提升缓慢,开发人员的时间也很宝贵。
与用空间去换取时间相比,算法设计更具决定性作用,这里又出现一个博弈,好的算法需要更多的设计时间,程序执行时间减少,开发时间却可能增加,因此一味优化算法也不可取。我认为算法设计中最关键的因素是宏观上的策略选择,而非细节的优化。因此在编码之前先放开思路,看能不能用某种巧妙的机制把算法复杂度减少一个数量级,如果想不出来,就只能按常规算法编码了,但此时还不要急于动手写代码,把要用到的数据结构画在纸上,找出存在大量重复计算的地方,并设法避免(通常要牺牲空间)。一句话,决定效率的最关键因素是算法,其次是数据结构,最后才是编码细节。
打破传统思路的回报是丰厚的,除了获得算法上质的飞跃,友好的界面往往也是创新的结果。为了在矩阵中快捷地定位单词,我给矩阵加了坐标系,它不是常规的从原点开始不断变大的两条坐标轴,而是四条包围整个矩阵的坐标轴,它有四个原点,分别是矩阵的四个角,目标离哪个角近就把哪个角作为原点。举例来说,假如矩阵的大小是12*12,在传统坐标系中的某个点(11,10),对应在新坐标系中的位置是(1,2),从而大大减少了从原点(0,0)出发进行检索的时间。
压缩包里有
1 EyeFree.c 程序代码
2 EyeFree.exe 可执行程序
3 matrix.txt 保存矩阵用的文本文件(确保此文件和可执行程序在同一目录下,且不要更改文件名)
程序使用说明
本程序是为了解决特定问题,因此只适用于12*12的方阵,如果想用来处理大小不为12的方阵,只要把代码中的宏N改成相应的值,然后重新编译。
windows命令行窗口的默认高度是25行,建议把窗口的高度改成26(仅对12*12的矩阵),这样看起来会更爽。
下面是本程序的350多行代码,逻辑复杂,晦涩难懂,再过半个月恐怕连我自己也看不懂了,但那并不重要。
#include <stdlib.h>
#include <string.h>
//////////////////////////////////////////////////////// macro and enum
#define N 12
//#define DEBUG
enum direction_t{NORTH, NE, EAST, SE, SOUTH, SW, WEST, NW, MAX};
enum edge_t{TOP, RIGHT, BOTTOM, LEFT};
//////////////////////////////////////////////////////// type definition
typedef struct{
char str[N + 1];
char *begin;
char *end;
int direction;
}node_t;
//////////////////////////////////////////////////////// function prototype
void calc_seq(int seq[N]);
void init_dic(char *edge[4][N], const char matrix[N][N]);
void init_edge(char *edge[4][N], const char matrix[N][N]);
void load_matrix(char matrix[N][N]);
void make_invert(int *i_dic, int drct, int offset);
void mask(int begin, int len, node_t node, const char matrix[N][N]);
void match_word(char *str, const char matrix[N][N]);
void print_matrix(const char matrix[N][N], const int seq[N]);
void show_answer(const char matrix[N][N], const int seq[N]);
//////////////////////////////////////////////////////// global variable
int match[N][N];
node_t dic[N * 12 - 4]; // 4N + 4(2N-1) = 12N - 4
int main(void)
{
char matrix[N][N];
char * edge[4][N];
char str[N + 1];
int seq[N];
#ifdef DEBUG
int i;
int drct = EAST;
#endif
(void)load_matrix(matrix);
(void)init_edge(edge, matrix);
(void)init_dic(edge, matrix);
(void)calc_seq(seq);
#ifdef DEBUG
// print dictionary
for(i = 0; i < N * 12 - 4; ++i){
if(dic[i].direction != drct)
printf("--------------------------\n");
printf("%s\n", dic[i].str);
drct = dic[i].direction;
}
#endif
printf("\
-------------------------------\n\
Eye Free \n\
\n\
dc10101@gmail.com \n\
-------------------------------\n");
while(1){
print_matrix(matrix, seq);
printf("Search word: ");
scanf("%s", str);
match_word(str, matrix);
show_answer(matrix, seq);
}
return 0;
}
void calc_seq(int seq[N])
{
int i;
int mid = (N % 2) ? (N / 2 + 1) : (N / 2);
for(i = 1; i <= mid; ++i)
seq[i - 1] = seq[N - i] = i;
}
void init_dic(char *edge[4][N], const char matrix[N][N])
{
int i_dic = 0;
int i_str;
int i; // edge iter
char *p;
int maxlen;
// EAST
for(i = 0; i < N; ++i){
dic[i_dic].direction = EAST;
memcpy(dic[i_dic].str, matrix[i], N);
dic[i_dic].begin = edge[LEFT][i];
dic[i_dic].end = edge[RIGHT][i];
++i_dic;
}
// WEST
make_invert(&i_dic, WEST, N);
// NORTH
for(i = 0; i < N; ++i){
dic[i_dic].direction = NORTH;
p = edge[BOTTOM][i];
dic[i_dic].begin = p;
for(i_str = 0; i_str < N; ++i_str){
dic[i_dic].str[i_str] = *p;
p -= N;
}
dic[i_dic].end = p + N;
++i_dic;
}
// SOUTH
make_invert(&i_dic, SOUTH, N);
// NE
maxlen = 1;
for(i = 0; i < N; ++i){
dic[i_dic].direction = NE;
p = edge[LEFT][i];
dic[i_dic].begin = p;
for(i_str = 0; i_str < maxlen; ++i_str){
dic[i_dic].str[i_str] = *p;
p -= N - 1;
}
dic[i_dic].end = p + (N - 1);
++maxlen;
++i_dic;
}
maxlen = N - 1;
for(i = 1; i < N; ++i){
dic[i_dic].direction = NE;
p = edge[BOTTOM][i];
dic[i_dic].begin = p;
for(i_str = 0; i_str < maxlen; ++i_str){
dic[i_dic].str[i_str] = *p;
p -= N - 1;
}
dic[i_dic].end = p + (N - 1);
--maxlen;
++i_dic;
}
// SW
make_invert(&i_dic, SW, 2 * N - 1);
// SE
maxlen = N;
for(i = 0; i < N; ++i){
dic[i_dic].direction = SE;
p = edge[LEFT][i];
dic[i_dic].begin = p;
for(i_str = 0; i_str < maxlen; ++i_str){
dic[i_dic].str[i_str] = *p;
p += N + 1;
}
dic[i_dic].end = p - (N + 1);
--maxlen;
++i_dic;
}
maxlen = N - 1;
for(i = 1; i < N; ++i){
dic[i_dic].direction = SE;
p = edge[TOP][i];
dic[i_dic].begin = p;
for(i_str = 0; i_str < maxlen; ++i_str){
dic[i_dic].str[i_str] = *p;
p += N + 1;
}
dic[i_dic].end = p - (N + 1);
--maxlen;
++i_dic;
}
// NW
make_invert(&i_dic, NW, 2 * N - 1);
}
void init_edge(char *edge[4][N], const char matrix[N][N])