contributed by < LiChiiiii
>
測驗題目:quiz3
研讀 treesort.c ,考慮一個 Tree sort 的實作,搭配 Red–black tree 作為排序所需的自我平衡樹。
定義一個列舉型別 color_t
,包含兩個值,分別為 CMAP_RED
與 CMAP_BLACK
,代表紅色節點和黑色節點。
定義一個結構 node_t
,其中 color
代表節點顏色, left
和 right
分別代表紅黑樹的左節點和右節點,next
代表在原始陣列中下一個節點, value
代表節點的值。
為了節省記憶體空間,這裡把父節點的位址存在 uintptr_t color
的最高位中。在 64 位元的系統中,考慮到 alignment,記憶體以 8 bytes 為一個單位進行管理,也就是說 node_t
的位址的最後 3 個 bits 一定為 0 ,因此只要對給定的引數 (r)
清除後面 3 個 bits 就可以得到父節點的位址,並且在引數 (r)
最後一個 bit 存放顏色信息, (r)->color & 1
取出最後一個 bit 。
在設定顏色時,以 bitwise 操作,若節點是紅色則設定最後一個bit 為 0 ,反之。
cmap_insert()
是在紅黑樹中插入節點的功能,若比較結果 res
小於零代表往左邊走,否則往右邊走,直到走訪到的節點為 NULL,或迴圈結束,即插入節點。
tree_sort()
會建立一個 map 配置 key 和 element 足夠的空間,接著在 while 迴圈將 list 中的每個節點一個一個插入 map 中。定義 *node
為 map 中擁有最小值的節點,透過 for 迴圈,從最左子開始在紅黑樹找到下一個節點(cmap_next(node)
),由小到大放入 list 中,完成 sort 功能。
定義一個結構 avl_node
,其中 parent_balance
最低位 2 bits 紀錄 node 的平衡狀態,高位紀錄父節點的位址,left
和 right
分別代表 AVL 樹的左節點和右節點。
因此想取出得到父節點就將 parent_balance
的高位取出,因為是 64 位元的系統,所以 avl_node
的位址的最後 3 個 bits 一定為 0,因此只要把最後 3 bits 設為 0 ,即可得到父節點的位址。
定義一個列舉型別 avl_node_balance
,包含三個值,分別為 AVL_NEUTRAL
、 AVL_LEFT
和 AVL_RIGHT
。
AVL_NEUTRAL
: 左子樹和右子樹的深度一樣AVL_LEFT
: 左子樹的深度 - 右子樹的深度 = 1AVL_RIGHT
: 右子樹的深度 - 左子樹的深度 = 1因為 parent_balance
最低位 2 bits 存放節點的平衡狀態,利用 & 3
將最低位 2 bits 取出。
透過 node->parent_balance & 3
保留原本的 balance 資訊,利用 or 設定 parent。
透過 node->parent_balance & ~7
保留原本的 parent 資訊,利用 or 設定 balance。
插入新節點後需要確認 tree 是否平衡。換句話說,會先檢查插入節點是左子還是右子,再檢查 parent 的 balance 狀態在插入新節點後是否平衡。
如果插入的新節點是左子,就去檢查 parent 的 balancd 狀態,會出現三種可能的狀態:AVL_RIGHT
、 AVL_NEUTRAL
、 AVL_LEFT
,當parent 是 AVL_LEFT
的狀態時表示左子樹高度比右子樹高 1 ,接著檢查插入節點的 balance ,如果是 AVL_LEFT
或 AVL_NEUTRAL
,那麼就進行右旋操作(第42行),如果是 AVL_RIGHT
,那麼就進行左右旋操作,先將右子樹向左旋轉,再將原節點向右旋轉(第45行)。
這段程式碼是一個長度為 256 的表格,被稱為「對數表」(log table),用於計算二進制數字的位數(也就是 log_table_256[x]
= )。例如, log_table_256[6] 的值為 2 ,代表著二進位表示法中的 00000110
,第一個 1 位元出現在位置 2 。
在程式碼中,它首先檢查最高的 32 位元,如果它們不是 0 ,則將它們向右移動 32 位元,並繼續檢查下一個 16 位元和 8 位元。找到的最高位元的對數與某些固定的值相加,以計算整個 64 位元的對數。因為表格中的值是以 2 為底的對數,所以它們的值域在 0 到 7 之間,可以直接用來計算每個 8 位元組的對數,而不需要進行實際的對數運算。
這段程式碼是用來將一個 64 位元整數 x 根據 N_BITS 和 N_BUCKETS 的設定,將其分配到對應的桶(bucket)中。其中,mask111 和 mask011 分別是用來產生全為 1 的二進位數字,mask111的長度為 N_BITS + 1, mask011 的長度為 N_BITS 。
考慮 ceil_log2
可針對給定無號 32 位元數值 x,找出 ,舉例來說:
ceil_log2(7)
= 3ceil_log2(8)
= 3ceil_log2(9)
= 4主要概念是找到最高位元的 1
是在 32 bits 中的哪一個 bit,因為可以根據最高位元的 1
來觀察這個數值是介於哪兩個 2 的次方數之間。
程式碼運作方式如下:
r
初始值為 0 ,目的是儲存計算後的對數結果。變數 shift
初始值為 0 ,目的是用來儲存計算每次 x
的偏移量。x
先減 1 ,確保四捨五入可以到最近的 2 的次方數x
是否大於 0xFFFF
,若是的話, r
會被設為 16
,否和 r
= 0 ,並將 x
右移 16
bits;x
是否大於 0xFFF
,若是的話, shift
會被設為 8
,否則 shift
= 0 ,並將 x
右移 8
bits,接著 r
和 shift
做結合,以此類推檢查 0xF
和 0x3
。r
會和最後剩下的偏移量 shift
(可能是 0、1 或 2 個位元)及其移位後的結果結合,得出最終的對數結果。該結果最後會加 1
,以取得向上取整後的對數值。從 0xFFFF
開始檢查,是因為當輸入值大於 0xFFFF
時,右移 16 位元可以將輸入值的高 16 位元右移到低 16 位元,這樣可以把輸入值的位數減少一半,從而加快計算速度。接著,再根據輸入值的大小,從 0xFF
、0xF
、0x3
分別開始檢查,進行對應的右移操作,最後計算出以 2 為底數的對數的位數。
Trace ceil_log2(0x0FFFFFFF)
的執行過程。