# 2024q1 Homework2 (quiz1+2) contributed by < [`blue76815`](https://github.com/blue76815/Linux-2024q1-Homework2-quiz1-2-?tab=readme-ov-file) > [第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) ## 測驗 `1` :::success 延伸問題: 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 2. 使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 ::: ### 1. 解釋程式碼運作原理 ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t ``` :::success ### 疑問: 此 Link list 節點,指向下一個節點位址時,是儲存到 `*next` 結構成員 問題是 `*left`, `*right` 沒指向下一個節點位址, 請問是在程式哪個步驟中有處理下個節點位址,儲存到`*left` 和 `*right;`結構成員的? ::: #### **`list_tail()`** **尋找**鏈結串列 ( link list ) 的**末端節點位址** ```c node_t *list_tail(node_t **left) { /* 當 (*left)->next 還有紀錄節點位址 * 就繼續指向下一個節點 * */ while ((*left) && (*left)->next) left = &((*left)->next );//AAAA return *left; } ``` #### **`list_length()`** **計次**鏈結串列 ( link list ) 的**節點個數** ```c int list_length(node_t **left) { int n = 0; while (*left) { /* 一直指向下一個節點,順便 ++n 計次 * 直到 *left 節點位址為NULL尾端時,才跳出不計次 * */ ++n; left = &((*left)->next);// BBBB } return n; } ``` #### **`list_construct()`** **建立新節點**,並將資料 `n`,存入指定節點內的 `value` 成員裡 ```c node_t *list_construct(node_t *list, int n) { node_t *node = malloc(sizeof(node_t));//建一個新節點 node->next = list; // node->value = n;//插入節點資料 return node;//回傳節點位址 } ``` :::info `list_construct()` 在測驗1中,是用在**將 `test_arr[count]` 陣列裡的資料**一個一個的**存放到 `list` 鍊結串列的節點內** ```c int main(int argc, char **argv) { node_t *list = NULL; size_t count = 100000; int *test_arr = malloc(sizeof(int) * count); for (int i = 0; i < count; ++i) test_arr[i] = i; shuffle(test_arr, count);/* 對 test_arr[] 陣列內的資料洗牌*/ while (count--) list = list_construct(list, test_arr[count]); /*將 test_arr[count] 陣列裡的資料 * 一個一個的存放到 `list` 鍊結串列的節點內 * */ ... } ``` `list_construct()`和 `list_add()`不同 **`list_add()`** 是在鏈結串列 (link list)中,**插入一個節點** ( `list_add()`函式內,沒有建立一個新節點的功能) ::: #### **`list_add()`** 在鏈結串列 ( link list )中,**插入一個節點** 註明: `list_add()`函式內,沒有建立一個新節點的功能 ```c void list_add(node_t **list, node_t *node_t) { node_t->next = *list; *list = node_t; } ``` #### **`list_free()`** 鏈結串列 ( link list ),**釋放節點** ```c void list_free(node_t **list) { node_t *node = (*list)->next; while (*list) { free(*list); *list = node; if (node) node = node->next; } } ``` #### **`quick_sort()`** :::success 根據 [動畫圖解資料結構:使用C語言](https://www.sanmin.com.tw/product/index/006550630) > **快速演算法(Quick Sort)** 又稱為**分割交換排序法**, > 其觀念是先在資料中找到一個**中間值**, > 把**小於中間值**的資料**放在左邊** > 而**大於中間值**的資料**放在右邊**, > 再以同樣的方式分別處理左右邊兩邊的資料,直到完成為止 備註:在 [Introduction to Algorithms, 4/e](https://www.tenlong.com.tw/products/9780262046305) CH7 Quicksort 介紹中,不叫**中間值**,而是稱為 **pivot** pivot: 樞紐 ::: ```c void quick_sort(node_t **list) { int n = list_length(list); int value; int i = 0; int max_level = 2 * n; node_t *begin[max_level], *end[max_level]; node_t *result = NULL, *left = NULL, *right = NULL; begin[0] = *list; end[0] = list_tail(list); while (i >= 0) { node_t *L = begin[i], *R = end[i]; if (L != R) { node_t *pivot = L; value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; while (p) { node_t *n = p; p = p->next;//CCCC list_add(n->value > value ? &right : &left, n); } begin[i] = left; end[i] = list_tail(&left) ;//DDDD begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] =list_tail(&right) ;//EEEE left = right = NULL; i += 2; } else { if (L) list_add(&result, L); i--; } } *list = result; } ``` 詳細流程介紹參閱 [quit sort 非遞迴版 程式步驟詳解](https://hackmd.io/@blue76815/BybOD3mxA) ### 2. 改成 Linux 核心的 List API 撰寫風格 在我們的專案文件夾裡,引入 [lab0](https://github.com/sysprog21/lab0-c/blob/master/list.h) 的 `list.h` ![image](https://hackmd.io/_uploads/Skcl76Pe0.png) ```c #include <assert.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include "list.h" ``` Linux List API 使用方法,參閱 * [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC) * [The Linux Kernel API](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html) 目標把 ```c void list_add(node_t **list, node_t *node_t) node_t *list_tail(node_t **left) int list_length(node_t **left) node_t *list_construct(node_t *list, int n) void list_free(node_t **list) static bool list_is_ordered(node_t *list) ``` 替換成 `list.h` 內的 API 有參考 [vax-r](https://hackmd.io/@vax-r/linux2024-homework2#%E6%B8%AC%E9%A9%97%E4%B8%80) 同學的作法,寫成 ```c void quick_sort(struct list_head **head) { int n = list_length(*head); int pivot_value; int i = 0; int max_level = 2 * n; struct list_head *begin[max_level]; begin[0] = *head; for (int i = 1; i < max_level; i++) begin[i] = list_new(); struct list_head *result = list_new(), *left = list_new(), *right = list_new();//OK while (i >= 0) { struct list_head *L = begin[i]->next, *R=begin[i]->prev; if (L!=R) { node_t *pivot = list_remove_head(begin[i]); pivot_value = pivot->value; node_t *entry, *safe; list_for_each_entry_safe(entry, safe, begin[i], list){ list_del(&entry->list); list_add(&entry->list,entry->value > pivot_value? right:left); } list_splice_init(left, begin[i]); list_add(&pivot->list, begin[i + 1]); list_splice_init(right, begin[i + 2]); i += 2; } else { if (list_is_singular(begin[i])) list_splice_init(begin[i], result); i--; } } for (int i = 0; i < max_level; i++) list_free(begin[i]); list_free(left); list_free(right); *head = result; } ``` 詳細變更,請看我的 Github [3723ace](https://github.com/blue76815/Linux-2024q1-Homework2-quiz1-2-/commit/3723aceaa7f77c3d8c7ef79c36eb7a8f0a7a6b15) 紀錄 ### 3. 提出可避免最差狀況的快速排序實作,設計效能評比 ### 3.1 撰寫 gunplot 繪圖程式 根據 [在 Linux 中以特定的 CPU 核心執行程式](https://blog.gtwang.org/linux/run-program-process-specific-cpu-cores-linux/) :::warning makefile 中 `sudo taskset -c 0 ./main 10000 > data.csv ` 其中 `sudo taskset -c 0 ` 就是要將程式綁定在1個cpu上執行 ::: 根據 [man clock_gettime](https://man7.org/linux/man-pages/man3/clock_gettime.3.html) 使用 clock_gettime()函數,計時 nanosecond 先作到量測 1000筆 資料做 q_sort 計時為例 ```c int main(int argc, char **argv) { //for (int i = 10; i <= 100000; i++) { for (int i = 10; i <= 1000; i++) { unsigned long long time_non_sort = 0, time_sort= 0; struct timespec t1, t2; struct list_head *list = list_new(); size_t count = i, data_number=i; int *test_arr = malloc(sizeof(int) * count); for (int i = 0; i < count; ++i) test_arr[i] = i; shuffle(test_arr, count); while (count--) list = list_construct(list, test_arr[count]); //list_print(list); clock_gettime(CLOCK_MONOTONIC, &t1); //撮記時間t1 quick_sort(&list);//真正跑分析的函式 clock_gettime(CLOCK_MONOTONIC, &t2); //撮記時間t2 time_non_sort += (unsigned long long) (t2.tv_sec * 1000000000 + t2.tv_nsec) - (t1.tv_sec * 1000000000 + t1.tv_nsec);//轉成 nanosecond assert(list_is_ordered(list)); clock_gettime(CLOCK_MONOTONIC, &t1); //撮記時間t1 quick_sort(&list);//真正跑分析的函式 clock_gettime(CLOCK_MONOTONIC, &t2); //撮記時間t2 time_sort += (unsigned long long) (t2.tv_sec * 1000000000 + t2.tv_nsec) - (t1.tv_sec * 1000000000 + t1.tv_nsec);//轉成 nanosecond assert(list_is_ordered(list)); printf("%ld,%llu,%llu\n", data_number, time_non_sort, time_sort); //list_print(list); list_free(list); free(test_arr); } return 0; } ``` #### gunplot :撰寫 plot_qsort.gp ``` reset set xlabel 'data number' set ylabel 'nanosecond' set title 'perfomance comparison' set term png enhanced font 'Times_New_Roman, 10' set output 'qsort.png' set xtics 0,100 //x軸 每間隔100點,設一個座標 set datafile separator "," set key left plot \ "data.csv" using 1:2 with linespoints linewidth 1 title "qsort", \ "data.csv" using 1:3 with linespoints linewidth 1 title "non-qsort" ``` #### makefile ``` SHELL = /bin/bash CC = gcc CFLAGS = -g -pthread SRC = $(wildcard *.c) EXE = $(patsubst %.c, %, $(SRC)) SRC2 = $(wildcard *.csv) SRC3 = $(wildcard *.png) all: ${EXE} %: %.c ${CC} ${CFLAGS} $@.c -o $@ plot: sudo taskset -c 0 ./main > data.csv gnuplot plot_qsort.gp mv data.csv 1000_data.csv mv qsort.png 1000_qsort.png clean: rm ${EXE} rm ${SRC2} rm ${SRC3} ``` #### 編譯驗證程式 ``` $ make $ make plot sudo taskset -c 0 ./main > data.csv gnuplot plot_qsort.gp mv data.csv 1000_data.csv mv qsort.png 1000_qsort.png ``` ![image](https://hackmd.io/_uploads/SkuvLwhMC.png) ``` $ ls 1000_data.csv 1000_qsort.png cpucycles.h list.h main main.c makefile plot_qsort.gp ``` 可看到生成 1000_data.csv 1000_qsort.png 這兩個檔案 ![image](https://hackmd.io/_uploads/SyBmwPnzA.png) 1000_qsort.png ![1000_qsort](https://hackmd.io/_uploads/rkDrDD2zR.png) ### 4. 針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 ### 4.1 quick_sort worst case 根據這篇 [快速排序 (Quick Sort)](https://kopu.chat/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/) 提到 quick_sort worst case 是發生在 > **Pivot 恰好是該資料陣列的最小值或最大值**,使得切割無法產生一分為二的效果。 > > $T(n)=c$$\times$$n+T(n-1)+T(0),$ c$\times$n 為切割時間 > > $T(n)=T(n-1)+c$$\times$$((n-1)+n)=...=T(1)+c$$\times$$(2+...+n)$ > > $T(n)=c$$\dfrac{(n+2)(n-1)}{2}$ > > 故時間複雜度為 $O(n^2)$ ### 4.2 提昇效能方法: 根據 [避免 Quick Sort 的 Worst Case 發生](https://kopu.chat/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/#lwptoc4) > 有鑑於**時間複雜度高達 $O(n^2)$**,要怎麼樣才能避免取到最小或最大值的 Worst Case 呢? > > 以下提供三種可能解法: > 1. Middle-of-Three 方法 > 2. Randomized Quick Sort > 3. 使用 Median-of-Medians 作為基準 ### 4.3 撰寫程式 根據 [避免 Quick Sort 的 Worst Case 發生](https://kopu.chat/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/#lwptoc4) 提到的方法 ### 4.3.1 修改 find_middle 之前的 `find_middle()` 每次要先做 `list_length(head)/2; `時間複雜度太長 ```c struct list_head *find_middle(struct list_head *head) { //用 list_length(struct list_head *head) 方法取一半 int n = list_length(head)/2; struct list_head *middle,*safe; printf("list_length=%d\r\n",n); list_for_each_safe (middle, safe,head) { if (n == 0) break; n--; } return middle; } ``` 因次參照 Jserv 老師的 [分析「快慢指標」](https://hackmd.io/@sysprog/ry8NwAMvT)作法 改成 ```c node_t *get_middle_node(struct list_head *head) { struct list_head *slow = head->next; struct list_head *fast = head->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } return list_entry(slow, node_t, list); } ``` ### 4.3.2 實做 Middle-of-Three 根據 [1. Middle-of-Three 方法](https://kopu.chat/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/#%E9%81%BF%E5%85%8D_Quick_Sort_%E7%9A%84_Worst_Case_%E7%99%BC%E7%94%9F) > step1.取出 link list 頭 (low_node) 尾(high_node) 中間(middle_node) ,這三個節點 > > step2.前面三筆資料,找出中間值節點( `middle_value_node` )。 `find_median_of_three()` > > step3.中間值節點 和 頭節點 交換 `swap_nodes()` > > step4.讓新的 頭節點 作為 pivot 詳細程式實做,請看 github commit [Implement median_of_three qsort.](https://github.com/blue76815/Linux-2024q1-Homework2-quiz1-2-/commit/849adf0e659e6786e1df1e0163b1e551aae681dc) ```c /* 從三筆資料,找出中間值節點 */ node_t *find_median_of_three(node_t *a, node_t *b, node_t *c) { if ((a->value > b->value && a->value < c->value) || (a->value > c->value && a->value < b->value)) { return a; } else if ((b->value > a->value && b->value < c->value) || (b->value > c->value && b->value < a->value)) { return b; } else { return c; } } /** * https://github.com/torvalds/linux/blob/master/include/linux/list.h * list_replace - replace old entry by new one * @old : the element to be replaced * @new : the new element to insert * * If @old was empty, it will be overwritten. */ static inline void list_replace(struct list_head *old, struct list_head *new) { new->next = old->next; new->next->prev = new; new->prev = old->prev; new->prev->next = new; } /** * https://github.com/torvalds/linux/blob/master/include/linux/list.h * list_swap - replace entry1 with entry2 and re-add entry1 at entry2's position * @entry1: the location to place entry2 * @entry2: the location to place entry1 */ static inline void list_swap(struct list_head *entry1, struct list_head *entry2) { struct list_head *pos = entry2->prev; list_del(entry2); list_replace(entry1, entry2); if (pos == entry1) pos = entry2; list_add(entry1, pos); } struct list_head* swap_nodes(node_t *node1, node_t *node2, struct list_head *head) { if (node1 == node2) return head; list_swap(&node1->list,&node2->list); // If node1 or node2 was the head, update head if (&node1->list == head) { head = &node2->list; } else if (&node2->list == head) { head = &node1->list; } return head; } ``` 和 `q_sort` 差別是,加入 `median_of_three(begin[i])`預先處裡 link list ```c void quick_sort_median_of_three(struct list_head **head) { .... while (i >= 0) { struct list_head *L = begin[i]->next, *R=begin[i]->prev; if (L!=R) { begin[i]=median_of_three(begin[i]); // 在這行加入 median_of_three(begin[i])預先處裡 link list node_t *pivot = list_remove_head(begin[i]); pivot_value = pivot->value; .... } ``` ### 4.3.3 實做 Randomized Quick Sort 根據文章中 [2. Randomized Quick Sort](https://kopu.chat/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/#lwptoc4) 提到 > ## 2. Randomized Quick Sort > > **用亂數選取的方式,隨機挑一個值作為 pivot。** > > **當然,這還是可能會發生 Worst Case 高達 O(n2) 的問題,只是機率比較低。** Average Case 與 Best Case 同樣是 O(n log n)。 > > 所以我們也可以把以上兩種改進合併──**先用亂數任選三個,再用其中的中間值作為 pivot。** 詳細程式實做,請看 github commit [Implement random_pivot qsort.](https://github.com/blue76815/Linux-2024q1-Homework2-quiz1-2-/commit/7784fa7b66d2fb662eae42a87472b4c989d0b69e) 和 `q_sort` 差別是加入,這三行是 做 random pivot ```c /* step0. 先準備好 begin[i]的 head 節點,準備之後做交換用==> *head_node step1. 用亂數任選三個,再用其中的中間值作為 pivot。 step2. 再用其中的中間值作為 pivot。 step1~step2 就是 random_pivot_node=random_pivot(begin[i]); 在處裡 最後產生 random_pivot_node step4. swap_nodes(begin[i], head_node, random_pivot_node); 代表 begin[i] link list 的 pivot 要改成 random_pivot_node做排序 */ node_t *head_node = list_first_entry(begin[i], node_t, list); node_t *random_pivot_node=random_pivot(begin[i]); begin[i]=swap_nodes(head_node, random_pivot_node,begin[i]); ``` ```c void quick_sort_random_pivot(struct list_head **head) { ... while (i >= 0) { struct list_head *L = begin[i]->next, *R=begin[i]->prev; if (L!=R) { // 這三行是 做 random pivot /* step0. 先準備好 begin[i]的 head 節點,準備之後做交換用==> *head_node step1. 用亂數任選三個,再用其中的中間值作為 pivot。 step2. 再用其中的中間值作為 pivot。 step1~step2 就是 random_pivot_node=random_pivot(begin[i]); 在處裡 最後產生 random_pivot_node step4. swap_nodes(begin[i], head_node, random_pivot_node); 代表 begin[i] link list 的 pivot 要改成 random_pivot_node做排序 */ node_t *head_node = list_first_entry(begin[i], node_t, list); node_t *random_pivot_node=random_pivot(begin[i]); begin[i]=swap_nodes(head_node, random_pivot_node,begin[i]); node_t *pivot = list_remove_head(begin[i]); pivot_value = pivot->value; ... } ``` ### 4.4 量測效能 ~~因為我們的 quick_sort 排序完,節點是**升冪排序** 例如 1-->2-->3-->4-->5-->7~~ ~~所以 worst case 我們餵**降冪排序**的節點資料,給 quick_sort 排序~~ 詳細程式實做,請看 github commit [Implement plot_qsort.gp to measure 3 ways of qsort performance.](https://github.com/blue76815/Linux-2024q1-Homework2-quiz1-2-/tree/aa80cb545da54cc53ab2d8c60a1db66a192ee577) 編譯量測效能 以量測2000筆為例 ``` $ make $ make plot sudo taskset -c 0 ./main > data.csv gnuplot plot_qsort.gp mv data.csv 2000_data.csv mv qsort.png 2000_qsort.png ``` ![2000_qsort](https://hackmd.io/_uploads/SJgOBUSDC.png) :::info 加入 median_of_three 和 random_pivot 處理,**根本沒有使排序更省時**,相反的 **因為 `quick_sort_median_of_three()`內多了 `median_of_three(begin[i])`交換節點的處理步驟** ```c void quick_sort_median_of_three(struct list_head **head) { ... while (i >= 0) { struct list_head *L = begin[i]->next, *R=begin[i]->prev; if (L!=R) { /* type35 median_of_three 解決辦法 當`begin[i]` link list 長度有3個節點以上, 才丟進 `median_of_three(begin[i]);` 處裡, */ begin[i]=median_of_three(begin[i]); // 在這行加入 median_of_three(begin[i])預先處裡 link list node_t *pivot = list_remove_head(begin[i]); pivot_value = pivot->value; } ``` **`quick_sort_random_pivot`內多了`random_pivot(begin[i]);`交換節點的處理步驟** ```c void quick_sort_random_pivot(struct list_head **head) { ... while (i >= 0) { struct list_head *L = begin[i]->next, *R=begin[i]->prev; if (L!=R) { node_t *head_node = list_first_entry(begin[i], node_t, list); node_t *random_pivot_node=random_pivot(begin[i]); begin[i]=swap_nodes(head_node, random_pivot_node,begin[i]); node_t *pivot = list_remove_head(begin[i]); pivot_value = pivot->value; ... } ``` 反而比 `q_sort` 更耗費處理時間 - [ ] 問題:在找 median_of_three,其他**步驟更精簡**的實做方法,才能**降低時間複雜度** ::: --- ## 測驗 `2` :::success 延伸問題: 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 2. 將改進過的 Timsort 實作整合進 [sysprog21/lab0-c](https://github.com/sysprog21/lab0-c),作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 ::: ## 1. 解釋上述程式碼的運作原理 ### 1.1 Timsort 演算法介紹 Timsort 混和了合併排序法(Merge Sort)及插入排序法(Insertion Sort) 根據 [測驗 2](https://hackmd.io/@sysprog/linux2024-quiz1#%E6%B8%AC%E9%A9%97-2) 描述提到 > Timsort 致力於從以下三個方面,改進合併排序演算法: > > 1. 加快合併(merge)過程 > 2. 減少進行合併的次數。 > 3. 在某些特定情況下,尋找比合併排序更有效的排序方法 ### 1.2 Timsort 演算法步驟 :::success 先歸納成以下3個主要步驟 step1. 將遞增數列分割成一組一組 run step2. 合併(用 Merge sort 做合併) step3. Galloping mode ::: 以下詳細介紹步驟細節 #### step1.分割成 run 分割要領 1. 數列中,**一路遞增**的**數組**,**當成一組run數列** 例如 [測驗2](https://hackmd.io/@sysprog/linux2024-quiz1#%E6%B8%AC%E9%A9%97-2) 中 1,2,3,4,3,2,4,7,8 數列中 可以拆分成3個run * 1,2,3,4==>**一路遞增,當成一組run數列** * 3,2==>遇到**遞減的數列**,會被反轉成**遞增**的序列,變成2,3 * 4,7,8==>**一路遞增,當成一組run數列** 2. 合併序列時, * 若 run 的數量**等於**或**小於 2 的冪( $2^n$ 個)**,**效率會最高([指方便待會的 Merge sort 效率最高](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2))**; ![image](https://hackmd.io/_uploads/S1h_SpiOC.png) * 若**大於** **2 的冪( $2^n$ 個)**,**效率就會特別低**。 3. 每個 run 的長度,定義一個 minrun (最小運行長度) * 若長度太短,就用**二分插入排序**,將 run 後面的元素插入到前面的 run 裡面。 例如 run 的長度設定 minrun=5,則在分割 1,2,3,4,3,2,4,7,8數列時, 按照**一路遞增,當成一組run數列**規則 只有1,2,3,4是一組run,但是**因為設定 minrun=5(一組run數列最小長度得5個數字)**,因此得取 **1,2,3,4,3 當成一組run數列**,還要用**二分插入排序**整理 run 變成 **1,2,3,3,4** 4. minrun要定義多少個,參照 [最貼近現實的排序演算法- Timsort](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2) > 那問題來了,該如何知道這個 minrun 的值是多少呀?這個 minrun 必須滿足三個條件, > > 1. **minrun必須小於 64 ,因為大於 64 的 Insertion sort 效率會變差。** > 2. 盡可能的平均分配,不能有全部都剛好是 minrun 而最後一個只剩下很少元素的情況發生 > 3. 盡可能將**切割完的 run 的數量控制在二的次方(前面講的 2的冪),方便待會的Merge sort** > > 因此 Tim 想了一個非常聰明的方法,先將序列的總長度表示成二進位,取出前六個數之後如果後面還有值,就+1,舉個例子:如果總長度是197,換成二進位那就是 11000101,取出前六個數 110001 = 49,後面還有 01 所以將49+1=50,那麼在最壞的情況下,會被拆成 50 50 50 47,剛好滿足了上面的那三個條件,這個方法真的相當聰明。以上我們已經將 Timsort 的切割完全介紹完畢。 #### step2.合併 根據 [最貼近現實的排序演算法- Timsort](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2) > ## 合併 > 為了達到高效率且省空間的合併,Tim 精心設計了合併的細節,先聊大方向的合併,**我們手上現在有非常多個 run,我們從最右邊抓三個 run 出來,由左到右依次命名為 runX, runY, runZ,只要不滿足以下兩個條件的其中之一,就將 runY 與 ( runX 和 runZ 較小的 )合併:** > > 1. runX 長度 > runY + runZ 長度 > 2. runY 長度 > runZ 長度 上面那段合併條件限制,就是對應到 成大 [測驗二](https://hackmd.io/@sysprog/linux2024-quiz1#%E6%B8%AC%E9%A9%97-2) 描述的 > Timsort 採用一組堆疊 (stack) 來暫存 run,此舉是為了避免在一開始就掃描整個資料範圍來產生 run,從而降低記憶體開銷。過程中,Timsort 運用 merge_collapse 函式來確保堆疊上的 run 長度保持平衡。該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則: > > **A 的長度要大於 B 和 C 的長度總和。 > B 的長度要大於 C 的長度。** ![image](https://hackmd.io/_uploads/ByEPeAidA.png) #### step3.Galloping mode 根據 [最貼近現實的排序演算法- Timsort](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2) > 接下來我們來聊兩個 run 之間的合併,為了解決 Merge sort 在處理空間上的不足(可以往上回去看第一張圖),Tim 想出了底下的合併方法: > ![image](https://hackmd.io/_uploads/ryfLJ0odA.png) > 1. 將比較小的 run 當成圖中的 X > 2. 把 runX 複製到一個新的空間 temporary > 3. 接下來用 Merge sort 合併 > > 上面這個合併的方法可以確保在最壞的情況下,Timsort 使用的空間是 n/2 (Tim真的是個很務實的人),到這邊其實已經可以算是很圓滿的結束了,**但是 Tim 覺得這樣不行,還必須對 Merge sort 做出優化,所以 Tim 提出了 Galloping mode**。(本次作業我最想釐清的點) > Galloping > ========= > > 再強調一次! > > > Tim 認為在現實世界中的序列大多都是已經部分排序的(partial sorted) > > 所以**假設有兩個序列 {1, 2, 3, 4, 5, …,1000} {2001, 2002, 2003, …,3000}做 Merge sort,總共需要比對一千次,這實在是太耗資源了,所以 Tim 想了一個新的做法,就是只比二的次方的數,比完之後再用 binary search(前面已經排序好了) 找到那個對的位子,這樣總共比較的次數可以壓低到 O(logn),** 但是Tim又發現了一個問題: > ![image](https://hackmd.io/_uploads/HkMjMCiOR.png) > 從上面那張圖可以發現在比較 2, 4 的時候 linear search 的效果是比 galloping 的效果來的好的(分別是左二欄和右一欄),**所以 Tim 決定當兩個序列比較到第七次都是由同一個序列取值時,才啟動 Galloping mode。** 以上就是 Timsort 所有合併的細節了。 --- ### 1.3 對應到程式碼介紹原理 介紹篇幅太長,另開共筆頁面 [Tim sort 程式步驟詳解](https://hackmd.io/@blue76815/timsort) :::info 參考資料: * [Timsort wiki](https://en.wikipedia.org/wiki/Timsort#Galloping_mode_during_merge) * [Timsort 研究與對 Linux 核心貢獻嘗試](https://hackmd.io/@yanjiew/linux2023q1-timsort#%E5%90%88%E4%BD%B5%E6%BC%94%E7%AE%97%E6%B3%95) * [(台大計組中心) Python小技巧:Python 中的 list 排序演算法](https://www.cc.ntu.edu.tw/chinese/epaper/home/News_Content_n_103858_s_232975.html) * [最貼近現實的排序演算法- Timsort](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2) * [TimSort - Python 內建排序法](https://codingman.cc/timsort-python-built-in-sorting-algorithm/) * [[演算法] TimSort — 以人命名的排序法](https://mailtojacklai.medium.com/%E6%BC%94%E7%AE%97%E6%B3%95-timsort-%E4%BB%A5%E4%BA%BA%E5%91%BD%E5%90%8D%E7%9A%84%E6%8E%92%E5%BA%8F%E6%B3%95-5d5e6ed7872c) ::: ### 1.4 提出改進方案並予以實作。 #### 1.4.1 改善記憶體洩漏 用 valgrind 檢查 :::info ``` $ valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=filename ./timtest ``` 這裡的指令選項解釋如下: - `--leak-check=full`:告訴 Valgrind 執行完整的記憶體泄漏檢查。 - `--show-leak-kinds=all`:顯示所有類型的記憶體泄漏。 ### 其他常用選項 - `--track-origins=yes`:這個選項可以幫助追蹤未初始化的記憶體讀取的來源,這對於確定未初始化記憶體使用的位置非常有用。 - `--verbose`:提供更多的調試信息。 - `--log-file=filename`:將 Valgrind 的輸出導向到一個文件。 ### 範例 假設你的程序叫做 `my_program`,並且已經編譯成執行檔 `my_program`,並產生`valg_log`將 Valgrind 的輸出導向到一個文件,你可以這樣使用 Valgrind: ``` valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --verbose --log-file=valg_log ./my_program ``` ::: --- ``` $ valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=valg_log ./timtest ``` 其中顯示出 ``` ==127653== Searching for pointers to 3 not-freed blocks ==127653== Checked 109,392 bytes ==127653== ==127653== 24,000 bytes in 1 blocks are definitely lost in loss record 1 of 3 ==127653== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==127653== by 0x109673: main (main.c:116) ==127653== ==127653== 24,000 bytes in 1 blocks are definitely lost in loss record 2 of 3 ==127653== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==127653== by 0x109681: main (main.c:117) ==127653== ==127653== 24,000 bytes in 1 blocks are definitely lost in loss record 3 of 3 ==127653== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==127653== by 0x10968F: main (main.c:118) ==127653== ==127653== LEAK SUMMARY: ==127653== definitely lost: 72,000 bytes in 3 blocks ==127653== indirectly lost: 0 bytes in 0 blocks ==127653== possibly lost: 0 bytes in 0 blocks ==127653== still reachable: 0 bytes in 0 blocks ==127653== suppressed: 0 bytes in 0 blocks ==127653== ==127653== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0) ``` valgrind 指出,我的 main.c main (main.c:116) main (main.c:117) main (main.c:118) 第 116-118 行顯示錯誤 發現 ```c=116 element_t *samples = malloc(sizeof(*samples) * SAMPLES); element_t *warmdata = malloc(sizeof(*warmdata) * SAMPLES); element_t *testdata = malloc(sizeof(*testdata) * SAMPLES); ``` 在Tim sort後沒有釋放動態記憶體 加入 ```c=139 free(samples); free(warmdata); free(testdata); ``` 再用 valgrind 檢查,顯示 ``` All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` ``` ==128843== HEAP SUMMARY: ==128843== in use at exit: 0 bytes in 0 blocks ==128843== total heap usage: 4 allocs, 4 frees, 73,024 bytes allocated ==128843== ==128843== All heap blocks were freed -- no leaks are possible ==128843== ==128843== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ```