contributed by < Joshualee0321
>
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.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: 43 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: AuthenticAMD
Model name: AMD Ryzen 5 2600X Six-Core Processor
CPU family: 23
Model: 8
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 2
Frequency boost: enabled
CPU max MHz: 3600.0000
CPU min MHz: 2200.0000
BogoMIPS: 7199.10
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht
rb.h
筆記和程式碼本來就要讓各方指教,不用特別提及。 jserv
展開該巨集,利用 rb_gen(static, ex_, ex_t, ex_node_t, ex_link, ex_cmp)
把以下的巨集展開之後可以得到
static void ex_new(ex_t *tree);
static ex_node_t *ex_search(ex_t *tree, ex_node_t *key);
static void ex_insert(ex_t *tree, ex_node_t *node);
static void ex_remove(ex_t *tree, ex_node_t *node);
static void ex_destroy(ex_t *tree, void (*cb)(ex_node_t *, void*), void *arg);
避免非必要的項目標示 (亦即 *
開頭的描述),並改進你的漢語表達。
jserv
接下來我們可以觀察 ex_insert
的實作方法,其中大致可以分成幾個部分
static void ex_insert(ex_t *rbtree, ex_node_t *node)
{
ex_path_entry_t path[RB_MAX_DEPTH];
ex_path_entry_t *pathp;
rbt_node_new(ex_node_t, ex_link, rbtree, node);
/* Wind. */
path->node = rbtree->root;
for (pathp = path; pathp->node; pathp++) {
int cmp = pathp->cmp = x_cmp(node, pathp->node);
assert(cmp != 0);
if (cmp < 0) {
pathp[1].node = rbtn_left_get(ex_node_t, ex_link, pathp->node);
} else {
pathp[1].node = rbtn_right_get(ex_node_t, ex_link, pathp->node);
}
}
pathp->node = node;
ex_path_entry_t path[RB_MAX]
以及 ex_path_entry_t *pathp
作為 iteratorex_path_entry_t
的陣列當中digraph BST {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.8]
"B:35"[fillcolor=red]
"C:60"[fillcolor=red]
"A:50"
"A:50" -> "B:35"
"A:50" -> "C:60"
}
15
,則會先以 BST
的特性找到該插入的地方並且插入注意這邊先行設定愈插入的 node 為紅色
digraph BST{
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled, width=.8]
"B:35"[fillcolor=red]
"C:60"[fillcolor=red]
NULL[color=transparent]
"D:15"[color=red]
"A:50" -> "B:35"
"A:50" -> "C:60"
"B:35" -> "D:15"
"B:35" -> NULL
}
此時的 path
會長這樣
digraph {
node [shape=plaintext, fontcolor=black, fontsize=16];
"Iter:" -> "Values:" [color=white];
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
pointers [label="<f0> pathp | <f2> pathp[1]", color=white];
values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "];
{ rank=same; "Iter:"; pointers }
{ rank=same; "Values:"; values }
edge [color=blue];
pointers:f0 -> values:f1;
pointers:f2 -> values:f2;
}
依照 rb_insert
中的 pathp->node = node
,把 D:15
插入在 NULL
中,接下來就進入到程式碼的第二個片段
for (pathp--; (uintptr_t) pathp >= (uintptr_t) path; pathp--) {
x_type *cnode = pathp->node;
if(pathp->cmp < 0) { ... }
else { ... }
}
由於在上一個迴圈中 pathp
的位置如下
digraph {
node [shape=plaintext, fontcolor=black, fontsize=16];
"Iter:" -> "Values:" [color=white];
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
pointers [label="<f0> pathp | <f2> ", color=white];
values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "];
{ rank=same; "Iter:"; pointers }
{ rank=same; "Values:"; values }
edge [color=blue];
pointers:f0 -> values:f2;
}
故要把原本的指針往前調整如下
digraph {
node [shape=plaintext, fontcolor=black, fontsize=16];
"Iter:" -> "Values:" [color=white];
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
pointers [label="<f0> pathp | <f2> ", color=white];
values [label="<f0> 50, -1| <f1> 35, -1| <f2> NULL| <f3> | <f4> | <f5> "];
{ rank=same; "Iter:"; pointers }
{ rank=same; "Values:"; values }
edge [color=blue];
pointers:f0 -> values:f1;
}
所以才會在這個迴圈先做一次 pathp--
往前走一格。
再來,把以上 if
的部分展開之後可以看到 else
在下一個部分再看
// cnode = 當前位置
if(pathp->cmp < 0) {
ex_node_t *left = pathp[1].node;
rbtn_left_set(ex_node_t, ex_link, cnode, left);
if (!rbtn_red_get(ex_node_t, ex_link, left))
return;
ex_node_t *leftleft = rbtn_left_get(ex_node_t, ex_link, left);
if(leftleft && rbtn_red_get(ex_node_t, ex_link, leftleft)) {
ex_node_t *tnode; // 暫存
rbtn_black_set(ex_node_t, ex_link, leftleft);
rbtn_rotate_right(ex_node_t, ex_link, cnode, tnode);
cnode = tnode;
} else{...}
}
if(pathp->cmp < 0){...} else{...}
,並且由於是左傾斜的樹,他只需要對右邊的特殊狀況處理即可RB_t
並不平均,那他就會旋轉,圖示如下digraph BST {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.8]
left,leftleft[fillcolor=red]
null,null1[fontsize=0,color=transparent]
cnode -> left
cnode -> null[color=transparent]
left -> leftleft
left -> null1[color=transparent]
fix_left[fillcolor=red]
fix_left -> fix_leftleft
fix_left -> fix_cnode
}
當然,這邊只考慮了往左的狀況,那如果往右邊呢?
else {
ex_node_t *right = path[1].node;
rbtn_right_set(ex_node_t, ex_link, cnode, right);
// 把 current node 的右邊設定為當前的下一個點
if (!rbtn_red_get(ex_node_t, ex_link, right))
// 如果沒有連續兩個紅色
return;
ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, cnode);
// 這邊對右邊的狀況作特殊處理
if (left && rbtn_red_get(ex_node_t, ex_link, left)) {
rbtn_black_set(ex_node_t, ex_link, left);
rbtn_black_set(ex_node_t, ex_link, right);
rbtn_red_set(ex_node_t, ex_link, cnode);
} else {
/* 因為並不是兩個連續的紅色 node,又是右邊,所以必須回復左傾斜的特性*/
ex_node_t *tnode;
bool tred = rbtn_red_get(ex_node_t, ex_link, cnode);
// 看當前的點是否為紅
rbtn_rotate_left(ex_node_t, ex_link, cnode, tnode);
rbtn_color_set(ex_node_t, ex_link, tnode, tred);
rbtn_red_set(ex_node_t, ex_link, cnode);
// 旋轉後根據顏色設定
cnode = tnode;
}
}
pathp->node = cnode;
最後再根據紅黑樹的特性,把樹根設定為黑色即可 rbtree-root = path->node; rbtn_black_set(ex_node_t, ex_link, rbtree->root);
以上為 linux 左傾斜紅黑樹插入的實作
確認 Linux 核心的 rbtree 是否為 LLRBT,若不是,考慮因素在哪?
jserv
rb_tree
的刪除部分,由於太長,這邊只列出想法以及答案此程式碼也可以分成幾段來觀看,並且於先前 insert
一樣,都會創建一個 iteration_list
來記錄當前走過的路。第一段
for(pathp = path; pathp->node; pathp++) {
int cmp = pathp->cmp = x_cmp(node, pathp->node);
if(cmp < 0) {...} // 代表往左走
else { // 代表往右走 或是找到了
if(cmp == 0) {
// 找到要刪除的點
pathp->cmp = 0; // 這邊可以視為用 0 紀錄要刪除的點
for(pathp++; pathp->node; pathp++) { ... } // 往左找替換點
}
}
}
successor
) 互相交換,並且把要刪除的點放在 path[-1]
,反之,則調整顏色pathp--; // 因為會跑到 NULL 節點 要往回走一格
if (pathp->node != node) { ... } // left successor
else {
ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, node);
rbtn_black_set(ex_node_t, ex_link, left);
if (pathp->node != node) { ... }
else {
ex_node_t *left = rbtn_left_get(ex_node_t, ex_link, node);
if(left) { ... }
else if(!rbtn_right_get(ex_node_t, ex_link, node)) {}
}
}
successor
以及設定顏色後) 開始從當前位置往前調整紅黑樹參照更權威的書籍和文獻,例如大學教科書,避免從語焉不詳的網誌學習。
jserv
首先先來看到程式碼片段的 FFFF
if (leftrightleft && \
rbtn_red_get(x_type, x_field, leftrightleft)) { \
/* || */ \
/* pathp(b) */ \
/* / \\ */ \
/* (r) (b) */ \
/* \ */ \
/* (b) */ \
/* / */ \
/* (r) */ \
x_type *unode; \
rbtn_black_set(x_type, x_field, leftrightleft); \
rbtn_rotate_right(x_type, x_field, pathp->node, \
unode); \
rbtn_rotate_right(x_type, x_field, pathp->node, \
tnode); \
rbtn_right_set(x_type, x_field, unode, tnode); \
FFFF(x_type, x_field, unode, tnode);
欲刪除的節點為目前節點的右節點,為了要使刪除時維持左傾斜的特性,作者選擇右旋轉兩次,以下為圖示
digraph BST {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.8]
n1, n2, n3[color=transparent, fontsize=0]
"A"
"B:unode"[fillcolor=red]
"C"
"D"[fillcolor=red]
"A" -> "B:unode"
"A" -> "E"
"B:unode" -> n1[color=transparent]
"B:unode" -> "C"
"C" -> "D"
"C" -> n2[color=transparent]
}
在最後一步之前會轉化為這樣
digraph BST {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.8]
n1, n2, n3, n4[color=transparent, fontsize=0]
"A"
"B:unode"[fillcolor=red]
"C"
"D"[fillcolor=red]
tnode -> n4[color=transparent]
tnode -> "B:unode"
"B:unode" -> n1[color=transparent]
"B:unode" -> "A"
"A" -> "C"
"A" -> "E"
"C" -> "D"
"C" -> n2[color=transparent]
}
在經過最後一次左旋轉即可得到左傾斜的紅黑樹
digraph BST {
graph[ordering="out"]
node [shape=circle, fixedsize=true, style=filled,width=.8]
n1, n2, n3, n4[color=transparent, fontsize=0]
"A"
"B:unode"[fillcolor=red]
"C"
"D"[fillcolor=red]
"B:unode" -> tnode
"B:unode" -> "A"
"A" -> "C"
"A" -> "E"
"C" -> "D"
"C" -> n2[color=transparent]
}
如此一來即恢復平衡
在 GGGG
則是可以直接以 rbtn_rotate_right
填入來恢復平衡
以上為 linux 左傾斜紅黑樹刪除的想法
DDDD
= 0
: 為了要記錄刪除的點
EEEE
= !rbtn_right_get(x_type, x_field, node)
: 為了看是否右邊沒有東西
FFFF
= rbtn_rotate_left
GGGG
= rbtn_rotate_right
TimSort
O(nlogn)
(comparison based sort 的最佳時間複雜度),其中利用了 insertion_sort
跟 merge_sort
來分別處理比較少的資料以及比較多的資料。__inplace_merge
為了要合併兩個 run
並且保持 inplace
, 這邊採用的方法為反轉裡面的資料來達到排序的效果
insertion_sort
排序,所以為升冪排序,以下給予圖片解說來解釋裡面程式碼的作用[1, 2, 4, 5]
跟 [3, 6, 10]
流程如下:
cur_1
指到比 first_2
更小的位置 利用while(cur_1 < last_2 && !cmp(cur_2, cur_1)) cur_1 += size;
cur_2
指到比 cur_1
更小的位置 利用while(cur_2 < last_2 && cmp(cur_2, cur_1)) cur_2 += size;
即可得到目前的狀況
digraph {
node [shape=plaintext, fontcolor=black, fontsize=16];
"" -> "Values:" [color=white];
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
pointers [label="<f0> first_1 |<cur> cur_1| <f1> last_1 | <f2> first_2| <f5> cur_2| <f3> last_2", color=white];
values [label="<f0> 1| <f1> 2| <f2> 4| <f3> 5 | <f4> 3 | <f5> 6 | <f6> 10"];
{ rank=same; ""; pointers }
{ rank=same; "Values:"; values }
edge [color=blue];
// pointers:f0 -> values:f2;
pointers:f0 -> values:f0;
pointers:f2 -> values:f4;
pointers:f1 -> values:f3;
pointers:f3 -> values:f6;
pointers:cur -> values:f2;
pointers:f5 -> values:f4;
}
接下來把 cur_1
到 first_2
之前相反過來
__reverse(cur_1, first_2, size)
再來將 first_2
與 cur_2
之前相反過來
__reverse(first_2, cur_2, size)
即可得到當前的陣列
digraph {
node [shape=plaintext, fontcolor=black, fontsize=16];
"" -> "Values:" [color=white];
node [shape=record, fontcolor=black, fontsize=14, width=4.75, fixedsize=true];
pointers [label="<f0> first_1 |<cur> cur_1| <f1> last_1 | <f2> first_2| <f5> cur_2| <f3> last_2", color=white];
values [label="<f0> 1| <f1> 2| <f2> 5| <f3> 4 | <f4> 3 | <f5> 6 | <f6> 10"];
{ rank=same; ""; pointers }
{ rank=same; "Values:"; values }
edge [color=blue];
// pointers:f0 -> values:f2;
pointers:f0 -> values:f0;
pointers:f2 -> values:f4;
pointers:f1 -> values:f3;
pointers:f3 -> values:f6;
pointers:cur -> values:f2;
pointers:f5 -> values:f4;
}
可以注意到,根據剛剛的翻轉,中間已經呈現了一個降冪的關係,此時只需要重新反轉一次 即可得到最後排序好的兩個 run
__reverse(cur_1, cur_2, size)
merge
速度變差tim_sort
的人有稍微考慮到,當兩個 run
的長度不一致,達到非常懸殊的等級的時候,假設兩個 run
分別為 A_run, (len(A_run) == 10)
以及 B_run, (len(B_run) == 100)
,此時的 merge
會在兩個已經sort好的陣列中做出相當多不需要的操作,而當兩個 run
的長度差不多時,此時的 merge
才算比較有效率。merge_rule
如下static inline bool merge_rule(run_t *stack, size_t top)
{
if (top < 1)
return false;
if (top < 2)
return run_length(stack[top]) > run_length(stack[top - 1]);
return run_length(stack[top]) > run_length(stack[top - 1]) ||
run_length(stack[top]) + run_length(stack[top - 1]) >
run_length(stack[top - 2]);
}
意即若當前這個 run
加上前一個 run
沒有比前兩個 run
大時,不會進入 merge
的片段
不用抄寫答案,只要專注程式碼本身和你的理解。 jserv
RB_LOG2_MAX_NODES
#define RB_LOG2_MAX_MEM_BYTES (sizeof(void *) << 3)
/* clang-format off */
#define RB_LOG2_MAX_NODES \
(RB_LOG2_MAX_MEM_BYTES - \
(sizeof(void *) >= 8 ? 4 \
: sizeof(void *) >= 4 ? 3 \
: 1) - 1)
/* clang-format on */
可以根據 rb_tree
的定義,樹高為 2 * log(n + 1)
以及不超出記憶體限制的規則得到以下結論。
sum_tree
沒必要將程式碼註解改為中文,直接解讀程式碼的作用,避免逐行解說,而該揣摩設計者的思維。
jserv
static int sum_subtree(node_t *node)
{
int result = 0;
while (node) {
node_t *pre;
if (!rbtn_left_get(node_t, link, node))
goto do_print;
for (pre = rbtn_left_get(node_t, link, node);
rbtn_right_get(node_t, link, pre) &&
rbtn_right_get(node_t, link, pre) != node;
pre = rbtn_right_get(node_t, link, pre)) {
}
/* 根據每一個點往右走*/
if (!rbtn_right_get(node_t, link, pre)) {
rbtn_right_get(node_t, link, pre) = node;
node = rbtn_left_get(node_t, link, node);
/* 右邊沒有找左邊 */
} else {
rbtn_right_get(node_t, link, pre) = NULL;
do_print:
result += node->value;
printf("value: %d\n", node->value);
node = rbtn_right_get(node_t, link, node);
/* cache */
}
}
return result;
}