# 2023 年「[資訊科技產業專案設計](https://hackmd.io/@sysprog/info2023)」作業2 --- 黑衣: Interviewer. 白衣: Interviewee. 影片連結: [施新豐-Crazy: Homework2 (漢)](https://youtu.be/E3-tv6w8lBY) --- ## 作業目標 * 「這是一個最好的時代, 這是一個最壞的時代.」from 雙城記 by [狄更斯](https://zh.wikipedia.org/zh-tw/%E6%9F%A5%E5%B0%94%E6%96%AF%C2%B7%E7%8B%84%E6%9B%B4%E6%96%AF). 活著雖然不易, 既然陰錯陽差讀了 engineering, 也許不是個稱職的 engineer, 但還是要試著學習 engineer的精神, 試著找到出路養活自己. * 養活自己就要找工作, 需要在 interview中脫穎而出. 然而, interview本身充滿著一定程度的**主觀且不透明**, 課程期望透過**同學互評**來減低不透明程度讓學員提早瞭解自身優缺點及可以改進的方向. ## HW1的 interview練習可改進的部份實在太多, 需要循序漸進的練習, 在此列出 HW2預計完成的目標 * 學會調整螢幕錄製畫質字體尺寸讓觀眾可清晰觀看影片. * 練習 follow 「REACTO」framework, 學習減少說話時的贅字或語助詞. * 完成 HW1提及的部份 Optimization. * HackMD附上程式碼供學員討論. --- ## [Leetcode No. 1 Two Sum (Easy)](https://leetcode.com/problems/two-sum) ### 過程討論 ***黑衣:*** 有個男孩每天都會將辛苦打工賺的錢存入銀行, 但某天接到一通陌生來電告知過去有二天銀行帳戶交易發生錯誤, 可能會平白損失那些賺來的辛苦錢. 已知過去 N天的交易記錄, 希望你能幫忙想方法幫男孩找出是 N天中的哪二天可能發生交易錯誤. 另外, 若電話通知訊息屬實, 也只會有一組正確的答案. 否則, 該電話可能為詐騙電話. ``` Example 1: Input: nums = [2,7,11,15], size = 4, target = 9 Output: [0,1] Explanation: 因為 nums[0] + nums[1] == 9, 所以 function需要回傳 [0, 1]. Example 2: Input: nums = [3,2,4], size = 3, target = 6 Output: [1,2] Example 3: Input: nums = [3,3], size = 2, target = 6 Output: [0,1] ``` ***白衣:*** 我將這個問題 formulate成以下. | | nums[0] | nums[1] | ... | nums[size - 2] | nums[size - 1] | | -------------- | ------- | --------- | --- | ---------------- | ----------------------- | | nums[0] | N/A | sum(0, 1) | ... | sum(0, size - 2) | sum(0, size - 1) | | nums[1] | N/A | N/A | ... | sum(1, size - 2) | sum(1, size - 1) | | ... | N/A | N/A | ... | ... | ... | | nums[size - 2] | N/A | N/A | ... | N/A | sum(size - 2, size - 1) | | nums[size - 1] | N/A | N/A | ... | N/A | N/A | 這樣是為了 enumerate陣列中任意二個 element的加總結果, 而就在每個 pair的加總過程中順便比對是否有與 target value相同, 找到的話就 return. 若是所有 pair在 enumerate過後都沒有等於 target value的結果, 就代表陣列中找不到符合條件的 element pair. ``` int* twoSum(int* nums, int numsSize, int target) { int i, j; int *pair = (int *)malloc(2 * sizeof(int)); // enumerate all possible results // once encounter the result matching the target // return their indices // Otherwise, return NULL for (i = 0; i < numsSize - 1; i++) { for (j = i + 1; j < numsSize; j++) { if (nums[i] + nums[j] == target) { pair[0] = i; pair[1] = j; return pair; } } } return NULL; } ``` ***黑衣:*** 男孩每天還是很努力打工賺錢, 但三年後, 再次接到類似來電, 這就代表可能累積了 1000多筆交易記錄. 原本方法隨著交易天數增加所需的運算成本 scale得很快. 請問還有沒有其他可以改進的部份呢? ***白衣:*** 在 enumerate完所有與 element 0, e.g., nums[0], 若都沒找到等於 target value的 pair的話, 進行 element 1的 enumeration時, 同樣需要再次存取 nums[2], ..., nums[size-1]. 因此, 或許可以在第一次讀取陣列時進行這些動作. 1. 把 nums[0]與其他 nums[j]的加總結果順便記錄下來, 也記錄下 difference(i, j) = nums[j] - nums[i]的內容, 即 difference(0, 1), difference(0, 2), ..., difference(0, size - 1). 2. 進行比對 pair sum與 target value時, 一定需要 traverse陣列 nums[], for nums[i], __target = target - nums[i], 這時 __target就需要在其他 nums[j]中尋找, where j != i. 3. If 第 0 round的 traversal沒有找到 target pair sum的話, 進行比對 nums[i + 1]時, __target += difference(i, j), 然後使用這一 round的 __target與陣列內容作比對即可. 這樣一來, 就不用再讀取 enumeration element i時已讀取過的 element value. ``` int* twoSum(int* nums, int numsSize, int target) { int i, j; int *pair = (int *)malloc(2 * sizeof(int)); int *sums = (int *)malloc(numsSize * sizeof(int)); int *diffs = (int *)malloc(numsSize * sizeof(int)); pair[0] = -1; i = 0; for (j = 1; j < numsSize; j++) { if (nums[0] + nums[j] == target) { pair[0] = 0; pair[1] = j; goto out; } sums[j] = nums[j] + nums[0]; diffs[j] = nums[j] - nums[0]; } for (i = 1; i < numsSize - 1; i++) { for (j = i + 1; j < numsSize; j++) { if (diffs[i] + sums[j] == target) { pair[0] = i; pair[1] = j; goto out; } } } out: free(sums); free(diffs); if (pair[0] >= 0) return pair; return NULL; } ``` 驗證過後發現方法一與方法二的執行時間沒有明顯差異, 探究之下發現方法二在第 0 round之後的 pair sum比對所需的資料存取次數並沒有比較少, e.g., 方法二(diffs[i], sums[j]) V.S. 方法一(nums[i], nums[j]). 並無法有預期的減少資料存取的效果. 而且還多了 diffs, sums的額外的記憶體空間配置與 maintain的成本. ### Other proposal 在老師的作業要求中有看到[面試範例影片](https://youtu.be/kVgy1GSDHG8)提到可以使用 Hash來改進, 但 C程式語言中沒有內建的 STL, 還需要研究 Hash Table的實作. ## [Leetcode No. 119 Pascal's Triangle II (Easy)](https://leetcode.com/problems/pascals-triangle-ii) ### 過程討論 ***黑衣:*** Function input只有一個. - rowIndex, 一個大於等於 0的 int. Function需要回傳第 rowIndex列的 Pascal's Triangle的內容. (0 <= rowIndex <= 33) ``` Example 1: Input: rowIndex = 3 Output: [1,3,3,1] Example 2: Input: rowIndex = 0 Output: [1] Example 3: Input: rowIndex = 1 Output: [1,1] ``` ***白衣:*** By definition, Pascal's Triangle中每個 element的值可從其前一列的 element的值來決定. 但這就代表要得知第 n列的內容, 需要把第 n-1列~第 1列的內容都算完. 我想知道有沒有其他的方法可以直接算出第 n列的內容, 回想起高中數學介紹排列組合的問題. Pascal's Triangle中第 n列的 element內容其實就是 (a + b)^n的結果中按升冪或降冪排列後的, 各項次的係數. 分別是 C(n, 0), C(n, 1), ..., C(n, n). 這樣一來, 這個問題就可簡化成排列組合的問題. 另外, C(n, m)與 C(n, m + 1)有這樣的關係. * C(n, m + 1) = C(n, m) * (n - m) / (m + 1) * 這代表每次都可從前一項 C(n, m)分子分母乘以對應的數值來算出 C(n, m + 1)的值. ``` int* getRow(int rowIndex) { int *row = (int *)malloc((rowIndex + 1) * sizeof(int)); int i; row[0] = 1; for (i = 1; i < rowIndex + 1; i++) { row[i] = row[i - 1] * (rowIndex - i + 1) / i; } return row; } ``` 排列組合的 Proposal雖可直接算出 Pascal's Triangle任一列的任一 element的值. 但是其運算過程會牽扯到 n!的計算, 程式語言的數值類資料結構都有值域限制, 當 n值一大, 這個 proposal就面臨數值 overflow挑戰.  我試著採取這個 action來減緩數值 overflow的問題. * 觀察 Pascal's Triangle後可發現, 每個 row的結果是左右對稱的. 因此, 每個 row的 element只需計算前半, 後半與前半是對稱的, 可以不用再計算一次. ``` int* getRow(int rowIndex) { int *row = (int *)malloc((rowIndex + 1) * sizeof(int)); int i; row[0] = 1; for (i = 1; i < rowIndex / 2 + 1; i++) { row[i] = row[i - 1] * (rowIndex - i + 1) / i; } for (; i < rowIndex + 1; i++) { row[i] = row[rowIndex - i]; } return row; } ``` 可惜 rowIndex == 30時還是會發生 int overflow, 目前採取 dirty fix的解法. 轉成較大型別的 data type, e.g., long, 避掉 int overflow的發生. 但此方法的延展性不高. ``` int* getRow(int rowIndex){ int *row = (int *)malloc((rowIndex + 1) * sizeof(int)); int i; row[0] = 1; for (i = 1; i < rowIndex / 2 + 1; i++) { row[i] = (long)row[i - 1] * (rowIndex - i + 1) / i; } for (; i < rowIndex + 1; i++) { row[i] = row[rowIndex - i]; } return row; } ``` ### Other proposal 計算機科學與數學還是存在一些差異, 且計算機科學起步之時, 往往是計算資源非常侷限的時代, 難怪會需要有 dynamic programming等演算法, 時至今日, 這些演算法也許不能一步將結果計算出來, 但卻較有延展性, 即便 n值怎麼改變, 較不會引發數值 overflow的問題, 而能逐步將結果算出來. 往後我會想要完成這些. * 實作出 dynamic programming的方法與排列組合的 proposal做比較. * 排列組合的方法或許也有其他方式將 n!的計算造成的數值 overflow給避掉. 觀察了一下 Pascal's Triangle後發現 C(n, i)的結果一定能整除, 我認為在計算每個 element的結果時, e.g., C(n, i) = (n * (n - 1) * ... * (n - i + 1)) / i! 先不要乘開, 而使將依序將分子的 n, (n - 1), ..., (n - i + 1)都 insert到一個 list entry, 分母則是進行 list entry remove, 可以搭配質因數分解來完成 list entry insert / remove. 最後才是將 list entry讀出來做計算乘積. 至於這個 list要使用什麼資料結構來實作, 則可以再討論. * 也許真的有可以避掉數值 overflow的解法, 但我對怎麼處理數值 overflow的議題也感興趣, 或許有天也會遇到不得不處理的狀況. 數值型別的 type casting內部是怎麼轉換的, 我也想要多瞭解. ## [Leetcode No. 29 Divide Two Integers (Medium)](https://leetcode.com/problems/divide-two-integers) ### 過程討論 ***黑衣:*** Function input有二個. - dividend, 被除數, or 分子, int型別. - divisor, 除數, or 分母, int型別. Function需要回傳 dividend除以 divisor的商數 quotient. (divisor不會是 0. 若 quotient大於 2^31 - 1, 回傳 2^31 - 1. 若 quotient小於 -2^31, 回傳 -2^31.) ``` Example 1: Input: dividend = 10, divisor = 3 Output: 3 Explanation: 10/3 = 3.33333.. which is truncated to 3. Example 2: Input: dividend = 7, divisor = -3 Output: -2 Explanation: 7/-3 = -2.33333.. which is truncated to -2. ``` ***白衣:*** 我的想法很簡單, 先從 dividend與 divisor都是正數的情況下做思考, 這個情況下, 設計一個 while loop, remaining的初始值設定為 dividend. if remaining >= divisor, 就進入 while loop中把 remaining扣掉 divisor得出新的 remaining, 當 while loop終止時, while loop的執行次數就是 quotient的值. 接著考慮正負號的問題. 我會希望計算時都是以正整數來執行, 所以會需要將 dividend, divisor都轉成絕對值. 我設計一個 sign變數, 初始值為 1. ``` if (dividend < 0) sign--; if (divisor < 0) sign--; ``` 這就使得可以使用 sign == 0來判斷最終 quotient會是負數. 但這邊還會有另一個問題, int型別的正負數不對稱, 負整數有多一個 -2^31. 我目前的解法是將 dividend, divisor的絕對值都使用 long type來儲存. ``` int divide(int dividend, int divisor) { long remaining; char sign = 1; long abs_dividend = dividend; long abs_divisor = divisor; long count = 0; if (dividend == 0) return 0; if (dividend < 0) { abs_dividend = 0 - (long)dividend; sign--; } if (divisor < 0) { abs_divisor = 0 - (long)divisor; sign--; } remaining = abs_dividend; while (remaining >= abs_divisor) { remaining -= abs_divisor; if (sign == 0) count--; else count++; } return count; } ``` 然後, 不知道 Leetcode是不是會將 Medium難度的答案增加執行時間的標準. 以下這幾個 special case其實可以有更快的判斷方法. 1. dividend == 0, return 0; 2. dividend == divisor, return 1; 3. abs(dividend) < abs(divisor), return 0; 4. abs(divisor) == 1, return dividend; // 須注意正負號, 以及超出正負整數極值的 int overflow問題. * sign == 0, 答案為 0 - abs(dividend), 而 abs(dividend)的最大值為 -(INT_MIN), 0 - abs(dividend)最小值就會是 INT_MIN, 不需處理 int overflow的問題. * sign != 0, 答案為 abs(dividend), 因為 abs(dividend)的最大值為 -(INT_MIN), 所以需要額外處理 int overflow的問題. ``` int divide(int dividend, int divisor) { long remaining; char sign = 1; long abs_dividend = dividend; long abs_divisor = divisor; long count = 0; if (dividend == 0) return 0; if (dividend == divisor) return 1; if (dividend < 0) { abs_dividend = 0 - (long)dividend; sign--; } if (divisor < 0) { abs_divisor = 0 - (long)divisor; sign--; } if (abs_divisor > abs_dividend) return 0; if (abs_divisor == 1) { if (sign == 0) return 0 - abs_dividend; return abs_dividend > INT_MAX ? INT_MAX : abs_dividend; } remaining = abs_dividend; while (remaining >= abs_divisor) { remaining -= abs_divisor; if (sign == 0) count--; else count++; } return count; } ``` 再來, 我 submission時還是被回覆 Time Limit Exceeded! 因此, 會需要來處理 dividend >> divisor而使得 while loop需要跑多 round的問題. 我希望當 remaining還很大時, while loop中每次可以直接扣掉多次的 divisor, 節省 while loop執行次數. 因此我預先設計了數個 2的冪次方倍數 divisors, while loop會先從最高冪次的 divisor對 remaining進行扣除, 之後依序使用低冪次的 divisor. ``` int divide(int dividend, int divisor) { long remaining; char sign = 1; long abs_dividend = dividend; long abs_divisor = divisor; char seeds = 8; int weights_seed[9] = {1}; long __abs_divisor[9] = {0}; long count = 0; int order; if (dividend == 0) return 0; if (dividend == divisor) return 1; if (dividend < 0) { abs_dividend = 0 - (long)dividend; sign--; } if (divisor < 0) { abs_divisor = 0 - (long)divisor; sign--; } if (abs_divisor > abs_dividend) return 0; if (abs_divisor == 1) { if (sign == 0) return 0 - abs_dividend; return abs_dividend > INT_MAX ? INT_MAX : abs_dividend; } remaining = abs_dividend; __abs_divisor[0] = abs_divisor; weights_seed[0] = 1; for (order = 1; order <= seeds; order++) { __abs_divisor[order] = __abs_divisor[order - 1] + __abs_divisor[order - 1]; weights_seed[order] = weights_seed[order - 1] + weights_seed[order - 1]; } order = seeds; while (order >= 0) { while (remaining >= __abs_divisor[order]) { remaining -= __abs_divisor[order]; if (sign == 0) count -= weights_seed[order]; else count += weights_seed[order]; } order--; } return count; } ``` ### Other proposal 感覺對於 while loop中 divisor的設定可以更 smart一點, abs(dividend)與 abs(divisor)無法預先知道時, 就會有 over-provision, under-provision的問題. 目前做法的缺點是會 always計算出 divisor的二的冪次方倍數 divisors. * 遇到 abs(dividend) >> abs(divisor)時, 當然可以發揮預期效果. * 但若 abs(dividend)與 abs(divisor)相近的話, 這些預先準備的 divisors就會是個 overhead了, 這個 overhead有二個, e.g., 預先算好 divisors以及使用 2的高羃次方倍數 divisors與 remaining的比較. 所以這樣的作法雖可避免 under-provision使得計算時間拉長, 但卻可能造成 over-provision的浪費. 我認為可以透過 abs(dividend), abs(divisor)的 MSB來動態決定 divisor的大小. 但我目前對 bit operations還不熟悉, 還沒有對此進行研究. ## 作業心得與討論 ### HW2未完成的目標 * For many reasons, 我向來不太會 interview, 所以在我看來, 課程其他學員的表現都很不錯, 我實在沒有什麼立場來做評論. 只能勉強寫了一些 critics. * For presentations, 中英文口說都亟需改進. * 沒有演算法 Complexity Analysis. * HackMD Markdown語法, LATEX排版不會使用. * Leetcode選題以及使用的解題方式與數學背景高度相關, 實際面試時不一定能剛好都與數學相關, 可以多多練習其他題型. * For [Leetcode No. 1 Two Sum](https://leetcode.com/problems/two-sum), Week6課程老師有附上 C程式語言的 Hash Table實作, 還沒研究. * For [Leetcode No. 119 Pascal's Triangle II](https://leetcode.com/problems/pascals-triangle-ii), 可以僅針對分母進行質因數分解, 但還未實作. * For [Leetcode No. 29 Divide Two Integers](https://leetcode.com/problems/divide-two-integers), 有關 bit operation來找出 MSB的方法還沒研究. ### 其他心得 * 每個人都有盲點, 老師想出的同學互評的方式, 一來可以讓同學點出彼此的優缺點. 二來, 也是一個促進課堂互動的方式. * 我的心得是「老師難當」, 尤其評分的時候. * Sorry, 我沒有看很多其他同學的 interview錄影. 我也還沒時間欣賞 Leetcode同題目的 submissions. * 我的時間管理還是有待改進, 每次都被許多身旁的事情追著跑, 啥時才能羽扇綸巾, 談笑以對呢? * 我還是需要時間, 一步一步來, 要相信自己, 每個人的旅程都不一樣. ## 待學習與研究的主題 * Leetcode No. 1: Hash Table的實作. * Leetcode No. 119: dynamic programming的 proposal, numerical type casting的原理, big number problem. * Leetcode No. 29: numerical type casting的原理, bit operations. ## ## 他評01 * interviewer的檢討 * [06:11](https://youtu.be/E3-tv6w8lBY?si=VZmoOCTNCFmwDWmh&t=371) OMG! 男孩每天賺的錢可多達 10^9耶! 感覺這個條件可以放在延伸問題時再點出來, 改成"30年後, 男孩長大在證券交易所上班, 再次接到不明來電時, 每天的交易量為 ..."就較有可能符合這個 order的數字. * interviewee的檢討 * 針對 Two Sum這個問題, HackMD上介紹影片中優化的解法在驗證後沒有預期的效能改善. 仔細思考這個問題的背景後, 我認為這個問題設計上很特別, 輸入整數陣列索引範圍在[0~9999], 但這 10000個陣列元素的內容卻是散佈在[-10^9~10^9]. 許多解法 based on Hash Table, 但我認為在 Hash Table之前應該還有一個可能的解法, e.g., 建立 inverse array以陣列元素內容 map到其陣列索引. 這個 inverse array會是一個非常稀疏的陣列, 其陣列索引範圍會有[0~10^9 + 10^9], 陣列元素數值就為 [0~9999]. 理解上, 不確定作業系統中每個 task的 page table是否會是類似這樣的 mapping關係. 於是, 我有嘗試以這樣的概念進行以下實作. * 先以原始陣列來建構出 inverse array. * traverse原始陣列的每個 element, 在每個 iteration, 可以用 target扣除 element value後, 在 inverse array中找尋是否有對應的內容, 有的話, 該內容就是所求的另一索引. 沒有的話, 就進行下一 iteration. * 然而, 這樣的解法會需要許多空間, 且目前我並沒有實作成功. 以下有我的簡單實作. * 另外, 我認知的 page table會是以 radix tree進行實作, 這個還需要研究. 另外, 有關 sparse mapping引發配置空間卻沒有實際使用到的問題, 也凸顯出使用 radix tree, hash table等資料結構的必要性. * 其他 * HackMD中程式碼的部份可以選擇程式語言種類來 highlight keywords. ```clike #define BALANCE 100 int* twoSum(int* nums, int numsSize, int target) { int i, j; int *pair = (int *)malloc(2 * sizeof(int)); int *pos1 = (int *)malloc(100 * 2 * sizeof(int)); int *pos2 = (int *)malloc(100 * 2 * sizeof(int)); memset(pos1, 0, sizeof(pos1)); memset(pos2, 0, sizeof(pos2)); char flag = 0; // not found // establish the nums mapping array firstly for (i = 0; i < numsSize; i++) { if (pos1[nums[i] + BALANCE] <= 0) { pos1[nums[i] + BALANCE] = i + 1; } else if (pos2[nums[i] + BALANCE] <= 0) { pos2[nums[i] + BALANCE] = i + 1; } } // according to the mapping array, find out whether the pair exists for (i = 0; i < numsSize - 1; i++) { if (target == nums[i] + nums[i]) { if (pos1[nums[i] + BALANCE] > 0 && pos2[nums[i] + BALANCE] > 0) { pair[0] = pos1[nums[i] + BALANCE] - 1; pair[1] = pos2[nums[i] + BALANCE] - 1; flag = 1; goto out; } } else { if (pos1[target - nums[i] + BALANCE] > 0) { pair[0] = i; pair[1] = pos1[target - nums[i] + BALANCE] - 1; flag = 1; goto out; } } } out: free(pos1); free(pos2); if (flag) return pair; return NULL; } ```