115 高普考高考三級 資訊處理 資料結構 考古題詳解
第 一 題25 分
某系統 A 使用雜湊表(hash table)儲存不同的正整數鍵值,不允許重複鍵值。雜湊表有 11 個儲存格,索引從 0 開始,雜湊函數(hash function)為 ,並使用平方探查法(quadratic probing)處理碰撞(collision)問題,探查序列為:
刪除資料時,被刪除資料的位置標記為特殊符號 DELETED。請回答下列問題:
(一) 給定一個空的雜湊表,依序插入下列鍵值:
請畫出所有鍵值插入完成後的雜湊表狀態,並列出插入鍵值 46 時的完整探查過程。(10 分)
(二) 承上題,依序刪除鍵值 24、13,畫出刪除後的雜湊表狀態。並說明為什麼刪除鍵值時需用 DELETED 標記,而不能將該鍵值所在的儲存格恢復成「從未存放過鍵值」的空狀態。(5 分)
(三) 承上題,執行插入鍵值 12,請列出插入時的探查過程、操作停止的理由,並寫出 12 最後插入的儲存格。插入時,DELETED 標記視為可放入新鍵值的儲存格。請注意鍵值不能重複。(5 分)
(四) 相較於系統 A,考慮另一個採用平方探查法之系統 B。系統 B 的表格大小為 8,索引亦從 0 開始,雜湊函數 及探查序列 分別定義為:
在非均勻雜湊(non-uniform hashing)的情況下,也就是許多鍵值可能被分配到相同或少數幾個初始雜湊位置時,哪一個系統的雜湊表儲存格利用率可能較高?並說明理由。(5 分)
登入後即可作答並保存紀錄。
核心觀念
本題考查開放定址法中的平方探查、刪除標記,以及探查序列能否覆蓋足夠多的儲存格。
系統 A 的探查位置為
發生碰撞時,依 的順序探查。刪除時須將該格標記為 DELETED:搜尋鍵值時須越過此標記繼續探查;插入時則可將該標記格用來存放新鍵值。
解題方法
(一)插入 8 個鍵值
逐一依探查序列尋找第一個空格:
| 插入鍵值 | 初始位置與探查結果 | 插入位置 |
|---|---|---|
| 22 | ,位置 0 空 | 0 |
| 1 | ,位置 1 空 | 1 |
| 13 | ,位置 2 空 | 2 |
| 24 | 初始位置 2 已占用; 探查 ,空 | 3 |
| 35 | 初始位置 2 已占用; 到 3 已占用; 到 ,空 | 6 |
| 46 | 初始位置 2 已占用; 到 3 已占用; 到 6 已占用; 到 已占用; 到 ,空 | 7 |
| 7 | 初始位置 7 已占用; 到 8,空 | 8 |
| 18 | 初始位置 7 已占用; 到 8 已占用; 到 0 已占用; 到 4,空 | 4 |
插入 46 時,其初始位置為 ,完整探查過程如下:
| 探查位置 | 狀態 | |
|---|---|---|
| 0 | 2 | 13 已占用 |
| 1 | 3 | 24 已占用 |
| 2 | 6 | 35 已占用 |
| 3 | 0 | 22 已占用 |
| 4 | 7 | 空,插入 46 |
插入完成後的雜湊表:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 鍵值 | 22 | 1 | 13 | 24 | 18 | 空 | 35 | 46 | 7 | 空 | 空 |
(二)刪除 24、13
鍵值 24 位於索引 3,鍵值 13 位於索引 2。刪除後以 DELETED 標記:
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 鍵值 | 22 | 1 | DELETED | DELETED | 18 | 空 | 35 | 46 | 7 | 空 | 空 |
第 二 題25 分
給定一個無向圖 ,每個頂點代表一個地點,每條邊 代表一條道路,邊的正整數權重 表示該道路的塞車程度,數值越大越壅塞。對於一條從起點 到終點 ()的路徑 ,其最大塞車程度 定義為路徑上所有邊權重的最大值:
本題透過修改 Dijkstra 最短路徑演算法中陣列 的定義與更新方式,求出從 到 可行路徑所能達到的「最大塞車程度的最小值」。
修改後的演算法流程與 Dijkstra 最短路徑演算法相同,差異僅在於 ()的定義與更新規則,其中,新的 表示目前已知從 到 的路徑中,最大邊權重的最小值。初始時令 ,其他頂點 的 ()。之後依照 Dijkstra 演算法,每一輪選出尚未被選定且 值最小的頂點 ,並將原本的更新方式:
改為:
其中 為邊 ()的權重。重複進行,直到終點 被選定為止。
(一) 以下列無向圖為例,令起點 為 A,終點 為 F,依照修改後的演算法,逐步列出每次選定一個頂點後陣列 的變化過程。陣列中的頂點順序請依字母順序排列。(15 分)
(此題原卷圖形文字擷取不全,請參考原卷。)
(二) 說明修改後演算法之正確性,是基於 更新規則具有何種性質。(5 分)
(三) 假設圖以相鄰串列(adjacency list)表示。若要在尚未選定的頂點中找出 值最小者,可使用以下兩種方法:
方法一:每次以線性方式掃描所有尚未選定的頂點找出最小 值。
方法二:使用最小堆積(min-heap)維護目前 值最小的頂點。
分別就這兩種方法,分析修改後演算法最壞情況的時間複雜度。(5 分)
登入後即可作答並保存紀錄。
核心觀念
本題將 Dijkstra 演算法中的「路徑總權重最小」改為「路徑上的最大邊權重最小」,也就是求解最小瓶頸路徑(minimax path)。
對任一路徑 :
因此,若目前從 到 的最佳瓶頸值為 ,再經過邊 ,新的瓶頸值為:
更新規則為:
依圖可讀得相關邊如下:
(一)逐步執行修改後的 Dijkstra 演算法
頂點順序固定為:
初始狀態:
1. 選定
由 更新:
因此:
2. 選定
目前未選定頂點中, 最小。
由 更新:
- 經 :,所以
- 經 :,所以
- 經 :,所以
因此:
3. 選定
目前 最小。
由 更新:
- 經 :,不改善原本的
- 經 :,故
- 經 :,故
因此:
4. 選定
目前 最小。
由 更新:
- 經 :,故
因此:
5. 選定
目前 最小。
由 更新:
- 經 :,故
- 經 :,故
因此:
6. 選定
目前 最小。
由 更新:
- 經 :,故
- 經 :,不改善原本的
因此:
第 三 題25 分
給定一棵二元搜尋樹(binary search tree),且該樹同時也是一棵 AVL 樹。樹的節點在 C 語言中宣告如下:
typedef struct Node { int key; // 節點的鍵值,所有節點的鍵值皆互不相同 int size; // 以該節點為根的子樹節點總數(包含自己) struct Node *left; // 指向左子節點 struct Node *right; // 指向右子節點 } Node;
並定義以下函式:
int size(Node *node):
若傳入的 node 為 NULL,則回傳 0;否則回傳 node->size。
int count_less_equal(Node *node, int val):
回傳以 node 為根的子樹中,所有鍵值小於等於 val 的節點總數。
Node* select(Node *node, int r):
回傳以 node 為根的子樹中,第 小的節點指標, 從 1 開始算。
Node* greater_k_smallest(Node *root, int val, int k):
找出以 root 為根的整棵樹中,所有鍵值大於 val 的節點裡,第 小的節點, 從 1 開始算。若第 小的節點不存在,則回傳 NULL。
(一) 完成下列程式碼的空格。(20 分)
int count_less_equal(Node *node, int val) {
if (node == NULL) return 0;
if (node->key > val)
return count_less_equal(node->left, val);
else
return size(node->left) + (1);
}
Node* select(Node *node, int r) {
int left_size = size(node->left);
if (r == (2)) return node;
else if (r <= left_size)
return select(node->left, r);
else
return (3);
}
Node* greater_k_smallest(Node *root, int val, int k) {
int x = count_less_equal(root, val);
int y = (4);
if (y > size(root)) return NULL;
return select(root, y);
}
(二) 下圖為一棵包含 5 個節點且滿足 AVL 平衡特性的二元搜尋樹,圖中顯示每個節點的鍵值。若將鍵值為 70 的新節點插入此樹,為保持 AVL 樹的平衡,會觸發旋轉。請畫出旋轉後的樹狀結構圖。除新插入的節點 70 外,若原有節點的 size 欄位值在旋轉後發生改變,請在旋轉後的圖中,於該節點旁標示其新的 size 欄位值。(5 分)
(此題原卷圖形文字擷取不全,請參考原卷。)
登入後即可作答並保存紀錄。
核心觀念
本題考查二元搜尋樹的下列觀念:
- 利用
size欄位快速取得左、右子樹的節點數。 - 以遞迴方式計算鍵值小於等於
val的節點數。 - 以「左子樹節點數」判斷目前節點是否為第 小。
- AVL 樹插入後,若左右子樹高度差超過 ,必須透過旋轉恢復平衡。
- 樹旋轉後,受影響節點的
size必須重新計算。
(一)程式碼填空
空格(1)
若 node->key > val,目前節點及右子樹的鍵值都大於 val,只需搜尋左子樹。
若 node->key <= val,則:
- 左子樹全部符合;
- 目前節點本身符合;
- 右子樹中仍須繼續搜尋符合條件的節點。
因此:
故空格(1)為:
1 + count_less_equal(node->right, val)
空格(2)
左子樹有 left_size 個節點,因此目前節點在以 node 為根的子樹中排名:
故空格(2)為:
left_size + 1
空格(3)
當 時,目標節點位於右子樹中。扣除左子樹與目前節點後,在右子樹中的排名為:
故空格(3)為:
select(node->right, r - left_size - 1)
空格(4)
令:
x = count_less_equal(root, val);
表示整棵樹中鍵值小於等於 val 的節點數。所有鍵值大於 val 的節點,從整棵樹的第 小開始。
其中第 個大於 val 的節點,在整棵樹中的排名為:
故空格(4)為:
x + k
完整答案如下:
第 四 題25 分
考慮以下兩個互相呼叫(mutually recursive)的 C 語言函式:
int foo(int n) { if (n <= 1) return 1; return foo(n - 1) + bar(n - 1) + 2; }
int bar(int n) { if (n <= 1) return 1; return foo(n - 1) + bar(n - 1); }
請回答下列問題。每一小題請寫出推導過程,無推導過程不予計分。
(一) 當執行 foo(10) 時,一共會呼叫 foo() 函式幾次(包含最外層 foo(10) 的這一次呼叫)?(10 分)
(二) 執行 foo(10) 的最終回傳結果為何?(10 分)
(三) 以 Big-O 表示 foo(n) 的時間複雜度(time complexity)。本題若同一個「函式與參數」組合被呼叫多次,每次都重新計算,不會儲存先前的計算結果供之後使用。(5 分)
登入後即可作答並保存紀錄。
核心觀念
本題考兩個互相遞迴函式的呼叫次數、回傳值與時間複雜度。解題時分別建立「函式呼叫次數」與「函式回傳值」的遞迴式;兩者代表不同的量,不能混為一談。
題目指定同一個「函式與參數」組合再次被呼叫時,仍須重新計算,沒有使用記憶化,因此遞迴呼叫會形成指數級的計算量。
解題方法
令:
- :執行一次
foo(n),過程中呼叫foo的總次數,包含最外層這一次。 - :執行一次
bar(n),過程中呼叫foo的總次數。
當 時,函式直接回傳,不再呼叫其他函式,因此:
當 時,foo(n) 本身算一次 foo 呼叫,接著呼叫 foo(n-1) 和 bar(n-1);而 bar(n) 會呼叫這兩個函式,但 bar(n) 本身不計入 foo 呼叫次數。因此:
相減可得 。令 ,則:
解此遞迴式:
因此,當 時:
(一)計算 foo(10) 呼叫 foo 的次數
此處結果非整數,表示上面的閉式在 的邊界條件下不成立。直接將 遞迴式解開:
又因為 ,所以:
故: