# 2021 年「資訊科技產業專案設計」課程第 1 次作業 contributed by < `老藍 - Old Blue` > >[英文影片](https://youtu.be/dESR_jP4Vpk) >[中文影片](https://youtu.be/TxlgiarQ04k) > [作業要求](https://hackmd.io/@sysprog/info2021-homework1) **R**epeat: 重複提問,確認自己理解原本的要求 **E**xamples: 針對題目條件,舉出符合的案例,不限於特例 **A**pproach: 說明自己打算採用的方案 **C**ode: 撰寫程式 **T**est: 若不能實際測試,說明驗證程式的方案 **O**ptimization: 優化你的演算法 * LeetCode 題號 * Easy * [1. Two Sum](https://leetcode.com/problems/two-sum/) * [27. Remove Element](https://leetcode.com/problems/remove-element/) * [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) * [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) * Medium * [116. Populating Next Right Pointers in Each Node](https://leetcode.com/problems/populating-next-right-pointers-in-each-node/) * [138. Copy List with Random Pointer](https://leetcode.com/problems/copy-list-with-random-pointer/) --- # 改進目標 ## interviewer * 站在 interviewer 的角度思考,進而可以學習題目的多變性,不會只站在背誦者的角度。 * 中文影片雙方幾乎都是背誦者,並沒有辦法了解人。 * 注重在 "inter" ,讓自己學習面試不是背誦,而是表現自己。 --- # 英文題目 ## [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) ```cpp class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* pre = new ListNode(0); ListNode* next = head -> next; while(head){ head -> next = pre; pre = head; head = next; next = head -> next; } return pre; } }; ``` 1. 這樣做會出現 UndefinedBehavior 因為在最後一次 next 先做了 head → next,此時 head 已經指向 NULL ,那 head → next 會是什麼?顯然很奇怪。 2. 所以我們可以改進一下,仔細看 `next = head -> next;` 我們其實多宣告了一次,第一次初始化,第二次是指標操作。但其實每次 while 操作都會需要 `next = head -> next;` ,直到 head 已經指向 NULL。 3. 然後可以發現 `next = head -> next;` 其實不一定要先指向 `head -> next;` 可以先確定 head 在哪裡再操作 next。 所以應該變成這樣 ```cpp class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* pre = NULL; while(head){ ListNode* next = head -> next; head -> next = pre; pre = head; head = next; } return pre; } }; ``` Recursion ```cpp class Solution { public: ListNode* reverseList(ListNode* head) { if (!head || !(head -> next)) { return head; } ListNode* node = reverseList(head -> next); head -> next -> next = head; head -> next = NULL; return node; } }; ``` 延伸閱讀: * [你所不知道的C語言:遞迴呼叫篇 - HackMD](https://hackmd.io/@sysprog/c-recursion#%E9%81%9E%E8%BF%B4%E7%A8%8B%E5%BC%8F%E6%B2%92%E6%9C%89%E4%BD%A0%E6%83%B3%E5%83%8F%E4%B8%AD%E7%9A%84%E6%85%A2) ## [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) 1. 這樣做會出現 UndefinedBehavior 因為在最後一次 next 先做了 head → next,此時 head 已經指向 NULL ,那 head → next 會是什麼?顯然很奇怪。 2. 所以我們可以改進一下,仔細看 `next = head -> next;` 我們其實多宣告了一次,第一次初始化,第二次是指標操作。但其實每次 while 操作都會需要 `next = head -> next;` ,直到 head 已經指向 NULL。 3. 然後可以發現 `next = head -> next;` 其實不一定要先指向 `head -> next;` 可以先確定 head 在哪裡再操作 next。 所以應該變成這樣 ```cpp class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* pre = NULL; while(head){ ListNode* next = head -> next; head -> next = pre; pre = head; head = next; } return pre; } }; ``` Recursion ```cpp= class Solution { public: ListNode* reverseList(ListNode* head) { if (!head || !(head -> next)) { return head; } ListNode* node = reverseList(head -> next); head -> next -> next = head; head -> next = NULL; return node; } }; ``` 利用傳入的 parameter `l1`, `l2` 互相比較大小。 多宣告一個 `ListNode dummy;` ,**這個 dummy 之後會被拋棄,因為他是宣告出來指向第一個 node 的節點**,之後回傳的會是 `[dummy.next]` 。 再來多宣告一個 `ListNode *tail = &dummy;` ,我們會利用它來修改 each node 應該指向的下一個 node。 相比結束後,第一步會是將 tail 指向 loser ,如果 l1 == l2 那指向誰都沒差,所以我們選擇保留原位,不修改指向的人。之後記得將 loser 只移動到下一個。 舉例: l1 = 1 → 2 → 4 → NULL l2 = 2 → 2 → 3 →8 → NULL ```cpp 1 round: l1=1 , l2=2 l1 < l2 dummy → 1(l1), l1 ++, l1 = 2 2 round: l1=2 , l2=2 l1 == l2 dummy → 1(l1) → 2(l2), l2 ++, l2 = 2 3 round: l1=2 , l2=2 l1 == l2 dummy → 1(l1) → 2(l2) → 2(l2) , l2 ++ ,l2 = 3 4 round: l1=2 , l2=3 l1 < l2 dummy → 1(l1) → 2(l2) → 2(l2) → 2(l1) , l1 ++ ,l1 = 4 5 round: l1=4 , l2=3 l1 > l2 dummy → 1(l1) → 2(l2) → 2(l2) → 2(l1) → 3(l2), l2 ++ ,l2 = 8 6 round: l1=4 , l2=8 l1 < l2 dummy → 1(l1) → 2(l2) → 2(l2) → 2(l1) → 3(l2) → 4(l1), l1 ++ ,l1 = NULL 7 round: break while ,因為 NULL && 8 = NULL 因為 8 還沒被 tail 指到,所以要判斷誰先到 NULL,要把剩下沒指到的 node,指回去。 tail->next = (l1 != NULL) ? l1 : l2; l1 == NULL 所以 tail->next = l2; ``` ```cpp class Solution { public: ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) { ListNode dummy(0); ListNode *tail = &dummy; while (l1 && l2) { if (l1->val < l2->val) { tail->next = l1; l1 = l1->next; } else { tail->next = l2; l2 = l2->next; } tail = tail->next; } tail->next = (l1 !=NULL) ? l1 : l2; return dummy.next; } }; ``` 可以寫得更簡潔 ```cpp ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { ListNode l3; ListNode* cur = &l3; for (; l1 && l2; cur = cur->next) (l1->val < l2->val) ? (cur->next = l1, l1 = l1->next) : (cur->next = l2, l2 = l2->next); cur->next = (l1) ? l1 : l2; return l3.next; } ``` ## [138. Copy List with Random Pointer](https://leetcode.com/problems/copy-list-with-random-pointer/) 這題因為是要 [deep copy](https://en.wikipedia.org/wiki/Object_copying#Deep_copy),意思是需要複製一個一模一樣的資料,是有實際佔用記憶體的複製,稱為 copy by value。 而另一種我們只創建一個 pointer 並且只有指向預複製的對象,稱之為 [shallow copy](https://en.wikipedia.org/wiki/Object_copying) ,又稱為 copy by address。 ```cpp= int *first, *second; first = new int[10]; second = first; //shallow copy second = new int[10]; for (int idx=0;idx<10;idx++) //deep copy second[idx] = first[idx]; ``` 如果要 deep copy linklist,會面臨一些問題: 1. **next link & random link 的目標尚未創建** * 因為 next node or random node 尚未創建出來,所以只能先配置 NULL 。 * 等全部創建完再 assign link 。 2. **The new node loses its address** * 創建的新 node 如果沒有任何 pointer 認養,大家都會變孤兒。宣告一個 pointer array 好像不是很好的解法,畢竟這會造成 **assign random link O(n^2)**。 * 因為是 linklist 所以在配置的時候需要一個一個 node 去掃才能到達目標。所以如果每次 random link 最慘都在 linklist 的最尾巴那就需要 O(n),然後有 n 個 node ,造就 Time complexity O(n^2)。 而 HashMap 剛好適合處理這個問題。 1. 建立一個 HaspMap ,裡面存放 < Old node , New node > 的記憶體位置。 2. 在 assign link 可以透過先訪問 Key (Old node) 再訪問 Vaule (New node)。 ![](https://i.imgur.com/ZCTsLW6.jpg) 依照上圖 node1 的 random link (紅線)是 node3,可以先找 HashMap 中的 Old node3 再去訪問 New node3。 ```cpp= /* // Definition for a Node. class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */ class Solution { public: Node* copyRandomList(Node* head) { Node *cur = head; unordered_map<Node*,Node*> Hashmap; while(cur){ Node *copy = new Node(cur -> val); // 先建立 new node Hashmap[cur] = copy;// 放入 HashMap cur = cur->next; } cur = head; while(cur) { // assign link Hashmap[cur]->next = Hashmap[cur->next]; Hashmap[cur]->random = Hashmap[cur->random]; cur=cur->next; } return Hashmap[head]; } }; ``` --- # 中文題目 ## [1. Two Sum](https://leetcode.com/problems/two-sum/) ### C++ **R:** 要在一組必有解的陣列中回傳兩個相異數字的 index,這兩個相異數字可以組成 target。 **E:** 所以假設我給一個 {3,3,4} 的陣列,並且我要找 6,喔應該是要回傳 {0,1},不可以是 {0,0},{1,1}。 $O(n^2)$ **A:** 很簡單利用兩個 for ,第一個 for 從 array 中指定 1 個 element ,再用第二個 for scan array 其他的 element ,如果第二個 for scan 的其中一個 element 跟第一個 for 指定的 element 加出來為 target ,就可以回傳兩個 for 現在指向 array 的位置 。 $O(n)$ **O:** 這邊引用兩個想法 1. target = x + y - target - x = y - 減去 x 後,我們只需要再搜尋 y 是否存在。 2. Hash table - 為什麼要用 Hash table ,這是因為 Hash table "平均搜尋"時間通常只需要 O(1),worst case 為O(n) (因為最慘可能要找 n 次。n 為陣列長度,因為可能 hash function 很慘,將所有數字都 hash 到同一個 bucket 裡),O(1)∗n=O(n)。 ```cpp class Solution { public: vector<int> twoSum(vector<int> &nums, int target) { unordered_map<int, int> m; for (int i = 0; i < nums.size(); i++) { if (m.count(target - nums[i])) { return {m[target - nums[i]], i}; } else { m[nums[i]] = i; } } return {}; } }; ``` 我們利用 C++ 來解決這個問題。 C++ STL 中的 [unordered_map](https://en.cppreference.com/w/cpp/container/unordered_map) 就是用 hash table 所建立。 儲存的方式會是{key ,value} pair (Key = 陣列中的數值, Value = 陣列中的位置) ***{key = nums[i] , value = i}*** 1. 先宣告一個 `unordered_map<int, int> m` 2. 利用 for loop 來 sacn 整個 array(vector 是加強版的 array) 3. 再利用 if 判斷 target - x = y ,這個 y 是否在這個 Hash table 裡面 * .count 這個 member function 可以搜尋指定的 key 值的個數 * 所以按照剛剛的說法,m.count(裡面是要放 target - nums[i] (x) ) * target - nums[i](x) = y 4. 如果有找到這個 key ,就回傳 vaule 跟當前 i 指向的位置 5. 如果找不掉就將這個數值跟它的位置放入 Hash table **以 vector = {3,3,4}, target = 6 為例** 1. i = 0, target - num[0] = 3 2. Hash table 找不到 3 (因為剛建立) * m.count 回傳 0 3. 將 {Key = 3,value = 0} 丟入 hash table 4. i = 1, target - num[0] = 3 5. Hash table 找到 3 (因為剛剛丟入 {3,0}) 6. return {0,1} --- ### C 08/20/2021 22:07 Accepted 12 ms 8.8 MB c 沒找到 target 就插入 hash ,所以{ 2, 11, 15} 找 26 時會先插入 11 之後再直接用 15 。 $O(log_n)$ 可以再更快,譬如鏈結的方式可以用 [Skip List](https://en.wikipedia.org/wiki/Skip_list),或許時間複雜度可以降到 O(logn),但是會增加空間複雜度。 ```c #include <stdio.h> #include <stdlib.h> #include <math.h> typedef struct node{ long value; int index; struct node *next; }newnode; //每次都插頭 void insert(newnode **hashtable,long value ,int index, int n){ int t = abs(value) % n; newnode *temp = hashtable[t]; newnode *add = (newnode*)malloc(sizeof(newnode)); add->value = value; add->index = index; add->next = temp->next; temp->next = add; } int search(newnode **hashtable, long target,int n){ int hashindex = abs(target) % n; newnode *temp = hashtable[hashindex]->next; while(temp){ if(temp->value == target) return temp->index; temp = temp->next; } return -1; } void freeHashTable(newnode **hashtable, int n){ int i = 0; newnode *temp = NULL; newnode *delete = NULL; for(i = 0 ; i < n ; i++){ temp = hashtable[i]; delete = temp; while(temp){ delete = temp; temp = temp->next; free(delete); } } free(hashtable); } int* twoSum(int nums[], int numsSize, int target) { int i = 0, j = 0; int n = numsSize, index = 0; int* result = NULL; newnode** hashTable = (newnode**)malloc(n * sizeof(newnode*)); // init head node for(i = 0; i < n; i++) hashTable[i] = (newnode*)calloc(1, sizeof(newnode)); for(i = 0; i < n; i++) { index = search(hashTable, target - nums[i], n); if(-1 == index) insert(hashTable, nums[i], i, n); else { result = (int*)malloc(sizeof(int) * 2); result[0] = index; result[1] = i;//因為減去 target 所以直接回傳 i freeHashTable(hashTable, n); return result; } } freeHashTable(hashTable, n); return result; } int main(int argc ,char** argv){ int *result = (int*)malloc(sizeof(int) * 2);; int numss[2] = {3,3}; result = twoSum(numss,2,6); printf("%d,%d \n",result[0],result[1]); free(result); return 0; } ``` --- For leetcode ```c **/** * Note: The returned array must be malloced, assume caller calls free(). */ #include <stdio.h> #include <stdlib.h> #include <math.h> typedef struct node{ long value; int index; struct node *next; }newnode; void insert(newnode **hashtable,long value ,int index, int n){ int t = abs(value) % n; newnode *temp = hashtable[t]; newnode *add = (newnode*)malloc(sizeof(newnode)); add->value = value; add->index = index; add->next = temp->next; temp->next = add; } int search(newnode **hashtable, long target,int n){ int hashindex = abs(target) % n; newnode *temp = hashtable[hashindex]->next; while(temp){ if(temp->value == target) return temp->index; temp = temp->next; } return -1; } void freeHashTable(newnode **hashtable, int n){ int i = 0; newnode *temp = NULL; newnode *delete = NULL; for(i = 0 ; i < n ; i++){ temp = hashtable[i]; delete = temp; while(temp){ delete = temp; temp = temp->next; free(delete); } } free(hashtable); } int* twoSum(int* nums, int numsSize, int target , int* returnSize ) { int i = 0, j = 0; int n = numsSize, index = 0; // *returnSize = 2; int *returnValues = malloc((*returnSize) * sizeof(int)); // newnode** hashTable = (newnode**)malloc(n * sizeof(newnode*)); // init head node for(i = 0; i < n; i++) hashTable[i] = (newnode*)calloc(1, sizeof(newnode)); for(i = 0; i < n; i++) { index = search(hashTable, target - nums[i], n); if(-1 == index) insert(hashTable, nums[i], i, n); else { returnValues[0] = index; returnValues[1] = i; freeHashTable(hashTable, n); return returnValues; } } freeHashTable(hashTable, n); return returnValues; }** ``` --- ## [27. Remove Element](https://leetcode.com/problems/remove-element/) ### C++ **R:** 要從一組陣列 `nums` 中移除 `val`,並且不可以出現空洞,所有被移除數值的位置需要有其他陣列的原本數字填上。 **E:** `nums` = {3,2,2,3}, `val` = 3,那麽應該輸出 {2,2,_,_} **A:** 宣告一個 `iterator it`,並且用一個 foor loop 來 scan 這個陣列(scan 陣列,會利用指標來 scan `num[i]`)。 `it`會跟著 num[i] 一起移動,如果 num[i] == val ,那麼因為 num [vector](https://en.cppreference.com/w/cpp/container/vector),vector 有一個名為 erase 的 Member function,可以用來直接移除數字,並將 vector 後面的數字往前遞補,完全符合題目要求。 ```cpp= class Solution { public: int removeElement(vector<int>& nums, int val) { vector<int> :: iterator it; it = nums.begin(); for(int i=0;i<nums.size();i++){ if(nums[i] == val){ nums.erase(it); it--; i--; } it++; } return nums.size(); } }; ``` ## [116. Populating Next Right Pointers in Each Node](https://leetcode.com/problems/populating-next-right-pointers-in-each-node/) * [台詞](https://www.notion.so/116-6c21746d74fa4871bf5e8716bfedf43c) **R**epeat: 給定一個 perfect binary tree,他所有的 leaf 都在同一層,並且只要是 parent node 一定會有兩個children node。 我們需要將這顆 tree 中所有的右節點往它右邊的節點(如果存在,不存在則為 NULL)透過 next 指標指向它。 **E**xamples: 1. 以下圖為例,3 在 2 右邊、5 在 4 右邊、6 在 5 右邊、7 在 6 右邊因此需要透過 struct Node 中的 next 指標來指向。 ![](https://i.imgur.com/a4kcrTn.png) --- 2. 如果只有一個 node ,它既是 root 也是 leaf,因此沒有 chidren ,不需要做任何動作。 但是!他是特殊 case,會這樣說是因為 `Node* root` 可以只傳一個 node ,因此如果用 Recursion method 則需要視為跟“空集合”一樣的 case。 `if(!root || !root->right) return root;` ### C++ 這題可用 Recursion 來做,那用 Recursion 的話就要考慮是用 Preorder(DLR)、Inorder(LDR)、Postorder(LRD) 來走訪。先考慮一件事: 如果我們先走訪 node 2(LRD or LDR),Tree 走訪到 node 2 時,我們應該如何將 node 2 鏈結到 node 3 、node 5 鏈結到 node 6 ? 很顯然沒有辦法將其鏈結起來,這問題也稱為 Local view。因為要鏈結到 node 3 or node 6 都需要 node 2 parent node 1 的幫助(view)。 ![](https://i.imgur.com/QxmJMh8.jpg) 那 DLR 可以行嗎?答案是肯定的! 如果我們在走訪 Node 1 時先幫 Node 2 中的 Next link 鏈結到 node 3,那就可以解決 Local View 的問題!因為我走訪到 Node 2 就可以直接透過 Next link 訪問 Node 3 進而訪問 Node 6。 ![](https://i.imgur.com/2iHoWOh.jpg) 所以我們將想法撰寫成 code 其實只有三行。 1. 先將 Current Node 的 left Node 的 Next 指向 Current Node 的 right Node(如果有 Left Node 的話)。 `current->left->next = Current->right;` 2. 第一步破除 Local View 之後,我們再撰寫一個判斷式 `if(current→Next)` 為何需要它?因為如果沒有這條鏈結代表這個 current Node 應該是 Tree 中最右邊的 Node 之一,代表不需要再做鏈結。 再來就是處理會跨越左右子樹的鏈結。 `current->right->next = current->next->left;` 以上三行就貫徹這題的精神 Recursion method 的精神。 ```cpp class Solution { public: Node* connect(Node* root) { if(!root || !root->right) return root; root->left->next = root->right; if(root->next) root->right->next = root->next->left; connect(root->left); connect(root->right); return root; } }; ``` ----