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