1. 程式人生 > >【20180927】【C/C++基礎知識】程式的模組化設計,拿球遊戲(組合問題),漢諾塔遊戲(遞迴問題),報數遊戲(斷點+記憶體除錯),遞迴與迭代

【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 呼叫自己,並在合適時機退出

看到的極易理解的解釋:

(參考:遞迴與迭代