contributed by <Charlie-Tsai1123
>
list_insert_before
下圖的黑色部份是題目所用的 linked list
,紅色是 list_insert_before
傳入的參數,藍色則是指標的指標,這裡用箭頭來表示指標,所以由圖可以知道 p
初始話的位子在指向 head
指標的位子
由於要從 linked list
中找到 before
的位子 p
要移動到指向下一個指標的位子,由 *p
先找到現在指向的指標,再取下一個指標,最後藉由 &
取指向下一個指標的位子
由下圖可知當找到 before
時( *p = before
),需要變動指標,可以直接改動 p
所指向的指標,就可以直接變動 linked list
了,最後新元素的 next
也要指向 before
所在位子
程式在執行 binary search tree (BST) 的 remove ,以下為 BST 的架構,每個節點會有指向左子樹的指標跟指向右子樹的指標,而 size
是記憶體區塊的大小
find_free_tree
是從 BST 中找到要被釋放的記憶體區塊位子 (target
)
find_predecessor_free_tree
則是找到 node
左子樹中的最大值
在 remove_free_tree
中,把操作分成了三個 case :
在 remove
節點之前要找到代替它位子的節點,那就是左子樹中最大的點
step 1 : 求左子樹最大的點
因為最大的節點會在右子樹中,所以從左子樹不斷往右找,找到底就是左子樹中最大的節點了( step 2 兩張圖中藍色的節點就是要找的,而紅色是要移除的節點 )
step 2 : 移除節點並加上代替位子的節點
左子樹的 root
是最大的節點 :
old_right
pred_ptr
移過去old_right
加到節點 *node_ptr
中左子樹更深的地方才是最大的節點:
old_left
, 右節點 old_right
, 左子樹最大節點 pred_node
remove_free_tree
移除左子樹最大節點 pred_ptr
(做完後可能變成 NULL)pred_node
代替欲移除的節點old_left
, 右子樹 old_right
接到節點 *node_ptr
中為什麼還要再存一次左子樹最大節點明明已經有 pred_ptr
因為經過 remove_free_tree(&old_left, *pred_ptr)
後,可能會變成 NULL
刪除的點是紅色的點,如果只有一個子樹,代替的點就是子樹的 root (藍色點)
find_free_tree
及 find_predecessor_free_tree
並測試rebuild_list_link
黑色是原本的 linked list
可以發現一開始是 singly linked list
,那 rebuild_list_link
是將這種結構變成 circular doubly linked list
circular doubly linked list
中的藍色部份, prev
指標指向前一個 nodecircular doubly linked list
中 cirsular 的部份,也就是紅色部份quick_sort
實作非遞迴 (non-recursive; iterative) 的快速排序法程式碼,用 begin
儲存切割的每個片段起始點,而對片段的處理分成以下幾種 :
片段長度為 1 :
加入到 result
內 (不需要排序了)
片段長度不為 1 :
分成三個片段, pivot
(最左邊的節點)、比 pivot
還要小的片段、比 pivot
還要大的片段
List API
實作排序與上週測驗不同的是這周採用遞迴的方式實作,而相同的點是一樣會用三個 linked list
來存(pivot
、較大、較小),首先找到 pivot
(最左邊的點)並移除
接著找到較大跟較小的節點分別存到 list_greater
跟 list_less
中
用遞迴方式排序好 list_less
跟 list_greater
到這步時理論上 head
會是 singular 的節點,節點都分布在 pivot
、list_less
、list_greater
當中,接下來將節點依序放入 head
中
random_shuffle_array
Stable sorting & unstable sorting
Stable sorting 代表相同數值的元素在經過排序後,相對順序不會改變,這對於多索引排序很重要;unstable sorting 則不能保證相同數值的元素排序後的相對順序能不改變。
因為 list_for_each_entry_safe
是由前到後去遍歷的,如果用 list_move
會移到 linked list 前面,相對的順序就改變了
使用 list_move
加在 linked list 前面 (兩個 2 順序對調了) :
使用 list_move_tail
加在 linked list 後面 (兩個 2 順序不會對調) :
clz
sqrti
實作 sqrti
其實是用到二分逼近法,那什麼是二分逼近法呢?可以聯想到國中所教的十分逼近法。
想想國中是如何求 的小數,首先找到它介於哪兩個整數間
接著往更小的精度下去找
所以 可以表示成
有了十分逼近法的概念就能對二分逼近法有更深的理解了,假設我們想要求 使的
那我們可以先求 跟 的精度,若 換成二進位後,第 位元是最高為 1 的位元,那可以獲得以下資訊 :
因此我們只需要求得 ~ 的係數值 ( 0 或 1 )就能算出 的估計值,以下說明如何只用二分逼近的概念實作效率超極低的開更號程式
首先找出 的範圍, total_bits - 1 - clz64(x) 可以算出 最大為 1 的位元位子也就是上述的 ,接著除以 2 (>>1) 就能找出 可能最大為 1 的位元位子也就是 (shift) 而 m 就是 2k/2 ,m 就是檢查的第一個值
eg:
x = 11001
k | k/2 (shift) | m |
---|---|---|
5 | 2 | 22 = 4 |
接著開始找係數也就是估計, b 是估計值看看係數補上 1 後也就是加上 m 的值看看是否會大於 x 如果不會將 y 加上 m ,最後檢查下一位 (k/2 - 1) 然後重複直到 m = 0
eg:
x = 11001 = 25
k/2 = 2
1 0 1
y = 4 + 1 = 5
以下是第一版沒效率算法
為甚麼說沒效率呢?
因為乘除法相較於 bitwise 的操作使用了更多的 CPU cycles
原本的 b 是估計值,用來估計 y 可能得值,那在優化的版本,我們將 b 的定義改為相較於上次的 y2 新的 (y + m)2 增加的值 :
y + m 是預估的值 (y 是上一次的值)
讓 2shift 保持等於 m ,所以乘上 m 只用往左移動 shift 就好
然後將 x 減掉,相較於上次的 y2 , (y+m)2 所增加的值, if 的條件也變為只用判斷增加的值會不會超過 x 的範圍
由於要保持 2shift 等於 m ,所以 shift 要 -1
以下是第二版完整的 code ,可以發現已經沒有使用乘法了,效率理論上會好上許多,但我們可以再簡化
測試效率 [To do]
在第二版每次計算新增的值時,都會使用 shift
對 y 跟 m 處理,我們是否有辦法不要使用 shift ?
以下針對 b = m2 + 2my 中的 m2 、 y 以及 b 分開討論
仔細觀察第二版中 m2 的變化,因為 m 每次都向右位移一個 bit 也就是檢查完 2k 下一個會檢查 2k-1 ,所以 m2 其實每次是變化兩個 bit,所以我們把 m 的定義直接改成 m2 ,每次變化 2 bit
那改成 m2 後 y 會有什麼變化呢 ?
首先我們先想想第二版中的 while 迴圈會跑幾次,若從 2k 開始估計,迴圈會跑 k + 1 次
接著可以思考若 y += m 在第三版中要怎麼修改 :
以第一次 while 迴圈來說,若 m 是 22k ,實際上估計值要加上 2k 但因為直接加上 m ,所以會多乘以 2k ,那我們就讓倒數第 k 次迴圈時 y 的值是實際估計值乘上 2k。以下面為例還剩 k 次迴圈
第二次 while 迴圈時, m 變成 22k-2 ,實際估計值要加上的是 2k-1 ,那我們先把 y (上一個實際估計值乘以 2k) 除以 2 (y >>= 1) ,y 就會是上一個實際估計值的 2k-1 倍,那在加上 m 時 :
所以 y 的定義變成了 2倒數第k次迴圈 x (實際估計值)
那到最後一次是
那確認了 y 跟 m 的定義後,接著我們要來驗證 b 的定義是否跟第二版相同 :
假設
m' 是理論上估計值要加上的值
y' 是上一個理論上的估計值
b 是現在理論上的估計值與上一個理論上估計值的差值
所以
根據上面所提
m = m'2
y = 2倒數第k次迴圈 x y'
所以
所以接下來只要證明
也就是
確實因為倒數第 k-1 次回圈就是檢查第 k-1 個 bit ,也就是 2k-1 ,所以 m' 的值就是 2k-1 由此得證
以下是最終版的程式碼