contributed by < Holycung
>
2020q3 Homework4 (quiz4) 題目
LeetCode 461. Hamming Distance 提及,兩個整數間的 Hamming distance 為其二進位的每個位元的差。請計算輸入參數兩整數 x 與 y 的 Hamming distance,例如整數 1 的二進位為 0 0 0 1,而整數 4 的二進位為 0 1 0 0,則 1 與 4 的 Hamming distance 為 2。
請補完程式碼。
這邊運用 xor 就可以找到哪些位元是不相同的,最後在使用 __builtin_popcount
就可以得到 Hamming distance,所以 OP = ^
。
練習 LeetCode 477. Total Hamming Distance,你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時
稍微觀察一下下表就可以發現,只要看第 n 個位置的 bit 總共有幾個 0 跟 1,一對 0 跟 1 就會有一個 hamming distance,所以最後我們只要找出第 n 個 bit 有幾個 1 跟 0 就可以計算出 hamming distance。
decimal | bit 2 | bit 1 | bit 0 |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 |
2 | 0 | 1 | 0 |
實作方式如下,用 bit_count
的陣列紀錄每個 bit 1 出現的次數,第 5 行的 while 迴圈則是運用作業 3 的技巧去找到當中是 1 的 bit 位置並記錄到 bit_count
,最後的 for 迴圈則是計算出每個位元的 hamming distance。
提交到 Leetcode 上的結果。
TODO
LeetCode 1483. Kth Ancestor of a Tree Node 大意:
給你一棵樹,樹上有 n 個節點,編號自 0 到 。樹以父節點陣列的形式提供,其中 parent[i] 是節點 i 的父節點。樹的根節點是編號為 0 的節點。請設計
treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)
函式,回傳節點 node 的第 k 個祖先節點。若不存在這樣的祖先節點,回傳 -1。樹節點的第 k 個祖先節點是從該節點到根節點路徑上的第 k 個節點
以下是參考 C 程式實作:
請補完。
範例當中 [[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]]
,第一個陣列的 7 代表 7 個節點,後面的陣列則是代表每個節點的 ancestor 是誰。再來這題並沒有說到這個 tree 是 binary 或是 full、complete tree,所以都是有可能的。
因此一個節點最多有可能會有 n-1 個 ancestor。如果一個一個找起了話時間是複雜度是 空間複雜度也是 ,那如果在建樹的時候把所有的 ancestor 都用表紀錄下來,那查尋的時間複雜度就是 但是空間複雜度就是 ,這邊題目為了取得一個平衡,所以選擇記錄下每個節點的 的祖先,那在空間上我們就只需要花 ,在查詢的時間複雜度是 。
再探討一下這個方法,為何只要記錄下每個節點的 祖先,就保證能找到所有的祖先,那這邊要先知道一件事情,假設我們今天要找某個節點的第 k 個祖先,可以替換成某個節點的第 1 個祖先的第 k-1 個祖先,或是第 2 個祖先的第 k-2 個祖先,以此類推。所以第一個條件,不能去找某個節點大於 的祖先。
再來要怎麼確定再這樣的條件下一定可以找到所有的祖先,那就是本題的方法,從二進位的角度來想,假設 5 可以寫成, 這樣的表示,那我們今天要找某一個節點的第 5 個祖先,就可以換成找第 個祖先的第 個祖先,那今天要找第 n 個祖先,這樣我們最多就只會找到第 個祖先,就可以滿足第一個條件,並且可以在時間複雜度 下面完成。接下來看到程式碼。
第 11 行可以看到先配置了 n 個節點的大小,可以知道 TreeAncestor 裡面的 AAA 是一個二維的資料結構,所以是 **parent
。
第 13 行變數 max_level = 32 - __builtin_clz(n)
就是 ,但是這邊 32 - __builtin_clz(n)
還多加了一個 1,後面會提到。
第 14 行的迴圈則在初始化,第 19 行是先記錄下每個節點的第一個 ancestor。
第 25 行是把每個節點的 ancestor 記錄下來,所以 obj->parent[i][j + BBB] == -1
先判斷上一個祖先是不是 -1,不是的話就把去找到這個祖先記錄下來。
第 36 行 treeAncestorGetKthAncestor 這個是找到第 k 個 ancestor,迴圈的中止條件就是已經找到 -1 代表不存在,或是大於等於 max_level。第 40 行這邊就是我們上面講的方法,對二進為每個 bit 去做替換,假設找某一個節點的第 5 個祖先,5 的二進為是 101
,就可以換成找第 個祖先的第 個祖先,所以 CCC 就是 1 << i
,從最低位的開始替換。
在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析;
剛剛上面有提到第 13 行變數 max_level = 32 - __builtin_clz(n)
就是 ,但是這邊 32 - __builtin_clz(n)
還多加了一個 1,透過剛剛的解題思維,給定第 k 個祖先,我們最大只會用到第 個位元,所以這邊不需要 +1,節省記憶體的空間。
在 LeetCode 1483. Kth Ancestor of a Tree Node 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者;
在改進過程當中發現 leetcode 繳交同一份程式的時間會有些落差,連 memory 的使用也會有小誤差,所以以下都是繳交數次截圖比較優的一次,作為參考。
這邊的修改就是把 max_level 的 +1 拿掉,然後第 15 行的 for 迴圈要加上終止條件 j < max_level
。
第 10 行原本的 for 迴圈改以 memset 進行初始化。
查詢第 k 個祖先這邊原本是從最低位元一個一個開始找起,找到 1 的 bit 就做替換,那這個也可以換成上一個作業的技巧,透過 __builtin_ctz
直接找到最低位為 1 的位元,所以這邊改用 while 迴圈,中止條件多了一個 k == 0
。
不過這裡應該要對 k 的大小進行判斷,仔細觀察發現原本的程式也沒有進行判斷,如果今天 k 給一個大於 max_level 的值,然後剛好是 2 的次方這個方法就會噴錯,於是自己寫了程式跑了一下驗證,果然是會有錯誤的,詳細的 code 放在 Github,神奇的是 leetcode 都通過了
max_level
修正 -1
出處: 簡單的 FizzBuzz 藏有深度 (Google 面試題)
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
我們若能精準從給定輸入 i 的規律去控制 start 及 length,即可符合 FizzBuzz 題意:
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
請補完。
這題判斷是否能整除有用到 作業二 quickmod 的技巧來判斷是否能整除,並且透過精準地控制 start、length,即可正確的輸出。
第 14 行可以看到 length 原本是 2,如果能整除 3 或 5 就往右邊 shift 一位,也就是乘二的意思,兩個都能整除就 shift 兩次,得到 8。
接下來看到 17 行要控制 string literal "FizzBuzz%u"
的 start 位置,於是用下表觀察一下。
div3 | div5 | length | start |
---|---|---|---|
0 | 0 | 2 | 8 |
1 | 0 | 4 | 0 |
0 | 1 | 4 | 4 |
1 | 1 | 8 | 0 |
這邊觀察 div3、div5、start 三個變數,在 div3 = 1 的時候 start 都要為 0,所以可以知道 KK2 << KK3
這邊應該是 div3 << 2
這樣就會把前面歸 0。剩下的推倒一下就可以得到 KK1 = div5
。
評估 naive.c 和 bitwise.c 效能落差
避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
這邊要記得將所有的 printf 換成 sprintf,不然會大大影響實驗的結果,測試從 1 到 100000,並且重複執行 100 次,進行觀察。
cmp_fizzBuzz.c
getTime.sh
實驗結果如下,可以看出 bitwise 的方法確實是比 naive 版本快,詳細的程式可以參考 Github。
TODO
考慮到整數 PAGE_QUEUES
可能有以下數值:
給定 size_t offset
,試著將原本運算:
由於 PAGE_QUEUES 的數值分佈已知,在整數除法時,可思考加速運算的機制。需要注意,某些硬體平台缺乏整數除法指令 (如 Arm Cortex-A8),即使 Intel 公司出品的 Core 處裡器 Sandy Bridge 微架構中,針對 64 位元整數的除法運算,會佔用 40 到 103 個處理器週期,運算成本仍屬沈重。
於是我們可預先進行計算,從而避免整數除法指令的使用: (假設在 x86_64 硬體執行 GNU/Linux 並透過 gcc/clang 編譯)
其中 CASES 可能形如:
這樣的組合,請用最少的 case-break 敘述做出同等 size_t blockidx = offset / PAGE_QUEUES; 效果的程式碼。
這邊我們已知除數 PAGE_QUEUES
的範圍是 ,k 的範圍是 1~7,n 是 1~14,然後要對其整數除法進行加速,可以透過對除數跟被除數先進行約分的動作,就是先把 用 shift 方式處理掉。
看到程式當中的 __builtin_ffs
可以參考 Built-in Functions Provided by GCC,所以這邊的 ffs32(x)
就是代表最低位有多少個 0,也就可以知道我們要往右 shift 幾位。
Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
第 5 6 行就是對被除數 offset
跟除數 divident
先進行 shift,最後再透過 switch case 進行剩下的除法。不過因為已知除數 PAGE_QUEUES
的範圍是 ,所以把 除掉後只剩下 ,可能的 k 有 3、5、7,因此 cases 需要包含這三個數字。
TODO
練習 LeetCode 51. N-Queens,應選擇 C 語言並善用上述的 __builtin_ffs,大幅降低記憶體開銷;