contributed by < eleanorLYJ
>
gawei1206
測試分數的部分有提到偶爾會有95分的情況,想請問你知道這個問題發生在哪個部分,有對這個問題做出相對應的改善嗎
謝謝建議,目前嘗試讀懂〈Dude, is my code constant time?〉與檢測常數時間的程式碼但還未參透 eleanorLYJ
fatcatorange
部份 commit message 可以更加詳述
謝謝建議,已新增敘述了 eleanorLYJ
BennyWang1007
fatcatorange
提到,提交訊息有很多可以改善4a2ff9f
。得分: 100/100 (偶爾 95/100)
q_new
對應到數學公式,function 應翻譯為「函數」,後者就像是台「機器」,它能夠將集合 A 裡面的每個元素「唯一地」對應到集合 B 裡的一個元素。但在 C 語言一類的通用程式語言中,function 未必具備數學函數的意涵,也就是缺乏「唯一對應」的特性,例如對於 time 來說,給定同樣的輸入,卻會在不同時間點得到不同的結果,在 C 語言一類的通用程式語言中,甚至可省略傳回值,後者就不符合「函數」的行為。因此,當在通用的程式設計中,function 應翻譯為「函式」,表示「衍生自數學函數的常式 (routine)」。
資訊科技詞彙翻譯
函數 函式目的:
建立空的佇列,回傳指標
實作:
為了回傳指向有效結構的指標,考慮到使用 malloc 函式配置記憶體時,當配置失敗,函式會回傳 NULL
指標,為了避免出現初始化佇列發生 未定義行為 (Undefined Behavior),因此在此有多做判斷。
發現 list.h
中有 INIT_LIST_HEAD(struct list_head *head)
,該函式會將head
的prev
與 next
指向 head
完成初始化。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
commit message 中應可更詳細描述作法和想法
fatcatorange
新增敘述的 Commit 8e11a
q_free
將佇列所佔據的記憶體都釋放
list_for_each_safe
允許在迭代中刪除節點。
閱讀 Linux 核心原始程式碼巨集: container_of 理解container_of 的用法,在此函式中,用於尋找佇列中的各個 element_t
。
在我完成 q_insert_head()
與 q_insert_tail()
兩個函式後,使用make check
發現佔據的記憶體反而比在完成上述兩個函式時變多了,因此才發現我未在q_free()
中將 struct element_t
中的 char *value
釋放。
參考 2024 年 Linux 核心設計/實作課程作業 —— lab0 (A) 的圖片:
因此我將程式碼改成以下
然後,我發現 queue.h
中有 q_release_element()
,可以避免如以上撰寫時的粗心並且增加整體的可讀性。
未修改結尾空终止字元: commit c4b77a2
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
在佇列開頭插入給定的新節點
閱讀 strncpy 的使用方式
在完成 insert_head()
與 q_remove_head()
後,發現make test
的結果,並未通過主要測試 insert_head
與 remove_head
兩函式的 trace-01-ops
。因此我找 /scripts
中 trace-01-ops.cmd
,根據 trace-01-ops.cmd
裡的命令,逐一找出未通過測試的答案。
結果出現非預期的 "���"亂碼,這時我才發現複製字串的結尾缺少了結尾空终止字符串 字串 (null-terminated string)。在複製的字串最後加上'/0'後,便通過 trace-01-ops
的測試。
string 的翻譯是「字串」
列出對應的 GitHub commit 超連結即可,不用張貼完整程式碼。僅在需要討論時,才列出關鍵程式碼。
在佇列尾端插入給定的新節點
與 q_insert_head()
邏輯相似
在佇列開頭移去 給定的節點
實作過程發現忘記檢查 bufsize,之後又在補上。
避免非必要的英語和漢語交雜書寫,若你想用英語書寫開發紀錄,則整份都該以英語書寫,反之亦然。
我多次rebase 此commit 的基礎,但它無法正常工作。 Cppcheck不斷告訴我靜態分析失敗。
我只是加了{},然後它就通過了靜態分析。 太奇怪了。
你可能找到 Cppcheck 的錯誤,確認最新的 Cppcheck 是否存在同樣的問題。
查閱第一手材料,例如 C 語言規格,不要把自己專業的成長交給大語言模型。
q_size
計算目前佇列的節點總量
移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked List
使用快慢指標(Fast-slow Pointers)找到中間節點
為了省略指向中間節點的前一個節點的指標,因此我慢指針的型態為指標的指標。
為了讓快指標走完單向鏈結串列時,讓慢指標停在中間節點的前一個節點,因此慢指標從下一個指向head的dummy開始移動。
沒必要先用 C++ 程式語言撰寫,直接以 C 語言開發。
後來想到只要讓快指標提前移動,就不需要 dummy 節點了。
程式碼 version 2
善用 diff
,列出存在差異的程式碼。
回到倉儲(Repository),想起意識到自己 leetcode 僅是移除 (remove) 非刪除 (delete)。
又回到 leetcode 上,將程式碼改成釋放記憶體的版本:
程式碼 version 3
然而作業中不同於 leetcode 題目中是處理單向鏈結串列,而是實作環狀雙向鏈結串列,想想只需要快慢指標的想法就可以完成這個函式。
commit 1e0fc45
針對環狀雙向鏈結串列,能否提出更快的程式碼?
todo: 嘗試將一指標從頭向後,另一指標從尾向前移動找中間節點,這樣是否會更快
q_delete_dup
用指標的指標cur
指向確定不重複的節點。
迭代過程中的 ptr
與 ptr2
同樣地,我在 leetcode Remove Duplicates from Sorted List II上使用 C 驗證以上想法
沒必要用 C++,強化 C 語言程式設計。
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
交換佇列中鄰近的節點
1. 記錄當前節點對的尾節點。
2. 反轉當前節點對的順序。
3. 如果有前一組交換節點對,則將其與當前組連接。
4. 更新兩個變數,指向代表前一對尾的指標 (prevPairEnd
) 和指向該對開始的指標 (currPairStart
) ,為迭代到下一對節點做準備。
注意用語。
我先在 leetcdoe 24. Swap Nodes in Pairs 上使用 C 驗證想法。
寫得很醜,有改進空間
- 因為以上用了兩個if判斷head,希望能夠減少if的使用
- 因為寫以上程式的時候,靠著leetcode測試,不斷修改,因此整個迭代邏輯是用拼湊的
接著想看其他人是用什麼邏輯寫完這題
具體說明哪邊要改進,而非籠統的「寫得很醜」。
沒必要看上面這些實作不佳的討論,你可以做更好!
新的嘗試,雖然不符合 leetcode 題意,但若作業只追求兩兩翻轉的效果就可以用以下程式碼
說好的空白字元呢?
指標 cur
指向每一對 pair 的第一個節點
將 pair 值用 XOR swap 作交換,每次迭代移動兩步,直到 cur 指向的pair內不足兩個節點,便停止迭代
使用作業規範的程式碼縮排風格。
作業內的實作也是 XOR Swap
Commit d72248
uintptr_t
將 char *
轉成整數型別the following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:
uintptr_t
(C99 7.18.1.4)
查閱辭典關於 assign 的解釋,考慮到漢語的書寫慣例,不該偷懶用「賦值」,本處應為「指派」之義。
An assignment operator stores a value in the object designated by the left operand. An assignment expression has the value of the left operand after the assignment, but is not an lvalue. (C99 6.5.16)
為什麼要用 xor 運算判斷 cur == head 以及 cur->next == head 呢?一方面可讀性偏低,優化也應交由編譯器來完成?
此外,通常在swap節點時應該是交換節點指向其他節點的指標(prev, next等)而不是直接更改 element。
BennyWang1007
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點;
避免非必要的項目縮排 (即 *
和 -
),以清晰、明確,且流暢的漢語書寫。
參考影片 Reverse a Doubly Circular Linked List中的作法:
將每個節點的 prev
與 next
交換,最終將 head
指向原鏈結串列的tail
使用作業規範的程式碼縮排風格
上述程式碼中,要讓 head 指向原鏈結串列 tail 的地方,靜態檢查失敗。
回想起在你所不知道的C語言:指標篇看到的概念:
C 語言中,萬物皆是數值 (everything is a value),函式呼叫當然只有 call-by-value。「指標的指標」(英文就是 a pointer of a pointer) 是個常見用來改變「傳入變數原始數值」的技巧。
使用指標的指標更改傳入函式的數值。
指定每 k 個節點為一組,進行反向順序排列
詳見 LeetCode 25. Reverse Nodes in k-Group
想法: 迭代過程,將每k個節點截斷成獨立的list,再呼叫q_reverse(),再將這k個節點接回原始lisr
實作參考yanjiew1
q_sort
呼叫 mergesort_list()
遞迴呼叫 mergesort_list()
, 使用快慢指標找到中間節點, 接著拆成兩個環狀串列, 直到把串列拆至只剩一個節點,接著呼叫 mergeTwoLists()
此函式參數接受將兩個已排序的環狀雙向串列合併為一個環狀串列。
迭代將使用strcmp比較後確定的節點加到額外的空串列後, 直到把某一串列為空。
當一個串列用盡,使用 list_splice_tail_init 和 list_splice 追加另一個串列的剩餘節點。
結合作業2的 timsort 測試,小幅修改commit fa03314寫的遞迴版本 mergesort ,一併做測試
結果在commit 3fe74
我測試資料設計成6種類別,分別為(0)全部隨機資料、(1)遞增資料、(2)遞減資料、(3)遞增資料並隨機交換3次、(4)遞增資料隨機交換7次、(5)每64個數字作為一個 run 組成的資料、(6)每64個數字作為一個 run 組成的資料並在同一個 run 以20%機率隨機交換 (7)全部資料以同一個隨機值組成
而此 commit fa03314能通過所有隨機資料的測試, 但在遞增資料時,mergeTwoLists 函式會進入無窮迴圈中。
TODO: 暫時我看不出來哪裡有問題,稍後用GDB看變數的變化
小幅修改 andy155224 的 mergeTwoLists 函式,仍然沒有通過全部測試
另外發現了幾個原本實作的缺陷
mergeTwoList
跟 visitorckw
實作 timsort
的 merge
函式有相同目的, 我決定重複使用後者函式, 但要注意的 merge
函式考慮的是兩鏈結串列非環狀的並且沒有 dummy node每一種資料測試的大小為 32788 =
以下每一種資料測試的大小仍為 32788 =
在 LeetCode 2487. Remove Nodes From Linked List 寫
ptr
為指向符合題意的節點, biggest
為每次迭代範圍中能找到的最大節點head
開始迭代, 判斷最大節點 biggest
, ptr
指向biggest
, 下一次迭代的起點就是從biggest
的下一節點開始, 直到把此鏈結串列的最後一節點加到 ptr
上若鏈結串列上的值是遞減,最壞時間複雜度就會達
移除 C++ 程式碼,除非你要彰顯 C++ 特有之語言特徵。
程式語言的標注是 cpp
,而非 c++
,後者不為 HackMD 所識別。
減少對 C++ 的參照,本作業要強化學員對 C 語言的掌握,過程中應適度查閱 C 語言規格書。
因為作業實作環狀鏈結串列,換個方向迭代就可以改善天真版的效率問題
反向迭代紀錄最小節點,串列尾端因為不需再跟下一節點故是當前最小,接著向前迭代,若當前最小節點比該比較節點大就刪除,反之更新當前最小節點
Commit 0a53875
想法相似於 q_descend
合併所有已排序的佇列,並合而為一個新的已排序佇列
sortTwolists()
去實現頭尾相接的合併作法,但未得到預期結果queue.c
裡的 q_merge()
處,希望執行下一行(step
或 next
),卻會跳去 harness.c
中exception_setup()
中的 sigsetjmp
。回頭看 qtest.c
呼叫 q_merge
的地方
題目的要求: 不能把 queue_contex_t 的節點移除掉 0.0
參考 SPFishcool
寫法
想法簡單,將所有的串列相接再排序
Commit fa03314
考慮因素是什麼?量化表現如何?
拾人牙慧不難,但精準評判很難。
此 commit 的修改較多, commit message 描述應更詳細。
fatcatorange
新增敘述的 Commit 8f750
$ make valgrind
沒有看到記憶體相關問題。
閱讀 Massif: a heap profiler 與參考cymizer/
原始程式運行時配置的記憶體,約在執行 340000 ms 時 memory heap size 急速地降低,最終記憶體會歸 0。
TODO: 用其他方式表達
文字訊息不要用圖片展現!
然而我將 harness.c
中的 test_free()
的 free() 註解掉。最終記憶體並未歸0。
如果要 GDB 要執行的該行是函式呼叫 (function call)
執行 next,下一行會停在函式完成之後。
執行 step,下一行會停在被呼叫的函式的第一行
因此
閱讀 lab0 的 qtest 命令直譯器的實作,期望解我在使用 GDB 執行我失敗版本的q_merge
時的疑惑。
從上述文章能得知,qtest執行順序
main → run_console → cmd_select → interpret_cmd → parse_args
因此我去找倉儲(Repository)中對應的程式碼,期望能找到與 harness.c
的exception_setup()
相關的線索。
在 console.c
中的 interpret_cmd
函式負責解析參數,之後將參數傳給 interpret_cmda()
中
interpret_cmda
函式目的是要在 cmd_list 找對應指令,接著執行指令。
next_cmd
的結構體為 cmd_element_t
,而在 cmd_element_t
結構體裡有 operation 成員,這成員為指向函式的指標。
執行的指令就會對應 qtest.c 中的 do_*
接下來我看 qtest.c
的 do_merge
函式,因為我對 q_merge
函式有興趣。找到對 do_merge
中其中一段程式碼:
在執行 q_merge 前會呼叫 exception_setup()
函式。
而我中斷點設q_merge
函式內,執行run
與 next
命令後,卻也會停在exception_sepup()
上。
接著查 sigsetjmp 是甚麼?
int sigsetjmp(sigjmp_buf env, int savesigs);
sigsetjmp的功能: 負責處理錯誤與中斷。保存env
中堆疊(stack)的內容/環境(context/environment)。而若 savesigs 非零, 會把行程(process)的 signal mask 也存進 env
中
疑問signal mask是甚麼?
The collection of signals that are currently blocked is called the signal mask. Each process has its own signal mask.
sigsetjmp 的回傳值,只分 0 與非 0。回傳 0代表是直接使用,而回傳非 0 代表是 siglongjmp 跳躍到此處。
因此,我使用 GDB 觀察到執行 exception_setup
的部分,假設是因為執行到 q_merge.c
if 判斷式內中使用的 exception_sepup
函式,那我預期在 GDB 中執行 next 後就會停在jmp_ready = true;
行。並非如此,反而是停在jmp_ready = false;
因此這說明是因為 siglongjmp 而執行到了 if (sigsetjmp(env, 1))
搜尋整個 harness.c
找 siglongjmp,只有 trigger_exception
函式有用到 siglongjmp。從函式註解得知,該函式是要返回最近的異常設置 (exception setup)。
因此是異常(exception) 造成我 GDB 觀察的結果。然而這部分是什麼異常造成,我目前不知道。 這部分我應該再學其他GDB使用方式。
在 qtest.c
新增 shuffle 命令與 do_shuffle 函式後,在 queue.h 新增 q_shuffle prototype
開始在 queue.c 撰寫 q_shuffle 函式
q_shuffle實作Fisher–Yates shuffle 演算法
- 先用 q_size 取得 queue 的大小 len。
- 隨機從 0 ~ (len - 1) 中抽出一個數字 random,然後 old 將指向從前面數來第 random 個節點,new 會指向最後一個未被抽到的節點,將 old 和 new 指向的節點的值交換,再將 len - 1。
- 隨著 len 大小變小,已經被抽到過,並交換值到 queue 後面的會愈來愈多,直到所有的節點都已經被抽到過,shuffle 就結束。
然後當我想要寫 commit message 時,被 pre-commit.hook 擋下來了
所以最後我拿掉在 queue.h 的改動。
拿掉後是否需要額外的動作來完成此函式?若沒有為何一開始要改動 queue.h?
fatcatorange
最終完成的commit 2a66a61
列出程式碼的主要目的是討論和檢討,若你沒有這舉措,就不該列出程式碼。
在 ./qtest
測試命令效果
虛無假說為
(虛無假說): shuffle 的結果發生的機率相同,遵守 Uniform distribution
使用 lab0(D) 提供的測試程式,計算 chi-squared test statistic
以下 shuffle 的結果明顯只4種組合,在 q_shuffle 函式印出亂數值,在./qtest 手動測試 q_shuffle 函式值,就明顯感覺亂數重複。
更改程式碼
計算 chi-squared test statistic 就降低至18.8
自由變換的變數只有 24 個,我自由度選擇 23。
因為 為 18.8,P value介於 0.10 至 0.9
因為 P value(0.1~0.9)> alpha(0.05),統計檢定的結果不拒絕虛無假說( )
圖片來源: 卡方分布表
rand 函式用途於計算出 0 到 RAND_MAX 的偽隨機整數。
The rand function computes a sequence of pseudo-random integers in the range 0 to RAND_MAX.
(C99 7.20.2.1)
而 RAND_MAX 在規格書上寫至少有32767。
The value of the RAND_MAX macro shall be at least 32767.
srand 函式的參數會作為 rand 函式計算時的亂數種子。
The srand function uses the argument as a seed for a new sequence of pseudo-random numbers to be returned by subsequent calls to rand. (C99 7.20.2.2)
time 函式用途於得知當前時間。
The time function returns the implementation’s best approximation to the current calendar time
然而在 linux 上 才有定義 time 函式 的單位是秒數。
time() returns the time as the number of seconds since the Epoch,
1970-01-01 00:00:00 +0000 (UTC).
因此原本以為在迴圈中重新指派當前時間當亂數種子就能每次獲得亂數,然而這樣看來,迴圈跑得速度太快,所以在同一秒中內的所有亂數都會相同啊。
將 Linux 核心的 linux/list.h 標頭檔與 hlist 相關的程式碼抽出,成為 hlist.h 並修改 qtest 程式,使其新增 ttt 命令
Commit 0667a9f
新增 option 中 mode 的這個變數。若 mode 為 0,代表允許使用者切換「人 vs.電腦」, mode 為 1 指 「電腦 vs. 電腦」的對弈,最後 mode 2 為引入 coroutine ,並且處理鍵盤事件,當按下 'p' 能夠暫停或回復棋盤畫面,按下'q' 能夠中止當局電腦與電腦的對弈。
TODO:
看懂此專案,最核心的大概就是每次 setjmp 或 longjmp,該會到程式碼的哪裡。
首先會在 main()
先初始化好變數與確定任務( task )的執行順序,之後就進入 schedule()
。
一進入 schedule()
,就會用 setjmp 設跳轉點。 tasks 是存函式指標的陣列,在此函數的第 9 行,就會運行指定函式。假設這裡的 tasks 為 {task0, task1},ntasks 為 2 ,那麼首先執行 task0
函式。
INIT_LIST_HEAD(&task->list);
是要創建 task0
的節點。執行到函式的第 11 行的 setjmp()
,設置一個跳轉點,並且由於是直接呼叫 setjmp
,所以進入 if 判斷式中,將此任務的插入到存有全部任務的串列(tasklist
),之後做 longjmp
,跳回 schedule()
第 5 行,並接續執行 task1
函式。而 task0
與 task1
的結構相似,同樣會在會進入 if 判斷式之後,又再次回到 schedule()
的第 5 行的 setjmp
。
將 tasks 裡的任務都執行完了,就不會再進入 while 迴圈,此時做 task_switch()
,從 tasklist
取得目前第一個任務,並且執行。目前這任務就是 task0,longjmp
會跳至 task0 的第 11 行,但不會進入 if 判斷式中,之後,執行到 task0
的 18 行 又存下當下的 env,並將該任務的節點又加入 tasklist 中,之後又換 task1
的 if 判斷式處,之後處理 task1 的任務內容 …。
結果就會是 task0 ,從數字從 i 開始,每次加 2 直到達於n為止,每次將數字算斐波那契數列後,控制權就會轉給task1。因此 task0 就會與 task1 交錯執行。
todo: 解釋
ualarm - schedule signal after given number of microseconds
The ualarm() function causes the signal SIGALRM to be sent to the invoking process after (not less than) usecs microseconds. The delay may be lengthened slightly by any system activity or by the time spent processing the call or by the granularity of system timers.
Unless caught or ignored, the SIGALRM signal will terminate the process.
-> usecs:第一次触发SIGALRM信号的时间 interval:第一次触发SIGALRM信号之后每隔 interval 微秒再觸發一次SIGALRM信号 以微秒为单位
todo: 加解釋