contributed by < lumynou5
>
list_insert_before
函式巧妙地利用指標的指標來避免特例:
list_item_t **p;
for (p = &l->head; *p != before; p = &(*p)->next)
;
*p = item;
(*p)->next = before;
由於 p
指向的是「節點指向下一個節點的指標」,因此可以用來判斷是否到達 before
和修改前一個節點的值,而不需要兩個 list_item_t *
,且避免了 before
為 l->head
時的特例,因為 list_t
是不同的結構體,如果使用 list_item_t *
,要將節點插入為第一個節點,就得額外處理。
一個簡單的合併排序實作是:
list_item_t const DUMMY_LIST_ITEM = { .value = INT_MAX };
static void list_merge_sort_impl(list_item_t **pbegin, list_item_t *end)
{
if ((*pbegin)->next == end) // We have a single node in this sub.
return;
list_item_t *slow = *pbegin;
for (list_item_t *fast = slow->next; fast != end && fast->next != end;) {
slow = slow->next;
fast = fast->next->next;
}
list_merge_sort_impl(pbegin, slow->next);
list_merge_sort_impl(&slow->next, end);
list_item_t *l = *pbegin, *r = slow->next, **p = pbegin;
while (l != &DUMMY_LIST_ITEM || r != &DUMMY_LIST_ITEM) {
if (l->value < r->value) {
*p = l;
l = l->next != slow->next ? l->next : &DUMMY_LIST_ITEM;
} else {
*p = r;
r = r->next != end ? r->next : &DUMMY_LIST_ITEM;
}
p = &(*p)->next;
}
*p = end;
}
void list_merge_sort(list_t *l)
{
if (!l)
return;
list_merge_sort_impl(&l->head, NULL);
}
首先以快慢指標找到第一段的結尾、第二段的開頭(slow->next
),將兩段分別排序,然後再合併起來,很普通的遞迴式合併排序。
函式 remove_free_tree
的核心部分為:
block_t **node_ptr = find_free_tree(root, target);
if ((*node_ptr)->l && (*node_ptr)->r) {
block_t **pred_ptr = &(*node_ptr)->l;
while ((*pred_ptr)->r)
pred_ptr = &(*pred_ptr)->r;
if (*pred_ptr == (*node_ptr)->l) {
block_t *old_right = (*node_ptr)->r;
*node_ptr = *pred_ptr;
(*node_ptr)->r = old_right;
} else {
block_t *old_left = (*node_ptr)->l;
block_t *old_right = (*node_ptr)->r;
block_t *pred_node = *pred_ptr;
remove_free_tree(&old_left, *pred_ptr);
*node_ptr = pred_node;
(*node_ptr)->l = old_left;
(*node_ptr)->r = old_right;
}
} else if ((*node_ptr)->l || (*node_ptr)->r) {
block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r;
*node_ptr = child;
} else {
*node_ptr = NULL;
}
target->l = NULL;
target->r = NULL;
target
的左子節點,表示前導節點沒有右子節點,直接以其替換 target
,然後將原本的右子樹移過去即可。target
的位置替換之。因為遞迴呼叫了 remove_free_tree
,前導節點自身的子節點(若有)會替換到合適位置,然後直接將原本的左右子樹移過去即可。target
只有一個子節點,以該子節點替換之即可。target
沒有子節點,直接將指向它的指標設為 NULL
,就將它從樹中移除了。第 21 行與第 24 行的分支可以合併,因為如果 target
沒有子節點,(*node_ptr)->r
就是 NULL
,刪除這個分支能夠省去 4 道指令(x86-64 gcc 14.2 -O3
,Compiler Explorer 演示)。
第 15 行的 pred_node
似乎完全不必要,只在一處使用,完全可以用 *pred_ptr
替代。乍看之下像是儲存原本的值,避免函式呼叫中修改了,但 pred_ptr
指向哪實際上不可能被修改。而在一開始,*node_ptr
和 target
指向的是同一個物件,差別在於前者實際上是「在 target
原位的節點」,透過將程式碼中一些 *node_ptr
改為 target
能讓程式碼更簡短且意圖更清楚,old_left
、old_right
也可以用 target->l
、target->r
替換:
block_t **node_ptr = find_free_tree(root, target);
if (target->l && target->r) {
block_t **pred_ptr = &target->l;
while ((*pred_ptr)->r)
pred_ptr = &(*pred_ptr)->r;
if (*pred_ptr != target->l) {
remove_free_tree(&target->l, *pred_ptr);
(*node_ptr)->l = target->l;
}
(*node_ptr)->r = target->r;
*node_ptr = *pred_ptr;
} else {
block_t *child = target->l ? target->l : target->r;
*node_ptr = child;
}
target->l = NULL;
target->r = NULL;
但我發現這樣修改後,-O3
下產生的組合語言反而更長。在閱讀過組合語言後,我發現是一些分支被展開了,事實上在 -O0
下以上程式碼產生的組合語言確實短非常多,-O1
下更短,但 -O2
、-O3
卻都比各自的上一等級產生更長的組合語言。我認為這是因為編譯器判斷分支足夠短,直接重複部分指令兩次的收益高過膨脹的檔案大小。
TODO: 測試實際效能。
函式 quick_sort
的主體:
int n = list_length(list);
int value;
int i = 0;
int max_level = 2 * n;
struct list_head *begin[max_level];
struct list_head *result = NULL, *left = NULL, *right = NULL;
begin[0] = list->next;
list->prev->next = NULL;
while (i >= 0) {
struct list_head *L = begin[i], *R = list_tail(begin[i]);
if (L != R) {
struct list_head *pivot = L;
value = list_entry(pivot, node_t, list)->value;
struct list_head *p = pivot->next;
pivot->next = NULL;
while (p) {
struct list_head *n = p;
p = p->next;
int n_value = list_entry(n, node_t, list)->value;
if (n_value > value) {
n->next = right;
right = n;
} else {
n->next = left;
left = n;
}
}
begin[i] = left;
begin[i + 1] = pivot;
begin[i + 2] = right;
left = right = NULL;
i += 2;
} else {
if (L) {
L->next = result;
result = L;
}
i--;
}
}
list->next = result;
rebuild_list_link(list);
流程(以最外層迴圈第一次迭代的視角描述):
left
和 right
中。left
、pivot
和 right
分別推入堆疊。right
。這會對每個推入堆疊的串列重複,直到(不是直到所有串列都,而是「深度優先」地,因為堆疊先進後出)串列中只有零或一個節點。result
的最前面,然後從堆疊彈出這個串列。由於堆疊先進後出的特性,以及最後得到結果時是插到最前面,值相等的節點順序會反過來,因此排序並不是穩定的。
函式 list_quicksort
的主體:
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
pivot = list_entry(head, struct listitem, list);
list_del(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
list_move_tail(&item->list, &list_greater);
}
list_quicksort(&list_less);
list_quicksort(&list_greater);
list_add(&pivot->list, head);
list_splice(&list_less, head);
list_splice_tail(&list_greater, head);
迴圈是從前往後迭代,而 list_move_tail
將節點從串列移到子串列的最後面,因此值相等的節點順序維持不變。如果改為 list_move
移到最前面,那順序就會倒過來,並且因為遞迴,最終的順序其實不一定。