用來整合資料的陣列變數
陣列是什麼?
陣列是一堆變數的集合體,
這些變數內的資料要透過一個共同的名稱及一個附屬的數字來存取,
例如︰iScores[0], iScores[1], iScores[2]...,
這和數學中數列 Ai 或是矩陣成員 Mij 有一些相像。
為什麼程式中需要陣列?
陣列變數和一般變數在使用上最不一樣的地方︰
一般的變數例如 iPrice 在整個程式段落中可能會出現好多次,
它們都是指相同的一個變數,
陣列成員變數 iScores[0] 如果在程式段落中出現好多次的話,
和一般變數一樣也是指相同的一個變數。
但是 iScores[i] 就不一定永遠指著相同的變數了,
i 是另外的一個整數變數,
如果變數 i 內存放的資料是 0,
那麼 iScores[i] 和 iScores[0] 指的是同一個變數,
如果變數 i 內存放的數值是 20,
那麼陣列變數 iScores[i] 和 iScores[20] 指的是相同的一個變數,
有了這樣子變化的陣列變數,
我們可以用更短的程式碼寫出更有用的程式來。
注意:
就好像 printf("%d",x); 比 printf("%d",5);
變化要多得多一樣,
printf("%d",5); 永遠只能印出 5,
而 printf("%d",x) 可以視變數 x 的內容列印不同的資料出來,
如果你寫 printf("%d",iScores[i]); 則甚至可以視變數 i
內存放資料的不同選擇不同的變數內的資料來列印,
彈性是不是大太多了呢?
舉例來說︰
假設由鍵盤輸入 20 個學生的成績,
希望算出有幾個學生的成績是低於平均成績的?
程式一︰
int score, count;
double average;
int i;
printf("請輸入 20 個學生的成績︰");
for (i=0,average=0.0; i<20; i++)
{
scanf("%d", &score);
average = average + score;
}
average = average / 20.0;
printf("請再一次輸入 20 個學生的成績︰");
count = 0;
for (i=0; i<20; i++)
{
scanf("%d", &score);
if (score < average)
count = count + 1;
}
printf("%d students are below average \n",count);
注意︰
這個程式希望使用者由鍵盤敲進 20 個學生的成績兩次,
使用者用到這程式一定會猛搖頭,
都輸入了一次怎麼還要再輸入一次呢?
不會記下來嗎?
程式二︰
讓我們用變數把 20 個成績記錄下來吧!
那麼當然需要 20 個變數囉!
程式如下︰
int score1,score2,score3,score4,score5,score6,
score7,score8,score9,score10,score11,score12,
score13,score14,score15,score16,score17,
score18,score19,score20;
int count;
double average;
printf("請輸入20個學生的成績︰");
scanf("%d %d %d %d %d",score1,score2,score3,score4,score5);
scanf("%d %d %d %d %d",score6,score7,score8,score9,score10);
scanf("%d %d %d %d %d",score11,score12,score13,score14,score15);
scanf("%d %d %d %d %d",score16,score17,score18,score19,score20);
average = (score1+score2+score3+score4+score5+score6+
score7+score8+score9+score10+score11+score12+
score13+score14+score15+score16+score17+
score18+score19+score20)/20.0;
count = 0;
if (score1<average) count ++;
if (score2<average) count ++;
.....
.....
if (score20<average) count ++;
printf("%d students are below average \n",count);
注意︰
-
使用者在用這個程式的時候應該不會再抱怨了,
一樣的東西只要輸入一次就好了,
可是寫這個程式的人可就辛苦了,
一樣的東西不斷地重覆,
還不能夠用迴圈呢!
-
CPU 能夠記錄資料的地方有兩種︰變數及檔案,
檔案大家都沒學到,
否則程式一的問題可以用檔案來解決,
程式二中用變數來解決程式一的問題,
但是技巧不夠純熟,
以至於程式有一點笨拙,
如果要改成處理一百個的資料,
那真得是要投降了。
-
程式三中我們將使用陣列變數來解決程式一及程式二遇見的困難。
程式三︰
int scores[20], count;
double average;
int i;
printf("請輸入 20 個學生的成績︰");
for (i=0,average=0.0; i<20; i++)
{
scanf("%d", &scores[i]);
average = average + scores[i];
}
average = average / 20.0;
for (i=0,count=0; i<20; i++)
if (scores[i]<average)
count ++;
printf("%d students are below average \n",count);
這個程式中︰
定義了 scores[0], scores[1]....scores[19]
這 20 個整數變數來存放學生的成績,
由於我們可以用 scores[i] 來使用這 20 個變數,
20 遍 scanf() 來由鍵盤讀成績的困擾,
就可以用一個簡潔的迴圈來處理,
以上是為什麼程式中需用有陣列變數的原因。
一維陣列的語法與使用
語法︰
例如︰
int iScores[1000];
double dAverages[10], dWeights[2];
使用:
大多會配合迴圈來使用
for (i=0; i<1000; i++)
{
scanf("%d", &iScores[i]);
dAverages[i%10] += iScores[i] * dWeights[iScores[i]>50 ? 0 : 1];
}
請特別注意上面三個例子中,
iScores 是由 iScores[0], iScores[1],....到 iScores[999],
dAverages 是由 dAverages[0], dAverages[1],....到 dAverages[9],
iWeights 則包括 iWeights[0] 及 iWeights[1],
如果你用到 iScores[-1] 或是 iScores[1000], iScores[2000],
Compiler 不會給你任何文法錯誤的訊息,
但是程式在執行時你會誤將別的變數的內容更改,
例如:
大部分的 compiler 中 x[0] 到 x[9] 是合規定的陣列元素,
x[10] 可能就是 y,
x[11] 可能就是 z,
於是當你用
時 y 的資料就被你不小心改掉了,
會造成你的程式邏輯錯誤。
注意:
編譯器在編譯程式的時候不會加入檢查陣列 index
是否超過宣告範圍的程式碼在你的程式當中,
所以你自己必須很小心的檢查才可以,
萬一你懷疑你自己的程式碼可能會使用到沒有定義的陣列元素時,
你也可以自己加一些程式碼來檢測 index。
C 語言中陣列變數的記憶體使用狀況:
理論上在程式語言中陣列只是給你一堆變數的集合體,
程式設計者不需要管記憶體究竟是如何配置給陣列變數的,
陣列變數在使用時也應該嚴格地限制用類似 iScores[index] 的方式來存取。
實際上
C 語言之所以被稱為「High Level Assembly Language」,
就是因為它把底層記憶體配置的狀況都讓程式設計者一清二楚地看到,
以便程式設計者可以自行以最有效率的方式處理他的資料,
C 的一維陣列是由連續的記憶體構成,
記憶體的位元組數目即為該型態變數的位元組數乘上陣列中元素的個數,
例如:
-
int x[100];
sizeof(x) = sizeof(int [100]) = sizeof(int)*100 = 200 位元組
-
char y[50];
sizeof(y) = sizeof(char[50]) = sizeof(char)*50 = 50 位元組
如果你已經懂得指標的話,
對於這樣子一連串的記憶體而言,
你可以將任何一個陣列成員的位址傳入另一個函式中處理,
型態可以自由轉變,
完全沒有任何的限制。
注意:
下面這兩種定義陣列變數的方法是完全一樣的:
-
int x[100], y[100];
-
typedef int INTARY[100];
INTARY x, y;
應用範例:
y = sin(x) 這個數學函式當 x
的範圍在 0 到 π 之間時其值在 0 到 1 之間,
請以 x 軸上 0.001 的間隔由 0 到 π
逐一計算 y 值並在 y 軸上以 0.02 的間隔統計 y
值出現在各個區間的個數,
最後以 TURBO C 文字模式做一圖表。
步驟一︰
-
y 的值在 0 到 1 中間,
若以 0.02 為間隔,
共有 50 個區間,
因此需要 50 個元素的整數陣列來統計,
其中 iCount[i] 要記錄的是 i*0.02 < y <= (i+1)*0.02 的個數,
-
將 iCount 陣列內元素通通清為 0。
for (i=0; i<50; i++) iCount[i] = 0;
-
x 軸在 0 到 π 之間以間隔 0.001 逐項記算
for (x=0; x<=PI; x+=0.001)
-
sin() 函式在 math.lib 中有此標準函式
其函式原型為:
-
計算出 y=sin(x) 之後如何知道 y 值為於哪一個區間?
要將第幾個 iCount[i] 值加 1 呢?
有兩種方法,
-
方法一︰逐一測試
for (i=0; i<50; i++)
if (sin(x)<=(i+1)*0.02)
{
iCount[i]++;
break;
}
-
方法二︰運算式
iCount[(int)(sin(x)*50.0)]++;
請注意︰
-
參考資料型態的轉換,
當浮點數轉換為整數時,
小數點之後的位數都無條件捨去。
-
請試看看下面這種錯誤的方法,
邏輯觀念是對的,
但是就是對浮點數的認識不太清楚,
看看你能不能找出下面程式片段裡有問題的兩個地方:
double y;
for (y=0.0; y<=1.0; y=y+0.02)
if (sin(x)<=y+0.02)
{
iCount[(int)(y/0.02)]++;
break;
}
-
上面第二種方法其實有可能會發生錯誤,
當 sin(x) 值為 1 時,
此敘述會存取到變數 iCount[50] 的內容,
這是一個不容許的錯誤,
請自行以 if 敘述解決。
-
讓我們以 TURBO C 的文字模式畫出這 50 個數值,
橫軸有 80 行,
我們可以由 1 畫至 50,
縱軸因為只有 24 列,
我們必須計算一下 iCount[i] 的最大值將 iCount[i] 內的值等比例縮小來呈現,
並在圖的左側顯示出座標數字。
max = 0;
for (i=0; i<50; i++)
if (iCount[i]>max)
max = iCount[i];
-
如果我們計畫利用 20 列來顯示 0 至 max 的 iCount[i] 值,
可以用下面的程式碼將 iCount[i] 的值改變為顯示座標。
for (i=0; i<50; i++)
iCount[i] = iCount[i]/((double)max)*20.0;
-
清除螢幕:
#include <conio.h>
clrscr();
-
畫 x 座標軸:
gotoxy(10,22);
for (i=0; i<50; i++)
putchar('-');
gotoxy(50,23);
printf("50");
-
畫 y 軸座標軸:
for (i=0; i<20; i++)
{
gotoxy(9, 2+i);
putchar('|');
}
gotoxy(9, 22);
putchar('+');
-
標示 y 軸範圍:
for (i=0; i<5; i++)
{
gotoxy(5,22-i*20/4);
printf("%4d",(int)(max/4.0*i));
}
-
畫 iCount[] 陣列資料
for (i=0; i<50; i++)
{
gotoxy(10+i,22-iCount[i]);
putchar('*');
}
-
用 getch() 暫停一下程式來觀看結果。
注意︰
這個程式很多部分都是獨立的,
為了怕相互影響,
你可以做一小部分就測試一下,
確保程式每一部分都安份地工作,
才不會整個程式錯成一堆,
不知道從何處下手去除錯。
步驟二︰
組合起整個程式,
類似這種資料處理的程式流程很簡單,
一個區塊一個區塊執行就是了。
步驟三︰
測試一下,
如下圖:
注意︰
-
因為我們只取了 3142 個點來看這個半週期的
sine 函式,
在 y 座標上又分為 50 個區間,
因此你在輸出的地方會看到有一點上下震動,
這是正常的,
只要你把取點數增加的話,
例如每隔 0.0001 看一次 sine 函式的數值,
應該就會好很多了。
-
這個程式輸出的結果看起來不太漂亮,
這是因為我們只用 50 x 20 個點的畫布來繪製,
如果你希望繪出來的比較好看的話,
需要用 TURBO C 的圖形模式來做。
請下載 SineCountGraf.exe
egavga.zip 並執行,
結果如下圖:
二維陣列
語法︰
定義︰
使用︰
for (i=0; i<15; i++)
for (j=0; j<50; j++)
average = average + twoDimArray[i][j];
多半配合迴圈使用。
為何需要二維的陣列? (一維的陣列不夠用嗎?)
-
人的思考與記憶方式都是相當結構化的,
如果要替一個公司的所有員工建立資料,
我們可以先依部門分類,
再依姓名筆劃記錄每一員工的資料,
如此每次在尋找某一個員工的資料時,
可以先尋找該員工部門的資料,
再尋找該名員工的資料,
如此可能節省搜尋時間,
也可以防止資料混亂。
要是寫一個程式來處理這些資料的話,
我們可以用一個陣列來放人事單位的員工叫做 personnel[30],
會計單位的員工叫做 accounting[30],....
假設有二十個單位,
就有二十個陣列變數來存放這些人事資料,
那麼如果要發薪水的話就可以用下面的這段程式來做︰
for (i=0; i<numberPersonnel; i++)
pay(personnel[i]);
for (i=0; i<numberAccounting; i++)
pay(accounting[i]);
...
...
...
看一下上面這段 "冗長"的程式,
大家可以再回憶一下前面那個替每一個學生成績宣告一個變數的程式,
該程式中之所以無法用迴圈處理相同的事情,
原因在於沒有用陣列來表達性質一致的資料,
現在這個程式也是一樣的情形。
如果我們能夠用二維的陣列 staffMember[20][30] 來記錄員工資料的話,
上面的程式就可以取代為
for (dept=0; dept<20; dept++)
for (i=0; i<member[dept];i++)
pay(staffMember[dept][i]);
其中陣列變數 int number[20] 內存放的是各部門員工之人數。
當然上面這個範例也可以只用一維的陣列來處理,
例如︰staffMember[600] 如此可以將所有的員工資料依序存放而不分部門,
也可以將此陣列分為 30 個 30 個的群組,
0 至 29 存放人事部門的員工資料,
30 至 59 存放會計部門的員工資料....,
這樣子的陣列理論上可以達到和二維陣列完全一樣的效果。
例如︰
for (dept=0; dept<20; dept++)
for(i=0; i<number[dept]; i++)
pay(staffMember[30*dept+i]);
這兩段程式是等效的,
唯一不同的是程式設計者及閱讀程式的人對於這些資料的認知,
是否看到一維陣列時能夠理解到
這些資料實際上是有依照部門來分類的?
我們製作程式時如果讓程式內資料的表示方法盡量和實際世界中資料的組織方法一致的話,
在設計演算法時困難度較低,
維護程式或是修改程式時需要付出的代價相對的也比較低。
-
實際世界中有很多資料是二維的,
例如影像資料很自然地用二維陣列來存放。
範例︰井字遊戲平台
讓我們寫一個簡單的井字遊戲平台,
程式要求遊戲雙方依序輸入○╳的座標,
利用 TURBO C 文字模式將遊戲狀況在螢幕上印出來,
並檢查遊戲是否結束。
範例執行程式
-
遊戲狀態之表達:
3x3 之九宮格可以用一個二維字元陣列來表示,
char game[3][3];
for (i=0; i<3; i++)
for (j=0; j<3; j++)
game[i][j] = ' ';
其內容可為空白字元 ' ','x',或是 'o'。
-
螢幕界面之表現:
程式在螢幕上除了顯示 3x3
圈圈叉叉之外還需要顯示格線如下圖:
我們使用 TURBO C 中的文字模式來處理這個畫面:
clrscr();
gotoxy(10,10);
printf(" | |");
gotoxy(10,11);
printf("-+-+-");
gotoxy(10,12);
printf(" | |");
gotoxy(10,13);
printf("-+-+-");
gotoxy(10,14);
printf(" | |");
顯示陣列內圈圈叉叉的資料:
for (i=0; i<3; i++)
for (j=0; j<3; j++)
{
gotoxy(10+j*2, 10+i*2);
putchar(game[i][j]);
}
-
檢查遊戲是否結束?
程式充作裁判,
檢查所有八種情況下是否連成一直線,
若是的話就結束遊戲。
gameOver = 0;
for (i=0; i<3; i++)
{
if ((game[i][0] == game[i][1]) &&
(game[i][1] == game[i][2]) &&
(game[i][0] != ' ')
{
gameOver = 1;
break;
}
if ((game[0][i] == game[1][i]) &&
(game[1][i] == game[2][i]) &&
(game[0][i] != ' '))
{
gameOver = 1;
break;
}
}
if (! gameOver)
{
if ((game[0][0] == game[1][1]) &&
(game[1][1] == game[2][2]) &&
(game[1][1] != ' '))
gameOver = 1;
}
if (! gameOver)
{
if ((game[2][0] == game[1][1]) &&
(game[1][1] == game[0][2]) &&
(game[1][1] != ' '))
gameOver = 1;
}
-
鍵盤界面:
依次詢問雙方使用者下一步之位置,
並檢查是否為合法的位置。
首先,必須要有一個變數記錄下一步是哪一方 ( x 或 o ) 需動作:
其次印出請使用者輸入下一步座標的訊息:
gotoxy(10,20);
printf("next move is %c", nextMove);
gotoxy(10,21);
printf("input x (1..3) y (1..3) coordinate : ");
讀入 x, y 座標值,
並檢查是否在允許的區間內,
當然也要檢查一下是否該位置上已經有人佔據啦:
do
{
gotoxy(47,21);
printf(" "); // 清除螢幕上現有的資料
scanf("%d %d", &x, &y);
}
while ((x<=0)||(x>=4)||(y<=0)||(y>=4)||(game[x-1][y-1]!=' '));
上面這個迴圈看起來應該可以正常運作,
其實不然,
原因是在一個複合的條件測試裡,
哪一個測試先被執行是無法預估的,
上面的條件中有可能在使用者輸入 -100, -100 時,
不先做 (x <= 0) 的測試而先做 (game[-101][-101] != ' ')
的測試而導致 "不合法存取記憶體 (illegal access)"
的錯誤,
因此我們必須改用下面的測試:
while (1)
{
gotoxy(47,21);
scanf("%d%d",&x,&y);
if ((x>=1)&&(x<=3)&&(y>=1)&&(y<=3))
if (game[x-1][y-1] == ' ') break;
gotoxy(47,21);
printf(" "); // 清除螢幕上現有的資料
}
在確定使用者要在哪一個位置下一子之後,
用下面的程式碼來改變遊戲的狀態
game[x-1][y-1] = nextMove;
if (nextMove == 'o')
nextMove = 'x';
else
nextMove = 'o';
-
程式流程:
一般情形下遊戲程式必須考慮和使用者之間的互動,
因此流程比較需要設計,
如下圖:
-
測試
-
本程式若是在加上網路的界面,
就可以允許兩台機器上兩個人同時執行了,
就像一些象棋、圍棋、或是西洋棋的線上遊戲了。
另外還有一種比較有趣的模式是電腦和你比賽,
井字遊戲是有捷徑的,
例如如果你先佔據中央位置的話,
遊戲可能發生的狀況就大大減少了,
此種程式請參考
範例執行程式
。
當然此種遊戲標準的解法可以參考人工智慧中的 AND / OR Tree 的搜尋,
例如:
以 MinMax 方法搜尋
。