# 2025q1 Homework2 (quiz1+2)
contributed by < JUSTUSVMOS >
## Week 1 Quiz - Linked List
[Week1 Quiz](https://hackmd.io/@sysprog/linux2025-quiz1)
### Quiz 1 - Head Linked List `list_insert_before`
#### 解釋程式碼
**1. 線性尋找插入位置**
- 迴圈條件 `*p != before`:只要目前節點不是欲插入點,就把 p 移到下一個「指標的位址」──亦即 `p = &(*p)->next`。
- 若 before 為 NULL,迴圈會跑到最後一個節點的 next 欄位位址,使 p 最終指向「尾端指標」(即目前為 NULL 的那格)。
**2. 重寫指標,完成插入(兩步)**
- `*p = item;`
把原本指向 before 的指標改成指向新節點 item。
若 p 是 `&l->head`,這行相當於更新 head。
- `item->next = before`;
將新節點的 `next` 接回 `before`。
如果 `before == NULL`,就讓 item 成為新的尾節點 `(next == NULL)`。
**test_list() 共有三段情境**
1. 插入到 開頭 (before l.head)
```c
for (i = 0; i < N; i++)
list_insert_before(&l, l.head, &items[i]);
```
- 第一次 l.head == NULL ⇒ 行為相當於 push-front;插入後 head 指向 items[0].
- 第二次之後,before == l.head ⇒ 新節點不斷插到現行 head 前面,故串列最終逆序 (N-1 … 2 1 0).
2. 插入到 結尾 (before NULL)
```c
list_insert_before(&l, NULL, &items[i]);
```
把 before == NULL 解讀為「附加在尾端」。
由於每次都選擇尾端,最終串列保持順序 (0 1 2 … N-1).
3. 再次驗證尾端插入
與 2 相同,確保重複 reset → re-insert 仍正確、沒有記憶體殘留影響。
---
#### 在現有的程式碼基礎上,撰寫合併排序操作
```c
// 合併 a 與 b,回傳排序後的 head
static list_item_t* merge_two(list_item_t *a, list_item_t *b) {
list_item_t *head = NULL;
list_item_t **ptr = &head; // ptr 永遠指向「下一條要填入節點的那個指標」
while (a && b) {
if (a->value <= b->value) {
*ptr = a; // 接上 a
a = a->next; // a 前進
} else {
*ptr = b; // 接上 b
b = b->next; // b 前進
}
ptr = &((*ptr)->next); // ptr 跟著移動到剛接上節點的 next 欄位
}
// 其中一邊耗盡,直接把剩下的整串接上
*ptr = a ? a : b;
return head;
}
// 其餘遞迴拆半、排序邏輯不變
static list_item_t* merge_sort_rec(list_item_t *head) {
if (!head || !head->next)
return head;
list_item_t *slow = head, *fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
list_item_t *mid = slow->next;
slow->next = NULL;
return merge_two(merge_sort_rec(head),
merge_sort_rec(mid));
}
void list_merge_sort(list_t *l) {
l->head = merge_sort_rec(l->head);
}
```
---
### Week 1 Quiz 2 - Binary Search Tree
#### 解釋程式碼
在二元搜尋樹(Binary Search Tree,BST)中,某個節點 `x` 的 **predecessor**(前驅)指的是「在中序(左→根→右)遍歷中,緊接在 `x` 之前的那個節點」。換句話說,它是所有小於 `x->key` 節點中,鍵值最大的那一個。
---
**如何找前驅**
假設我們要找節點 `x` 的前驅,主要有兩種情況:
1. **左子樹非空**
* 前驅就是 `x` 左子樹中的最大節點。
* 作法:從 `x->left` 開始,一直往右走到不能再右→即為前驅。
2. **左子樹為空**
* 這時候前驅必定在 `x` 的祖先節點中。
* 作法:沿著父指標(或用遞迴/棧模擬)往上走,直到某個節點 `p` 滿足:
* 你剛從 `p->right` 走進 `p`(也就是 `x` 在 `p` 的右子樹裡)。
* 那麼 `p` 就是前驅。
* 若一路走到根都沒找到,代表 `x` 是整棵樹中最小的節點,沒有前驅。
---
**範例**
```
20
/ \
10 30
/ \ \
5 15 40
\
17
```
* **前驅(20)**
* 左子樹非空 → 在子樹 `10–15–17` 中找最大 → `17`
* **前驅(10)**
* 左子樹非空 → 在子樹 `5` 中最大 → `5`
* **前驅(5)**
* 左子樹空,向上看:5 是 10 的左子節點 → 繼續往上,到 20…都不是從右子樹來 → 無前驅
* **前驅(30)**
* 左子樹空,向上看:30 是 20 的右子 → 前驅就是 `20`
---
**remove\_free\_tree 的運作步驟**
1. **定位到要刪除的節點指標**
```c
block_t **node_ptr = find_free_tree(root, target);
```
`node_ptr` 指向「指向目標節點的那個指標」(pointer-to-pointer),方便後面直接改寫它。
2. **如果有兩個子節點**
* 必須找一個「替身」來頂替自己,常用中序前驅 (in-order predecessor)。
* 前驅定義:左子樹裡最大的那個 → 往左一次,再一路往右跑到底。
* 有了 `pred_ptr` 指向前驅節點的指標之後,分兩種情況:
1. **前驅就是左子節點**
```c
if (*pred_ptr == (*node_ptr)->l) {
block_t *old_right = (*node_ptr)->r;
*node_ptr = *pred_ptr; // 左子節點取代自己
(*node_ptr)->r = old_right; // 接回原本的右子樹
}
```
2. **前驅在更深處**
```c
else {
block_t *old_left = (*node_ptr)->l;
block_t *old_right = (*node_ptr)->r;
block_t *pred_node = *pred_ptr;
remove_free_tree(&old_left, *pred_ptr);
*node_ptr = pred_node; // 用前驅頂替自己
(*node_ptr)->l = old_left; // 接回拆掉的左子樹
(*node_ptr)->r = old_right; // 接回原本的右子樹
}
```
3. **只有一邊子節點或沒有子節點**
```c
else if ((*node_ptr)->l || (*node_ptr)->r) {
*node_ptr = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r;
} else {
*node_ptr = NULL;
}
```
- 若只有一個子節點,就讓它直接頂上來;若都沒有,直接把指標設 `NULL`。
4. **清理原目標節點指標**
```c
target->l = target->r = NULL;
```
避免外面還留著懸空指標。
---
**為什麼要找左子樹最右邊**?
* 前驅要比 `target->size` 小(必在左子樹),又要是所有小於它之中「最大的」。
* 在 BST 上,左子樹的每個節點都小於根,而最右邊的那個正是最大值,所以往右跑到底就對了。
---
整段程式利用 **pointer-to-pointer** 技巧,將各種指標重連都改寫在 `*node_ptr` 或 `*pred_ptr` 上,免去處處判頭尾、記前驅父節點等繁瑣分支。
#### 嘗試補完程式碼,使其可獨立執行
`find_free_tree` 在二元搜尋樹中找到大於或等於 target 值的最小節點,並返回指向該節點的指標。
```c
block_t **find_free_tree(block_t **root, block_t *target)
{
if (!(*root))
return root;
if ((*root)->size >= target->size) {
block_t **left = find_free_tree(&(*root)->l, target);
return (*left) ? left : root;
}
return find_free_tree(&(*root)->r, target);
}
```
`print_tree`採用 中序遍歷 (in-order traversal) 來印出整棵二元搜尋樹(BST),順序為:
先走左子樹 -> 印出當前節點 -> 再走右子樹
```c
void print_tree(block_t *root) {
if (!root)
return;
print_tree(root->l);
printf("%zu ", root->size);
print_tree(root->r);
}
```
測試主程式 `main` :建樹、刪除並列印每次結果
```c
int main() {
block_t *root = NULL;
// 插入順序:產生值為 (i * 3) % 10 的節點,確保亂中有序且不重複
for (int i = 0; i < 10; i++) {
insert_tree(&root, (i * 3) % 10);
}
print_tree(root);
puts(""); // 印出初始樹狀結構
// 刪除順序:依序以 (i * 4) % 10 模擬實際記憶體釋放情境
block_t dummy;
for (int i = 0; i < 10; i++) {
dummy.size = (i * 4) % 10;
remove_free_tree(&root, &dummy);
print_tree(root);
puts(""); // 每次刪除後印出目前 BST 結構
}
return 0;
}
```