owned this note
owned this note
Published
Linked with GitHub
# 2024q1 Homework1 (lab0)
contributed by < `Willsonbo` >
:::danger
注意空白!
唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心。
:::
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz
CPU family: 6
Model: 166
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 0
CPU max MHz: 4700.0000
CPU min MHz: 400.0000
BogoMIPS: 3199.92
```
:::danger
無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)。
:::
在一週的時間發現自己不足,不補三週的內容,也把以前的 fundamental of data structure in c 與 C++ How to Program : Late Objects Version ,才找回對鏈結串列<s>連結串列</s> 鏈結串列與其他演算法的概念<s>感覺</s>
:::danger
注意用語。
$\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
你所不知道的 C 語言: linked list 和非連續記憶體中提及,指標的指標應用與如何寫出優雅的程式
首先是結構定義:
```c
typedef struct list_entry {
int value;//the value stored in noide
struct list_entry *next;
} list_entry_t;
```
要從給定的鏈結串列中,移走 (remove) 符合特定數值的節點時,可撰寫以下程式碼:
```c
list_entry_t *remove(list_entry_t *head, int value)
{
if (!head)//linked list without head
return NULL;
if (head->value == value)//the node with that value is pointed by head
return head->next;
list_entry_t *prev = head;
list_entry_t *cur = head->next;
while (cur) {//loop through the list to find the node with the value
if (cur->value == value) {
prev->next = cur->next;
return head;
}
prev = cur;
cur = cur->next;
}
return head;
}
```
這段程式碼考慮到開頭的節點是否被移走,於是要有對應的程式碼來處理特例,最終回傳新的開頭節點。改進方式為,引入一個暫時節點,使其在給定的開頭節點之前,這樣就不用特別撰寫程式碼來處理特例:
```c
list_entry_t *remove(list_entry_t *head, int value)
{
list_entry_t dummy = {.next = head};
for (list_entry_t *prev = &dummy; prev->next; prev = prev->next) {
if (prev->next->value == value) {
prev->next = prev->next->next;
break;
}
}
return dummy.next;
}
```
> The first operand of the . operator shall have a qualified or unqualified structure or union
type, and the second operand shall name a member of that type.
參考規格書中針對 `.` 的敘述,此函式將 dummy 視為 list 的一員
不過這段程式碼依舊可改進,因為我們根本不用回傳新的開頭節點,相反的,可將函式原型改為 `void remove(list_entry_t **head, int value)`,藉由間接指標來更動傳入的鏈結串列開頭節點。
```c
void *remove(list_entry_t **head, int value)//**head is the pointer to the head
{
for (list_entry_t *prev = &(**head); prev->next; prev = prev->next) {
if (prev->next->value == value) {
prev->next = prev->next->next;
break;
}
}
}
```
在閱讀作業時了解到 `e.g.` 是範例與舉例的簡寫
## 指定的佇列操作
參考 <pao0626> 、<ShallowFeather>、<vax-r>
### `q_new`
>建立新的「空」佇列
* 問題:如何建立佇列、何謂空
* 方法:利用The Linux Kernel API中的void INIT_LIST_HEAD(struct list_head *list)建立新的list head
* 實作:建立空序列時先用malloc配置list_head大小的記憶體,INIT_LIST_HEAD來初始化
### `q_free`
>釋放佇列所佔用的記憶體;
* 問題:鏈結串列佔用哪些記憶體、如何釋放
* 方法:釋放指向頭節點的指標和走訪並釋放各個節點的指標與內容
* 實作:利用list_for_each_entry_safe( The Linux Kernel API - List Management Functions)、q_release_element(queue.h)
### `q_insert_head`
>在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則)、`q_insert_tail`: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則);
* 問題:如何確保可以插入並插入新節點
* 方法:確保head指標非空、確保節點本身與value記憶體成功分配,插入節點方法
* 實作:利用malloc分配記憶體給節點本身、strdup分配記憶體給value、並使用list_add、list_add_tail插入新節點
strdup:
### `q_remove_head`在佇列開頭 (head) 移去 (remove) 給定的節點、`q_remove_tail`: 在佇列尾端 (tail) 移去 (remove) 給定的節點;
* 問題:如何確保有節點與內容可以移除並將其移除,如何找到佇列開頭與結尾
* 方法:判段head指標與佇列本身是否為空、找到佇列頭的或尾端、將節點移除佇列、回傳被移除的佇列的值
* 實作:可以呼叫head指向的值或是用list_first_entry,尾端則可以透過list_last_entry、list_del可以移除佇列的節點
bufsize:
### `q_size`
>計算目前佇列的節點總量;
* 問題:如何計算節點數量
* 方法:確保佇列不為空且有函數走訪佇列的每個值
* 實作:判斷head是否為空指標並利用list_for_each走訪每個節點
list_for_each:
### `q_delete_mid`
>移走佇列中間的節點
* 問題:何謂中間?如何找到、是否有東西可以刪
* 方法:因為 queue 是用 circuler linked list 實作,因此可以藉由分別往兩個方向訪問,直到碰頭代表已找到中間點
* 實作:從head開始一邊向first:head->next一邊向second:head->pre開始訪問,直到first==second or first->next == second停止,再將該first 點刪除
### `q_delete_dup`
>在已經排序的狀況,移走佇列中具備重複內容的節點
* 問題:如何找到重複的節點、是否有東西可以刪、是否會重複訪問要定義何時停止
* 方法:用兩個指針訪問每個節點,其中一個放入當前的值,下一個用要被檢查的值,若檢查到一樣的則將其刪除
* 實作:list_for_each_entry_safe(cur, tmp, head, list) ,cur 表示正在訪問的節點,tmp表示下一個要訪問的節點,用list_for_each_entry_safe的好處是當cur被刪除時還是可以,安全的從tmp接續訪問
### `q_swap`
>交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairs
* 問題:以兩個節點為一組,兩兩交換,在訪問每一組時將他們交換
Input: head = [1,2,3,4]->Output: [2,1,4,3]
要如何在走訪節點時可以兩兩一組?要如何讓節點位置交換
* 方法:void list_move(struct list_head *list, struct list_head *head)可以讓list節點接續head節點
* 實作:用list_for_each走訪節點,並用list_move將當前接續下一個節點,接著第二次遞迴時,他會從根據順序走訪第一個節點的下一個節點,也就是第三個節點,以此接續直到到,下一個節點為head時結束
list_move後為何用list_for_each還能走訪到被移動節點的下一個節點
### `q_reverse`
>以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
* 問題:將鏈結串列重新反方向排序
* 方法:從head->pre開始反向走訪節點直到head停止
* 實作:用list_for_each_reverse反向走訪與list_move調整節點位置
### `q_reverseK`
* 問題:如何將鏈結串列以每 k 個節點為一組進行反轉?
* 方法:預先配置一個新的鏈結串列,用於存儲反轉後的結果。每次從原始鏈結串列中取出 k 個節點,並進行反轉,然後將反轉後的結果添加到新鏈結串列的末尾。最後將新鏈結串列中的結果複製回原始鏈結串列。
* 實作:藉由list_cut_position
### `q_sort`
>以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法
* 方法:利用 mergesort 演算法進行排序,將鍵結串列切割為較小的子串列,然後逐步合併這些子串列。
透過遞迴實現 mergesort 演算法,每次將鍵結串列切割成兩半,然後分別對兩半進行排序和合併。
* 實作:首先判斷鍵結串列是否為空或只有一個節點,若是則直接返回,使用快慢指針找到鍵結串列的中間節點,將其分為兩個子串列,遞迴調用 mergesort 函式對這兩個子串列進行排序,最後,將排好序的兩個子串列進行合併,得到最終的排序結果。
### `q_descend`
>參閱 LeetCode 2487. Remove Nodes From Linked List
- 問題:如何在給定的鏈結串列中移除具有比其後面節點值更小的節點?
- 方法:反向遍歷鏈結串列,比較當前節點和其後面節點的值,如果當前節點的值比後面節點的值小,則刪除後面節點。
- 實作:
```c
int q_descend(struct list_head *head)
{
// 確保 head 不為空
if (!head)
return 0;
// 初始化節點計數
int len = 0;
// 從最後一個節點開始向前遍歷
struct list_head *cur = head->prev;
// 遍歷整個鏈結串列
while (cur != head) {
// 增加節點計數
len++;
// 如果下一個節點是 head,則跳出循環
if (cur->prev == head)
break;
// 將節點轉換為元素結構
element_t *c = container_of(cur, element_t, list),
*p = container_of(cur->prev, element_t, list);
// 如果下一個節點的值大於當前節點的值,則刪除下一個節點
if (strcmp(c->value, p->value) > 0) {
list_del(&p->list);
q_release_element(p);
len--;
} else {
// 否則,繼續向前走訪
cur = cur->prev;
}
}
// 返回節點計數
return len;
}
```
### `q_merge`
>合併所有已排序的佇列,並合而為一個新的已排序佇列
* 問題:如何將所有已排序的佇列合併成一個新的已排序佇列?
* 方法:
1. 檢查佇列是否為空,如果是,則返回。
2. 取得第一個佇列。
3. 如果只有一個佇列,則直接返回該佇列的大小。
4. 從第二個佇列開始,將每個佇列的節點合併到第一個佇列中。
5. 將合併後的佇列進行排序。
6. 返回合併後的第一個佇列的大小。
* 實作:
```c
int q_merge(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
// 取得第一個佇列
queue_contex_t *fir = container_of(head->next, queue_contex_t, chain);
// 如果只有一個佇列,直接返回大小
if (head->next == head->prev)
return fir->size;
// 從第二個佇列開始合併
for (struct list_head *cur = head->next->next; cur != head; cur = cur->next) {
// 取得當前佇列
queue_contex_t *que = container_of(cur, queue_contex_t, chain);
// 將當前佇列合併到第一個佇列中
list_splice_init(que->q, fir->q);
// 將當前佇列大小歸零
que->size = 0;
}
// 對第一個佇列進行排序
q_sort(fir->q);
// 更新第一個佇列的大小
fir->size = q_size(fir->q);
// 返回第一個佇列的大小
return fir->size;
}
```