# 2025 年「[資訊科技產業專案設計](https://hackmd.io/@sysprog/info2025)」課程第 1 次作業 > 貢獻者: 法拉第 Faraday video 👨‍💼:interviewer 👩‍💻:interviewee ::: spoiler 作業要求 以 HackMD 紀錄上述模擬面試過程的討論、程式碼、改進方案,和初步檢討自己的表現 * Repeat: 重複提問,確認自己理解原本的要求 * Examples: 針對題目條件,舉出符合的案例,不限於特例 * Approach: 說明自己打算採用的方案 * Code: 撰寫程式 * Test: 若不能實際測試,那說明驗證程式的方案 * Optimization ::: ## 模擬面試過程 ### [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/description/?envType=problem-list-v2&envId=linked-list) 👨‍💼 請你用 C 語言實作一個函式,把兩個已排序的單向鏈結串列合併成一個新的排序串列。 👩‍💻 好的。那我重複一下,題目的需求是要將兩個已排序排序的鏈結串列,合併成一個新的已排序鏈結串列,這樣對吧? ==R== 👨‍💼 沒錯。 👩‍💻 沒問題。那我想詢問一下,是否有限制排序方式要用升序還是降序? 👨‍💼 沒有限制,可以自行決定。 👩‍💻 好。那我就假設輸入的兩個 list 都是升序排列,假設是 `[1,2,4]` 跟 `[1,3,4]` 好了,那輸出的 list 就會是 `[1,1,2,3,4,4]` 這樣。==E== 👩‍💻 這題我打算先比較兩個串列目前節點的值,然後把比較小的節點接到結果串列的後面,一直重複直到其中一個串列結束,再把剩下的串列直接接上去。==A== 👨‍💼 很好。那麼你可以開始寫出你的程式碼。 ==C== ```c struct ListNode { int val; struct ListNode *next; }; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { struct ListNode *head = NULL; struct ListNode **ptr = &head; while (list1 != NULL && list2 != NULL) { if (list1->val < list2->val) { *ptr = list1; list1 = list1->next; } else { *ptr = list2; list2 = list2->next; } ptr = &(*ptr)->next; } *ptr = (list1 != NULL) ? list1 : list2; return head; } ``` 👩‍💻 接下來我舉一個例子來驗證程式碼。假設兩個串列 `1 → 3 → NULL` 及 `2 → 4 → NULL`,回傳值為 `head → 1 → 2 → 3 → 4 → NULL` ,是正確的結果。==T== 👨‍💼 很好。那時間與空間複雜度呢? 👩‍💻 假設兩個串列長度分別是 `m` 跟 `n` ,那時間複雜度是 `O(m+n)`,因為每個節點都會走過一次。空間複雜度是 `O(1)`,因為沒有額外配置新節點,只用了一些指標。 👨‍💼 不錯。你可以用遞迴的方式寫嗎? ==O== ```c struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) { if (l1 == NULL) return l2; if (l2 == NULL) return l1; if (l1->val < l2->val) { l1->next = mergeTwoLists(l1->next, l2); return l1; } else { l2->next = mergeTwoLists(l1, l2->next); return l2; } } ``` 👨‍💼 這個版本有甚麼風險嗎? 👩‍💻 但如果串列非常長,遞迴版本會一直呼叫自己的函式,就會有 stack overflow 的風險。這時候迭代版本會比較安全。 👨‍💼 很好,今天的面試就到這裡。 ### [83. Remove Duplicates from Sorted List](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/?envType=problem-list-v2&envId=linked-list) 👨‍💼 You are given a sorted linked list. Please write a function to delete all duplicates such that each element appears only once. 👩‍💻 So, I’m given a sorted linked list, and I need to remove the duplicates so that each value appears only once. Then I return the updated list. Is that correct? ==R== 👨‍💼 Yes, that’s correct. 👩‍💻 Okay. Let me give a quick example. Suppose the list is `1 → 1 → 2 → 3 → 3`, After removing duplicates, it should become `1 → 2 → 3`. ==E== 👨‍💼 Yes, that’s correct. 👩‍💻 Okay. Since the list is sorted, all duplicates will be next to each other. My idea is to use a pointer-to-pointer that initially points to the head of the list. I’ll keep moving through the list, and for each node I just check if the next node has the same value. If it does, I simply skip the duplicate by reconnecting the pointer. If not, I move forward to the next node. By doing this until the end of the list, I make sure that every value only appears once, and then I return the head of the updated list. ==A== 👨‍💼 Good. You can start implementing it. ==C== ```c struct ListNode { int val; struct ListNode *next; }; struct ListNode* deleteDuplicates(struct ListNode* head) { struct ListNode **ptr = &head; while (*ptr != NULL && (*ptr)->next != NULL) { if ((*ptr)->next->val == (*ptr)->val) { (*ptr)->next = (*ptr)->next->next; } else { ptr = &(*ptr)->next; } } return head; } ``` 👨‍💼 Nice. Why do you use a pointer-to-pointer here instead of just a normal pointer? 👩‍💻 Because using a double pointer makes it easy to handle the head node uniformly. For example, if duplicates occur at the beginning, I don’t need a separate condition for the head. I can always update the pointer that leads to the current node, whether it’s head or some next field. 👨‍💼 Good point. Can you explain with an example? 👩‍💻 Sure. Suppose the list is `1 → 1 → 2 → 3`, after processing, the result is `1 → 2 → 3 → NULL`. And that’s the correct answer. ==T== 👨‍💼 Great. What’s the time and space complexity? 👩‍💻 Time complexity: `O(n)`, because we visit each node at most once. Space complexity: `O(1)`, since we only use a few pointers, no extra memory. 👨‍💼 What if the list is not sorted? Will your solution still work? 👩‍💻 No, this solution only works for sorted lists, because it assumes duplicates are always next to each other. If the list is unsorted, we’d need a different approach, such as using a hash set to track values we’ve seen. ==O== 👨‍💼 Great answer. That’s all for this question. ### [142. Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/description/?envType=problem-list-v2&envId=linked-list) 👨‍💼 請找出鏈結串列中的循環,如果有循環就回傳循環的起點,沒有就回傳 NULL。 👩‍💻 好的,。題目的需求是:輸入一個單向鏈結串列。那如果有循環,就回傳循環的起點節點;如果沒有,就回傳 `NULL`。請問我的理解是正確的嗎? ==R== 👨‍💼 沒錯,你的理解沒問題。 👩‍💻 假設串列是:3 → 2 → 0 → -4,-4 指回節點 2,那就要回傳2。 假設串列是:1 → 2 → 3,就要回傳NULL。 ==E== 👩‍💻 我打算用快慢指標法來解。思路是用兩個指標:`slow` 一次走一步,`fast` 一次走兩步。如果有循環,`fast` 和 `slow` 一定會在某個點相遇;那要如何找到相遇的點,透過一個推導得知,從 `slow` 最後的所在節點,以及 `head` 節點,往前各走相同的步數,相遇的地方就會是循環的起始節點。 ==A== 👨‍💼 好,那請你開始時做程式碼。==C== ```c struct ListNode { int val; struct ListNode *next; }; struct ListNode *detectCycle(struct ListNode *head) { struct ListNode *fast = head; struct ListNode *slow = head; while (fast != NULL && fast->next != NULL) { fast = fast->next->next; slow = slow->next; if (fast == slow) { while (head != slow) { head = head->next; slow = slow->next; } return head; } } return NULL; } ``` 👩‍💻 我接下來舉個例子來驗證我的程式碼。假設串列是 `3 → 2 → 0 → -4`,`-4` 指回節點 `2`,形成環。`slow` 每次走一步,`fast` 每次走兩步。最終兩個指標在 `-4` 相遇,然後從 `head` 和 `-4` 各走一步,再相遇時就是節點 `2`,這就是環的起點。==T== 👨‍💼 很好,你有考慮到 `head` 是 `NULL` 或只有一個節點的情況嗎? 👩‍💻 有的,如果 `head` 是 `NULL`,迴圈條件就不會進入,直接回傳 `NULL`。單節點也是同理。 👨‍💼 如果用 hash set 來解可以嗎?會不會比較優化? ==O== 👩‍💻 可以。做法是遍歷鏈結串列,每個節點存到 hash set,如果碰到已經存在的節點,就表示環的起點是該節點;遍歷完沒重複就回傳 NULL。但這個做法需要額外的空間來存遇到的節點的地址,所以空間複雜度會比較高,因此不會比較優化。 👨‍💼 很好,你講得很完整。那今天的面試就到這裡。 ## 初步檢討 1. 空白太多,要保持編寫程式邊講話。 2. 講解內容多為程式碼照念,儘量改為表達含意。 3. Hash set 版本未實作。