# 第二週課程內容 (2018/09/07) # 何謂資料結構(data structure) 資料結構即我們==如何保存、放入或拿出資料==的方法。 在資料結構中,我們在意的是存取資料的==操作==。 每種資料結構,會各自有對應的可行操作, 在選擇合適的資料儲存方式時,最優先考慮的便是我們打算對資料進行什麼樣的操作, 以此選擇能夠為這些操作特化的資料結構。 目前我們學過的資料結構,應該只有『陣列』一種。 # 最基本的資料結構:陣列(array) 陣列用以儲存==複數的相同形別資料==, 能做的操作是透過指定 index 取得特定的資料。 可以看成是一個大櫃子,每一格可以存放一個東西, 並且每一格被賦予一個編號,編號為從 0 開始的連續非負整數。 編號稱作索引(index),而每個被存放的東西稱為元素(element)。 例如,透過陣列儲存 5 個整數:3, 7, 5, 6, 4 儲存起來會像這樣子: | 索引 | 0 | 1 | 2 | 3 | 4 | | --- | - | - | - | - | - | | 內容 | 3 | 7 | 5 | 6 | 4 | ## 陣列的宣告 以下為宣告一長度 1024 元素型別 int 的陣列,名為 ary: ```cpp= int ary[1024]; // 型別 變數名[元素個數]; ``` 依此類推,宣告 100 個 string 的方式如下: ```cpp= string ss[100]; // 編號 0 ~ 99 ``` 注意:由於編號從 0 開始,若我們宣告大小為 k 的陣列, 從 0 開始數 k 個元素的話,最大索引將只會到 k-1。 陣列不能改變大小,所以必須考慮最差情形來宣告。 例如題目最多給 1000 個數字,則宣告時就要考慮至少開 1005 的大小。 :::info 為避免出錯,當需要長度 k 的陣列時,通常會宣告 k+5 ~ k+10 的長度。 ::: ## 陣列的宣告位置 :::info 安全起見,陣列最好一律宣告為全域變數。 ::: 主因是解題過程,時常需要開相當大的陣列, 可能是數百、甚至上千萬個 int。 能夠宣告在區域變數中的大小, 遠較全域來得小,而且每台電腦未必一致。 同時他會佔用系統 stack 的空間, 會更容易發生遞迴過深等 stack overflow 相關的問題。 這些屬於潛在的、不一定發生且難以預見和判斷的問題, 為了規避不必要的風險,最安穩的方法就是將陣列一律宣告於全域。 此外,==宣告在全域的陣列,每個元素皆會被初始化為 0。== 宣告為==區域變數則沒有這個性質==。 更多細節可以參考 Lys0829 所寫的 [這篇文章](https://hackmd.io/tTAs-GXpQIqGLfIrEkfLRA) 或者 google 相關的關鍵字。 ## 陣列的初始化 可以用 {} 在宣告時對陣列進行初始化,例如: ```cpp= int dx[4] = {0, 1, 0, -1}; ``` 注意:{} 僅用於初始化,並不能用於對已宣告好的陣列做賦值(assign)的動作, 例如以下是錯誤行為: ```cpp= int dx[4]; dx = {0, 1, 0, -1}; // compile error!! ``` 初始化時未指定的部份,會自動填入 0,例如以下: ```cpp= int dx[4] = {0}; for (int i=0; i<4; i++) { cout << dx[i] << '\n'; } ``` 一個常見的誤會是將上述寫法誤解成把每一格都填成 0,這是==錯==的; 雖然真的每格都變成 0,但真正的意思是:==把第一格填成 0,剩下沒填到的補 0==。 差別在:如果你認為以下 code 可以把陣列每一格初始化成 -1 的話。 事實上==並不會==。 ```cpp= int dx[4] = {-1}; for (int i=0; i<4; i++) { cout << dx[i] << '\n'; } ``` 如果你有正確的認識,你會知道:變成 -1 的只有第一格。 若你對陣列有初始化,你可以略過長度, 則此陣列長度自然會變成你初始化時給的元素個數: ```cpp= int ary[] = {3, 7, 5, 6, 4}; cout << (sizeof ary) / (sizeof ary[0]) << '\n'; ``` sizeof 可以計算變數所佔用的空間大小,單位是位元組(byte)。 陣列佔用空間為:元素個數 x 元素大小 藉由除掉一個元素的大小,可以得出元素個數。 ## 取得陣列特定索引的元素 這件事可以透過中括號 [] 做到: ```cpp= int ary[] = {3, 7, 5, 6, 4}; for (int i=0; i<5; i++) { cout << ary[i] << '\n'; } ``` 陣列中的每個元素都相當於是一個變數, 一個 int 陣列,每個元素都相當於是一個 int 變數。 平常能對 int 變數做的事,都可以對 int 陣列中的某個元素來做。 例如賦值: ```cpp= int ary[5]; for (int i=0; i<5; i++) { ary[i] = i + 1; } ``` # 記憶體(memory)與位址(address) 任何變數都有以下三項: - 名字(name) - 儲存的值(value) - 實際在記憶體中所在的位址(address) 名字是讓我們指名變數用的識別代號, 實際上變數是 C++ 向作業系統所要來的一塊記憶體空間, 位址則是此記憶體空間開始的位址,實際上可能佔用多個地址。 而儲存的值,則是這塊記憶體空間中保存的內容。 ## 陣列在記憶體中的長相 陣列會是記憶體中一塊==連續的空間==,依序儲存從索引 0 開始的每個元素。 例如一個 int 陣列可能長得如下: `int ary[5] = {3, 7, 5, 6, 4};` | 位址 | 100 | 104 | 108 | 112 | 116 | | -------- | -------- | -------- | -------- | -------- | -------- | | 索引 | 0 | 1 | 2 | 3 | 4 | | 值 | 3 | 7 | 5 | 6 | 4 | 可知實際元素位址是由 `起始位址 + 元素索引 * 元素大小` 計算得出。 # 陣列應用 陣列除了可用來保存複數個相同型別的東西外, 也能直接當作表格,來保存、查詢指定整數所代表的值。 例如,以 `tbl[n] = k;` 來儲存二年 n 班修課人數有 k 人。 例如,以 `tbl[n] = k;` 來儲存第 n 個字母,在一篇文章中出現了 k 次。 # 多維陣列(multi-dimension array) 一個 n 維陣列,將會是一個==所有元素皆為 n-1 維陣列==的一維陣列。 宣告時,有 n 維的陣列,就會有 n 組大括號,代表每一維的長度。 例如二維陣列,依上述將會是一個『所有元素皆為 1 維陣列』的一維陣列。 `int tbl[5][3];` 在記憶中的長相如下: | 索引 | 0 | 1 | 2 | 3 | 4 | | -------- | -------- | -------- | -------- | -------- | -------- | | 值 | 陣列0 | 陣列1 | 陣列2 | 陣列3 | 陣列4 | 每一個元素是一個 int 的一維陣列,所以長得如下(值我隨便填的): | 索引 | 0 | 1 | 2 | | -------- | -------- | -------- | -------- | | 值 | 4 | 1 | 6 | 實際上 `tbl[i][j]` 代表存取第 i 個元素(它是一個長度 3 的陣列)中的第 j 個元素。 展開會長得像這樣: | tbl索引(i)↓ | tbl[i]索引(j)→ | 0 | 1 | 2 | | -------- | -------- | -------- | -------- | -------- | | 0 | tbl[0] | 4 | 1 | 6 | | 1 | tbl[1] | 4 | 1 | 6 | | 2 | tbl[2] | 4 | 1 | 6 | | 3 | tbl[3] | 4 | 1 | 6 | | 4 | tbl[4] | 4 | 1 | 6 | 二維陣列當作一維陣列來看待的話,就很容易理解 `tbl[i]` 是什麼意思。 它是這個陣列中,索引 i 的元素,型別是一維陣列。 `tbl[i][j]` 由此來理解,就是 `tbl[i]` 這個陣列中,索引 j 的元素: `(tbl[i])[j]` 更高維的陣列依此遞迴規則類推即可。 ## 二維陣列的初始化 在此只提較常用的二維,三維或更高維的初始化可依此類推。 上面提過一維陣列的初始化,與此同理: ```cpp= int tbl[5][3] = { // 5 個元素,以 , 隔開 {4, 1, 6}, // 每個元素 tbl[i] 亦是一個陣列,同樣用 {} 初始化 {4, 2, 6}, // tbl[1] {5, 1, 2}, // tbl[2] {2, 5, 6}, // tbl[3] {1, 2, 8} // tbl[4] }; ``` ## 座標系 (x, y) vs 表格的橫列 (row) 直排 (column) 由於英文的書寫習慣上,是先左到右,再上到下。 陣列在記憶體中的長相,也更接近如此。 在第 r 橫列的第 c 直排上,通常會表達作 `tbl[r][c]`。 然而,座標系卻習慣先寫 x 再寫 y, 座標系上的 x 相當於直排(x=1 是上下延伸的直線), y 相當於橫列(y=1 是左右延伸的直線), 和上述的先橫列再直排是相反的,在使用上要十分注意不要搞混。 建議大部份情形,如果迴圈這樣寫: ```cpp= for (int i=0; i<n; i++) { for (int j=0; j<m; j++) { cin >> ary[i][j]; } } ``` 可知 i 在第一維、j 在第二維,剛好按照英文書寫的順序。 這個順序基於 C++ 的一些優化,會是最快速的。 若將第一維、第二維的順序反過來,變成先上到下,再左到右,效能會差很多。 # 指標(pointer) - 儲存『位址』的變數 指標是一種特殊的變數,不過本質還是變數,因此同樣具有以下三個要素: - 名字 - 值 - 位址 只是它的『值』的實際意義會是一個『位址』。 當一個指標 p 儲存的『值』相等於另一個變數 a 的『位址』, 或者說指標 p 儲存著另一個變數 a 的『位址』時, 我們說==指標 p 指向變數 a==。 ## 指標的宣告 指標的宣告方式為:指向對象的型別 *變數名字; 例如指向 int 的指標: ```cpp= int *p; ``` 此時 p 的型別為 int\* 但我們通常不以 `int* p;` 來宣告,原因如下: ```cpp= int* p, q; ``` 在這個宣告下,p 的型別為 int\*,q 的型別為 int。 也就是說實際上意思較接近是: ```cpp= int *p, q; ``` 這個很好驗證,只要宣告一個肯定跟指標不同大小的型別便知: ```cpp= char *p, q; cout << sizeof(p) << " <> " << sizeof(q) << '\n'; ``` 附帶一提,指標所佔用的空間大小依系統而定, 在 32 位元系統下佔 32 位元(4 位元組), 在 64 位元系統下佔 64 位元(8 位元組)。 這也是為什麼需要 64 位元系統的原因: 32 位元(4 位元組)只能表達 4294967296 種不同的東西, 這意味著位址的可能性只有 4 * 10^9 種,約是 4GB 大小。 要讓記憶體擴充至 4GB 以上,就需要更多空間來存位址。 ## 取址 && 指向 如果用過 scanf() 想必對於 & 這個『取址運算子』十分熟悉。 在變數名前加上 & 可取得該變數的位址: ```cpp= int a; cout << &a << '\n'; ``` 由於指標儲存的『值』就是『位址』的意思, 因此將變數取址得出的『位址』存到指標的『值』中,就相當於是讓指標指向該變數: ```cpp= int a, *p; p = &a; ``` ## 取值 在指標前加上 * 可存取指標所儲存位址的內容, 例如讓指標 p 指向變數 a 的位址, 則 \*p 相當於存取變數 a 所在位址, 此時 \*p 等價於 a。 ```cpp= int a = 5; int *p = &a; *p = 16; cout << &a << ": " << a << '\n'; cout << p << ": " << *p << '\n'; ``` # 指標與陣列 陣列本身其實也是一種指標, 只是一來它的值(指向的位址)不能夠被更動,並且會指向自己; 二來它會要來一大塊的空間來存放他的元素們。 ```cpp= int a[5]; cout << a << " : " << &a << '\n'; ``` 如此一來,陣列便不需要額外要一塊空間,來儲存『元素們所在的位址』, 同時自己可以當作指標(值是位址)來用。 ## 指向陣列的指標 由於陣列本身的值就是陣列自身的位址,因此以下兩種寫法都一樣: ```cpp= int ary[5]; int *p; p = ary; p = &ary; ``` ## 指標的加法 指標的加法代表意義是:前進多少個單位。 因此,指向 int 的指標 p, `p+1` 代表的是指標 p 指向的位址,再前進一個 int 的位址。 ```cpp= int ary[5], *p; p = ary; cout << (p+1) << " : " << &ary[1] << '\n'; ``` 由於前進的是一個 int 的位址,實際位址是前進了 4, 因為位址的單位是 byte,而 int 佔用 4 byte。 同理,若是 char 指標,則實際會前進 1 byte; long long 指標則會前進 8 byte。 ## [] 的真意 從上可知,`&ary[i]` 其實等價於 `ary+i`; 而 `ary[i]` 等價於 `*(ary+i)`。 它只是一種為了方便的縮寫而已。 這也表示 [] 其實是指標用的,而不只是陣列的專利: ```cpp= int ary[5] = {3, 7, 5, 6, 4}, *p; p = ary; cout << p[1] << '\n'; p[2] = 16; cout << *(p+2) << '\n'; ``` 更甚者,a[b] 運算其實是 *(a+b) 的關係,我們甚至可以這麼寫: ```cpp= int ary[5] = {3, 7, 5, 6, 4}, *p; p = ary; cout << 1[p] << '\n'; *(p+2+1) = 16; cout << (2+1)[ary] << '\n'; ``` ## 指標的減法 指標減去一個整數,等同於加上該整數的變號。 例如 p-5 等價於 p+(-5),意義上是往後移 -5 格,相當於往前移 5 格的意思。 兩個指標相減則代表它們之間的距離差了幾格, 例如兩個 `int*` 相減,就代表這兩個位址差了多少個 int。 如果 `p+5 == ary` 則 `ary-p == 5`,這樣的感覺。 ## 指向虛無(void)型別的指標 `void*` 型別可以指向任何型別的東西。 但因為它自身不代表任何型別,所以不能做取值 \* 的運算。 函數某些時候會需要它來吃不特定型別的參數, 比如說排序用的 qsort() 就需要它來吃不特定型別的陣列, 以便對應任何型別的陣列。 ## 不指向任何位址的指標 我們以指向 `NULL` (其值相當於 0), 來代表一個指標其實並沒有被指向任何一個地方。 NULL(0)這個位址是特別的,是系統保留用的,平常無法使用。 # 指標的應用 - 鏈結串列(linked list) 鏈結串列和陣列同樣是儲存多個相同類型的元素使用。 由於陣列在刪去特定元素、或加入特定元素時,會需要大量的資料改動; 例如將陣列 {3, 7, 5, 6, 4} 刪去元素索引 2: | 索引 | 0 | 1 | 2 | 3 | 4 | | -------- | -------- | -------- | -------- | -------- | -------- | | 值 | 3 | 7 | ~~5~~ | 6 | 4 | 變成: | 索引 | 0 | 1 | 2 | 3 | 4 | | -------- | -------- | -------- | -------- | -------- | -------- | | 值 | 3 | 7 | 6 | 4 | 在索引 2 之後每個元素都要往前搬移,同理將元素 8 插入至索引 2, 則需要將索引 2 之後每個元素往後搬移: | 索引 | 0 | 1 | 2 | 3 | 4 | 5 | | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 值 | 3 | 7 | 5 | 6 | 4 | 變成: | 索引 | 0 | 1 | 2 | 3 | 4 | 5 | | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 值 | 3 | 7 | 8 | 5 | 6 | 4 | 若陣列有 n 個元素,則插入和刪除最多需要搬動 n 個元素, 需要的計算量十分可觀。 ## 鏈結串列的原理 鏈結串列不需要連續的記憶體,每個元素可打散到記憶體中不同位址, 每個元素會記得他的下一個元素的位址,因此只要記住第一個元素, 就可以一路找到下一個元素,直到最後一個為止。 最後一個元素通常會被指向 NULL,表示後面已經沒有東西了。 ![](https://i.imgur.com/A2vnFIJ.png) 儘管在找第 i 個元素時相當慢,需要一路問 i 次, 但在刪除和插入卻相當快。 例如上圖,要刪去 5 的時候, 只需將 5 的上一個(也就是 7),指向 5 的下一個(也就是 6), 5 就消失在這一串鏈結串列中了。 下圖藍色代表更動的部份: ![](https://i.imgur.com/yDxIfuf.png) 從 3 出發只會找到 3, 7, 6, 4 而不會走到 5 去,刪除成功。 插入同理,只需將要插入的 8 指向它的上一個(7)的下一位(5), 再把它的上一個(7)指向自己(8),如下圖藍色部份: ![](https://i.imgur.com/tK8C9tK.png) 如此從 3 出發尋訪時,會依序找到 3, 7, 8, 5, 6, 4,插入成功。 從此可發現,插入與刪除都無須做大量的資料搬移。 ## 實作面 我們會需要在一個元素中儲存我們要的值,以及一個指向下一個元素的指標。 用 struct 可以將任意相異的變數,包成一個新的型別: ```cpp= struct node { int value; node *next; }; ``` 上述的 code 可以打造出一個新的名為 node 的型別, 其包含兩個變數:value(int)以及 next(node\*) 則從此可以宣告 node 型別的變數,使用 . 來存取 node 型別裡宣告的變數: ```cpp= node ele, eve; ele.value = 16; ele.next = &eve; eve.value = 32; eve.next = NULL; for (node *ptr=&ele; ptr!=NULL; ptr=(*ptr).next) { cout << (*ptr).value << '\n'; } ``` 如此便成功串成了長度為 2 的鏈結串列:{16, 32} 由於每次要寫 `(*ptr).value` 實在太麻煩,C++ 同樣提供了等價的縮寫: `ptr->value` 因此,上述 code 可寫成如下: ```cpp= node ele, eve; ele.value = 16; ele.next = &eve; eve.value = 32; eve.next = NULL; for (node *ptr=&ele; ptr!=NULL; ptr=ptr->next) { cout << ptr->value << '\n'; } ``` ## 創造足夠多的 node 來串成鏈結串列 通常的做法是:需要插入新元素時,new 一個新的 node 型別實體出來, 但比賽使用 new 效能不佳,也通常沒有必要。 這裡我們提競賽用的做法,new 的做法作為額外參考,有興趣者自行參考。 競賽中通常我們會知道最差情況,要串幾個元素,因此可做預先宣告: ```cpp= node pool[10000005]; ``` 用一個指標指向 pool 中下一個可用的元素,每一組測資重置: ```cpp= node *pptr = pool; ``` 每次要一個新東西時: ```cpp= node *next = pptr++; ``` `pptr++` 會得出 pptr 原本的值,但在這之後 pptr 的值會被 += 1, next 則會指向新的可用的 node。 # 額外內容:即時創造新的實體 使用 new 可在執行階段動態向系統要一塊新的記憶體空間, 而非在編譯時期決定要多少記憶體: ```cpp= node *next = new node; ``` 不過要來的記憶體,在你 delete 它之前是不會不見的。 new 需要時間,delete 需要時間,而且比起你直接要一整塊大塊的, 分成一塊塊每次 new 是更花費時間的。 每組測資重新 new 起,也很容易不小心用到超出記憶體限制。 只考慮競賽的話是十分不推薦的方法。 # 額外內容:函數的傳值、傳址、傳參考 一般函數呼叫時,會將傳遞的參數直接複製一份傳進去(call by value), 這時函數內不管如何改動,對原本的變數而言只是塗改被複製出來的一份罷了。 在 C 中,需要在函數中變更變數的值時,則需要將位址傳進去(call by address), 這時函數內可透過 address 更改到目標變數的值。 在 C++ 則追加了能對參數宣告為參考(call by reference), 例如將參數宣告為 int &a 則 a 會參照至傳遞進來的變數, 也就是讓 a 實際的位址相等於傳遞的變數。 ```cpp= void f(int p, int *q, int &r) { p = 6; *q = 8; r = 9; cout << "p: " << p << ", " << &p << '\n'; cout << "q: " << *q << ", " << q << '\n'; cout << "r: " << r << ", " << &r << '\n'; } int main() { int a = 2, b = 3, c = 5; cout << "a: " << &a << ", b: " << &b << ", c: " << &c << '\n'; f(a, &b, c); cout << "a: " << a << ", b: " << b << ", c: " << c << '\n'; return 0; } ``` 從以上測試可發現:p 和 a 毫無瓜葛,b 的位址被複製進 q, 而 c 和 r 雖不同名,取址卻會得到完全相同的位址。 簡單地說,call by value 就是東西讓你抄一份拿去用; call by address 告訴你我把東西放在哪裡,但不直接給你那個東西; call by reference 就是直接把原本的東西拿給你。 # 額外內容:指標的指標 既然指標也是變數,也有位址來存一個值, 那麼就可以用指標指向它。 一個指向 `(int*)` 型別的指標,照前面的規則應是要再加上一個 \* 成為: `int **pp;` 則其取值後的型別為 `int*`,仍是指標。 依此邏輯將指標 `int*` 看成一個大型別, 一層一層剝開就可以較不混亂地去理解指標的指標。 一般而言競賽不至於用到指標的指標,並不是那麼地重要。 # 額外內容:可變長度二(多)維陣列 直接宣告二維陣列時,由於是看成每個元素皆為一維陣列, 陣列中每個元素會相同,因此長度其實會完全一致。 若希望有個陣列的陣列, 每條陣列不一樣長,那就會變得麻煩一些: 考慮每條陣列需要一個位址,因此會是一個指標 int\*; 我們需要很多條陣列,所以需要很多個 int\*。 很多個指標就是指標的陣列: ```cpp= int *pary[64]; for (int i=0; i<64; i++) { pary[i] = new int[i+1]; } ``` 這樣第 i 條陣列就有 i+1 的長度,實現了並不是很整齊的二維陣列。 同理,如果連有幾條陣列都動態宣告的話, 由於每個元素 pary[i] 都將是 int\* 型態, 而型態 T 的陣列,可以 T\* 來動態配置, 例如將 T 置換成 int: ```cpp= int *ary = new int[128]; ``` 那如果每個元素都是 int\* 型態的話,就將 T 換成 int\*, 很自然地得到 int** 型態: ```cpp= int n = 64; int **pary = new int*[n]; for (int i=0; i<n; i++) { pary[i] = new int[n+i]; } ``` :::info 困擾時,將型別用 T 代換掉,用 T 去思考該怎麼做, 最後再換回目標型別,邏輯就會很清楚 ::: 或者我們可以善用 typedef 將型別賦予別名: ```cpp= typedef int* T; // 賦予 int* 新的別名:T int n = 64; T* pary = new T[n]; for (int i=0; i<n; i++) { pary[i] = new int[n+i]; } ``` 這樣整體邏輯也會十分清楚, 把 T 換成更好懂的名稱(ARY 之類的)會再更清楚。 多維陣列依此類推。 # 額外內容:二(多)維陣列的指標 整齊的二維陣列,其實是可以透過每個元素的長度, 當作一條一維陣列看待的; 但是在上面整理出來的 int\*\* 卻不包含這些資訊。 對 int\*\* 而言,每個元素就是 int\* 這麼大而已。 假設有個二維陣列 `int tbl[8][7];` 那麼每個元素大小應為 `sizeof(int) * 7` 而不是 `sizeof(int*)` 而每個元素大小為 `sizeof(int) * 7` 的指標,可以被這麼宣告: ```cpp= int tbl[8][7]; int (*ptbl)[7]; ptbl = tbl; ``` 多維陣列依此類推。 # 額外內容:函數的指標 函數實際上在 C++ 中,也是擁有位址的, 因此也可以用指標來指向,只要需求參數相同。 這實質上主要是在 C 刻大型程式比較會用上, 例如將「攻擊」這個動作用「函數指標」型態的變數來存, 這樣要把主角的攻擊從「揮劍」改為「詠唱火球術」時, 只需將「攻擊」指標從指向「揮劍」改為指向「詠唱火球術」即可完成, 而呼叫時同樣只需要 `main_chara.attack();` 不需要改變呼叫的函數。 其它可以用在像是,對於「NPC」的移動AI,用函數指標來儲存, 這樣對不同 NPC 只要指向不同函數,就可以用同樣變數來應對不同移動方式。 寫法如下: ```cpp= int wood_sword(int str) { return str; } int excalibur(int str) { return str * str; } int main() { int (*attack)(int str); // 宣告一回傳值為 int 參數列 (int) 的函數指標 attack attack = wood_sword; cout << attack(5) << '\n'; attack = excalibur; cout << attack(5) << '\n'; return 0; } ``` 只要回傳值型別相同、參數列也相同(順序相同、型別相同、個數相同,參數名不重要且可省略), 都可以指。 函數指標一樣用 () 把參數傳進去就能呼叫。 # 作業 這次作業以熟悉陣列與其運用為主,但開始摻雜其它題型。 畢竟真正考 APCS 時,題型是要讀完題目後自行判斷的。 指標由於很難找到單純的題目,畢竟它能做的事有太多替代案, 指標相關以鏈結串列的練習為主。 :::info 注意:題目未按難易或上課提及之順序排列。卡住時建議先看後面題目。 ::: [SkyOJ 連結](http://sky.tfcis.org/rank.php?mod=commonboard&id=61) - UVa 245 - UVa 350 - UVa 484 - UVa 10010 - UVa 10500 - UVa 11192 - UVa 11946 - UVa 11988 - UVa 12195 - UVa 12650 ## 建議 面對複雜的題目時,特別是==資料結構==時, 需要先判斷我們==想要儲存的資料長什麼樣子==, 以及==存取時有什麼特性==, 再根據這些特性來尋找或思考合適的方法。 在實作變得複雜時,事先想好==整體流程==, 再將流程==拆成一個個的步驟==,分開完成。 這樣每次只需考慮一個步驟,會比考慮如何實作一個完成的大問題來得容易。 想好流程再動手,動手時再針對目前步驟去想細節。 『總之先寫再說』的話流程容易亂、步驟間分工不明確; 動手之前先規劃細節,很可能因經驗不足想得不完整,以致難以實行, 甚至卡在思考無法進展。細節可以一邊嘗試、實驗,一邊修飾。 :::danger 寫百行程式碼的難度,絕不只是五十行的兩倍。 ::: 如果拆了還是太難,就把步驟當成大問題再往下分析流程、拆成小步驟。 ## 給資訊社競賽組的作業 :::warning 非資訊社競賽組的同學們可以無視這部份。 當然也歡迎挑戰,雖然可能會十分吃力, 並且可能會需要你們完全沒學過的技能與知識。 ::: 這次會集中在遞迴、排序、或者基本資料結構上。 雖然有主題,但還是需要自行判斷一下題型,以及小心是否有例外。 :::info 注意:有些題目在 Lucky 貓仍未有翻譯, 若有閱讀上的困難可以拿去社團求助。 ::: [SkyOJ 連結](http://sky.tfcis.org/rank.php?mod=commonboard&id=62) - UVa 10324 - UVa 110 - UVa 196 - UVa 198 - UVa 536 - UVa 640 - UVa 727 - UVa 10017 - UVa 10858 - UVa 11351 - UVa 10063 - UVa 271 ###### tags: `APCS2018上`