or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
2025q1 Homework2 (quiz1+2)
contributed by <
Huadesuwa
>第 1 周測驗題
測驗
1
解釋上方程式碼運作原理
首先看到
struct list_item
為節點的結構,其中有一個整數型別value
用於儲存資料,以及一個 a pointer namednext
to astrcut list_item
,因此可以得知這是一個單向的鏈結串列, 而head
則會指向整個鏈結串列其關鍵操作
list_insert_before
函式的語意如下:函式會逐步走訪整個鏈結串列,定位到
before
之前指向的節點,並將item
插入該位置before
指向鏈結串列的頭 ,意謂著會插入在鏈結串列最前面的位置NULL
,則會插入在尾巴Undefined
其測試程式
test_list
將會對list_insert_before
進行測試,驗證函式功能是否正確:before
設置為鏈結串列的頭(l.head
),測試插入item
到前面的功能before
設置為NULL
,測試插入item
到後面的功能Unexpected Value
的情況在現有的程式碼基礎上,撰寫合併排序操作
使用間接指標
**ptr
指向鏈結串列的頭,避免配置暫時指標的記憶體空間,而迴圈的中止條件為其中一串鏈結串列被走訪完畢 (NULL
) ,剩餘的則透過bitops
操作串上。測驗
2
解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼
程式碼函式
此程式碼實作了一個二元搜尋樹 (Binary Search Tree, BST) 來管理記憶體區塊,其中每個節點 (
block_t
) 代表一個記憶體區塊。以下是程式中的主要函式及其用途:
1.
block_t
(二元搜尋樹的節點結構)size
:記錄該記憶體區塊的大小。l, r
:指向左右子節點,形成二元搜尋樹 (BST) 結構。2.
find_free_tree
(搜尋記憶體區塊)target
的節點,回傳該節點的位置 (**node_ptr
)。3.
find_predecessor_free_tree
4.
remove_free_tree
(刪除 BST 節點)刪除節點的處理邏輯
Case 1:刪除的節點沒有子節點
此情況最簡單,直接刪除節點即可。
處理方式
*node_ptr = NULL;
Case 2:刪除的節點只有一個子節點
需要讓子節點取代被刪除的節點。
處理方式
target
是只有左子節點還是只有右子節點。target
的唯一子節點 直接取代target
。Case 3:刪除的節點有兩個子節點
此情況最複雜,須透過 in-order predecessor 來找到取代節點。
處理方式 :
find_free_tree
找到目標節點,**node_ptr
指向目標位置。**pred_ptr
指向target
左子樹的根節點。find_predecessor_free_tree
找到target
左子樹中的最大節點 (即target
的 in-order predecessor)。Still Case 3 (進階情境)
當
target
有兩個子節點時,會進入 Case 3 的處理邏輯,在其中有以下細節需注意:while
迴圈持續向右走訪左子樹,直到找到最右側的節點 (in-order predecessor
)。EEEE
EEEE
為(*pred_ptr)->r
,確保當pred_ptr
已無右子節點時跳出迴圈。FFFF
FFFF
為pred_ptr
所指向節點的右子節點的地址&(*pred_ptr)->r
,如此才能使while
迴圈不斷向右走訪。in-order predecessor
find_predecessor_free_tree
再次搜尋,並使用assert
檢查找到的in-order predecessor
是否正確,若不相同則終止程式。target
為pred_ptr
pred_ptr
剛好是target
的左子節點,則直接替換,並保留target
的右子樹。pred_ptr
位於target
左子樹的更深處,則需要先刪除pred_ptr
,然後用它取代target
。測試程式碼
參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現
allocator
結構體定義首先,這是
allocator
的結構體定義:size
是指分配給mem
區塊的空間大小,不包括 header。root
是二元搜尋樹的根節點。mem
為長度為 0 的陣列,這樣可以讓它的空間取決於分配給它的大小,而不會佔用額外的空間。block
結構體定義接著是
block
的結構體定義:size
用於記錄包含 header 的區塊大小,這樣可以減少運算。prev_size
用於記錄前一個區塊的大小,有助於計算上一個區塊的位置,並且能夠用來判斷區塊是否已被使用。l
和r
依然是代表二元搜尋樹的左、右子節點。mem
和上方一樣的概念以下是三個主要函式的說明:
alloc_create
用於創建一個新的
allocator
,並指定最大可用的記憶體大小。alloc_alloc
用來從
allocator
中分配一塊指定大小的記憶體。alloc_free
用來釋放一塊已分配的記憶體區塊。
alloc_create
函式的實作如下:size
會被對齊至 8 位元組,然後分配空間。inuse
的block_t
。NEXT_BLOCK
)。block_t
,並插入到二元搜尋樹中。alloc_alloc
函式的實作如下:size
進行ALIGN_UP
操作,確保對齊。header
和所需的記憶體大小size
。alloc_free
函式的實作如下:block
前後區塊是否正在使用中。巨集定義
以下是用於區塊管理和記憶體對齊的幾個重要巨集:
ALIGN_UP
向上對齊到 8 位元組
PREV_INUSE
上一個區塊是否被使用中
NEXT_INUSE
下一個區塊是否被使用中
INUSE
現在區塊是否被使用中
CLEAR_USE_BIT
將大小去除
USE_BIT
NEXT_BLOCK
下一個區塊
PREV_BLOCK
前一個區塊
container_of
將
list.h
中使用的巨集引入,可以從成員得到結構體使用gnuplot繪製

測驗
3
解釋上述程式碼運作原理
〈Optimized QuickSort — C Implementation (Non-Recursive)〉提出非遞迴 (non-recursive; iterative) 的快速排序法。此處提到的方法是以 swap 為主體,利用
L
與R
去紀錄需交換的數量,再用begin[]
與end[]
作為堆疊,用來紀錄比較的範圍。在此使用了 Linux 風格的 List API 來建立鍊結串列,並使用 value 來紀錄數值。
以下為測試與輔助函式:
list_construct
: 在list
中建立新的節點。list_free
: 釋放整個鏈結串列list
已分配的記憶體空間。list_is_ordered
: 驗證鏈結串列list
是否為有序排列。shuffle
: 打亂陣列的排列, 運作條件為n < RAND_MAX
list_tail
: 找到鏈結串列list
的尾巴list_length
: 計算鏈結串列list
大小(內含多少節點)rebuild_list_link
: 將單向鏈結串列轉換為雙向環狀鏈結串列NULL
)head->prev=prev
,如此才能做到首尾相連接著便開始進行排序的動作,
排序的過程中,先將
L
及R
兩指標指向鏈結串列的頭及尾。若
L
及R
兩者不同,則將piviot
指向L
所指的節點,並將piviot
的數值存於value
。使用
p
指向piviot
的下一個節點,並斷開piviot
(指向NULL
)。使用
p
逐步走訪整個節點,將小於value
的置於left
中,大於value
則置於right
中。接著將
begin[]
指向對應的位置。在下一輪,同樣將
L
指向begin[i]
即begin[2]
(從較大子序列開始), 而R
則指向begin[2]
尾端。此時按照先前的步驟將
piviot
設置在L
並將p
指向piviot
下一個節點同樣逐步走訪節點,將小於
value
的置於left
中,大於value
則置於right
中,並將begin[]
指向對應的位置。此時
L
將指向begin[4]
,R
指向begin[4]
的尾端,兩者皆指向NULL
,且L
為NULL
因此i= i-1 = 3
。又3
也同樣為單一一個節點,因此i
會不斷扣一並在過程中逐步將元素加到 result 中。此時依照先前的方法同樣為 2 及 1 兩個節點設置
begin[]
。最後使用 result 收回
接著再使用
rebuild_list_link
設置回原本的雙向環狀鍊結串列。研讀〈A comparative study of linked list sorting algorithms〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作
論文起因於 Sediment Sort ——一個聲稱針對 double linked-list 最快且最有效率的排序演算法。
為驗證其效能,作者整理並比較了六種針對不同鏈結串列結構的排序演算法進行比較,分類如下:
針對 Double Linked-List:
Sediment Sort
針對 Single Linked-List:
Selection Sort
Merge Sort
Bubble Sort
Quick Sort
equal
鏈結串列,來記錄排序時具有相同大小的元素,以維持排序的穩定性 (stable)。作者針對上述六種排序演算法進行實驗,比較它們在不同資料量下的效能。排序資料量 \(n\) 從 \(100\) 遞增至 \(1000\),並記錄每種演算法在對應資料量下的執行時間。
下表為各演算法在不同資料量下的執行時間比較:
由結果可觀察到,Sediment Sort 雖然設計上是針對 Double Linked-List 的排序演算法,但在所有測試資料量下,其執行時間皆為最長,效能遠遜於其他排序演算法。
第 2 周測驗題
測驗
1
延伸問題
1
在此使用 Linux 核心風格 List API 來開發快速排序程式碼。使用
list_head
來建立鍊結串列,並使用變數型別uint16_t
來紀錄要被排序的數值。以下為測試與輔助函式:
cmpint
: 實作quicksort
的比較,因為傳入的參數是void *
不能做bitops
,因此要先做強制型別轉換(const uint16_t *)
random_shuffle_array
: 打亂operations
裡的排列順序,於迴圈中get_unsigned16
將決定每階段互相交換的位置getnum
:s1、s2、s3
以static
宣告,因此只會初始化一次get_unsigned16
: 呼叫上述getnum
函式兩次製造16位元的亂數main
:對含有 256 個數字的陣列
value
進行隨機排序,並建立一個鏈結串列加入這些數字。接著對鏈結串列執行list_quicksort
快速排序,最後比較陣列value
和鏈結串列的值i
,若不同會因為巨集assert
而終止程式。主要函式:
list_quicksort
pivot
,由於head
會指向整個鏈結串列,因此使用list_first_entry
,找到指向的鏈結串列第一個元素。list_for_each_entry_safe
逐步走訪整個鏈結串列, 將值小於pivot
的用list_move_tail
移入list_less
,反之,大於的值移入list_greater
pivot
左右兩邊的鏈結串列進行排序,當鏈結串列排到不能在排時就返回(只有單一元素)。pivot
、list_less
、list_greater
合併。延伸問題
2
測驗
2
延伸問題
1
在此嘗試開發整數的開平方根運算。
首先是
clz2
函式,其作用是藉由遞迴呼叫以計算 count leading zero (clz)。每次呼叫clz2()
函式時,代表將目前關注的位元(bits)「切成一半」,以檢查 leading zero 的數量。以下藉由一個數值(0x0001F000)來說明其步驟:將此數值分為兩部分: 較高位 ( upper ) 與較低位 ( lower ) 。
此時,依據
upper
的數值判斷下一次遞迴呼叫應該處理哪一部份,以累計 leading zero 的數量。upper
等於0
,則下一次遞迴呼叫應檢查 lower 部分,並用計數器(counter)記錄目前已累計的 leading zero(等於 upper 位元數)。upper
不等於0
,則下一次遞迴呼叫應繼續檢查 upper 部分。由於
upper
不為 0,下一次遞迴呼叫將繼續檢查upper
部分的 leading zero。遞迴呼叫
clz2()
函式,直到僅剩 2 位元(bits)需要檢查 leading zero,然後回傳結果。mask
: 決定遮罩的大小,若upper
不等於0
,則下一次遞迴呼叫時被使用magic
: 當遞迴呼叫clz2()
函式至僅剩 2 位元需要檢查,也就是c = 3
時,建表判斷有多少 leading zero如果
upper
不等於0
,則遞迴呼叫clz2(upper, c+1)
。由於 leading zero 只會出現在upper
,因此這部分的計算與lower
無關。若
upper
等於0
,則需要計算當前區間16 >> c
中的 leading zero 數量,然後再遞迴呼叫clz2(lower, c+1)
來計算lower
的 leading zero 數量。延伸
clz2
函式 count leading zero 想法:clz64 (計算 64-bit 整數的前導零數量)
clz64
(計算 64-bit 整數的前導零數量)對於 64-bit 無符號整數
x
,我們可以根據其高 32 位與低 32 位來決定如何計算前導零數量:clz32(high 32 bits)
。clz32(low 32 bits) + 32
,因為這表示高 32 位的所有位元都是 0。sqrti
(計算 64-bit 無符號整數的整數平方根)sqrti(x)
的目標是求得floor(sqrt(x))
,即x
的整數平方根。其核心思想類似於二進位搜尋,從
x
的最高有效位開始逐步逼近平方根。步驟解析
確定最高有效位 (MSB, Most Significant Bit)
透過
clz64(x)
計算出x
的 MSB 位置,並利用:\begin{align} \text{total_bits} - 1 - \text{clz64}(x) \end{align}
來確定 MSB 在二進位數字中的實際位置。
確保起始的位移量是偶數
由於平方根的位數變化與整數平方的位數變化相對應,因此對
MSB
進行位元操作:\begin{align} m = (\text{MSB} - 1) \& \sim 1 \end{align}
這確保
m
是偶數,使得m
每次遞減 2 時能夠對應二進位平方的變化。sqrti
計算範例:以 \(N^2 = 17\) 為例,求 \(\lfloor \sqrt{17} \rfloor\)。
首先,將 \(17\) 轉換為二進位表示:
\begin{align} N^2 = (17)_{10} = (10001)_2 \end{align}
最高位在 \(b_4\),因此從 \(m=4\) 開始,逐步降低並嘗試構造平方根。
最終,平方根為:
\begin{align} N = P_0 = P_1 = P_2 = 2^2 = 4 \end{align}
因此,\(17\) 的整數平方根為 \(4\)。
延伸問題
2
延伸問題
3
測驗
3
延伸問題
1