Data Structure第五次作業講解
寫給讀者的話(務必閱讀)
期中以來,有不少同學向我反應代碼寫的慢、正確率不高等問題。由於OS已經爆炸閑的沒事干 因此我決定將自己原來寫的代碼重構重構,並整理成博客附上整個思路的講解。首先,我必須申明,博客所寫的東西,是供想提升自己代碼水平,理清寫碼思路的同學用的。我希望同學們能夠明白:作為一個考試分數占 80% 的學科,抄襲他人代碼完成作業不是白賺了那 20% 的分數,而是失去了一次良好的練兵的機會(從本次開始,文章中給出的代碼均已提交評測,抄襲需謹慎)。其次,個人所寫代碼只是提供一個寫碼、思考問題的思路,並不代表題目的唯一解法,更不代表最優解法沒有提交到課程網站上測試,只在本地通過測試數據,甚至可能有bug噢。我希望我的代碼能夠起到拋磚引玉的作用,能夠讓同學對課上內容有更深刻的理解,寫代碼時能夠有更多更好的想法。最后,我希望同學們在完成作業的同時,能夠對自己的代碼進行復雜度的分析。數據結構的使用,往往離不開對性能的約束,因此,掌握復雜度的分析也是這門課程重要的一環。
關於代碼風格
本文中所有的代碼風格皆采取 OO 的標准,同時作者也希望同學們能夠以這種標准約束自己,這樣也會方便助教 debug。簡單來說,大致約束如下:
1、符號后帶空格。
2、大括號不換行。
3、if、while、for 等括號兩端應該帶空格,並且一定要用大括號括起來。
4、一行不要寫過多字符(不超過60),較長的判斷可以換行處理。
5、縮進為 4 個空格,不同層次間要有合適的縮進。
6、一行只聲明一個變量,只執行一個語句。
關於使用到的工具
采取了dhy大佬的意見,決定新添加這個欄目,對本次代碼中使用到的基礎的一些數據結構或是函數進行一些簡單的講解,便於大家的使用和理解。
1、快速讀入
inline int read() { //快速讀入,可以放在自己的缺省源里面
int x = 0; //數字位
int f = 1; //符號位
char ch = getchar(); //讀入第一個字符
while (!isdigit(ch)) { //不是數字
if (ch == '-') { //特判負號
f = -1;
}
ch = getchar();
}
while (isdigit(ch)) { //讀入連續數字
x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1)
ch = getchar();
}
return x * f;
}
快速讀入是比較好用的一種讀入的寫法,我這里的實現是通過循環讀入直到得到下一個數字,在具體的題目中也可以根據自己的需要對循環的條件和結束條件做更改來讀入字符串等。(由於只涉及到簡單循環,這里不作更深入的講解)。切忌不經思考和理解就使用,容易出現讀入死循環等問題。
2、鏈式前向星
在解決需要建邊(如:樹、圖)相關的問題時,比較方便的一種數據結構。寫完后發現這次的作業並不會用到,可能會留到第七次作業再講。
第一題:樹葉節點遍歷(樹-基礎題)
題目描述
【問題描述】
從標准輸入中輸入一組整數,在輸入過程中按照左子結點值小於根結點值、右子結點值大於等於根結點值的方式構造一棵二叉查找樹,然后從左至右輸出所有樹中葉結點的值及高度(根結點的高度為1)。例如,若按照以下順序輸入一組整數:50、38、30、64、58、40、10、73、70、50、60、100、35,則生成下面的二叉查找樹:
從左到右的葉子結點包括:10、35、40、50、60、70、100,葉結點40的高度為3,其它葉結點的高度都為4。
【輸入形式】
先從標准輸入讀取整數的個數,然后從下一行開始輸入各個整數,整數之間以一個空格分隔。
【輸出形式】
按照從左到右的順序分行輸出葉結點的值及高度,值和高度之間以一個空格分隔。
【樣例輸入】
13
50 38 30 64 58 40 10 73 70 50 60 100 35
【樣例輸出】
10 4
35 4
40 3
50 4
60 4
70 4
100 4
【樣例說明】
按照從左到右的順序輸出葉結點(即沒有子樹的結點)的值和高度,每行輸出一個。
題目大意
按照題目所給的要求建一顆排序二叉樹。
題目思路
我們構造一個存儲權值、左右兒子信息的結構體來存儲這顆排序二叉樹即可。
代碼實現
#include<stdio.h>
#include<ctype.h>
#define maxn 1000005
inline int read() { //快速讀入,可以放在自己的缺省源里面
int x = 0; //數字位
int f = 1; //符號位
char ch = getchar(); //讀入第一個字符
while (!isdigit(ch)) { //不是數字
if (ch == '-') { //特判負號
f = -1;
}
ch = getchar();
}
while (isdigit(ch)) { //讀入連續數字
x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1)
ch = getchar();
}
return x * f;
}
typedef struct tree {
int son[2]; //存儲兒子,0代表左兒子,1代表右兒子。
int val;//存儲權值
} Tree;
Tree tr[maxn];
int root;
int cnt;
void ins(int* rt, int val) {
if (!(*rt)) {//不存在結點,可以新建一個存下當前值
*rt = ++cnt; //分配一個新的編號為 cnt + 1 的結點
tr[*rt].val = val;
return;
}
ins(&tr[*rt].son[tr[*rt].val <= val], val);
//當前結點存在,按照排序二叉樹的約束繼續嘗試插入
//顯然 t[*rt].val <= val 時,應該在右子樹 ,正好編號為 1
}
void dfs(int rt,int dep) {//遍歷子樹輸出
if (!rt) {
return;
}
if (!tr[rt].son[0] && !tr[rt].son[1]) {//為葉子結點。
printf("%d %d\n", tr[rt].val, dep);
}
// 繼續遍歷子樹,記得增加深度。
dfs(tr[rt].son[0], dep + 1);
dfs(tr[rt].son[1], dep + 1);
}
int main() {
int n = read();
int i;
for (i = 0; i < n; ++i) {
int x = read();
ins(&root, x);
}
dfs(root, 1);
return 0;
}
復雜度分析
最壞的情況,插入成一條單鏈,每次插入的復雜度是 O(n) ,總體的復雜度是 O(n²)。
第二題:詞頻統計(樹實現)
題目描述
【問題描述】
編寫程序統計一個英文文本文件中每個單詞的出現次數(詞頻統計),並將統計結果按單詞字典序輸出到屏幕上。
要求:程序應用二叉排序樹(BST)來存儲和統計讀入的單詞。
注:在此單詞為僅由字母組成的字符序列。包含大寫字母的單詞應將大寫字母轉換為小寫字母后統計。在生成二叉排序樹不做平衡處理。
【輸入形式】
打開當前目錄下文件article.txt,從中讀取英文單詞進行詞頻統計。
【輸出形式】
程序應首先輸出二叉排序樹中根節點、根節點的右節點及根節點的右節點的右節點上的單詞(即root、root->right、root->right->right節點上的單詞),單詞中間有一個空格分隔,最后一個單詞后沒有空格,直接為回車(若單詞個數不足三個,則按實際數目輸出)。
程序將單詞統計結果按單詞字典序輸出到屏幕上,每行輸出一個單詞及其出現次數,單詞和其出現次數間由一個空格分隔,出現次數后無空格,直接為回車。
【樣例輸入】
當前目錄下文件article.txt內容如下:
"Do not take to heart every thing you hear."
"Do not spend all that you have."
"Do not sleep as long as you want;"
【樣例輸出】
do not take
all 1
as 2
do 3
every 1
have 1
hear 1
heart 1
long 1
not 3
sleep 1
spend 1
take 1
that 1
thing 1
to 1
want 1
you 3
【樣例說明】
程序首先在屏幕上輸出程序中二叉排序樹上根節點、根節點的右子節點及根節點的右子節點的右子節點上的單詞,分別為do not take,然后按單詞字典序依次輸出單詞及其出現次數。
題目大意
題目的核心和第一題並沒有什么區別,只不過我們處理的東西從數字變成了字符串。
題目思路
和上一題類似,我們采取快讀類似的思路改下讀入就能簡單的解決這個問題了。
代碼實現
#include<stdio.h>
#include<ctype.h>
#include<string.h>
#define maxn 1000005
typedef struct tree {
int son[2];
int num;
char s[35];
} Tree;
Tree tr[maxn];
int root;
int cnt;
int len;
char s[maxn];
char buffer[maxn];
char deal(char c) {
return islower(c) ? c : c - 'A' + 'a';
}
int get_nxt(int pos, int buffer_len) {
len = 0;
while (pos < buffer_len && !isalpha(buffer[pos])) { //不是字母,說明不是單詞
++pos;
}
while (pos < buffer_len && isalpha(buffer[pos])) { //循環讀入連續字母。
s[len++] = deal(buffer[pos++]);
}
return pos;
}
void ins(int *rt) {//與第一題類似,這里不作贅述
if (!(*rt)) {
*rt = ++cnt;
strcpy(tr[*rt].s, s);
tr[*rt].num = 1;//初始化個數
return;
}
if (!strcmp(tr[*rt].s, s)) { // strcmp 返回 0,說明是同一個單詞。
++tr[*rt].num;
return;
}
ins(&tr[*rt].son[strcmp(s, tr[*rt].s) > 0]); //大於 0 進入右子樹。
}
void dfs(int rt) { //遍歷樹輸出
if (!rt) {
return;
}
dfs(tr[rt].son[0]);
printf("%s %d\n", tr[rt].s, tr[rt].num);
dfs(tr[rt].son[1]);
}
int main() {
FILE *IN;
IN = fopen("article.txt", "r");
while (fgets(buffer, maxn - 5, IN) != NULL) {
int pos = 0;
int buffer_len = strlen(buffer);
while (pos < buffer_len) {//當前字符串沒有處理完
pos = get_nxt(pos, buffer_len); //獲得下個單詞,並更新當前位置
if (len) { //len不為 0,說明讀到了單詞。
s[len] = '\0';
ins(&root);
}
}
}
int nw = root;
int i;
for (i = 0; i < 3; ++i) { //按照題目要求,從根開始,往右子樹走
printf("%s ",tr[nw].s);
nw = tr[nw].son[1];
}
puts("");
dfs(root);
return 0;
}
復雜度分析
和上題復雜度類似,最壞復雜度是 O(n² * 最長單詞長度) 的。
第三題:計算器(表達式計算-表達式樹實現)
題目描述
【問題描述】
從標准輸入中讀入一個整數算術運算表達式,如24 / ( 1 + 2 + 36 / 6 / 2 - 2) * ( 12 / 2 / 2 )= ,計算表達式結果,並輸出。
要求:
1、表達式運算符只有+、-、*、/,表達式末尾的=字符表示表達式輸入結束,表達式中可能會出現空格;
2、表達式中會出現圓括號,括號可能嵌套,不會出現錯誤的表達式;
3、出現除號/時,以整數相除進行運算,結果仍為整數,例如:5/3結果應為1。
4、要求采用表達式樹來實現表達式計算。
表達式樹(expression tree):
我們已經知道了在計算機中用后綴表達式和棧來計算中綴表達式的值。在計算機中還有一種方式是利用表達式樹來計算表達式的值。表達式樹是這樣一種樹,其根節點為操作符,非根節點為操作數,對其進行后序遍歷將計算表達式的值。由后綴表達式生成表達式樹的方法如下:
l 讀入一個符號:
l 如果是操作數,則建立一個單節點樹並將指向他的指針推入棧中;
l 如果是運算符,就從棧中彈出指向兩棵樹T1和T2的指針(T1先彈出)並形成一棵新樹,樹根為該運算符,它的左、右子樹分別指向T2和T1,然后將新樹的指針壓入棧中。
例如輸入的后綴表達為:
ab+cde+**
則生成的表達式樹為:
【輸入形式】
從鍵盤輸入一個以=結尾的整數算術運算表達式。操作符和操作數之間可以有空格分隔。
【輸出形式】
首先在屏幕上輸出表達式樹根、左子節點及右子節點上的運算符或操作數,中間由一個空格分隔,最后有一個回車(如果無某節點,則該項不輸出)。然后輸出表達式計算結果。
【樣例輸入】
24 / ( 1 + 2 + 36 / 6 / 2 - 2) * ( 12 / 2 / 2 ) =
【樣例輸出】
* / /
18
題目大意
表達式計算,要求用表達式樹來實現。
題目思路
表達式樹的實現有很多方式,一種是通過類似於遞歸下降的方式來實現(建議同學們自行去了解一下),而這個題比較經典的是轉成后綴表達式再生成表達式樹。
這里我們還是給出后綴表達式的實現過程:
1、依次讀取輸入的表達式,如果是操作數,則把它放入到輸出中。
2、如果是操作符,棧為空的話直接將該操作符入棧;如果棧非空,則比較棧頂操作符和該操作符優先級,如果棧頂操作符優先級小於該操作符,則該操作符入棧;否則彈出棧頂操作符並將其放入到輸出中,直到棧為空或者發現優先級更低的操作符為止。
3、如果是括號,比如'('和')',則特殊處理。如果是'('的話,直接入棧;如果是')',那么就將棧頂操作符彈出寫入到輸出中,直到遇到一個對應的'(',但是這個'('只彈出不寫入到輸出中。注意:"("可以理解為優先級最高。
4、當表達式讀取完畢后,如果棧中還有操作符,則依次彈出操作符並寫入到輸出中。
計算后綴表達式的步驟:
1、是數字,直接壓入棧
2、是符號,取出棧頂的兩個值計算后成為新棧頂。
最后,給出后綴表達式轉表達式樹的步驟:
1、遇到數字,創建葉節點, 壓棧;
2、遇到運算符, 創建運算符節點, 彈出棧頂兩個節點作為運算符節點的左右子節點, 壓棧;
代碼實現
#include<stdio.h>
#include<ctype.h>
#include<string.h>
#define maxn 1000005
int len; //記錄字符串長度
int top; //記錄棧頂
char s[maxn]; //讀入並轉換的表達式
char sta[maxn]; //當前的符號棧
int pri[256]; //定義符號的優先級
void getSuf() { // 讀入並將表達式轉換為后綴形式
char c;
pri['+'] = pri['-'] = 1;
pri['*'] = pri['/'] = pri['%'] = 2;
while (1) {
c = getchar();
if (isdigit(c)) {
while(isdigit(c)) {
s[len++] = c;
c = getchar();
}
s[len++] = ' '; // 后綴表達式數字可能相連,因此添加空格避免數字連續。
}
if (c == '=') {
break;
}
if (pri[c]) { //說明是一個符號,需要與棧中符號優先級進行比較
while (top && pri[sta[top]] && pri[sta[top]] >= pri[c]) {//注意 () 優先級為 0
s[len++] = sta[top--];
}
sta[++top] = c;
}
if (c == '(') {
sta[++top] = c;
}
if (c == ')') { //需要將 ( 之前所有符號彈出
while(sta[top] != '(') {
s[len++] = sta[top--];
}
--top;
}
}
while (top) {
s[len++] = sta[top--];
}
}
int numSta[maxn]; //數字棧
int numTop; //數字棧棧頂
int calc(int x, char c, int y) {
if (c == '+') {
return x + y;
}
if (c == '-') {
return x - y;
}
if (c == '*') {
return x * y;
}
if (c == '/') {
return x / y;
}
if (c == '%') {
return x % y;
}
}
typedef struct tree {
int type;
int num;
char c;
int son[2];
} Tree;
Tree tr[maxn];
int tree_sta[maxn];
int tree_top;
int cnt;
void print(int x) {
if (tr[x].type == 1) {
printf("%d ", tr[x].num);
} else {
printf("%c ", tr[x].c);
}
}
void calcSuf() {
int i = 0;
while (i < len) {
if (isdigit(s[i])) { //是數字,准備壓入數字棧
int x = s[i++] - '0';
while(isdigit(s[i])) {
x = (x << 3) + (x << 1) + s[i++] - '0';
}
numSta[++numTop] = x;
//新建數字類型結點,壓入結點棧
++cnt;
tr[cnt].type = 1; //數字類型為 1
tr[cnt].num = x;
tree_sta[++tree_top] = cnt;
}
if (pri[s[i]]) { //是符號,對棧頂兩個數字計算得到新棧頂
int a = numSta[numTop--];
int b = numSta[numTop--];
numSta[++numTop] = calc(b, s[i], a);
//新建符號類型結點,將棧頂的兩個結點記作左右結點壓入結點棧。
++cnt;
tr[cnt].type = 2; //字符類型為 2
tr[cnt].c = s[i];
//注意棧頂的應該是右兒子
tr[cnt].son[1] = tree_sta[tree_top--];
tr[cnt].son[0] = tree_sta[tree_top--];
tree_sta[++tree_top] = cnt;
}
++i;
}
}
void print_ans() {
int root = tree_sta[tree_top]; //如果運行正常,樹根應該在棧頂
print(root);
print(tr[root].son[0]);
print(tr[root].son[1]);
puts("");
printf("%d\n", numSta[numTop]);
}
int main() {
getSuf();
calcSuf();
print_ans();
return 0;
}
后綴表達式的部分就不再贅述,而建樹部分穿插在其中就能十分簡單地完成這道題。
復雜度分析
每個字符被處理了兩次,復雜度是 O(|S|) 的。(S代表字符串長度)
第四題:網絡打印機選擇
題目描述
【問題描述】
某單位信息網絡結構呈樹型結構,網絡中節點可為交換機、計算機和打印機三種設備,計算機和打印機只能位於樹的葉節點上。如要從一台計算機上打印文檔,請為它選擇最近(即經過交換機最少)的打印機。
在該網絡結構中,根交換機編號為0,其它設備編號可為任意有效正整數,每個交換機有8個端口(編號0-7)。當存在多個滿足條件的打印機時,選擇按樹前序遍歷序排在前面的打印機。
【輸入形式】
首先從標准輸入中輸入兩個整數,第一個整數表示當前網絡中設備數目,第二個整數表示需要打印文檔的計算機編號。兩整數間以一個空格分隔。假設設備總數目不會超過300。
然后從當前目錄下的in.txt讀入相應設備配置表,該表每一行構成一個設備的屬性,格式如下:
<設備ID> <類型> <設備父節點ID> <端口號>
<設備ID>為一個非負整數,表示設備編號;<類型>分為:0表示交換機、1表示計算機、2表示打印機;<設備父結點ID>為相應結點父結點編號,為一個有效非負整數;<端口號>為相應設備在父結點交換機中所處的端口編號,分別為0-7。由於設備配置表是按設備加入網絡時的次序編排的,因此,表中第一行一定為根交換機(其屬性為0 0 -1 -1);其它每個設備結點一定在其父設備結點之后輸入。每行中設備屬性間由一個空格分隔,最后一個屬性后有換行符。
【輸出形式】
向控制台輸出所選擇的打印機編號,及所經過的交換機的編號,順序是從需要打印文檔的計算機開始,編號間以一個空格分隔。
【樣例輸入】
37 19
in.txt中的信息如下:
0 0 -1 -1
1 0 0 0
2 0 1 2
3 1 1 5
4 0 0 1
5 1 4 0
6 2 2 2
7 0 4 2
8 0 0 4
9 0 2 0
10 0 9 0
11 2 10 3
12 0 9 2
13 0 7 0
14 0 13 0
15 2 7 3
16 0 8 1
17 0 16 0
18 1 17 5
19 1 9 5
20 0 12 1
21 1 14 1
22 1 14 2
23 1 13 2
24 1 12 5
25 0 20 1
26 1 20 2
27 0 14 7
28 0 16 1
29 1 4 3
30 0 16 7
31 0 28 0
32 2 31 0
33 1 30 2
34 1 31 2
35 0 31 5
36 1 35 3
【樣例輸出】
11 9 10
題目大意
要求在樹上,對指定類型的結點,找到最近的前序遍歷最小的對應類型結點。
題目思路
居然是新題,可惡啊,雖然還是一 A 了 這個題理論上存在用 DP 的方式 O(n) 求出解,再用 O(n) 把答案求出來的方法,過程比較復雜,這里就不作贅述,實在想知道的同學可以私戳我或者課上找我給你講講。我們這里就講一種比較好寫的暴力 O(n²) 的做法,我們先讀入建樹,DFS 一遍求出前序遍歷的順序(即DFN)。之后從給定的起點暴力 DFS,每經過一個結點打上標記防止重復訪問並壓入棧,到達打印機結點時比較當前最優解看是否替換就可以了。當然這個題也可以通過暴力枚舉打印機倍增爬樹法來實現,復雜度是 O(nlogn) 的,同樣需要同學們自行學習一些知識才能實現(
代碼實現
#include<stdio.h>
#include<ctype.h>
inline int read() { //快速讀入,可以放在自己的缺省源里面
int x = 0; //數字位
int f = 1; //符號位
char ch = getchar(); //讀入第一個字符
while (!isdigit(ch)) { //不是數字
if (ch == '-') { //特判負號
f = -1;
}
ch = getchar();
}
while (isdigit(ch)) { //讀入連續數字
x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1)
ch = getchar();
}
return x * f;
}
#define maxn 1000005
typedef struct tree{
int son[8];
int prt;
int type;
} Tree;
Tree tr[maxn];
int n, m;
int root;
int DFN[maxn];
int dfn;
void getDFN(int x) { //遍歷一遍求出DFN序,便於我們后續的操作
if (!x) {
return;
}
DFN[x] = ++dfn;
int i;
for (i = 0; i < 8; ++i) {
if (!tr[x].son[i]) {
continue;
}
getDFN(tr[x].son[i]);
}
}
void pre() { //預處理建樹
FILE* IN = fopen("in.txt", "r");
int i;
for (i = 0; i < n; ++i) {
int id;
fscanf(IN, "%d", &id);
++id; //為了便於操作,我們對id做一個偏移。
if (i == 0) {
root = id;
}
fscanf(IN, "%d%d", &tr[id].type, &tr[id].prt);
int pos;
fscanf(IN, "%d", &pos);
if (tr[id].prt != -1) {
++tr[id].prt;//同樣是偏移
tr[tr[id].prt].son[pos] = id;
}
}
getDFN(root);
}
#define INF 0x7fffffff
int vst[maxn];
int sta[maxn];
int top;
int ans[maxn];
int ansLen = INF;
int ansDFN;
int check(int x) { //看當前解是否比歷史最優解更優
return ansLen > top || (ansLen == top && ansDFN > DFN[x]);
}
void dfsAns(int x) {
if (tr[x].type == 2) {
if (check(x)) {
//更新最優解
ansLen = top;
int i;
for (i = 1; i <= ansLen; ++i) {
ans[i] = sta[i];
}
ansDFN = DFN[x];
}
return;
}
if (tr[x].prt != -1 && !vst[tr[x].prt]) {
vst[tr[x].prt] = 1; //開始訪問 prt
sta[++top] = tr[x].prt; //壓入棧
dfsAns(tr[x].prt);
--top;
vst[tr[x].prt] = 0; //結束訪問
}
int i;
for (i = 0; i < 8; ++i) {
if (!tr[x].son[i] || vst[tr[x].son[i]]) {
continue;
}
vst[tr[x].son[i]] = 1; //開始訪問 son[i]
sta[++top] = tr[x].son[i]; //壓入棧
dfsAns(tr[x].son[i]);
--top;
vst[tr[x].son[i]] = 0; //結束訪問
}
}
int main() {
n = read();
m = read() + 1;
pre();
vst[m] = 1;//不要忘記初始結點隨時都被訪問
dfsAns(m);
//ansLen 顯然存儲着我們打印機的編號,其余都是我們經過的交換機
printf("%d ", ans[ansLen] - 1); //輸出時記得消去偏移
int i;
for (i = 1; i < ansLen; ++i) {
printf("%d ", ans[i] - 1); //輸出時記得消去偏移
}
return 0;
}
復雜度分析
如上面所述,這種暴力方法的復雜度是 O(n²) 的。
偷懶警告
選做題哈夫曼樹大家應該都會建看着比較麻煩,實驗題限制比較多,也沒有什么特別的簡化的思路,這里就不贅述了。