吴昊品命令行解释程序核心算法(首映式) Round 1 —— miniSQL解释器(map容器映射+vector容器存储)(POJ 3699)
2012年上学期,华科大的Dian团队让我第一次感受到了工业级别代码 的独特魅力,尽管我离这些仍然比较遥远,但是已经颇为感慨(这里提下华科的一个妹纸,虽然知道我非常非常地水,不过,一直告诉我“学长加油学长加油”,直 到最后知道我其实是一个酱油,不过,真的很感谢她)。种子杯一向是偏研究,偏底层的计算机课题,当时我也准备过一些经典的DBMS系统,并认为不会超过这 个范围,没有想到初赛题目居然是一个小型的SQL解释器,复赛居然是一个小型的C语言编译器,震精了。不过,我也愿意写文章来总结那一次的活动,以及自己 的一些感悟与体会——关于SQL解释器更多的内容,我会在《吴昊品工业级别软件项目》中详细叙述。
SQL简介:
结构化查询语言(Structured Query Language)简称SQL,结构化查询语言是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统;同时也是数据库脚本文件的扩展名。结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统可以使用相同的结构化查询语言语言作为数据输入与管理的接口。结构化查询语言语句可以嵌套,这使他具有极大的灵活性和强大的功能。
LAMP四件套:
在网页编码中,存在一种强力的“四件套”,组合如下(听说是最为王道的组合)—— LAMP,也就是Linux+Apache+MySQL+PHP
至于MySQL,则是SQL的一个变种,我们这里只考虑实现一个最简易的SQL的功能,(比较复杂的SQL实现需要用到标准的递归下降语法树的方法,我会将其列入到《吴昊品工业级别软件项目》中),输出要求有一个比较靓丽的UI,也就是在外围布上一道边框啦!
Source:POJ 3699
我们的miniSQL具备一个标准的SQL最基本的功能,首先说下我们的miniSQL具备什么。
miniSQL的输入:
输入的第一部分是一个单一的线组成的三个整数,m(1≤m≤10),n(1≤N≤10000),K(1≤K≤100),表示在表中的列的数目(也就是一个record的项目(item)也就是一个数据的具体属性),n表示记录的record的数目,而K表示查询的总数目。
输入的第二部分是表中的描述,由m行。的第i个线的这部分由两个字符串组成,COLUMN_NAME类型描述第i列的项目(item)的名字。
COLUMN_NAME含有只能使用字母(a--z,A--Z),数字(0-9)和下划线(_)。
类型可以是“STR”或“INT”(数字或者是字符),表示此列的类型。
输入端的第三部分,示出的表,它包含n行的内容。这部分的第i个线示出的第i个记录中的表,该表由m的项。
如果第i列的类型是“INT”,第i个项目将是一个32位的整数。
如果在第i列的类型是“STR”,第i个项目将是一个字符串只包含字母(a--z,A--Z),数字(0-9),下划线(_)。
最后一部分的输入给出的k是类似于SQL select语句的查询。该查询的格式选择column_list中的WHERE条件
column_list中包含一个列表,这是一个逗号分隔的列名。发生的所有名称的表中的描述。
条件是在格式COLUMN_NAME OP值(运算符的值)
column_name是一个项目的描述,该表中的列名。
如果列的类型是“STR”,OP值是“=”。否则,它可以是“=”,“<”或“>”。
如果列的类型是“STR”,value将为带引号的字符串——包含字母(a--z,A--Z),数字(0-9)和下划线(_)。否则, 这将是一个32位的整数。
输入的尺寸将不超过1M字节。
miniSQL的输出:
类似于如下形式,每一个这样的表格之间必须有一行的间隔。
我们的miniSQL的实现技巧(转载):
1.读入子段信息,保存在flist中。并使用map<string, int> ftoindex对象将字段名字与字段实际的列序号对应起来。
2.读入所有数据,将这些数据存到一个二维vector对象table中(这里是用二维vector容器来模拟二维数组)。虽然数据中有数字,有字符串,但是为了方便,一切当成是string对象处理。
3.逐个查询读入,逐个处理。
(1)先把select整个语句拆开(这里相当于语法分析),重点关注一下信息:要显示的字段的名称,要比较的字段,怎么比较(大于,小于,等于),和什么值作比较(这里相当于词法分析)。要显示的字段的编号保存在vector<int> display中。
(2)对整个table扫描。这时,map<string, int> ftoindex(映射容器)这个东西就发挥作用了,它能够很快地帮我们定位列的位置。然后进行比较。数字可以用atoi转换出来。
(3) (输出的格式,很纠结)对于符合要求的字段,将序号存储到vector<int >ans_set中。这时我们还关注另外一个信息,就是字段值的长度。因为表格输出时,每一个字段的宽度=最长的字段值长度+2,注意,如果字段标 题很长,那么也要进行考虑。这个信息在我的程序中保存在display_len中。
Highlights:
这一段代码的亮点实在是太多了!我将自己的感悟都写在注释中了,真的是非常非常地可圈可点,利用STL来模拟真心是一个很方便的事情,map容器和vector容器的配合很默契。
Solve:
2 #include <string>
3 #include <cstring>
4 #include <cstdlib>
5 #include <vector>
6 #include <map>
7
8 //用宏来定义数据类型的值,STR为字符串型而INT为整型
9 #define STR 1
10 #define INT 2
11
12 //用宏来定义运算符的值,在正规的SQL中需要用enum存储所有的关键词,并且在词法分析中用有限状态机来定义逻辑
13 #define EQUAL 1
14 #define BIGGER 2
15 #define SMALLER 3
16
17 using namespace std;
18
19 //分别来描述列的数目(column number),记录的数目(record number)和查询的数目(query number)
20 int nc, nq, nr;
21
22 //每一个item的类型和名字
23 typedef struct
24 {
25 int type;
26 string name;
27 }FIELD;
28
29 //这里的STL定义各种纠结啊!解释下:第一个vector容器定义了一个二维表table,第二个vector存储数据类型结构体,map根据项目的下标映射出具体的位置
30 vector<vector<string> > table;
31 vector<FIELD> flist;
32 //这里开了三个装载整型的容器,其中display装载每一个item的位置,display_len装载那个item名的长度(便于扩充),ans_set用于记录要输出的每一条record所在的行
33 vector<int> display, display_len, ans_set;
34 map<string, int> ftoindex;
35
36 //判断是不是一个数据,注意,这里所谓下划线和中划线都是可以的
37 int isdigit(char ch)
38 {
39 return ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0'&& ch <= '9') || ch == '_' || ch == '-');
40 }
41
42 //这里巧妙地用了一个for循环,将printf(" ")和最后的i++附加在一起了,这种写法适用于for循环内语句很少的情况
43 void draw_blank(int x)
44 {
45 for (int i = 0; i < x; i++,printf(" "));
46 }
47
48 //同理,上面是根据给出的数目画空白,而这里是根据给出的数目画中划线
49 void draw_line(int x)
50 {
51 for (int i = 0; i < x; i++,printf("-"));
52 }
53
54 //这里来完成最后的输出任务,其中的一些铺垫是附着在solve函数中实现的
55 void print_field()
56 {
57 int total_len = 0;
58 unsigned int i, j;
59 int front_blank, end_blank;
60 //输出每一行的长度,注意这里的+2指的是每一个item填入-之后还需要在首尾进行增补,而后面的继续补入display_len.size()-1的长度也与格式有关
61 for (i = 0; i < display_len.size(); i++)
62 total_len += (display_len[i] + 2);
63 printf("+");
64 draw_line(total_len + display_len.size() - 1);
65 printf("+\n");
66 //考虑了行之后,我们再来考虑列(这里注意我们暂时只考虑item部分的内容,record部分暂时不考虑)
67 for (i = 0; i < display.size(); i++)
68 {
69 int tlen = display_len[i] - flist[display[i]].name.length();
70 //这里有点难以费解,其实就是每一个item(列名)首位都有一些间隙,我们需要做的是计算出间隙的多少,并最终输出
71 front_blank = end_blank = tlen / 2;
72 if (tlen % 2)
73 end_blank = tlen - front_blank;
74 printf("| ");
75 draw_blank(front_blank);
76 printf("%s", flist[display[i]].name.c_str());
77 draw_blank(end_blank + 1);
78 }
79 printf("|\n");
80 //这里是第二道-------,主要是item名(项目名)下面的一道下划线
81 for (j = 0; j < display.size(); j++)
82 {
83 printf("|");
84 draw_line(display_len[j] + 2);
85 }
86 printf("|\n");
87 //现在我们来考虑record部分,考虑每一行的具体情况
88 for (i = 0; i < ans_set.size(); i++)
89 {
90 for (j = 0; j < display.size(); j++)
91 {
92 int tlen = display_len[j] - table[ans_set[i]][display[j]].length();
93 front_blank = end_blank = tlen / 2;
94 if (tlen % 2)
95 end_blank = tlen - front_blank;
96 printf("| ");
97 draw_blank(front_blank);
98 //输出每一个对应行与对应列的table中的内容
99 printf("%s", table[ans_set[i]][display[j]].c_str());
100 draw_blank(end_blank + 1);
101 }
102 printf("|\n");
103 }
104 //这里画出最后一行
105 printf("+");
106 draw_line(total_len + display_len.size() - 1);
107 printf("+\n");
108 }
109
110 void solve(char *cmd)
111 {
112 int end, i, op;
113 unsigned j;
114 string tmp, cmpfield, cmpvalue;
115 vector<string> record;
116 //首先,将这三个容器清空
117 display.clear(), display_len.clear(), ans_set.clear();
118 //这里要将最前面的select以及后面的一个空格读掉
119 i = 7;
120 //这里代表读到where的起始处的前面一个字符
121 end = strstr(cmd, "where") - cmd;
122 while (i != end)
123 {
124 if (cmd[i] == ',' || i == end - 1)
125 {
126 //找到项目名所对应的下标
127 display.push_back(ftoindex[tmp]);
128 display_len.push_back(flist[ftoindex[tmp]].name.length());
129 tmp.clear();
130 }
131 //否则,被视为该数据类型名并没有被读完,继续读下去
132 else
133 tmp += cmd[i];
134 i++;
135 }
136 //这里将中间的where以及后面的一个空格读掉
137 i += 6;
138 //判断条件,不过这里不考虑嵌套,也不考虑一些复杂的运算符比如>=,<=等等的情况,op的左右两边一个是cmpfield一个是cmpvalue
139 while (isdigit(cmd[i]))
140 {
141 cmpfield += cmd[i];
142 i++;
143 }
144 //载入运算符,这里不考虑OP符号前面会有空格的情况
145 if (cmd[i] == '>')
146 op = BIGGER;
147 else if (cmd[i] == '=')
148 op = EQUAL;
149 else if (cmd[i] == '<')
150 op = SMALLER;
151 //这里继续往后读,还是语法分析那种扫描的感觉比较爽一些啊!考虑到引号的存在,这里到了STR类型或者INT类型的数据之后才继续进行存储
152 while (!isdigit(cmd[i++]));
153 //这里的i--退后,因为前面的i++多进了一步
154 i--;
155 while (isdigit(cmd[i]))
156 {
157 cmpvalue += cmd[i];
158 i++;
159 }
160 int index = ftoindex[cmpfield];
161 int type = flist[index].type;
162 int intcmp;
163 if (type == INT)
164 //这里重新将字符串转为数字类型,利用C语言的atoi函数
165 intcmp = atoi(cmpvalue.c_str());
166 for (i = 0; i < nr; i++)
167 {
168 //这里设置一个标志变量,来标记是否找到了相应的查询数据
169 int ok = 0;
170 if (type == INT)
171 {
172 int num;
173 num = atoi(table[i][index].c_str());
174 if (op == EQUAL && num == intcmp)
175 ok = 1;
176 else if (op == SMALLER && num < intcmp)
177 ok = 1;
178 else if (op == BIGGER && num > intcmp)
179 ok = 1;
180 }
181 //如果项目类型为字符串的话,也可以通过vector容器的便捷性直接进行字符串比对
182 else if (table[i][index] == cmpvalue)
183 ok = 1;
184 if (ok)
185 {
186 /*record.clear();
187 record.resize(display.size());*/
188 //这里主要是为了扩充item列的长度,因为在输出的时候列名与列的内容是需要保持一致的
189 for (j = 0; j < display.size(); j++)
190 {
191 //如果说列名小于对应的内容的话,就将列名的长度扩充,保持列表无误
192 if (display_len[j] < table[i][display[j]].length())
193 display_len[j] = table[i][display[j]].length();
194 /*record[j] = table[i][display[j]];*/
195 }
196 //将那个有必要输出的记录(record)所在的行载入到容器中
197 ans_set.push_back(i);
198 /*ans_set.push_back(record);*/
199 }
200 }
201 }
202
203 int main()
204 {
205 int i, j;
206 char tmp1[100], tmp2[100];
207 //读入每一个参数
208 scanf("%d%d%d\n", &nc, &nr, &nq);
209 //用vector容器的resize方法预先分配好内存,可以提高效率
210 flist.resize(nc);
211 for (i = 0; i < nc; i++)
212 {
213 FIELD tf;
214 //一个装载项目的名字,一个装载项目的类型
215 scanf("%s%s", tmp1, tmp2);
216 //如果是INT型的
217 if (tmp2[0] == 'I')
218 tf.type = INT;
219 //如果是STR型的
220 else
221 tf.type = STR;
222 tf.name = tmp1;
223 flist[i] = tf;
224 //这里运用map容器找到每一个项目(item)的下标
225 ftoindex[tf.name] = i;
226 }
227 //同理,为table分配内存
228 table.resize(nr);
229 for (i = 0; i < nr; i++)
230 {
231 //定义一个装填record的字符串容器,这都是临时的
232 vector<string> record(nc);
233 for (j = 0; j < nc; j++)
234 {
235 scanf("%s", tmp1);
236 record[j] = string(tmp1);
237 }
238 //撇开STL,table像是一个二维的字符串数组
239 table[i] = record;
240 }
241 char cmd[10000];
242 //这里读掉一个回车
243 getchar();
244 for (i = 0; i < nq; i++)
245 {
246 //每读一条命令,解决一条命令
247 gets(cmd);
248 solve(cmd);
249 print_field();
250 printf("\n");
251 }
252 return 0;
253 }
254
255