Contents

jserv - linux 核心設計 第一周題目二解題紀錄

單向 linked list

typedef struct __list {
    int data;
    struct __list *next;
} list;

在不存在環狀結構的狀況下,以下函式能夠對 linked list 元素從小到大排序: 請補完 LL0 ~ LL6 程式碼。

list *sort(list *start) {
    if (!start || !start->next)
        return start;
    list *left = start;
    list *right = left->next;
    /*
        LL0;
        (a): left->next = NULL;
        (b): right->next = NULL;
        (c): left = left->next;
        (d): left = right->next;
    */

    left = sort(left);
    right = sort(right);

    for (list *merge = NULL; left || right; ) {
        if (!right || (left && left->data < right->data)) {
            if (!merge) {
                /*
                    LL1:
                    (a): start = left;  
                    (b): start = right;
                    (c): merge = left;
                    (d): merge = right;
                    (e): start = merge = left;
                    (f): start = merge = right;
                */
            } else {
                /*
                    LL2;
                    (a): merge->next = left;
                    (b): merge->next = right;
                */
                merge = merge->next;
            }
            /*
                LL3;
                (a): left = left->next;
                (b): right = right->next;
                (c): left = right->next;
                (d): right = left->next;
            */
        } else {
            if (!merge) {
                /*
                    LL4;
                    (a): start = left
                    (b): start = right;
                    (c): merge = left;
                    (d): merge = right;
                    (e): start = merge = left;
                    (f): start = merge = right;
                */
            } else {
                /*
                    LL5;
                    (a): merge->next = left;
                    (b): merge->next = right;
                */
                merge = merge->next;
            }
            /*
                LL6;
                (a): left = left->next;
                (b): right = right->next;
                (c): left = right->next;
                (d): right = left->next;
            */
        }
    }
    return start;
}

解題思路

先觀察 sort 會看到該函式在內部會呼叫 sort,所以為遞迴函式。 知道是遞迴函式就要先關注遞迴的終止條件

if(!start || !start->next) {
    return start;
}

先觀察開頭的 if 敘述,有兩種情況會導致遞迴中止

  • start == NULL → 表示該 list 為空,return NULL;
  • start && !start->next → 表示該 list 只有一個元素,直接 return 自身。

再來觀察回傳的部份,先看一下函式的宣告

list *sort(list *start);

回傳的資料型態是 list*,所以可以得知呼叫 sort 之後會得到一串已經排序好的 list。

接著繼續往下看

list *left = start;
list *right = left->next;
/*
    LL0;
    (a): left->next = NULL;
    (b): right->next = NULL;
    (c): left = left->next;
    (d): left = right->next;
*/

left = sort(left);
right = sort(right);

這段可以知道我們的 list 被分成了兩部份

  • list *left = start;
  • list *right = left->next;

之後再分別呼叫 sort 排序,根據以前寫 merge sort 的經驗,這邊先認定要把 list 分成兩條獨立的 list,先判斷 LL0: left->next = NULL;

list *left = start;
list *right = left->next;
left->next = NULL;

left = sort(left);
right = sort(right);

遞迴呼叫思考

接著思考一下今天我們 input 的 list 有五筆資料好了

1 -> -3 -> 2 -> -4 -> 5

我們現在先不要看下面的邏輯,依照我們上面推斷的部份呼叫看看 sort

list *head;
// append datas

head = sort(head);

呼叫之後展開

if(!head || !head->next)
    return head;
    
list *left = head;
list *right = left->next;
left->next = NULL;

根據上面假設的資料,現在我們 left, right 應該分別長這樣

  • left : 1
  • right : -32-45
left = sort(left);
right = sort(right);

因為我們 left 只有一個元素,所以會直接 return right 呢? 目前 right 有四個元素,所以不符合終止條件,會繼續呼叫遞迴下去。

1 -> -3 -> 2 -> -4 -> 5

left = 1
right = -3 -> 2 -> -4 -> 5

    left = -3
    right = 2 -> -4 -> 5
    
        left = 2
        right = -4 -> 5
        
            left = -4
            right = 5 // 遞迴中止

試著思考一下,遞迴中止之後會發生什麼

left = -4, right = 5 先合併 → -4 -> 5

再跟上層的 left = 2 合併會變成這樣 left = 2, right = -4 -> 5-4 -> 2 -> 5

繼續 return 跟上層 left = -3 合併 left = -3, right = -4 -> 2 -> 5-4 -> -3 -> 2 -> 5

繼續 return 跟上層 left = 1 合併 left = 1, right = -4 -> -3 -> 2 -> 5

合併成最終排序完成的 list: -4 -> -3 -> 1 -> 2 -> 5

有發現嗎? 每次合併的時候都像是在做 insert sort,但是比較不一樣的是我們是從右邊開始一個一個元素 insert 到最左邊的元素。

所以上面我們已經完成了呼叫遞迴的邏輯,下面要來思考怎麼 merge,才可以把 leftright 組合,並且回傳以排序好的 list's head

merge 邏輯

left = sort(left);
right = sort(right);

for (list *merge = NULL; left || right; ) {
        if (!right || (left && left->data < right->data)) {
            if (!merge) {
                /*
                    LL1:
                    (a): start = left;  
                    (b): start = right;
                    (c): merge = left;
                    (d): merge = right;
                    (e): start = merge = left;
                    (f): start = merge = right;
                */
            } else {
                /*
                    LL2;
                    (a): merge->next = left;
                    (b): merge->next = right;
                */
                merge = merge->next;
            }
            /*
                LL3;
                (a): left = left->next;
                (b): right = right->next;
                (c): left = right->next;
                (d): right = left->next;
            */
        } else {
            if (!merge) {
                /*
                    LL4;
                    (a): start = left
                    (b): start = right;
                    (c): merge = left;
                    (d): merge = right;
                    (e): start = merge = left;
                    (f): start = merge = right;
                */
            } else {
                /*
                    LL5;
                    (a): merge->next = left;
                    (b): merge->next = right;
                */
                merge = merge->next;
            }
            /*
                LL6;
                (a): left = left->next;
                (b): right = right->next;
                (c): left = right->next;
                (d): right = left->next;
            */
        }
    }

由於我們上面經過了遞迴的過程

left = sort(left);
right = sort(right);

所以我們可以先假設,left, right 目前都已經是排序好的狀態 left, right 指標所指的 data 都是該 list最小值

之後要透過 for loopmerge 邏輯,把比較小的 data 當作 start 並接到正確的位置。

for(list *merge = NULL; left || right) {
    if(!right || (left && left->data < right->data)) {
        if(!merge) {
            /*
                LL1:
                (a): start = left;  
                (b): start = right;
                (c): merge = left;
                (d): merge = right;
                (e): start = merge = left;
                (f): start = merge = right;
            */
        } else {
             /*
                    LL2;
                    (a): merge->next = left;
                    (b): merge->next = right;
            */
            merge = merge->next;
        }
        /*
            LL3;
            (a): left = left->next;
            (b): right = right->next;
            (c): left = right->next;
            (d): right = left->next;
        */
    } else {
        ...
    }
}
if(!right || (left && left->data < right->data)) {
    if(!merge) {
        LL1
    } else {
        LL2
        merge = merge->next;
    }
    LL3
} else {
    if(!merge) {
        LL4
    } else {
        LL5
        merge = merge->next;
    }
    LL6
}

首先來觀察進入 if(!right || (left && left->data < right->data)) 這個區域的情況

  • left->data < right->data 成立時,我們應該要想辦法將 start 指向 left,因為 left 此時擁有 left, right 兩條 list 的最小值。

一開始進入 for loop 的時候 merge 還沒有經過初始化,會進入 if(!merge) 的區塊,可以合理推測 LL1 需要做到兩件事情

  • 初始化 merge
  • start 指向 left

所以 LL1: start = merge = left;

for(list *merge = NULL; left || right) {
    if(!right || (left && left->data < right->data)) {
        if(!merge) {
            start = merge = left;
        } else {
            LL2
            merge = merge->next;
        }
        LL3
    }
}

LL3 的部分,因為已經處理好 left 的第一個元素了(暫存在 merge),所以 LL3: left = left->next;,移動到下一個節點。

for(list *merge = NULL; left || right) {
    if(!right || (left && left->data < right->data)) {
        if(!merge) {
            start = merge = left;
        } else {
            LL2
            merge = merge->next;
        }
        left = left->next;
    } else {
        if(!merge) {
            LL4
        } else {
            LL5
            merge = merge->next;
        }
        LL6
    }
}

接著先考慮 left->data > right->data 的情況,這時不符合 if(!right || (left && left->data < right->data))的敘述,所以會進入下方的 else 區域

  • !merge 的情況下,照著上面的思路(同LL1思考邏輯),應該需要同時做到兩件事情
    • 初始化 merge
    • start 指向 right

所以可以推論出 LL4: start = merge = right;

同理 LL6: right = right->next; 與上方 LL3 思路一致。

for(list *merge = NULL; left || right) {
    if(!right || (left && left->data < right->data)) {
        if(!merge) {
            start = merge = left;
        } else {
            LL2
            merge = merge->next;
        }
        left = left->next;
    } else {
        if(!merge) {
            start = merge = right;
        } else {
            LL5
            merge = merge->next;
        }
        right = right->next;
    }
}

最後只剩下 LL2, LL5 需要解釋

根據一開始我們思考遞迴過程的部份,我們的 function 呼叫到最右側的時候就會開始 return 開始往左做類似 insert sort 的動作。

所以每次進入 merge 這段邏輯的時候,通常的情況都是 left 有一個元素,right 有一段已經排列好的 list

今天假設 left = -3, right = 2->5 進入 for loop 之中

經過第一輪的 for loop 情況會變成

  • start = merge = left(-3)
    • left = left->next (NULL)
  • right = 2->5

這時候因為不滿足 if(!right || (left && left->data < right->data)) 的條件所以進入 else 區塊,又因為 merge 已經經過初始化,所以最終會跑到 LL5 的區域

這時候我們需要把已經 merge 好的部份接上 right 那邊已經排列好的 list 所以 LL5: merge->next = right;

for(list *merge = NULL; left || right) {
    if(!right || (left && left->data < right->data)) {
        if(!merge) {
            start = merge = left;
        } else {
            LL2
            merge = merge->next;
        }
        left = left->next;
    } else {
        if(!merge) {
            start = merge = right;
        } else {
            merge->next = right;
            merge = merge->next;
        }
        right = right->next;
    }
}

經過這行之後

  • start 依舊指向 -3 的位置,將會作為之後的回傳值
    • 因為我們 sort 的設計是每次都會把排序好的 list head 回傳,所以才需要 start 紀錄開頭。
  • merge: -3->2->5
    • merge = merge->next(此時指向2)
  • right = right->next(此時指向5)

因為left, right 其中之一不為空,所以迴圈會繼續。

所以 LL2LL5 的思路同理,LL2: merge->next = left;

這樣我們基本上就把整個 sort 的空格填完了,以下是完整程式碼:

list *sort(list *start) {
    if (!start || !start->next)
        return start;
    list *left = start;
    list *right = left->next;
    left->next = NULL;
    
    left = sort(left);
    right = sort(right);

    for (list *merge = NULL; left || right; ) {
        if (!right || (left && left->data < right->data)) {
            if (!merge) {
                start = merge = left;
            } else {
                merge->next = left;
                merge = merge->next;
            }
            left = left->next;
        } else {
            if (!merge) {
                start = merge = right;
            } else {
                merge->next = right;
                merge = merge->next;
            }
            right = right->next;
        }
    }
    return start;
}

思考過程

如果沒辦法理解上面的內容,也可以嘗試著代入實際的值,用紙筆簡單跑過一次流程 不用太複雜的數字,可以用

left: -4, right: -5->2->8

這種 right 開頭比 left 小的 case,再想一個 left 開頭比較小的 case等等.. 這樣子就能更清楚的思考出該演算法的運作流程。

驗證正確性

// 待補充

之後會補上測試正確性的程式碼,目前完整的檔案可以參考 GitHub

reference