顯示具有 Chap3 標籤的文章。 顯示所有文章
顯示具有 Chap3 標籤的文章。 顯示所有文章

10/09/2006

第三章 程式結構與設計

前面介紹指令及函數檔的過程中,已陸續介紹一些程式指令之用法及功能。基本上程式是一些指令之集合,執行後可以獲得所希望的結果。複雜的程式則是採用主程式、副程式或函數檔間相互呼叫的方式,由各檔案專司解決特定的功能,如此形成一個程式之大架構。這些程式執行過程中,必須配合執行之中間值,進行比較、判斷,最後得到適當之結果。有些檔案可能需經過多次呼叫,才能達到程式預設之目的;有些則需判斷結果之適當性,依據最佳的決策選擇執行的路徑。

在程式結構中,一般除基本指令外,尚須配合流程之控制指令,使程式能配合特定條件運作。控制指令以邏輯值進行比較,其操作元包括關係操作元及邏輯操作元等,可以應用於變數及矩陣。在程式之運算過程中,為執行某一特定任務均有其基本之演算法。其中必須藉助控制指令以改變指令執行之順序,或稱為控制結構。這些結構必須藉其特定之關係比較,就比較結果決定程式進行的順序。這些運算法大體上可分為循序運算、條件運算及迴圈運算等三種方式。本節將針對這三種特定型式之指令作詳細的介紹。

3.1關係操作元

兩陣列間之相互比較是常有的事,若能配合程式之關係運算則可節省撰寫時間。一般關係操作元主要在兩個變數間作邏輯性比較,,然後選擇一個最佳的結果;在MATLAB之應用上實也脫離不了這些原則。只是在陣列之比較上,必須時時注意其大小之匹配。下面為一般操作元之定義:


表3.1 關係操作元










操作單元符號說明
<小於
<=小於或等於
>大於
>=大於或等於
==等於
~=不等於



上述關係操作元當中,比較特別需要注意的是,相等之關係操作元必須使用兩個等於,以便與設定變數值之等於有所區別。

在同樣大小的矩陣中,其比較可依元素與元素相互比較,將結果置於另一矩陣內。以下面兩矩陣A、B為例,兩者分別由魔方陣及亂數函數產生,大小均為4x4:

>>A=magic(4)
A =
16 2 3 13
5 11 10 8
9 7 6 12
4 14 15 1

>>B=round(rand(4)*16)
B =
7 14 13 13
15 8 0 8
7 3 11 11
7 11 6 7

>>C=A>=B
C =
1 0 0 1
0 1 1 1
1 1 0 1
0 1 1 0

新的矩陣C為兩個矩陣比較後之結果,其元素由邏輯值0或1組成,大於等於為1否則為0。

在矩陣之操作中,條件型態也可以加入,以歸納一些符合條件的元素。以上述A矩陣為例,若在A矩陣中,將所有A>=B之元素均設為100,則可利用上述C矩陣之邏輯值作為條件如下:

>>A(C)=100
A =
100 2 3 100
5 100 100 100
100 100 6 100
4 100 100 1

A矩陣中只要相對應於C矩陣中之值為真,或合於A>=B條件之元素因而均被設為100。這樣的結果僅需一個指令即可達成。實際上要達成上項目的並不一定要經過C矩陣,直接採用下列方式亦可:

>>A(A>=B)=100
>>A =
100 2 3 100
5 100 100 100
100 100 6 100
4 100 100 1

其結果與上述方法相同。這種方法並不只與其他矩陣比較,與自身矩陣之元素比較亦可,例如:

>>A(A<10)=10
A =
100 10 10 100
10  100 100 100
100 100 10 100
10 100 100 10

3.2邏輯操作元

邏輯操作元主要在連繫兩個邏輯值,應用上分為元素間、位元間等兩類。位元間讀者可自行參閱深入書籍,此處稍加介紹元素間之應用。其相關操作元如表3.2。



表3.2 邏輯操作元
操作元範例說明
&C=A&BAB兩元素均為1時為1,否則為0
|C=A|B任一A或B為1為正,否則為0
~C=~A非A,即A為0時為1,否則為0
xorC=xor(A,B)僅其中一個為非零時為1,否則為0

       

邏輯操作元常與關係條件諸元配合,作為判斷及分類之標準。下面舉例說明:

>>A=[1 0 1 1 0 0 1], B=[0 0 1 0 1 0 1]
A =
1 0 1 1 0 0 1
B =
0 0 1 0 1 0 1

>>C=A&B
C =
0 0 1 0 0 0 1

>>C=A¦B
C =
1 0 1 1 1 0 1

>>C=~A
C =
0 1 0 0 1 1 0

>>C=xor(A, B)
C =
1 0 0 1 1 0 0

應用時,兩矩陣必須同樣大小,除非其中一個為常數。若元素屬於非零元素(如10,8 -5等等)均視為邏輯1,或為真值。

在實際運作上,前面係直接用&~等三個符號進行邏輯運算,有些地方亦可使用函數型式,即and(A,B)代表A & B;or(A,B)代表A B;not(A)代表 ~A。

利用矩陣本身,進行分類的方式到處可用。這些指令函數亦常使用,其型式如下表:













操作元範例說明
anyC=any(A,dim)A矩陣中之任何行向元素不為0時為1,否則為0。dim=2時為列向。
allC=all(A)矩陣中之任何行向元素均為1時為1,否則為0。dim=2時為列向。
isprimeC=isprime(A)A矩陣中,元素為質素時為1時為1,否則為0
isequalC= isequal(A)僅其中一個為非零時為1,否則為0
isemptyC= isempty(A)若為空矩陣為1,否則為0
isintegerC= isinteger(A)若為整數為1,否則為0
isnumericC= isnumeric(A)若為數值為1,否則為0
isrealC= isreal(A)若為實數為1,否則為0
isfiniteC= isfinite(A)若為定值為1,否則為0
logicalC= islogical(A)將矩陣A轉為邏輯值,非零為1
islogicalC= islogical(A)若為邏輯值為1,否則為0
ischarC= ischar(A)若為文字值為1,否則為0
iscellC= iscell(A)若為細胞陣列為1,否則為0
findC=find(A);C=find(A>B)找尋矩陣A中不為零之序號,存於C找尋矩陣A大於B之序號存於C


例如從前面的原A矩陣找出質因數:

>>A=magic(3)
B=A;
B(~isprime(A))=0

>>A =
8 1 6
3 5 7
4 9 2

B =
0 0 0
3 5 7
0 0 2

在這裡,isprime是尋找矩陣中元素是否為質數,而~isprime即為非質數,將其對應元素設為零即可得到質數之矩陣。這些利用is*之型式逗起來的函數也是屬於矩陣邏輯操作函數,比較常用的有isempty、isequal等等。有興趣的讀者可以參考其輔助檔。

logical則是一個將矩陣轉換為邏輯常數函數,只要零值換為邏輯0值,非零(包括負值)均視為邏輯1。例如:

>> A=-5:5
A =

-5 -4 -3 -2 -1 0 1 2 3 4 5
>> islogical(A)
ans =
0

顯然矩陣A並非邏輯矩陣,但經過如下轉換,即可轉為邏輯矩陣:

>> logical(A)
Warning: Values other than 0 or 1 converted to logical 1.
ans =
1 1 1 1 1 0 1 1 1 1 1

此外,any與 all兩函數則是檢驗一個矩陣是否有非零元素或全為非零元素。兩者均可針對單行或單列檢驗,如:

>>any(A==B)
ans =
1 1 1

顯然它是行向作業,如改為列向則後面加一參數dim=2

>>any(A==B,2)
ans =
0
1
1

注意上述之關係等號必須使用兩個"="號。讀者可自行印證這兩個的結果。在行向上,A與B是沒有完全相等之元素,所以下面用all指令的結果應均為零。

>>all(A==B)
ans =
0 0 0

但由於第二列均為[3 5 7],故下面的結果第二項應為1,其餘均為0。

>>all(A==B, 2)
ans =
0
1
0

有關關係操作元之運用在未來程式寫作上甚為重要。簡潔的關係式可以使程式看起來更為流暢,其運作的時間也會降低。最後find之函數則是尋找某矩陣不為零之元素或具某條件之原始序號,如:

A=fix(rand(3)*10)
A =
9 8 8
9 0 0
4 3 1

>>find(A)'
ans =
1 2 3 4 6 7 9

由結果可知,第五項與第八項為零。此外:

>>find(A>5)'
ans =
1 2 4 7

其結果是:只有第一、二、四、七項元素大於五。若為矩陣間比較,其大小應相同。

3.3 IF條件敘述

條件敘述是一種重要的功能,可以擴大程式判斷的能力。MATLAB最簡單之條件敘述有下面之型式:


if {條件敘述1}
{指令敘述1};
end

if {條件敘述2}
{指令敘述2};
elseif{條件敘述3}
{指令敘述3};
else
{指令敘述4};
end


有關這兩種條件敘述之例子,前面已經有引述。上述條件敘述部份可能為關係式,可能為邏輯判斷值,亦可為邏輯函數的結果,如isreal(A)、isprime(A)等。簡單的邏輯式可以利用關係子給合成為複雜的關係式,但其最後之結果值僅有兩種狀態,即true或 false,或為 0 與1。若結果值不同於1,也視同1考慮。if若成巢狀結構,則必須以end結束。

複合式陳述時,MATLAB並不一定處理全部條件。通常採用部份評估的方式。以if (A&B)為例,只要A的條件評估結果為零時即不再評估B;同理,若為if(AB)時,則只要A為非零之結果,即不再評估B的結果。由於有是項特徵,有些條件陳述可以利用這項功能,如:

if (b~=0) & (a/b>20), …

在此只要(b~=0) 之結果為偽(即b=0),即不會進一步評詁 (a/b>20)這一項,以免造成divide by zero的錯誤。

同理,若條件陳述為

if exist('draw_number.m') & (draw_number(100)>=X)

則第一項為偽時(即檔案draw_number.m不存在時)即不再評估並執行第二項。下面一個例子也是如此,讀者可以自己體會:

if iscell(A) & any(cellfun('isnumeric',A))

下面使用一個抽號的方式作為例子:



function [C]=draw_number(no_of_draw)
% draw ball numbers within ndraw times
%
C=zeros(1,5);
n=1;
while n<=no_of_draw
ball=fix(rand*10);
if ball<2
C(1)=C(1)+1;
elseif ball<4,
C(2)=C(2)+1;
elseif ball<6,
C(3)=C(3)+1;
elseif ball<8,
C(4)=C(4)+1;
else
C(5)=C(5)+1;
end
n=n+1;
end



這是以隨機函數抽號碼球由0至9號分別置於C(1)-C(5)內。看其抽中的號碼分別置入五個箱子中。這個程式完全利用if {}; elseif {};else{};end之型式。最外圍則掛上while…end之迴圈,將在後面討論。執行本程式初期以1000次、5000次及10000次抽到之結果為:

>>[C]=draw_number(1000)
C =
168 209 214 191 218

>>[C]=draw_number(5000)
C =
1010 977 1020 1009 984

>>[C]=draw_number(10000)
C =
2036 2007 1875 2045 2037

依據上項關係得知,抽取到的或然率大概差不多哩!而且即使同樣的抽取次數,其結果亦會有不同,讀者不妨試試。

若以兩矩陣作大小比較時,通常均以元素間之值作比較,但矩陣之大小需相同。下面就實際兩個例子作比較:

>>A=[0 1;2 5], B=[1 0;2 3]
A =
0 1
2 5

B =
1 0
2 3

>>A<B
ans =
1 0
0 0

>>A<B+1
ans =
1 0
1 0

>>A&B
ans =
0 0
1 1

>>A<5
ans =

1 1
1 0

IF之判斷指令可以應用於一般輸入之參數之確定(如前面之evalsum.m程式)或一般變數輸入之交談用。

在此特別強調的一點是,有關"=="與"isequal"這兩個指令之比較。在一般常數間之比較時,此兩個指令之結果應該相同,但在矩陣間比較時,兩個結果可能不同。

>> A==B
ans =
0 0
1 0

>> isequal(A,B)
ans =
0


這種情況也可能發生在其餘類似的指令如 isempty、all、 any等。

3.4 FOR 迴圈之應用

通常for的迴圈都用在重覆執行固定次數的場合,其運算過程中雖亦可利用條件跳出迴圈,但仍以其設定之次數為上限,迴圈每循環一次稱為一回合。其中之expression 通常以純量表示,其型式如k=1:5、K=2:4:20等,等號右邊之最左值為初值,最後值為末值,有中間值時則為每回合之增量。在 MATLAB中 for 迴圈之型式如下:


for var = expression
{statements};
end


例如:



x=0, %先將x矩陣清為零,再進行迴圈
for i = 1:5,
  x(i) = i^2,
end;


x = 0
x = 1
x = 1 4
x = 1 4 9
x = 1 4 9 16
x = 1 4 9 16 25

由這個迴圈之運作,至少可以看出x列向量之大小逐漸因執行過程而擴大的情形。上述迴圈亦可寫成:

>>for i = 1:5, x(i) = i^2, end

但要先記得清除變數x,以免變數有殘餘值。即

>>clear x

若改成遞減的方式亦有同樣的結果:

>>x=0;for i = 5:-1:1, x(i) = i^2, end

x = 0 0 0 0 25
x = 0 0 0 16 25
x = 0 0 9 16 25
x = 0 4 9 16 25
x = 1 4 9 16 25


只是此時之x已先設定為1x5之向量,因為開始時x=5。

使用這種遞圈的觀念亦很容易產生向量矩陣,例如:

>>x=1:1:10
x =
1 2 3 4 5 6 7 8 9 10

這表示產生一個序列向量,起始值為1,增量為1,直到10為止。若增量為1個單位時,上述x向量亦可用x=1:10產生。而增量改為負值時,其順序應相反:

>>x=10:-1:1
x =
10 9 8 7 6 5 4 3 2 1

巢狀迴圈亦常使用,下面是產生修伯特矩陣(Hilbert matrix)的方式:



%hillbert.m for demonstration
%
h=5;
A=zeros(h,h); %預留空間
for k=1:h
for m=1:h
A(k,m)=1/(k + m -1);
end
end
A


>>hillbert
A =
1.0000 0.5000 0.3333 0.2500 0.2000
0.5000 0.3333 0.2500 0.2000 0.1667
0.3333 0.2500 0.2000 0.1667 0.1429
0.2500 0.2000 0.1667 0.1429 0.1250
0.2000 0.1667 0.1429 0.1250 0.1111

實際上,初值亦可為向量,然後依序計算:

>> for i=[2 18 3 1], x=i^3,end

x = 8
x = 5832
x = 27
x = 1


比較深入之運用,for後面之陳述也可以使用矩陣,只是每次以該矩陣之行向為單位,一行一行地執行,如


>>eye(3)

ans =

1 0 0
0 1 0
0 0 1

>> for m=eye(3)*3, m, end
m =
3
0
0
m =
0
3
0
m =
0
0
3


由於eye(3)是一個單位矩陣(對角線元素均為1其餘為0),故每回合所得之m值為其行向量值。在特別情況下,讀者也可以利用這項功能。這與下面之執行結果略有不同。


>> m=eye(3)*3
m =
3 0 0
0 3 0
0 0 3

3.5 While 迴圈

while與for最大不同點在於前者係依某種條件結束迴圈,故無法如for執行時可預知其迴圈數目。While 迴圈之其型式如下:



while expression
{statements};
end




在while後面之expression只要其評估結果為真,此迴圈會繼續執行,因此無法確知其執行次數,為此有時必須另設計數器計算次數。有時為使其形成永久迴圈,亦可採用while 1,…end之型式,使迴圈永遠有效。此時必須有特殊狀況才能跳出,或停電關機才能終止。前面之例子中,已有示範如何使用while...end這個迴圈。茲再以計算前N個自然數和之例說明:


% cumsumx.m for demonstration
N = 50;
n = 0;
sum = 0; %先設定儲存變數
while N>=n, %只要n 小於50,下面之指令陳述繼續執行
n = n+1;
sum = sum+n;
end
sum

sum=

1275


上面程式執行後,其值應為1275。此小程式中可以得知大寫的N與小寫的n是兩個不相干的變數。故大小寫的變數不能混淆,否則會有很大的麻煩。

下面的範例為利用while…end計算一台機器所能處理之最小值。這個最小值因電腦軟體不同而異,在MATLAB中有一個常數eps是直接可以拿來使用,如:

>>eps

eps=

2.2204e-016

此處用程式亦可計算該值:


% epsilon.m for demonstration
eps =1;
while (1+eps) >1
eps=eps/2;
end
eps=eps*2;

>>epsilon
eps = 2.2204e-016

其結果相同。此處之while…end則是評估(1+eps) >1之真偽決定結果,直到eps小到電腦無法分辨其大小為止。

3.6 SWITCH條件式

與前面之if-else-end 敘述相同的條件式,有時亦可利用switch 之敘述方式,使結構更條理化。switch 之陳述型式如下:

  
switch switch_expr
case case_expr,
statement, ..., statement
case {case_expr1, case_expr2, case_expr3,...}
statement, ..., statement
...
otherwise,
statement, ..., statement
end


在這裡只要第一個 CASE 的條件相符合時,其後之敘述指令即會被執行。若CASE 之條件是以數個單元之集合表示(如第二個CASE),則只要其中一單元符合SWITCH後之條件,即會執行第二CASE下之指令陳述。若所有條件均不符合,則會執行OTHERWISE後之指令陳述。不論如何,僅其中一個CASE的內容會被執行,執行後即會跳到 END後面之敘述去執行下一段之指令。這種方式可以說是最結構化之條件式,也最常用。

在沒離開這個題目之前,仍有幾句話附帶說明,在SWITCH後面之敘述switch_expr,可以為常數或字串。故只要switch_expr==case_expr 為真,則該CASE下之陳述會被執行。

下面為利用前面if的例子draw_number程式改寫為switch case之應用:

function [C]=draw_number2(no_of_draw)
% draw ball numbers within ndraw times
% Using switch case
C=zeros(1,5);
n=1;
while n<=no_of_draw
ball=fix(rand*10);
switch ball
case {0,1}
C(1)=C(1)+1;
case {2,3}
C(2)=C(2)+1;
case {4,5}
C(3)=C(3)+1;
case {6,7}
C(4)=C(4)+1;
otherwise
C(5)=C(5)+1;
end
n=n+1;
end


執行結果:

>>draw_number2(1000)
ans =
186 197 211 199 207

>>draw_number2(5000)
ans =
1017 1028 934 999 1022

>>draw_number2(10000)

ans =
2014 2021 1908 2036 2021

執行結果大致相同。switch case比較適合列舉型選項,if指令則可適用於各種範圍的選擇,其應用均很廣泛。

3.7 BREAK指令

break這一個指令常與迴圈while 或for 配合使用,可以自迴圈中跳出至上一層之迴圈,在巢狀迴圈中,break僅能自最內圈跳出圈外,到其對應之end後繼續執行。下面之範例配合while 1…end執行輸入的問答,只有回答N才能利用break跳出迴圈。


% Using break to terminate the execttion.
n=1;
while 1
a=input('Continue? (Y/N) ','s');
A=upper(a);
if isempty(A), A='Y';end
if A=='N', break; end
if A=='Y',
disp('OK, you know how to use BREAK, don''t you?')
disp(['You just try ',int2str(n),' time(s)! Do it again.'])
n=n+1;
else
disp('I don''t understand, please input again.')
end
end


執行之結果:

>>show_break
Continue? (Y/N) d
I don't understand, please input again.
Continue? (Y/N) y
OK, you know how to use BREAK, don't you?
You just try 1 time(s)! Do it again.
Continue? (Y/N) y
OK, you know how to use BREAK, don't you?
You just try 2 time(s)! Do it again.
Continue? (Y/N) n

3.8 CONTINUE & RETURN

continue與break兩指令具有相同的功能,但break大部份都是跳到外圈,continue則只跳到對應之end後繼續執行,在巢狀if…end內,其功能大致相同。

將上例稍作修正,可以相互比較其功用之不同。本例仍然有使用break中斷執行while…end。所以兩者有不同的功能。


% Using continue to terminate the execttion.
n=1;
while 1
a=input('To proceed? (Y/N) ','s');
A=upper(a);
if isempty(A), A='Y';end
if A=='N', break; end
if A=='Y',
disp('OK, you know how to use CONTINUE, don''t you?')
disp('This statement uses CONTINUE to bypass the rest ones.')
disp(['You have tried ',int2str(n),' time(s)! Try it again.'])
n=n+1;
continue
end
disp('I don''t understand, please input again.')
end


>> show_continue
To proceed? (Y/N) y
OK, you know how to use CONTINUE, don't you?
This statement uses CONTINUE to bypass the rest ones.
You have tried 1 time(s)! Try it again.
To proceed? (Y/N) n

return也是中斷指令,但通常用於被呼叫之函數中,一旦遇到return指令,執行權將回到呼叫程式繼續執行。


function dd=show_det(A)
% Find the determinant of A
if isempty(A),
dd=1;
return
else
dd=det(A);
end


執行例:

>> d=show_det('')
d = 1
>> d=show_det([1 2 3;4 -5 6;7 2 1])
d = 188

3.9 模組化設計

一般而言,程式均可以配合流程控制結構撰寫出所需的程式內容,而Matlab與其他電腦一樣,具備有結構化程式設計之能力。通常在這些程式設計中,不再使用goto這個指令,雖然這在Fortran與 Basic語言中,是常用的指令,但這種指令碼很容易產生秩序混亂的程式設計,容易使流程糾結不清。

一般結構化程式具有下列特色:

  1. 容易撰寫:先就問題內容瞭解後,再處理細節,較容易產生簡潔的大綱。
  2. 可重複使用模組:具有相同功能之程式模組可以一再使用。
  3. 容易除錯:流程簡單,容易除錯。
  4. 可以合作開發:在研發過程中可以將整個程式進行切割,最後再進行整合。
  5. 容易辨識及瞭解:程式寫作完成後,有如一篇完整脈絡的報告,容易按圖索驥。

結構化程式設計通常採用由上而下的設計,開始時才提綱要,然後再逐步進入細節。在文件說明上更容易利用結構圖及流程圖表示。前者為描述程式部份如何相互聯結,因此可以很清楚地顯示整個程式之架構。

例如一套乾燥系統之程式可以將整個程式作如下之分類:

  主程式
     。濕氣特性程式
。乾燥理論程式
     。乾燥機設備程式
。進料程式
。循環程式
。出料程式


流程圖對於特定程式之執行、判斷及分支之狀況有比較清晰的描述。如此,任何對程式有興趣的人,就流程之變換可以立即瞭解轉折點的位置及程式進行時所採取的步驟。結構圖及流程圖對解題上有也相當大的助益,在偵錯過程中,更能一目了然。





















在程式撰寫過程中,將程式文作作適當說明也是一件相當重要的步驟。對後來要對程式有深入瞭解時有相當大的幫助。在MATLAB中,可以充分利用%這個控制碼,因為在控制碼%後的文字程式並不執行,故可以用這個方式進行說明。在指令後面亦可加%進行個別說明,作為程式之註解。在程式文件中,亦可考慮下列動作,以協助程式清晰度:

  1. 為程式之函數、變數選取適當名稱,以反應該變數的量與意義。
  2. 在程式當中多加註解,並作變數之意義詳加定義。
  3. 配合結構圖加以說明及分區歸類。
  4. 配合流程圖說明判別功能點。
  5. 使用虛擬碼(Pseudocode)協助釐清程式的細節。配合文字及演算公式,使計算步驟清楚顯現。


有關於這些細節之運用將在未來章節中配合說明。