contributed by < ranvd >
diff
ShawnXuanc
可以減少完整程式碼的張貼
程式碼有善用 list 巨集以及將重複的程式碼再利用
Shiang1212
有幾份 commit 沒有撰寫 commit message。即便該 commit 只有簡單的程式更動,仍應該在 commit message 中描述程式碼更動的重點,加速其他開發人員理解這份程式更動的脈絡,進而提高專案的可維護性。
q_new
改進你的漢語表達。
一開始我沒有檢查 malloc
的回傳結果,因此當 malloc
失敗時回傳的 NULL 會導致在執行 INIT_LIST_HEAD
巨集 內的程式碼時對 null pointer 取值,進而引發 segmentation fault。因此增加了以下程式碼檢查 malloc
是否成功。
INIT_LIST_HEAD
不是巨集。
q_free
因為 head 有可能是 null pointer,因此在執行 q_free 動作前應該先檢查 head 是否為 NULL,否則會觸發 segmentation fault。
q_insert_head
與 q_insert_tail
由於一開始沒注意到 strdup
也透過巨集的方式替換成專案內的實作,在 make test
時一直想不透為什麼檢查了 malloc
的回傳值後 q_insert_head
還會觸發 segmentation fault。在發現 test_strdup
內也使用到 test_malloc
後就增加下列程式碼,透過檢查 new->value
是否為 null pointer 來判斷 strdup
是否有執行成功。(q_insert_tail
也有相同的更動)
閱讀 alanjian85 時,看到老師在多處提醒應該盡量重複使用程式碼,因此以下將 q_insert_tail
改寫,重複使用 q_insert_head
中的程式碼。
q_remove_head
與 q_remove_tail
因為 strncpy 會將字串中的前 n 個字元複製到指定的空間,因此當要複製的字串超過 bufsize 時,會使得指定指定的空間中的字串沒有 '\0'
,因此後續如果在執行 printf, strcpy 等函式的時候會出現錯誤。
也在閱讀 alanjian85 時,看到老師在多處提醒應該盡量重複使用程式碼,因此以下將 q_remove_tail
改寫,重複使用 q_remove_head
中的程式碼。
q_delete_mid
使用快慢指標來找出佇列中的中間點。在重新閱讀自己的實作後,發現 head == head->next
應該使用 list.h 內的 list_empty
替代,以提高程式碼的可讀性。
q_reverse
透過 list.h 提供的介面 list_move
與 list_for_each_safe
可以很方便的實作 q_reverse 的功能。
q_reverseK
由於 q_reverseK 函式要求在佇列中每 K 個節點要進行反轉,因此我將每 k 佇列中的節點放到暫時的佇列 tmp 中,並透過上面提到的 q_reverse 函式將佇列反轉。反轉過後的佇列可以使用 list.h 中提供的介面 list_splice_tail 放到新的佇列 result 中,這樣可以避免一個一個加入節點。
q_swap
在閱讀 yanjiew 時,看到 yanjiew 的 q_swap
的實作後,發現 q_swap
要求每兩個節點相互交換位置,因此可以視為 q_reverseK(head, 2)
,因此將實作改成以下方式。
q_descend
與 q_ascend
q_descend
與 q_ascend
要求剩下在佇列中的節點要符合嚴格的遞減/遞增序列,以下以 q_descend 作範例。由於 q_descend
要求佇列符合嚴格的遞減序列,因此可以得知,對於任意一個在佇列中的節點,其前面的節點必定要大於自己,且其後面的節點必定小於自己。因此可以從佇列的後面開始遍歷整個佇列,在過程中用指標 max 紀錄目前遇到的最大值的位置。如果在遍歷的過程中 max 有被更動即代表上個 max 與現在 max 的節點符合嚴格遞減序列的要求;反之,如果再遍歷的過程中 max 沒有被更動即代表 max 與現在走訪到的節點不符合嚴格遞減序列,因此要將目前走訪到的節點從佇列中移除。
q_ascend
與 q_descend
很像,只需將判斷式從 > 0 改成 < 0 即可,目前想到的重複利用程式碼的方式為使用巨集來做代換。不過這樣只有看起來程式碼比較少,實際上在巨集展開後是一樣的,因此目前沒有實作。
element_t_cmp
因為後續 q_sort
與 q_merge
需要考慮到不同的排序方式(由 descned 變數控制),因此實作一個比較函式來統一回傳的介面對後續的實作比較方便,程式也可以保持較高的可讀性。
根據 man 3 strcmp
The strcmp() and strncmp() functions return an integer less than, equal to, or greater than zero if s1 (or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater than s2.
由敘述只能得知當兩個不同的字串 s1 與 s2 放到 strcmp 的結果可能是大於 0 或小於 0,並不能確定是否為 1 或 -1,因此此函式也會遵循這個規定。
首先分析可能發生的情況
因此可以透過將 descend << 31
與 strcmp
的結果做 XOR 運算後做 -
運算來取得正確的會傳結果。例如 a < b 且 descned=1 時,回傳結果應該要小於 0,透過 strcmp(a, b) ^ descend << 31
,使得最高位元因為 XOR 運算被設為 0,此時結果大於 0,最後加上 -
運算就可使結果小於 0。
q_sort
在 q_sort
實作中採用部分 timsort 的想法與 buttom-up 的 merge sort 的合併方式,一開始會將佇列分成多個 run,每個 run 之間使用 prev 指標來連接,形成堆疊的結構,如下圖。每個 run 皆會符合 descend
變數中的要求,如果 descend==1
則 run 以遞減方式排列;反之,以遞增方式排列。當將所有佇列的節點分成不同的 run 後就會進行 run 之間的合併,合併的方式為每兩個連續的 run 一組,並以組為單位合併,以此方式不斷迭代直到堆疊中只剩下一個 run。
假設有 n 個 run,則在第一次迭代後會剩下 個 run,第二次迭代剩下 ,以此類推直到剩下一個 run 在堆疊中。
在合併堆疊中的 run 的過程中,除了需要紀錄堆疊的開頭外,也需要將合併後的 run 放回堆疊中的正確位置,為了省去特殊情況的判斷,在連接 run 與紀錄堆疊的開頭時可以使用指標的指標來實作。
由於在開發 q_sort 過程中一直觸發 segmentation fault,因此使用 GDB 進行除錯。在過程中一直遇到 ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
的問題。透過 GDB 的 bt
、up
、down
追蹤程式碼後發現是因為執行時間過長,導致系統發出 SIGALRM
的訊號。從 GDB 5.4 Signals 的手冊可以看到,GDB 可以針對不同的 Signal 做出不同的應對方式,在將 GDB 對 SIGALRM
的行為設定為 ignore 後就可以正常除錯。
不過在後續閱讀 README.md 的時候發現,文件已經告知使用 ./script/debug -d
就可以直接正常的除錯。
用來合併兩個環狀鏈結串列,使用兩層迴圈,外層走訪鏈結串列 from
,內層走訪鏈結串列 to
,因為呼叫 merge_list
的前提是兩個鏈結串列是已經排序好的,因此內層的迴圈會透過先前提到的 element_t_cmp
函式來判斷 cmp_ptr
是否指向第一個 list
可以插入的位置,等到找到位置後將 list
從 from
中移出並加入插入至 cmp_ptr
前。當兩個迴圈都走訪完後,如果 from
還有節點沒有被放入 to
,則將剩下的節點全部放入 to 的尾端。
這裡的 merge 因為要求需要將所有的鏈結串列合併至第一個,因此這個實作並沒有太多的考量,而是以實作上方便做優先,直接將除了第一個以外的鏈結串列合併至第一個鏈結串列。
既然你選擇合併排序演算法,你要如何確保輸入的資料涵蓋合併排序演算法的最差狀況呢?
目前只有使用 firefox 瀏覽器測試成功,使用 Chrome 會出現非預期錯誤,整個程式會卡在 web_recv,變成只剩下 web 功能,目前沒辦法解決。
原本在啟動 web 功能後 linenoise 的功能會因為 use_linenoise
被設定為 false
而關閉,因此我的想法是直接忽略這項變數,在 linenoise 內使用 select 系統呼叫同時監控 socket file descriptor 與 stdin file descriptor,就如同作業中的提示。
為了能夠在不同的檔案中存取到 socket file descriptor,應將 web_fd 以一般變數的方式宣告。並且在執行 interpret_cmd
後應將建立的連線 (web_connfd
) 關閉,防止 client 端無盡的等待。
接著是修改 linenoise 套件的內容,因為 stdin 與 socket 這兩邊的 file descriptor 隨時有可能有輸入,因此選擇使用 select
來等待可執行相應動作的 file descriptor。不過這兩個 file descriptor 的輸入行為有很大的區別,socket 這邊的輸入會是完整的指令 請求,不存在指令 命令只打到一半情況,例如使用 web 的方式執行 new
,傳送過去的指令 命令就只能是 new
,如果只傳了 ne
則會因為沒有辦再修改而直接跳出錯誤;反之,stdin 這邊的輸入可以在任何情況下針對目前還沒傳送出去的指令 命令做修改,且由於需要支援自動補齊,所以每當 stdin 有任何輸入時程式都應該做出相對應的反應。
「指令」?
更正:command 的中文翻譯是命令
因此我在 linenoise 中宣告了兩個新變數,第一個是 struct line_state
靜態全域結構 g
,用來備份 stdin 輸入時的狀態;第二個是靜態 bool
變數 need_swap
用來紀錄上一次處理的資料是否是來自 socket 端,如果是的話 need_swap = 1。
如果當 stdin 輸入到一半時 socket 要求建立連線,就可以先將目前的狀態存至 g
中,並處理 socket 的請求。如果目前處理的是 socket 連線,則 need_swap = 1
,這代表下一次在做 select 之前需要先將在 g 的資料寫回 l 作為目前的狀況。
在處理完 socket 的請求後,依據 need_swap
內的數值選擇是否要將 g
的資料寫回 l
,即恢復被中斷前的狀態,且應該要將 l.buf
內的資料輸出至畫面中,讓使用者知道自己剛才打的指令內容,這樣即使輸入到一半被中斷,stdin 端的功能不會受影響。
執行結果會如下面所展示。當使用者輸入 my input buffer
時,有一個 socket 端的請求,會先保存目前的狀態,並處理 socket 的執行內容。接著,恢復剛才 socket 進入前的狀態,並將 l.buf
的內容輸出至介面 (即 my input buffer
)。
文字訊息避免用圖片來表示,否則不好搜尋和分類
收到
重新張貼「關鍵」程式碼。
除了在一開始提到程式無法在 chrome 瀏覽器上正常的執行外,當按下 tab 鍵使用自動補齊功能時,為了維持功能的正常運作,在 complete_line
函式中會再次讀取一個字元,作為後續動作的執行依據。因此當按下 tab 鍵時,網頁端的請求會被 block 住,要等到 stdin 再次按下除了 tab 以外的鍵才可以。
以下當變數不為陣列時,使用
count[k]
表示第 k 個 bit;count[k-1:0]
表示第 k-1 到第 0 個 bits。
從以下註解可以得知 list_sort
為 2:1 balance merge sort。即意,在等待合併的鏈結串列中,有兩個長度為 的鏈結串列時,且在這鏈結串列之後的資料個數總和如果等於 時就將這兩個鏈結串列進行合併成長度為 的鏈結串列。
count
為 list_sort
中用來記錄目前已經放入等待合併的資料個數。根據以下註解可以得知,當 count
數量增加,導致 count[k]==1
且 count[k-1:0]==0
時,代表有兩個長度為 的鏈結串列可以被合併成 。除了 count[k]
第一次等於 1 時例外 (即 count
為 2 的冪時)。
註解中提到,需要透過 count[k-1]
與 count[n:k+1]
的資訊得知目前有幾個長度為 的鏈結串列在等待合併。因此可以分為六種不同的情況,以下 x
代表任意的數值;y
代表不為 0 的數值,且如果 k==0
時 -1 bit 當作是 1。其中第五種情況 (y01x
) 最為重要,因為代表有兩個長度為 的鏈結串列在等待合併,當發生上面提到的 count[k]==1
且 count[k-1:0]==0
時就將兩個 鏈結串列合併。考慮 count
為 (10101)2 時,等待合併的鏈結串列為 8+8+2+2+1,此時同時在 k=3 與 k=1 的位置皆出現第五種 y01x
的情況,但再將 count+1
後 count
為 (10110)2 這時只有 k=1
的位置發生了 count[1]==1
且 count[0:0]==0
的情況,因此將 合併,等待合併的鏈結串列變成 8+8+(4)+1+1。
由上面的例子中可以知道,當有同時有兩個不同的數字 與 使得正在等待合併的鏈結串列中分別有兩個 與 時,會從小的數字開始合併。可以看到,list_sort 中透過 for 迴圈將最低位的 0 找出來後,就去判斷 bits 是否為 0,如果不為 0 就代表符合 y01x
的情況,因為 count
會在 while 迴圈最後時加一,使得 count[k]==1
且 count[k-1:0]==0
,因此這邊可以先進行合併。
當所有的節點都已經從 list 中拿出,代表所有的節點都已經在 pending 中等待合併。因此這時候需要將在 pending 上的多個鏈結串列合併成一個鏈結串列。這時會從長度最小的開始合併,同時因為一開始的限制 (當 合併成 時,合併與未合併的鏈結串列長度總和必須要有 ),使得在最差的情況下只會是兩個長度為 2:1 的鏈結串列做合併。
在 Queue-Mergesort 中利用 Huffman encoding 的方式證明了在最差的情況下,Queue-Mergesort 是 Optimal 的,且 Top-down mergesort 也是。接著在 The Cost Distribution of Queue-Mergesort, Optimal Mergesorts, and Power-of-2 Rules 中提到不同的 mergesort 的比較次數可以寫成 的遞迴關係式,其中 代表合併時所需的比較次數。根據不同的實作可以將 分成三種
在論文中提到這個 是一個特別的數字,可以使最差的情況下要合併的兩邊資料長度為 2:1,如果使用其他的數字就可能導致 在分割時兩邊的差距過大。
Note that is the unique power of two lying between and and that the choice of rationals other than will not be more balanced. For example, if or then the sizes of two subproblems are for while the balanced power-of-two gives
因為 Queue-mergesort 可以表示成 Huffman Tree,因此在 On the Optimality of Huffman Trees 的內容可以套用至 Queue-mergesort,論文中說明 Huffman Tree 的遞迴關係中的 k 是 而不是其他數字或 。
Since the function is concave nondecreasing, the "power of 2" rule applies, and the optimal is the power of 2 satisfying
最後透過 Geogebra 觀察函式在不同 n 時的數值,下圖中 代表 , 與 分別代表 與 。可以看到確實 會在 與 之間。這讓 mergesort 在合併兩個鏈結串列時可以維持在最差長度為 2:1 的情況。
不過論文中很多部分還不能理解,例如為什麼 Buttom-up mergesort 的數值是 。還有許多推倒的過程不太能理解,只能記住結論而已。
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」