【20180927】【C/C++基礎知識】程式的模組化設計,拿球遊戲(組合問題),漢諾塔遊戲(遞迴問題),報數遊戲(斷點+記憶體除錯),遞迴與迭代
目錄
一、模組化設計思想和方法
模組化設計方法:自頂向下設計,自下向上程式設計實現的設計方法。
學生成績管理系統,若所有功能都在主函式中實現,程式的可讀性、可修改性都很差,因此我們每個功能分別用一個函式來實現,主函式方便快捷的呼叫這些函式即可(模組化)。
先分析問題——每個問題都是由若干個子問題構成——子問題也可能由若干個小問題構成——細分下去,直至每個小問題獨立並且足夠小——程式設計實現每個小問題——將小問題組合(自頂向下設計,自下向上程式設計實現)
模組化的優點:複用。(1. 函式+函式構成程式。 2. 成熟軟構件+修訂=新軟體。 3. 成熟軟構件1+成熟軟構件2+…=新軟體系統(複用性最佳))
二、幾個小例子
例1:以身份證號為引數比較年齡大小。
這是我寫的程式:
問題在於:
(1)身份證號不僅僅只有數值,有時候會有字母,因此應用字元陣列串表示。
(2)scanf_s獲取字串時,應該指明字串長度,否則可能會出錯!
(3)主函式中呼叫函式,不應該是int 函式名(),而是返回值=函式名()
正確的程式如下:
/* 以身份證號為引數,比較年齡大小 */ #include<stdio.h> #include<stdlib.h> #define IDLEN 19 // 定義一個長度:身份證號碼長為18,加上結束符共19個 int AgeCompare(char *p1,char *p2); // 函式宣告有分號! int main() { char id1[IDLEN],id2[IDLEN]; // 長度為18+1 int r; // 儲存函式返回值 printf("請輸入兩個18位的身份證號:\n"); scanf_s("%s%s",id1,IDLEN,id2,IDLEN); r=AgeCompare(id1,id2); printf("比較結果是:%d\n",r); system("pause"); return 0; } int AgeCompare(char *p1,char *p2) // 函式定義沒有分號! { int y1=0,y2=0,m1=0,m2=0,d1=0,d2=0; // 初始化!提取年、月、日 int i; // 計算年份 for(i=0;i<4;i++) { y1=y1*10+p1[6+i]-'0'; // 把字串轉化為數值,需要減去字元零'0'!!! y2=y2*10+p2[6+i]-'0'; // 使用前在等號右面使用了,因此使用前必須初始化! } // 計算月份 for(i=0;i<2;i++) { m1=m1*10+p1[10+i]-'0'; m2=m2*10+p2[10+i]-'0'; } // 計算天數 for(i=0;i<2;i++) { d1=d1*10+p1[12+i]-'0'; d2=d2*10+p2[12+i]-'0'; } // 比較大小 if(y1!=y2) { if(y1>y2) return 1; else return -1; } else { if(m1>m2) return 1; else if(m1<m2) return -1; else { if(d1>d2) return 1; else if(d1<d2) return -1; else return 0; } } }
下述錯誤原因:沒有定義函式就呼叫。
進行除錯時的技巧:
(1)加斷點:看輸入函式之前的資料是否正確;看引數傳遞是否正確;看函式輸出資料是否正確。(因此斷點加三處:進入函式前,進入函式後第一行,從函式中出來)
(2)用監視:看引數傳遞是否正確;看獲取資料是否正確;看指標是否正確;看返回值是否正確。(監視加幾處:y, m, d, id, p, r)
測試時的技巧:
為避免反覆輸入資料可能會造成錯誤,我們將scanf_s註釋掉,直接輸入資料,測試完成再取消註釋。
例2. 拿球遊戲。(根據這個例子,學習怎麼對組合問題進行求解。)
分析:這是一個組合問題。
演算法思想:
紅球和白球必須有,因此紅球i=1:3,白球j=1:5變化,求取8個球的情況下黑球數量k=8-i-j,用巢狀迴圈語句分別控制紅球、白球的變化,求出黑球,並進行方案數量累加。
函式原型:(三個整型引數、三個變數指明拿球數、還要知道取了多少球,因此一共七個整形引數。)
int balls(int red, int white, int black, int minRed, int minWhite, int minBlack, int sumBalls);
函式說明:
輸入:int red, int white, int black表示紅白黑三種球的最大數量;int minRed, int minWhite, int minBlack表示拿三種球的最少數量;int sumBalls表示總共需要拿的球數量。
輸出:總共方案數量。
功能:紅球從minRed到red;白球從minWhite到white;黑球為sumBalls-當前拿的紅球數-當前拿的白球數;如果這個黑球數量滿足minBlack到black之間,則是一個合理地方案。
/* 拿球遊戲 */ #include<stdio.h> #include<stdlib.h> int CountBalls(int red,int white,int black,int minRed,int minWhite,int minBlack,int sumBalls); int CountBalls(int red,int white,int black,int minRed,int minWhite,int minBlack,int sumBalls) // 函式原型後面不要有分號! { int i,j,k,result=0; for(i=minRed;i<=red;i++) { for(j=minWhite;j<=white;j++) { k=sumBalls-i-j; if(k>=minBlack && k<=black) { printf("%5d %5d %5d\n",i,j,k); ++result; } } } return result; } int main() { int r=3,w=5,b=6,sum=8,result; result=CountBalls(r,w,b,1,1,0,sum); printf("滿足條件的方案數有:%d\n",result); // 結果沒有輸出的原因:剛開始沒有加%d,那麼他就不會輸出數值結果 system("pause"); return 0; }
如圖錯誤:
原因在於:沒有加標頭檔案!
例3. 報數遊戲。
開始的時候要梳理思路,可先用小的數測試,例如n取4,看是哪位同學退出圈子,並且會造成什麼變化。
需要兩個變數:一個記錄報號,一個是編號。用一個整數狀態表示一個人是否在報數範圍內,狀態為1則報數,狀態為0則不報數。
用n=4的例子來分析演算法:
演算法思想:
假設有n個人,用長度為n的一維陣列data[n+1]儲存這n個人,假設值為1表示在圓圈中,值為0表示出局。迴圈從1數到3的時候,第三個人出局,我們用i進行1到3的迴圈計數,m表示已經數到第幾個人了,如果data[m]=0,則這個人不參與計數。另外,還需要統計剩餘人數,我們可以用整數變數j表示,初始值為n。j是控制迴圈是否結束的。只要j>1,表示剩餘人數不止一個,則迴圈下面操作:
(1)跳過data[m]=0
(2)i=3,則data[m]=0; i=1且m++,這時出局1人,j--,continue。
(3)i++,m++。
函式原型:int count(int n,int k);
輸入:n為人數,k為間隔數,這裡固定為k=3;
輸出:出局後剩下的最後一人的編號;
功能:從1到k計數,第k個人出局,直至最後餘下一人,返回這個人的編號。
/* 報數 */ #include<stdio.h> #include<stdlib.h> int count(int n,int k); int main() { int num,k=3; int s; scanf_s("%d",&num); printf("有%d個人參與報數\n",num); s=count(num,k); printf("餘下人的編號為%d\n",s); system("pause"); return 0; } int count(int n,int k) { int *data=(int *)malloc((n+1)*sizeof(int)); // 定義data指標,通過malloc函式分配n+1個整數空間 int i,j,m; // i報數,j判斷迴圈是否結束,m編號 if(data==NULL) // 如果空間分配失敗,返回-1 { printf("malloc error!\n"); system("pause"); return -1; } for(i=1;i<n+1;i++) data[i]=1; // 如果分配成功,那麼令這n個人狀態都為1 j=n; i=1; m=1; while(j>1) // j>1迴圈未結束 { while(data[m]==0) // 當遇到狀態為0的,報號i不變,編號+1 { ++m; m%=(n+1); if(m==0) ++m; } if(i==k) // 如果報數為3,那麼狀態置為0,並且報號重新從1開始,剩餘人數-1 { data[m]=0; --j; i=1; } else // 沒有報到3,報號+1 { ++i; } ++m; // 不管有沒有報到3,編號都在+1,因此把它放在了外面 m%=(n+1); // 加這一步驟的目的是:m可能會進行好幾輪,因此會有個輪迴 // 過了第n個人之後,就又從第一個人開始計數 if(m==0) ++m; } free(data); // 寫出malloc函式時,後面立馬跟上free,再在中間插入其他的,以免忘記! return m; // 剛開始忘記寫return返回值,因此輸出的都是0,原因就是輸出結果沒有成功傳遞! }
除錯時“監視”+“記憶體”一起使用。僅通過監視變數,看不到一塊連續的記憶體空間,通過(除錯——視窗——記憶體——複製想要看到的記憶體到裡面——回車)!
例上圖:當n設定為20時,這裡有20個1。
課後作業:
我的疑問:
關於例3報數的遊戲,執行結果好像不對,例如果有5個人,那麼應該剩下第4個人,程式執行結果是3,找不到問題出在哪裡。
例4. 逆序輸出字串。
要求:用兩個函式實現。第一個函式不改變原字串內容,第二個改變原字串內容,實現逆序輸出。
/* 逆序輸出字串 */ #include<stdio.h> #include<stdlib.h> #include<string.h> void reverse1(char *s,char *t); void reverse2(char *s); int main() { char s[]="Hello,everyone!"; char t[50]; reverse1(s,t); printf("Source string=%s\tReverse string=%s\n",s,t); reverse2(s); printf("Reverse string=%s\n",s); system("pause"); return 0; } void reverse1(char *s,char *t) // 沒有改變原字串內容 { int len=strlen(s); int i; for(i=0;i<len;++i) // 把原字串的內容逆序賦給t陣列 { t[len-1-i]=s[i]; } t[len]='\0'; // 結尾加結束字串 } void reverse2(char *s) { int len=strlen(s); char *t=(char *)malloc(sizeof(char)*(len+1)); if(t==NULL) { return; } int i; for(i=0;i<len;++i) { t[len-1-i]=s[i]; // 把原字串的內容逆序賦給t陣列 } t[len]='\0'; for(i=0;i<len;++i) { s[i]=t[i]; // 然後再把t的內容給s(唉,這波操作圖什麼???) } free(t); }
我的疑問:
(1)陣列和指標陣列的區別?
(2)缺少了‘;’這個問題是什麼原因?怎麼解決?
例5. 漢諾塔遊戲。(用這個例子瞭解遞迴問題)
我們要找規律,問題分析圖:
解釋:
第一步:將問題簡化。假設只有兩個圓盤。(將較小的暫存到C柱子上,將大的放到B,然後再將較小的放到B。)
第二步:對於有n個圓盤的漢諾塔,我們將圓盤分為兩部分:最大的一塊圓盤和其餘的圓盤(即第n塊圓盤和上面n-1個圓盤,上面n-1個圓盤看成一個整體)。
因此只需要兩個函式、三個步驟即可實現:
(1)Hanoi(int n,char a,char b,char c); // n為圓盤數量,a, b, c是三個柱子名稱
// 目的:將n個圓盤由a移到b,中間藉助c柱子
(2)Move(int n,char a,char b);
step 1. Hanoi(n-1, a, c, b); // 將前n-1個圓盤由a移到c,中間藉助b
step 2. Move(n, a, b); // 將第n個圓盤由a移到b
step 3. Hanoi(n-1, c, b ,a); // 把c上那n-1個圓盤放到b上,藉助a
心得:Hanoi函式意義在於遞迴呼叫,不見得它能移動什麼圓盤,真正起作用的是Move函式(即下面程式中的printf函式)!
(除錯觀察遞迴呼叫的過程)
(注意:若要移動n=64次,所需移動次數為1844億億次,我們看不到結果,因此我們用較小的數值進行測試)
/* 漢諾塔遊戲 */ #include<stdio.h> #include<stdlib.h> void Hanoi(int n,char x,char y,char z); int main() { int h; printf("請輸入圓盤數量:\n"); scanf_s("%d",&h); Hanoi(h,'a','b','c'); system("pause"); return 0; } void Hanoi(int n,char x,char y,char z) { if(n==1) // 使用遞迴函式時,一定要有遞迴結束條件!放在最前面! printf("%c->%c\n",x,y); // 只有一個圓盤,直接移動即可。 else { Hanoi(n-1,x,z,y); // 函式體內直接或間接呼叫自己本身,這種叫遞迴函式! printf("%c->%c\n",x,y); Hanoi(n-1,z,y,x); } }
遞迴函式
遞迴呼叫應該能夠在有限次數內終止遞迴!若遞迴呼叫不加限制,將無限迴圈呼叫,因此必須在函式內部加控制語句,僅當滿足一定條件時,遞迴終止,稱為條件遞迴。
任何一個遞迴呼叫程式必須包括兩部分:
(1)遞迴迴圈繼續的過程
(2)遞迴呼叫結束的過程
/* 遞迴問題模型 */ if (遞迴終止條件成立) return 遞迴公式的初值; else return 遞迴函式呼叫返回的結果值;
遞迴與迭代
優點:直觀、精煉、邏輯清楚、符合人的思維,逼近數學公式的表示,適合非數值計算領域(Hanoi塔、騎士遊歷、八皇后問題(回溯法))
缺點:增加了函式呼叫的開銷,每次呼叫都需要進行引數傳遞,現場保護等耗費更多的時間和棧空間,應儘量用迭代形式替代遞迴形式。
補充知識點:(遞迴與迭代的區別)
迭代:迴圈結構,例如for,while迴圈
遞迴:選擇結構,例如if else 呼叫自己,並在合適時機退出
看到的極易理解的解釋:
(參考:遞迴與迭代)