contributed by < rickywu0421
>
題目連結
1
考慮以下對二個無號整數取平均值的程式碼:
這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow:
接著我們可改寫為以下等價的實作:
我們再次改寫為以下等價的實作:
第一種實作方式為將 a
與 b
分別 right shift 1 bit 後相加, 此時還考慮到進位: a
與 b
的 LSB 是否都為 1, 故 EXP1 = a & b & 1
。
第二種實作方式為加法器的概念, x ^ y
表示為位元和, x & y
表示為進位, 則
a + b = x ^ y + (x & y) << 1
=>
(a + b) / 2 = ((x ^ y) + (x & y) << 1) >> 1 = (x & y) + (x ^ y) >> 1
故 EXP2 = x & y
, EXP3 = x ^ y
2
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
我們可以透過回傳值猜測題目中的表示式:
a > b
時, 回傳 a
;b > a
時, 回傳 b
;又 XOR 有兩個特性 (x 為任一整數):
x ^ x = 0
0 ^ x = x
故我們可以使用這兩個特性猜測題目的解,
a > b
時, 回傳 a ^ 0 = a
a < b
時, 回傳 a ^ a ^ b = b
因此 EXP4 = a ^ b
。
再根據上面的判斷, a > b
時 EXP4 & -(EXP5) = 0
, a < b
時 EXP4 & -(EXP5) = a ^ b
可知, EXP5 在 a > b
時為 0
, 在 a < b
時為 -1
(全部位元為 1), 故可知 EXP5 = a < b
。
3
考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式:
改寫為以下等價實作:
第二種實作與第一種實作均是輾轉相除法, 但作法略為不同, 第一種利用除法運算, 而第二種利用減法運算, 以下是運算時會用到的操作, 以及對應的程式碼片段:
這個部分的程式碼是在 do-while loop 裡的, 因為每次 loop 完 v 都會更新而有機會變成 2 的倍數。
核心部分:輾轉相除法
loop 中首先進行上述的操作 5, 接著判斷 與 兩者誰大, 用大數剪去小數, 值更新為相減後的數, 值更新為 , 中較小的數, 重複進行上述操作直到兩數相減為 0
。
故 COND = v
, return 值則需要乘上透過操作 3 除去的倍率 (2
的冪), 故 RET = u << shift
。
4
在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
考慮 GNU extension 的 __builtin_ctzll
的行為是回傳由低位往高位遇上連續多少個 0 才碰到 1。
範例: 當 a = 16
16 這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000
從低位元 (即右側) 往高位元,我們可發現
0 → 0 → 0 → 0 → 1,於是 ctz 就為 4,表示最低位元往高位元有 4 個 0
用以改寫的程式碼如下:
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
若 bitmap 越鬆散 (即 1 越少),於是 improved 的效益就更高。
根據原程式碼片段:
其意義為從 LSB, 迭代檢查 bitset 的該 bit 是否為 1, 若是, 則 out[pos++] = k * 64 + 該 bit 的位置
。
改良的程式碼片段如下:
其概念為透過 __builtin_ctzll()
找出 bitset 中從低位元往高位元數來的第一個 1, 再透過 bitset ^= t
把那一個 1 clear, 以便進行下一個 set bit 的搜索。
假設 bitset = 0001 0100
, 則做完一次上述的操作後位置 2 的 bit 會被 clear:bitset = 0001 0000
, 而要怎麼達成這件事呢?
根據題目的提示 (-bitset), 根據二補數, -bitset = 1110 1100
, 我們可發現, 將 t = bitset & -bitset
後可以得到一個 bit string = 0000 0100
, 其只有一個 bit 被 set, 那就是 bitset 中最小位元為 1 的位置, 之後再透過 bitset ^= t
把 bitset 中最小位元的 1 clear 掉 (根據 xor 的特性), 故 EXP6 = bitset & -bitset
5
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作:
以下我們以循環小數 作為該題的例子。
該題記錄小數的作法為:用 hash table 記錄每次迭代的餘數及其在小數中的位置。
每次迭代時首先會檢查該次的餘數是否存在於 hash table 中,
若是, 則此數為循環小數, 並進行以下操作:
但首次進入迴圈時因尚未有餘數加入到 hash table 中, 故 if 判斷不成立, 所以我們先往後看:
以下操作不難看出為將目前的餘數及餘數的位置加入到 hash table 中, 以當前的狀態, 餘數為 12 % 11 = 1
, index 為 i = 0
。
加入 hash table 後將餘數 * 10 後除以除數:
*q++ = (1 * 10) / 11 + '0' = '0'
=>result = '0'
餘數則進行以下更新:
remainder = (1 * 10) % 11 = 10
。
此時 remainder = 10
, 故以下條件仍然不成立。
之後的操作與 iter 1 類似, 接近 loop end 時
*q++ = (10 * 10) / 11 + '0' = '9'
=> result = '09'
remainder = (10 * 10) % 11 = 1
此時 remainder = 1
, 故 if 條件成立:
第一個 while loop 的功能為是將像 1.34(15)
中未循環的小數寫入 result 中, 故 PPP = pos--
。(12 / 11
中不存在未循環的小數)。
之後將循環的小數用小括號包起來, 最後回傳 result = '0.(09)'
。
6
__alignof__ 是 GNU extension,以下是其可能的實作方式:
透過題目的 struct { char c; t _h; }
可以猜到 __alignof__
的實作想法為將欲驗證的 type 之變數宣告在 struct 中的 char 變數之後, 透過 _h
的位置減去 c
之位置可得之 type t
之 alignment。
故 M = _h
, 則 ((char *)(&((struct { char c; t _h; } *)0)->_h)
表示為變數 _h
的相對於 struct 起始的位置, 又因起始位置為 0
, 則 X = 0
。
7
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
直覺的實作程式碼如下: (naive.c)
觀察 printf 的(格式)字串,可分類為以下三種:
我們若能精準從給定輸入 i
的規律去控制 start
及 length
,即可符合 FizzBuzz 題意:
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
其中 is_divisible
函式技巧來自 Faster remainders when the divisor is a constant: beating compilers and libdivide,甚至 gcc-9 還內建了 FizzBuzz optimization (Bug 82853 - Optimize x % 3 == 0 without modulo)。
請補完,使得程式碼符合預期,儘量以最簡短且符合一致排版的形式來撰寫。
對於處理器來說,每個運算所花的成本是不同的,比如 add, sub 就低於 mul,而這些運算的 cost 又低於 div 。依據〈Infographics: Operation Costs in CPU Clock Cycles〉,可發現整數除法的成本幾乎是整數加法的 50 倍。
作答區
KK1 = ? (變數名稱)
KK2 = ? (變數名稱)
KK3 = ? (表示式,不包含小括號)
首先看題目的這段敘述:
根據是否能被 3 (div3
) 或 5 (div5
) 整除而產生的 offset
以及 length
變化可以做成下列表格, 以下欄位以 (div3, div5) 呈現:
(div3, div5) | (0, 0) | (1, 0) | (0, 1) | (1, 1) |
---|---|---|---|---|
offset | 8 | 0 | 4 | 0 |
length | 2 | 4 | 4 | 8 |
回到程式碼, 根據上述表格可以作答:
KK1 = div3
, KK2 = div5
KK3 = div3 << 2